'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 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;