731 lines
25 KiB
JavaScript
Raw Normal View History

2020-05-19 11:43:42 +03:00
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
// the same as before
var PREFIX = '__reactstandin__';
var REGENERATE_METHOD = PREFIX + 'regenerateByEval';
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var SIGNATURE = '__signature__';
function fresh (babel) {
var t = babel.types;
var registrationsByProgramPath = new Map();
function createRegistration(programPath, persistentID) {
var handle = programPath.scope.generateUidIdentifier('c');
if (!registrationsByProgramPath.has(programPath)) {
registrationsByProgramPath.set(programPath, []);
}
var registrations = registrationsByProgramPath.get(programPath);
registrations.push({
handle: handle,
persistentID: persistentID
});
return handle;
}
function isComponentishName(name) {
return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
}
function findInnerComponents(inferredName, path, callback) {
var node = path.node;
switch (node.type) {
case 'Identifier':
{
if (!isComponentishName(node.name)) {
return false;
}
// export default hoc(Foo)
// const X = hoc(Foo)
callback(inferredName, node, null);
return true;
}
case 'FunctionDeclaration':
{
// function Foo() {}
// export function Foo() {}
// export default function Foo() {}
callback(inferredName, node.id, null);
return true;
}
case 'ArrowFunctionExpression':
{
if (node.body.type === 'ArrowFunctionExpression') {
return false;
}
// let Foo = () => {}
// export default hoc1(hoc2(() => {}))
callback(inferredName, node, path);
return true;
}
case 'FunctionExpression':
{
// let Foo = function() {}
// const Foo = hoc1(forwardRef(function renderFoo() {}))
// export default memo(function() {})
callback(inferredName, node, path);
return true;
}
case 'CallExpression':
{
var argsPath = path.get('arguments');
if (argsPath === undefined || argsPath.length === 0) {
return false;
}
var calleePath = path.get('callee');
switch (calleePath.node.type) {
case 'MemberExpression':
case 'Identifier':
{
var calleeSource = calleePath.getSource();
var firstArgPath = argsPath[0];
var innerName = inferredName + '$' + calleeSource;
var foundInside = findInnerComponents(innerName, firstArgPath, callback);
if (!foundInside) {
return false;
}
// const Foo = hoc1(hoc2(() => {}))
// export default memo(React.forwardRef(function() {}))
callback(inferredName, node, path);
return true;
}
default:
{
return false;
}
}
}
case 'VariableDeclarator':
{
var init = node.init;
if (init === null) {
return false;
}
var name = node.id.name;
if (!isComponentishName(name)) {
return false;
}
if (init.type === 'Identifier' || init.type === 'MemberExpression') {
return false;
}
var initPath = path.get('init');
var _foundInside = findInnerComponents(inferredName, initPath, callback);
if (_foundInside) {
return true;
}
// See if this identifier is used in JSX. Then it's a component.
var binding = path.scope.getBinding(name);
if (binding === undefined) {
return;
}
var isLikelyUsedAsType = false;
var referencePaths = binding.referencePaths;
for (var i = 0; i < referencePaths.length; i++) {
var ref = referencePaths[i];
if (ref.node.type !== 'JSXIdentifier' && ref.node.type !== 'Identifier') {
continue;
}
var refParent = ref.parent;
if (refParent.type === 'JSXOpeningElement') {
isLikelyUsedAsType = true;
} else if (refParent.type === 'CallExpression') {
var callee = refParent.callee;
var fnName = void 0;
switch (callee.type) {
case 'Identifier':
fnName = callee.name;
break;
case 'MemberExpression':
fnName = callee.property.name;
break;
}
switch (fnName) {
case 'createElement':
case 'jsx':
case 'jsxDEV':
case 'jsxs':
isLikelyUsedAsType = true;
break;
}
}
if (isLikelyUsedAsType) {
// const X = ... + later <X />
callback(inferredName, init, initPath);
return true;
}
}
}
}
return false;
}
function isBuiltinHook(hookName) {
switch (hookName) {
case 'useState':
case 'React.useState':
case 'useReducer':
case 'React.useReducer':
case 'useEffect':
case 'React.useEffect':
case 'useLayoutEffect':
case 'React.useLayoutEffect':
case 'useMemo':
case 'React.useMemo':
case 'useCallback':
case 'React.useCallback':
case 'useRef':
case 'React.useRef':
case 'useContext':
case 'React.useContext':
case 'useImperativeMethods':
case 'React.useImperativeMethods':
case 'useDebugValue':
case 'React.useDebugValue':
return true;
default:
return false;
}
}
function getCalleeName(callee) {
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
return callee.object.name;
}
return callee.name;
}
function getHookCallsSignature(functionNode, scope) {
var fnHookCalls = hookCalls.get(functionNode);
if (fnHookCalls === undefined) {
return null;
}
return {
key: fnHookCalls.map(function (call) {
return call.name + '{' + call.key + '}';
}).join('\n'),
customHooks: fnHookCalls.filter(function (call) {
return !isBuiltinHook(call.name);
}).filter(function (call) {
return scope.parent.hasBinding(call.name);
}).map(function (call) {
return t.cloneDeep(call.callee);
})
};
}
function createArgumentsForSignature(node, signature, scope) {
var key = signature.key,
customHooks = signature.customHooks;
var args = [node, t.stringLiteral(key)];
var hooksInScope = customHooks.filter(function (call) {
return scope.hasBinding(getCalleeName(call));
});
if (hooksInScope.length > 0) {
args.push(t.arrowFunctionExpression([], t.arrayExpression(hooksInScope)));
}
return args;
}
var seenForRegistration = new WeakSet();
var seenForSignature = new WeakSet();
var hookCalls = new WeakMap();
var HookCallsVisitor = {
CallExpression: function CallExpression(path) {
var node = path.node;
var callee = node.callee;
// Note: this visitor MUST NOT mutate the tree in any way.
// It runs early in a separate traversal and should be very fast.
var name = null;
switch (callee.type) {
case 'Identifier':
name = callee.name;
break;
case 'MemberExpression':
name = callee.property.name;
break;
}
if (name === null || !/^use[A-Z]/.test(name)) {
return;
}
var fnScope = path.scope.getFunctionParent();
if (fnScope === null) {
return;
}
// This is a Hook call. Record it.
var fnNode = fnScope.block;
if (!hookCalls.has(fnNode)) {
hookCalls.set(fnNode, []);
}
var hookCallsForFn = hookCalls.get(fnNode);
var key = '';
if (path.parent.type === 'VariableDeclarator') {
// TODO: if there is no LHS, consider some other heuristic.
key = path.parentPath.get('id').getSource();
}
// Some built-in Hooks reset on edits to arguments.
var args = path.get('arguments');
if (name === 'useState' && args.length > 0) {
// useState second argument is initial state.
key += '(' + args[0].getSource() + ')';
} else if (name === 'useReducer' && args.length > 1) {
// useReducer second argument is initial state.
key += '(' + args[1].getSource() + ')';
}
hookCallsForFn.push({
callee: path.node.callee,
name: name,
key: key
});
}
};
return {
visitor: {
ExportDefaultDeclaration: function ExportDefaultDeclaration(path) {
var node = path.node;
var decl = node.declaration;
var declPath = path.get('declaration');
if (decl.type !== 'CallExpression') {
// For now, we only support possible HOC calls here.
// Named function declarations are handled in FunctionDeclaration.
// Anonymous direct exports like export default function() {}
// are currently ignored.
return;
}
// Make sure we're not mutating the same tree twice.
// This can happen if another Babel plugin replaces parents.
if (seenForRegistration.has(node)) {
return;
}
seenForRegistration.add(node);
// Don't mutate the tree above this point.
// This code path handles nested cases like:
// export default memo(() => {})
// In those cases it is more plausible people will omit names
// so they're worth handling despite possible false positives.
// More importantly, it handles the named case:
// export default memo(function Named() {})
var inferredName = '%default%';
var programPath = path.parentPath;
findInnerComponents(inferredName, declPath, function (persistentID, targetExpr, targetPath) {
if (targetPath === null) {
// For case like:
// export default hoc(Foo)
// we don't want to wrap Foo inside the call.
// Instead we assume it's registered at definition.
return;
}
var handle = createRegistration(programPath, persistentID);
targetPath.replaceWith(t.assignmentExpression('=', handle, targetExpr));
});
},
FunctionDeclaration: {
enter: function enter(path) {
return;
var node = path.node;
var programPath = void 0;
var insertAfterPath = void 0;
switch (path.parent.type) {
case 'Program':
insertAfterPath = path;
programPath = path.parentPath;
break;
case 'ExportNamedDeclaration':
insertAfterPath = path.parentPath;
programPath = insertAfterPath.parentPath;
break;
case 'ExportDefaultDeclaration':
insertAfterPath = path.parentPath;
programPath = insertAfterPath.parentPath;
break;
default:
return;
}
var id = node.id;
if (id === null) {
// We don't currently handle anonymous default exports.
return;
}
var inferredName = id.name;
if (!isComponentishName(inferredName)) {
return;
}
// Make sure we're not mutating the same tree twice.
// This can happen if another Babel plugin replaces parents.
if (seenForRegistration.has(node)) {
return;
}
seenForRegistration.add(node);
// Don't mutate the tree above this point.
// export function Named() {}
// function Named() {}
findInnerComponents(inferredName, path, function (persistentID, targetExpr) {
var handle = createRegistration(programPath, persistentID);
insertAfterPath.insertAfter(t.expressionStatement(t.assignmentExpression('=', handle, targetExpr)));
});
},
exit: function exit(path) {
//return;
var node = path.node;
var id = node.id;
if (id === null) {
return;
}
var signature = getHookCallsSignature(node, path.scope);
if (signature === null) {
return;
}
// Make sure we're not mutating the same tree twice.
// This can happen if another Babel plugin replaces parents.
if (seenForSignature.has(node)) {
return;
}
seenForSignature.add(node);
// Unlike with __register__, this needs to work for nested
// declarations too. So we need to search for a path where
// we can insert a statement rather than hardcoding it.
var insertAfterPath = null;
path.find(function (p) {
if (p.parentPath.isBlock()) {
insertAfterPath = p;
return true;
}
});
if (insertAfterPath === null) {
return;
}
insertAfterPath.insertAfter(t.expressionStatement(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(id, signature, insertAfterPath.scope))));
}
},
'ArrowFunctionExpression|FunctionExpression': {
exit: function exit(path) {
var node = path.node;
var signature = getHookCallsSignature(node, path.scope);
if (signature === null) {
return;
}
// Make sure we're not mutating the same tree twice.
// This can happen if another Babel plugin replaces parents.
if (seenForSignature.has(node)) {
return;
}
seenForSignature.add(node);
// Don't mutate the tree above this point.
if (path.parent.type === 'VariableDeclarator') {
var insertAfterPath = null;
path.find(function (p) {
if (p.parentPath.isBlock()) {
insertAfterPath = p;
return true;
}
});
if (insertAfterPath === null) {
return;
}
// Special case when a function would get an inferred name:
// let Foo = () => {}
// let Foo = function() {}
// We'll add signature it on next line so that
// we don't mess up the inferred 'Foo' function name.
insertAfterPath.insertAfter(t.expressionStatement(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(path.parent.id, signature, insertAfterPath.scope))));
// Result: let Foo = () => {}; __signature(Foo, ...);
} else {
// let Foo = hoc(() => {})
path.replaceWith(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(node, signature, path.scope)));
// Result: let Foo = hoc(__signature(() => {}, ...))
}
}
},
Program: {
enter: function enter(path) {
// This is a separate early visitor because we need to collect Hook calls
// and "const [foo, setFoo] = ..." signatures before the destructuring
// transform mangles them. This extra traversal is not ideal for perf,
// but it's the best we can do until we stop transpiling destructuring.
path.traverse(HookCallsVisitor);
}
}
}
};
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
var templateOptions = {
placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/
};
/* eslint-disable */
var shouldIgnoreFile = function shouldIgnoreFile(file) {
return !!file.split('\\').join('/').match(/node_modules\/(react|react-dom|react-hot-loader)([\/]|$)/);
};
/* eslint-enable */
function plugin(args) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// This is a Babel plugin, but the user put it in the Webpack config.
if (this && this.callback) {
throw new Error('React Hot Loader: You are erroneously trying to use a Babel plugin ' + 'as a Webpack loader. We recommend that you use Babel, ' + 'remove "react-hot-loader/babel" from the "loaders" section ' + 'of your Webpack configuration, and instead add ' + '"react-hot-loader/babel" to the "plugins" section of your .babelrc file. ' + 'If you prefer not to use Babel, replace "react-hot-loader/babel" with ' + '"react-hot-loader/webpack" in the "loaders" section of your Webpack configuration. ');
}
var t = args.types,
template = args.template;
var _options$safetyNet = options.safetyNet,
safetyNet = _options$safetyNet === undefined ? true : _options$safetyNet;
var buildRegistration = template('reactHotLoader.register(ID, NAME, FILENAME);', templateOptions);
var signatureHeader = template('var __signature__ = typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.default.signature : function (a) {return a;}', templateOptions);
var headerTemplate = template('(function () {\n var enterModule = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.enterModule : undefined);\n enterModule && enterModule(module);\n }())', templateOptions);
var footerTemplate = template('(function () {\n var leaveModule = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.leaveModule : undefined);\n leaveModule && leaveModule(module);\n }())', templateOptions);
var evalTemplate = template('this[key]=eval(code);', templateOptions);
// We're making the IIFE we insert at the end of the file an unused variable
// because it otherwise breaks the output of the babel-node REPL (#359).
var buildTagger = template(' \n(function () { \n \n var reactHotLoader = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.default : undefined);\n \n if (!reactHotLoader) {\n return;\n }\n\n REGISTRATIONS \n}());\n ', templateOptions);
// Gather top-level variables, functions, and classes.
// Try our best to avoid variables from require().
// Ideally we only want to find components defined by the user.
function shouldRegisterBinding(binding) {
var _binding$path = binding.path,
type = _binding$path.type,
node = _binding$path.node,
parent = _binding$path.parent;
switch (type) {
case 'FunctionDeclaration':
case 'ClassDeclaration':
case 'VariableDeclaration':
return true;
case 'VariableDeclarator':
{
var init = node.init;
if (t.isCallExpression(init) && init.callee.name === 'require') {
return false;
}
if (parent.declare) {
return false;
}
return true;
}
default:
return false;
}
}
var REGISTRATIONS = Symbol('registrations');
return {
visitor: {
ExportDefaultDeclaration: function ExportDefaultDeclaration(path, state) {
var file = state.file;
// Default exports with names are going
// to be in scope anyway so no need to bother.
if (path.node.declaration.id) {
return;
}
// Move export default right hand side to a variable
// so we can later refer to it and tag it with __source.
var id = path.scope.generateUidIdentifier('default');
var expression = t.isExpression(path.node.declaration) ? path.node.declaration : t.toExpression(path.node.declaration);
path.scope.registerDeclaration(path.insertBefore(t.variableDeclaration('const', [t.variableDeclarator(id, expression)]))[0]);
path.node.declaration = id; // eslint-disable-line no-param-reassign
// It won't appear in scope.bindings
// so we'll manually remember it exists.
state[REGISTRATIONS].push(buildRegistration({
ID: id,
NAME: t.stringLiteral('default'),
FILENAME: t.stringLiteral(file.opts.filename)
}));
},
Program: {
enter: function enter(_ref, state) {
var scope = _ref.scope,
node = _ref.node;
var file = state.file;
state[REGISTRATIONS] = []; // eslint-disable-line no-param-reassign
node.body.unshift(signatureHeader());
// Everything in the top level scope, when reasonable,
// is going to get tagged with __source.
/* eslint-disable guard-for-in,no-restricted-syntax */
for (var id in scope.bindings) {
var binding = scope.bindings[id];
if (shouldRegisterBinding(binding)) {
state[REGISTRATIONS].push(buildRegistration({
ID: binding.identifier,
NAME: t.stringLiteral(id),
FILENAME: t.stringLiteral(file.opts.filename)
}));
}
}
/* eslint-enable */
},
exit: function exit(_ref2, state) {
var node = _ref2.node;
var file = state.file;
var registrations = state[REGISTRATIONS];
state[REGISTRATIONS] = [];
// inject the code only if applicable
if (registrations && registrations.length && !shouldIgnoreFile(file.opts.filename)) {
if (safetyNet) {
node.body.unshift(headerTemplate());
}
// Inject the generated tagging code at the very end
// so that it is as minimally intrusive as possible.
node.body.push(t.emptyStatement());
node.body.push(buildTagger({ REGISTRATIONS: registrations }));
node.body.push(t.emptyStatement());
if (safetyNet) {
node.body.push(footerTemplate());
}
}
}
},
Class: function Class(classPath) {
var classBody = classPath.get('body');
var hasRegenerateMethod = false;
var hasMethods = false;
classBody.get('body').forEach(function (path) {
var node = path.node;
// don't apply transform to static class properties
if (node.static) {
return;
}
if (node.key.name !== REGENERATE_METHOD) {
hasMethods = true;
} else {
hasRegenerateMethod = true;
}
});
if (hasMethods && !hasRegenerateMethod) {
var regenerateMethod = t.classMethod('method', t.identifier(REGENERATE_METHOD), [t.identifier('key'), t.identifier('code')], t.blockStatement([evalTemplate()]));
classBody.pushContainer('body', regenerateMethod);
classBody.get('body').forEach(function (path) {
var node = path.node;
if (node.key.name === REGENERATE_METHOD) {
path.addComment('leading', ' @ts-ignore', true);
path.get('body').get('body')[0].addComment('leading', ' @ts-ignore', true);
}
});
}
}
}
};
}
var mergeRecord = function mergeRecord(sourceRecord, newRecord) {
Object.keys(newRecord).forEach(function (key) {
var action = newRecord[key];
if (typeof action === 'function') {
if (!sourceRecord[key]) {
sourceRecord[key] = function () {
return {};
};
}
var prev = sourceRecord[key];
sourceRecord[key] = function () {
prev.apply(undefined, arguments);
action.apply(undefined, arguments);
};
} else if ((typeof action === 'undefined' ? 'undefined' : _typeof(action)) === 'object') {
if (!sourceRecord[key]) {
sourceRecord[key] = {};
}
mergeRecord(sourceRecord[key], action);
}
});
};
var composePlugins = function composePlugins(plugins) {
return function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var result = {};
plugins.forEach(function (creator) {
var plugin = creator.apply(undefined, args);
mergeRecord(result, plugin);
});
return result;
};
};
module.exports = composePlugins([plugin, function () {
var p = fresh.apply(undefined, arguments);
// removing everything we dont want right now
// registration
// delete p.visitor.Program;
// delete p.visitor.Program.exit;
// registrations
// delete p.visitor.FunctionDeclaration.enter;
// delete p.visitor.FunctionDeclaration.leave;
// delete p.visitor.VariableDeclaration;
return p;
}]);
module.exports.shouldIgnoreFile = shouldIgnoreFile;