UNPKG

25.2 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5// the same as before
6var PREFIX = '__reactstandin__';
7var REGENERATE_METHOD = PREFIX + 'regenerateByEval';
8
9/**
10 * Copyright (c) Facebook, Inc. and its affiliates.
11 *
12 * This source code is licensed under the MIT license found in the
13 * LICENSE file in the root directory of this source tree.
14 */
15
16var SIGNATURE = '__signature__';
17
18function fresh (babel) {
19 var t = babel.types;
20
21
22 var registrationsByProgramPath = new Map();
23
24 function createRegistration(programPath, persistentID) {
25 var handle = programPath.scope.generateUidIdentifier('c');
26 if (!registrationsByProgramPath.has(programPath)) {
27 registrationsByProgramPath.set(programPath, []);
28 }
29 var registrations = registrationsByProgramPath.get(programPath);
30 registrations.push({
31 handle: handle,
32 persistentID: persistentID
33 });
34 return handle;
35 }
36
37 function isComponentishName(name) {
38 return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
39 }
40
41 function findInnerComponents(inferredName, path, callback) {
42 var node = path.node;
43 switch (node.type) {
44 case 'Identifier':
45 {
46 if (!isComponentishName(node.name)) {
47 return false;
48 }
49 // export default hoc(Foo)
50 // const X = hoc(Foo)
51 callback(inferredName, node, null);
52 return true;
53 }
54 case 'FunctionDeclaration':
55 {
56 // function Foo() {}
57 // export function Foo() {}
58 // export default function Foo() {}
59 callback(inferredName, node.id, null);
60 return true;
61 }
62 case 'ArrowFunctionExpression':
63 {
64 if (node.body.type === 'ArrowFunctionExpression') {
65 return false;
66 }
67 // let Foo = () => {}
68 // export default hoc1(hoc2(() => {}))
69 callback(inferredName, node, path);
70 return true;
71 }
72 case 'FunctionExpression':
73 {
74 // let Foo = function() {}
75 // const Foo = hoc1(forwardRef(function renderFoo() {}))
76 // export default memo(function() {})
77 callback(inferredName, node, path);
78 return true;
79 }
80 case 'CallExpression':
81 {
82 var argsPath = path.get('arguments');
83 if (argsPath === undefined || argsPath.length === 0) {
84 return false;
85 }
86 var calleePath = path.get('callee');
87 switch (calleePath.node.type) {
88 case 'MemberExpression':
89 case 'Identifier':
90 {
91 var calleeSource = calleePath.getSource();
92 var firstArgPath = argsPath[0];
93 var innerName = inferredName + '$' + calleeSource;
94 var foundInside = findInnerComponents(innerName, firstArgPath, callback);
95 if (!foundInside) {
96 return false;
97 }
98 // const Foo = hoc1(hoc2(() => {}))
99 // export default memo(React.forwardRef(function() {}))
100 callback(inferredName, node, path);
101 return true;
102 }
103 default:
104 {
105 return false;
106 }
107 }
108 }
109 case 'VariableDeclarator':
110 {
111 var init = node.init;
112 if (init === null) {
113 return false;
114 }
115 var name = node.id.name;
116 if (!isComponentishName(name)) {
117 return false;
118 }
119 if (init.type === 'Identifier' || init.type === 'MemberExpression') {
120 return false;
121 }
122 var initPath = path.get('init');
123 var _foundInside = findInnerComponents(inferredName, initPath, callback);
124 if (_foundInside) {
125 return true;
126 }
127 // See if this identifier is used in JSX. Then it's a component.
128 var binding = path.scope.getBinding(name);
129 if (binding === undefined) {
130 return;
131 }
132 var isLikelyUsedAsType = false;
133 var referencePaths = binding.referencePaths;
134 for (var i = 0; i < referencePaths.length; i++) {
135 var ref = referencePaths[i];
136 if (ref.node.type !== 'JSXIdentifier' && ref.node.type !== 'Identifier') {
137 continue;
138 }
139 var refParent = ref.parent;
140 if (refParent.type === 'JSXOpeningElement') {
141 isLikelyUsedAsType = true;
142 } else if (refParent.type === 'CallExpression') {
143 var callee = refParent.callee;
144 var fnName = void 0;
145 switch (callee.type) {
146 case 'Identifier':
147 fnName = callee.name;
148 break;
149 case 'MemberExpression':
150 fnName = callee.property.name;
151 break;
152 }
153 switch (fnName) {
154 case 'createElement':
155 case 'jsx':
156 case 'jsxDEV':
157 case 'jsxs':
158 isLikelyUsedAsType = true;
159 break;
160 }
161 }
162 if (isLikelyUsedAsType) {
163 // const X = ... + later <X />
164 callback(inferredName, init, initPath);
165 return true;
166 }
167 }
168 }
169 }
170 return false;
171 }
172
173 function isBuiltinHook(hookName) {
174 switch (hookName) {
175 case 'useState':
176 case 'React.useState':
177 case 'useReducer':
178 case 'React.useReducer':
179 case 'useEffect':
180 case 'React.useEffect':
181 case 'useLayoutEffect':
182 case 'React.useLayoutEffect':
183 case 'useMemo':
184 case 'React.useMemo':
185 case 'useCallback':
186 case 'React.useCallback':
187 case 'useRef':
188 case 'React.useRef':
189 case 'useContext':
190 case 'React.useContext':
191 case 'useImperativeMethods':
192 case 'React.useImperativeMethods':
193 case 'useDebugValue':
194 case 'React.useDebugValue':
195 return true;
196 default:
197 return false;
198 }
199 }
200
201 function getCalleeName(callee) {
202 if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
203 return callee.object.name;
204 }
205
206 return callee.name;
207 }
208
209 function getHookCallsSignature(functionNode, scope) {
210 var fnHookCalls = hookCalls.get(functionNode);
211 if (fnHookCalls === undefined) {
212 return null;
213 }
214 return {
215 key: fnHookCalls.map(function (call) {
216 return call.name + '{' + call.key + '}';
217 }).join('\n'),
218 customHooks: fnHookCalls.filter(function (call) {
219 return !isBuiltinHook(call.name);
220 }).map(function (call) {
221 return t.cloneDeep(call.callee);
222 })
223 };
224 }
225
226 function createArgumentsForSignature(node, signature, scope) {
227 var key = signature.key,
228 customHooks = signature.customHooks;
229
230 var args = [node, t.stringLiteral(key)];
231 var hooksInScope = customHooks.filter(function (call) {
232 return scope.hasBinding(getCalleeName(call));
233 });
234 if (hooksInScope.length > 0) {
235 args.push(t.arrowFunctionExpression([], t.arrayExpression(hooksInScope)));
236 }
237 return args;
238 }
239
240 var seenForRegistration = new WeakSet();
241 var seenForSignature = new WeakSet();
242
243 var hookCalls = new WeakMap();
244 var HookCallsVisitor = {
245 CallExpression: function CallExpression(path) {
246 var node = path.node;
247 var callee = node.callee;
248
249 // Note: this visitor MUST NOT mutate the tree in any way.
250 // It runs early in a separate traversal and should be very fast.
251
252 var name = null;
253 switch (callee.type) {
254 case 'Identifier':
255 name = callee.name;
256 break;
257 case 'MemberExpression':
258 name = callee.property.name;
259 break;
260 }
261 if (name === null || !/^use[A-Z]/.test(name)) {
262 return;
263 }
264 var fnScope = path.scope.getFunctionParent();
265
266 if (fnScope === null) {
267 return;
268 }
269
270 // This is a Hook call. Record it.
271 var fnNode = fnScope.block;
272 if (!hookCalls.has(fnNode)) {
273 hookCalls.set(fnNode, []);
274 }
275
276 var hookCallsForFn = hookCalls.get(fnNode);
277 var key = '';
278 if (path.parent.type === 'VariableDeclarator') {
279 // TODO: if there is no LHS, consider some other heuristic.
280 key = path.parentPath.get('id').getSource();
281 }
282
283 // Some built-in Hooks reset on edits to arguments.
284 var args = path.get('arguments');
285 if (name === 'useState' && args.length > 0) {
286 // useState second argument is initial state.
287 key += '(' + args[0].getSource() + ')';
288 } else if (name === 'useReducer' && args.length > 1) {
289 // useReducer second argument is initial state.
290 key += '(' + args[1].getSource() + ')';
291 }
292
293 hookCallsForFn.push({
294 callee: path.node.callee,
295 name: name,
296 key: key
297 });
298 }
299 };
300
301 return {
302 visitor: {
303 ExportDefaultDeclaration: function ExportDefaultDeclaration(path) {
304 var node = path.node;
305 var decl = node.declaration;
306 var declPath = path.get('declaration');
307 if (decl.type !== 'CallExpression') {
308 // For now, we only support possible HOC calls here.
309 // Named function declarations are handled in FunctionDeclaration.
310 // Anonymous direct exports like export default function() {}
311 // are currently ignored.
312 return;
313 }
314
315 // Make sure we're not mutating the same tree twice.
316 // This can happen if another Babel plugin replaces parents.
317 if (seenForRegistration.has(node)) {
318 return;
319 }
320 seenForRegistration.add(node);
321 // Don't mutate the tree above this point.
322
323 // This code path handles nested cases like:
324 // export default memo(() => {})
325 // In those cases it is more plausible people will omit names
326 // so they're worth handling despite possible false positives.
327 // More importantly, it handles the named case:
328 // export default memo(function Named() {})
329 var inferredName = '%default%';
330 var programPath = path.parentPath;
331 findInnerComponents(inferredName, declPath, function (persistentID, targetExpr, targetPath) {
332 if (targetPath === null) {
333 // For case like:
334 // export default hoc(Foo)
335 // we don't want to wrap Foo inside the call.
336 // Instead we assume it's registered at definition.
337 return;
338 }
339 var handle = createRegistration(programPath, persistentID);
340 targetPath.replaceWith(t.assignmentExpression('=', handle, targetExpr));
341 });
342 },
343
344 FunctionDeclaration: {
345 enter: function enter(path) {
346 return;
347 var node = path.node;
348 var programPath = void 0;
349 var insertAfterPath = void 0;
350 switch (path.parent.type) {
351 case 'Program':
352 insertAfterPath = path;
353 programPath = path.parentPath;
354 break;
355 case 'ExportNamedDeclaration':
356 insertAfterPath = path.parentPath;
357 programPath = insertAfterPath.parentPath;
358 break;
359 case 'ExportDefaultDeclaration':
360 insertAfterPath = path.parentPath;
361 programPath = insertAfterPath.parentPath;
362 break;
363 default:
364 return;
365 }
366 var id = node.id;
367 if (id === null) {
368 // We don't currently handle anonymous default exports.
369 return;
370 }
371 var inferredName = id.name;
372 if (!isComponentishName(inferredName)) {
373 return;
374 }
375
376 // Make sure we're not mutating the same tree twice.
377 // This can happen if another Babel plugin replaces parents.
378 if (seenForRegistration.has(node)) {
379 return;
380 }
381 seenForRegistration.add(node);
382 // Don't mutate the tree above this point.
383
384 // export function Named() {}
385 // function Named() {}
386 findInnerComponents(inferredName, path, function (persistentID, targetExpr) {
387 var handle = createRegistration(programPath, persistentID);
388 insertAfterPath.insertAfter(t.expressionStatement(t.assignmentExpression('=', handle, targetExpr)));
389 });
390 },
391 exit: function exit(path) {
392 //return;
393 var node = path.node;
394 var id = node.id;
395 if (id === null) {
396 return;
397 }
398 var signature = getHookCallsSignature(node, path.scope);
399 if (signature === null) {
400 return;
401 }
402
403 // Make sure we're not mutating the same tree twice.
404 // This can happen if another Babel plugin replaces parents.
405 if (seenForSignature.has(node)) {
406 return;
407 }
408 seenForSignature.add(node);
409
410 // Unlike with __register__, this needs to work for nested
411 // declarations too. So we need to search for a path where
412 // we can insert a statement rather than hardcoding it.
413 var insertAfterPath = null;
414 path.find(function (p) {
415 if (p.parentPath.isBlock()) {
416 insertAfterPath = p;
417 return true;
418 }
419 });
420 if (insertAfterPath === null) {
421 return;
422 }
423
424 insertAfterPath.insertAfter(t.expressionStatement(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(id, signature, insertAfterPath.scope))));
425 }
426 },
427 'ArrowFunctionExpression|FunctionExpression': {
428 exit: function exit(path) {
429 var node = path.node;
430 var signature = getHookCallsSignature(node, path.scope);
431 if (signature === null) {
432 return;
433 }
434
435 // Make sure we're not mutating the same tree twice.
436 // This can happen if another Babel plugin replaces parents.
437 if (seenForSignature.has(node)) {
438 return;
439 }
440 seenForSignature.add(node);
441 // Don't mutate the tree above this point.
442
443 if (path.parent.type === 'VariableDeclarator') {
444 var insertAfterPath = null;
445 path.find(function (p) {
446 if (p.parentPath.isBlock()) {
447 insertAfterPath = p;
448 return true;
449 }
450 });
451 if (insertAfterPath === null) {
452 return;
453 }
454 // Special case when a function would get an inferred name:
455 // let Foo = () => {}
456 // let Foo = function() {}
457 // We'll add signature it on next line so that
458 // we don't mess up the inferred 'Foo' function name.
459 insertAfterPath.insertAfter(t.expressionStatement(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(path.parent.id, signature, insertAfterPath.scope))));
460 // Result: let Foo = () => {}; __signature(Foo, ...);
461 } else {
462 // let Foo = hoc(() => {})
463 path.replaceWith(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(node, signature, path.scope)));
464 // Result: let Foo = hoc(__signature(() => {}, ...))
465 }
466 }
467 },
468 Program: {
469 enter: function enter(path) {
470 // This is a separate early visitor because we need to collect Hook calls
471 // and "const [foo, setFoo] = ..." signatures before the destructuring
472 // transform mangles them. This extra traversal is not ideal for perf,
473 // but it's the best we can do until we stop transpiling destructuring.
474 path.traverse(HookCallsVisitor);
475 }
476 }
477 }
478 };
479}
480
481var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
482 return typeof obj;
483} : function (obj) {
484 return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
485};
486
487var templateOptions = {
488 placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/
489};
490
491/* eslint-disable */
492var shouldIgnoreFile = function shouldIgnoreFile(file) {
493 return !!file.split('\\').join('/').match(/node_modules\/(react|react-dom|react-hot-loader)([\/]|$)/);
494};
495
496/* eslint-enable */
497
498function plugin(args) {
499 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
500
501 // This is a Babel plugin, but the user put it in the Webpack config.
502 if (this && this.callback) {
503 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. ');
504 }
505 var t = args.types,
506 template = args.template;
507 var _options$safetyNet = options.safetyNet,
508 safetyNet = _options$safetyNet === undefined ? true : _options$safetyNet;
509
510
511 var buildRegistration = template('reactHotLoader.register(ID, NAME, FILENAME);', templateOptions);
512
513 var signatureHeader = template('var __signature__ = typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.default.signature : function (a) {return a;}', templateOptions);
514
515 var headerTemplate = template('(function () {\n var enterModule = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.enterModule : undefined);\n enterModule && enterModule(module);\n }())', templateOptions);
516 var footerTemplate = template('(function () {\n var leaveModule = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.leaveModule : undefined);\n leaveModule && leaveModule(module);\n }())', templateOptions);
517 var evalTemplate = template('this[key]=eval(code);', templateOptions);
518
519 // We're making the IIFE we insert at the end of the file an unused variable
520 // because it otherwise breaks the output of the babel-node REPL (#359).
521
522 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);
523
524 // Gather top-level variables, functions, and classes.
525 // Try our best to avoid variables from require().
526 // Ideally we only want to find components defined by the user.
527 function shouldRegisterBinding(binding) {
528 var _binding$path = binding.path,
529 type = _binding$path.type,
530 node = _binding$path.node,
531 parent = _binding$path.parent;
532
533 switch (type) {
534 case 'FunctionDeclaration':
535 case 'ClassDeclaration':
536 case 'VariableDeclaration':
537 return true;
538 case 'VariableDeclarator':
539 {
540 var init = node.init;
541
542 if (t.isCallExpression(init) && init.callee.name === 'require') {
543 return false;
544 }
545 if (parent.declare) {
546 return false;
547 }
548 return true;
549 }
550 default:
551 return false;
552 }
553 }
554
555 var REGISTRATIONS = Symbol('registrations');
556 return {
557 visitor: {
558 ExportDefaultDeclaration: function ExportDefaultDeclaration(path, state) {
559 var file = state.file;
560 // Default exports with names are going
561 // to be in scope anyway so no need to bother.
562
563 if (path.node.declaration.id) {
564 return;
565 }
566
567 // Move export default right hand side to a variable
568 // so we can later refer to it and tag it with __source.
569 var id = path.scope.generateUidIdentifier('default');
570 var expression = t.isExpression(path.node.declaration) ? path.node.declaration : t.toExpression(path.node.declaration);
571 path.scope.registerDeclaration(path.insertBefore(t.variableDeclaration('const', [t.variableDeclarator(id, expression)]))[0]);
572 path.node.declaration = id; // eslint-disable-line no-param-reassign
573
574 // It won't appear in scope.bindings
575 // so we'll manually remember it exists.
576 state[REGISTRATIONS].push(buildRegistration({
577 ID: id,
578 NAME: t.stringLiteral('default'),
579 FILENAME: t.stringLiteral(file.opts.filename)
580 }));
581 },
582
583
584 Program: {
585 enter: function enter(_ref, state) {
586 var scope = _ref.scope,
587 node = _ref.node;
588 var file = state.file;
589
590 state[REGISTRATIONS] = []; // eslint-disable-line no-param-reassign
591
592 node.body.unshift(signatureHeader());
593
594 // Everything in the top level scope, when reasonable,
595 // is going to get tagged with __source.
596 /* eslint-disable guard-for-in,no-restricted-syntax */
597 for (var id in scope.bindings) {
598 var binding = scope.bindings[id];
599 if (shouldRegisterBinding(binding)) {
600 state[REGISTRATIONS].push(buildRegistration({
601 ID: binding.identifier,
602 NAME: t.stringLiteral(id),
603 FILENAME: t.stringLiteral(file.opts.filename)
604 }));
605 }
606 }
607 /* eslint-enable */
608 },
609 exit: function exit(_ref2, state) {
610 var node = _ref2.node;
611 var file = state.file;
612
613 var registrations = state[REGISTRATIONS];
614 state[REGISTRATIONS] = [];
615
616 // inject the code only if applicable
617 if (registrations && registrations.length && !shouldIgnoreFile(file.opts.filename)) {
618 if (safetyNet) {
619 node.body.unshift(headerTemplate());
620 }
621 // Inject the generated tagging code at the very end
622 // so that it is as minimally intrusive as possible.
623 node.body.push(t.emptyStatement());
624 node.body.push(buildTagger({ REGISTRATIONS: registrations }));
625 node.body.push(t.emptyStatement());
626
627 if (safetyNet) {
628 node.body.push(footerTemplate());
629 }
630 }
631 }
632 },
633 Class: function Class(classPath) {
634 var classBody = classPath.get('body');
635 var hasRegenerateMethod = false;
636 var hasMethods = false;
637
638 classBody.get('body').forEach(function (path) {
639 var node = path.node;
640
641 // don't apply transform to static class properties
642
643 if (node.static) {
644 return;
645 }
646
647 if (node.key.name !== REGENERATE_METHOD) {
648 hasMethods = true;
649 } else {
650 hasRegenerateMethod = true;
651 }
652 });
653
654 if (hasMethods && !hasRegenerateMethod) {
655 var regenerateMethod = t.classMethod('method', t.identifier(REGENERATE_METHOD), [t.identifier('key'), t.identifier('code')], t.blockStatement([evalTemplate()]));
656
657 classBody.pushContainer('body', regenerateMethod);
658
659 classBody.get('body').forEach(function (path) {
660 var node = path.node;
661
662
663 if (node.key.name === REGENERATE_METHOD) {
664 path.addComment('leading', ' @ts-ignore', true);
665 path.get('body').get('body')[0].addComment('leading', ' @ts-ignore', true);
666 }
667 });
668 }
669 }
670 }
671 };
672}
673
674var mergeRecord = function mergeRecord(sourceRecord, newRecord) {
675 Object.keys(newRecord).forEach(function (key) {
676 var action = newRecord[key];
677 if (typeof action === 'function') {
678 if (!sourceRecord[key]) {
679 sourceRecord[key] = function () {
680 return {};
681 };
682 }
683 var prev = sourceRecord[key];
684 sourceRecord[key] = function () {
685 prev.apply(undefined, arguments);
686 action.apply(undefined, arguments);
687 };
688 } else if ((typeof action === 'undefined' ? 'undefined' : _typeof(action)) === 'object') {
689 if (!sourceRecord[key]) {
690 sourceRecord[key] = {};
691 }
692 mergeRecord(sourceRecord[key], action);
693 }
694 });
695};
696
697var composePlugins = function composePlugins(plugins) {
698 return function () {
699 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
700 args[_key] = arguments[_key];
701 }
702
703 var result = {};
704 plugins.forEach(function (creator) {
705 var plugin = creator.apply(undefined, args);
706 mergeRecord(result, plugin);
707 });
708 return result;
709 };
710};
711
712module.exports = composePlugins([plugin, function () {
713 var p = fresh.apply(undefined, arguments);
714 // removing everything we dont want right now
715
716 // registration
717 // delete p.visitor.Program;
718 // delete p.visitor.Program.exit;
719
720 // registrations
721 // delete p.visitor.FunctionDeclaration.enter;
722 // delete p.visitor.FunctionDeclaration.leave;
723 // delete p.visitor.VariableDeclaration;
724
725 return p;
726}]);
727
728module.exports.shouldIgnoreFile = shouldIgnoreFile;