babel-plugin-react-ssr
Version:
Adds the ssrWaitsFor array and ssrFetchData HOC on required components automatically for react-ssr.
172 lines (130 loc) • 6.19 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global['babel-plugin-react-ssr'] = factory());
}(this, (function () { 'use strict';
var classHasRenderMethod = function classHasRenderMethod(path) {
if (!path.node.body) {
return false;
}
var members = path.node.body.body;
for (var i = 0; i < members.length; i++) {
if (members[i].type == 'ClassMethod' && members[i].key.name == 'render') {
return true;
}
}
return false;
};
var classDeclaration = function classDeclaration(babel, path, state) {
if (classHasRenderMethod(path)) {
var _ref = path.node.body || {},
_ref$body = _ref.body,
body = _ref$body === undefined ? [] : _ref$body;
// Store the display name of the class for recusive checks
state.file.set('displayName', path.node.id.name);
body.forEach(function (propertyOrMethod) {
if (propertyOrMethod.static && propertyOrMethod.key.name === 'fetchData') {
state.file.set('hasFetchData', true);
}
});
}
};
var transform = function transform(babel) {
var t = babel.types;
return {
visitor: {
ClassDeclaration: classDeclaration.bind(null, babel),
ExportDefaultDeclaration: function ExportDefaultDeclaration(path, _ref) {
var file = _ref.file;
// if (!path.get('declaration').isClassDeclaration()) return
if (!file.get('hasJSX') || file.get(path.node.declaration.name) || path.node.declaration.name === '_default') return;
var node = path.node,
scope = path.scope;
var ref = node.declaration.id || path.scope.generateUidIdentifier('default');
var waitsFor = file.get('ssrWaitsFor');
var hasFetchData = file.get('hasFetchData');
if ((waitsFor || hasFetchData) && typeof node.declaration.name === 'undefined') {
throw new Error('\n\n react-ssr found an export default that was not exporting with a plain variable, instead found ' + node.declaration.type + '.\n Assign that to a variable with a unique name and export that variable as the default instead.\n\n It should look like:\n const myUniqueVariable = someThingYouDo(Component)\n export default myUniqueVariable\n\n This error was found in ' + file.opts.sourceFileName + '\n\n ');
}
if (waitsFor) {
var waitsForIdentifiers = waitsFor.filter(function (waiter) {
return scope.hasBinding(waiter);
}).map(function (waiter) {
return t.identifier(waiter);
});
var ssrWaitsFor = t.assignmentExpression('=', t.memberExpression(t.identifier(node.declaration.name), t.identifier('ssrWaitsFor')), t.arrayExpression(waitsForIdentifiers));
path.insertBefore(ssrWaitsFor);
}
if (hasFetchData) {
var decorator = t.variableDeclaration('var', [t.variableDeclarator(ref, t.callExpression(t.identifier('ssrFetchData'), node.declaration.arguments || [node.declaration]))]);
var displayName = t.assignmentExpression('=', t.memberExpression(t.identifier(node.declaration.name), t.identifier('displayName')), t.stringLiteral(node.declaration.name));
path.insertBefore(displayName);
path.replaceWith(decorator);
file.set(node.declaration.name, true);
path.insertAfter(t.exportDefaultDeclaration(ref));
}
},
JSXOpeningElement: function JSXOpeningElement(path, _ref2) {
var file = _ref2.file;
file.set('hasJSX', true);
var element = path.node.name;
var isNotDOMElement = element.name && element.name !== element.name.toLowerCase();
if (isNotDOMElement) {
var parentComponentName = file.get('displayName');
var waitsFor = file.get('ssrWaitsFor') || [];
var currentComponentName = element.name;
// Only add in the wait for array when its not already there and its not the same component as the parent (recurisve components)
if (!waitsFor.includes(currentComponentName) && currentComponentName !== parentComponentName) {
waitsFor.push(currentComponentName);
}
file.set('ssrWaitsFor', waitsFor);
}
},
ExpressionStatement: function ExpressionStatement(path, _ref3) {
var file = _ref3.file;
var _ref4 = path.node.expression || {},
type = _ref4.type,
_ref4$left = _ref4.left,
left = _ref4$left === undefined ? {} : _ref4$left,
_ref4$right = _ref4.right,
right = _ref4$right === undefined ? {} : _ref4$right;
var isAssignment = type === 'AssignmentExpression';
if (isAssignment) {
var leftIsMember = left.type === 'MemberExpression';
var rightIsFunction = right.type === 'FunctionExpression' || right.type === 'ArrowFunctionExpression';
if (leftIsMember && rightIsFunction) {
var _ref5 = left.property || {},
name = _ref5.name;
if (name === 'fetchData') {
file.set('hasFetchData', true);
}
}
}
},
Program: {
enter: function enter(path, _ref6) {
var file = _ref6.file;
file.set('hasJSX', false);
},
exit: function exit(path, _ref7) {
var file = _ref7.file;
var node = path.node,
scope = path.scope;
if (!file.get('hasFetchData')) {
return;
}
if (!(file.get('hasJSX') && !scope.hasBinding('ssrFetchData'))) {
return;
}
// some way of checking if react-ssr fetchData import exists
// or even require...
// console.info('Bindings: ', scope.getAllBindings())
var ssrImport = t.importDeclaration([t.importDefaultSpecifier(t.identifier('ssrFetchData'))], t.stringLiteral('react-ssr/lib/fetchData'));
node.body.unshift(ssrImport);
}
}
}
};
};
return transform;
})));