main
js 861 lines 27.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 'use strict';
9
10 export default function (babel, opts = {}) {
11 if (typeof babel.env === 'function') {
12 // Only available in Babel 7.
13 const env = babel.env();
14 if (env !== 'development' && !opts.skipEnvCheck) {
15 throw new Error(
16 'React Refresh Babel transform should only be enabled in development environment. ' +
17 'Instead, the environment is: "' +
18 env +
19 '". If you want to override this check, pass {skipEnvCheck: true} as plugin options.',
20 );
21 }
22 }
23
24 const {types: t} = babel;
25 const refreshReg = t.identifier(opts.refreshReg || '$RefreshReg$');
26 const refreshSig = t.identifier(opts.refreshSig || '$RefreshSig$');
27
28 const registrationsByProgramPath = new Map();
29 function createRegistration(programPath, persistentID) {
30 const handle = programPath.scope.generateUidIdentifier('c');
31 if (!registrationsByProgramPath.has(programPath)) {
32 registrationsByProgramPath.set(programPath, []);
33 }
34 const registrations = registrationsByProgramPath.get(programPath);
35 registrations.push({
36 handle,
37 persistentID,
38 });
39 return handle;
40 }
41
42 function isComponentishName(name) {
43 return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
44 }
45
46 function findInnerComponents(inferredName, path, callback) {
47 const node = path.node;
48 switch (node.type) {
49 case 'Identifier': {
50 if (!isComponentishName(node.name)) {
51 return false;
52 }
53 // export default hoc(Foo)
54 // const X = hoc(Foo)
55 callback(inferredName, node, null);
56 return true;
57 }
58 case 'FunctionDeclaration': {
59 // function Foo() {}
60 // export function Foo() {}
61 // export default function Foo() {}
62 callback(inferredName, node.id, null);
63 return true;
64 }
65 case 'ArrowFunctionExpression': {
66 if (node.body.type === 'ArrowFunctionExpression') {
67 return false;
68 }
69 // let Foo = () => {}
70 // export default hoc1(hoc2(() => {}))
71 callback(inferredName, node, path);
72 return true;
73 }
74 case 'FunctionExpression': {
75 // let Foo = function() {}
76 // const Foo = hoc1(forwardRef(function renderFoo() {}))
77 // export default memo(function() {})
78 callback(inferredName, node, path);
79 return true;
80 }
81 case 'CallExpression': {
82 const argsPath = path.get('arguments');
83 if (argsPath === undefined || argsPath.length === 0) {
84 return false;
85 }
86 const calleePath = path.get('callee');
87 switch (calleePath.node.type) {
88 case 'MemberExpression':
89 case 'Identifier': {
90 const calleeSource = calleePath.getSource();
91 const firstArgPath = argsPath[0];
92 const innerName = inferredName + '$' + calleeSource;
93 const foundInside = findInnerComponents(
94 innerName,
95 firstArgPath,
96 callback,
97 );
98 if (!foundInside) {
99 return false;
100 }
101 // const Foo = hoc1(hoc2(() => {}))
102 // export default memo(React.forwardRef(function() {}))
103 callback(inferredName, node, path);
104 return true;
105 }
106 default: {
107 return false;
108 }
109 }
110 }
111 case 'VariableDeclarator': {
112 const init = node.init;
113 if (init === null) {
114 return false;
115 }
116 const name = node.id.name;
117 if (!isComponentishName(name)) {
118 return false;
119 }
120 switch (init.type) {
121 case 'ArrowFunctionExpression':
122 case 'FunctionExpression':
123 // Likely component definitions.
124 break;
125 case 'CallExpression': {
126 // Maybe a HOC.
127 // Try to determine if this is some form of import.
128 const callee = init.callee;
129 const calleeType = callee.type;
130 if (calleeType === 'Import') {
131 return false;
132 } else if (calleeType === 'Identifier') {
133 if (callee.name.indexOf('require') === 0) {
134 return false;
135 } else if (callee.name.indexOf('import') === 0) {
136 return false;
137 }
138 // Neither require nor import. Might be a HOC.
139 // Pass through.
140 } else if (calleeType === 'MemberExpression') {
141 // Could be something like React.forwardRef(...)
142 // Pass through.
143 }
144 break;
145 }
146 case 'TaggedTemplateExpression':
147 // Maybe something like styled.div`...`
148 break;
149 default:
150 return false;
151 }
152 const initPath = path.get('init');
153 const foundInside = findInnerComponents(
154 inferredName,
155 initPath,
156 callback,
157 );
158 if (foundInside) {
159 return true;
160 }
161 // See if this identifier is used in JSX. Then it's a component.
162 const binding = path.scope.getBinding(name);
163 if (binding === undefined) {
164 return;
165 }
166 let isLikelyUsedAsType = false;
167 const referencePaths = binding.referencePaths;
168 for (let i = 0; i < referencePaths.length; i++) {
169 const ref = referencePaths[i];
170 if (
171 ref.node &&
172 ref.node.type !== 'JSXIdentifier' &&
173 ref.node.type !== 'Identifier'
174 ) {
175 continue;
176 }
177 const refParent = ref.parent;
178 if (refParent.type === 'JSXOpeningElement') {
179 isLikelyUsedAsType = true;
180 } else if (refParent.type === 'CallExpression') {
181 const callee = refParent.callee;
182 let fnName;
183 switch (callee.type) {
184 case 'Identifier':
185 fnName = callee.name;
186 break;
187 case 'MemberExpression':
188 fnName = callee.property.name;
189 break;
190 }
191 switch (fnName) {
192 case 'createElement':
193 case 'jsx':
194 case 'jsxDEV':
195 case 'jsxs':
196 isLikelyUsedAsType = true;
197 break;
198 }
199 }
200 if (isLikelyUsedAsType) {
201 // const X = ... + later <X />
202 callback(inferredName, init, initPath);
203 return true;
204 }
205 }
206 }
207 }
208 return false;
209 }
210
211 function isBuiltinHook(hookName) {
212 switch (hookName) {
213 case 'useState':
214 case 'React.useState':
215 case 'useReducer':
216 case 'React.useReducer':
217 case 'useEffect':
218 case 'React.useEffect':
219 case 'useLayoutEffect':
220 case 'React.useLayoutEffect':
221 case 'useMemo':
222 case 'React.useMemo':
223 case 'useCallback':
224 case 'React.useCallback':
225 case 'useRef':
226 case 'React.useRef':
227 case 'useContext':
228 case 'React.useContext':
229 case 'useImperativeHandle':
230 case 'React.useImperativeHandle':
231 case 'useDebugValue':
232 case 'React.useDebugValue':
233 case 'useId':
234 case 'React.useId':
235 case 'useDeferredValue':
236 case 'React.useDeferredValue':
237 case 'useTransition':
238 case 'React.useTransition':
239 case 'useInsertionEffect':
240 case 'React.useInsertionEffect':
241 case 'useSyncExternalStore':
242 case 'React.useSyncExternalStore':
243 case 'useFormStatus':
244 case 'React.useFormStatus':
245 case 'useFormState':
246 case 'React.useFormState':
247 case 'useActionState':
248 case 'React.useActionState':
249 case 'useOptimistic':
250 case 'React.useOptimistic':
251 return true;
252 default:
253 return false;
254 }
255 }
256
257 function getHookCallsSignature(functionNode) {
258 const fnHookCalls = hookCalls.get(functionNode);
259 if (fnHookCalls === undefined) {
260 return null;
261 }
262 return {
263 key: fnHookCalls.map(call => call.name + '{' + call.key + '}').join('\n'),
264 customHooks: fnHookCalls
265 .filter(call => !isBuiltinHook(call.name))
266 .map(call => t.cloneDeep(call.callee)),
267 };
268 }
269
270 const hasForceResetCommentByFile = new WeakMap();
271
272 // We let user do /* @refresh reset */ to reset state in the whole file.
273 function hasForceResetComment(path) {
274 const file = path.hub.file;
275 let hasForceReset = hasForceResetCommentByFile.get(file);
276 if (hasForceReset !== undefined) {
277 return hasForceReset;
278 }
279
280 hasForceReset = false;
281 const comments = file.ast.comments;
282 for (let i = 0; i < comments.length; i++) {
283 const cmt = comments[i];
284 if (cmt.value.indexOf('@refresh reset') !== -1) {
285 hasForceReset = true;
286 break;
287 }
288 }
289
290 hasForceResetCommentByFile.set(file, hasForceReset);
291 return hasForceReset;
292 }
293
294 function createArgumentsForSignature(node, signature, scope) {
295 const {key, customHooks} = signature;
296
297 let forceReset = hasForceResetComment(scope.path);
298 const customHooksInScope = [];
299 customHooks.forEach(callee => {
300 // Check if a corresponding binding exists where we emit the signature.
301 let bindingName;
302 switch (callee.type) {
303 case 'MemberExpression':
304 if (callee.object.type === 'Identifier') {
305 bindingName = callee.object.name;
306 }
307 break;
308 case 'Identifier':
309 bindingName = callee.name;
310 break;
311 }
312 if (scope.hasBinding(bindingName)) {
313 customHooksInScope.push(callee);
314 } else {
315 // We don't have anything to put in the array because Hook is out of scope.
316 // Since it could potentially have been edited, remount the component.
317 forceReset = true;
318 }
319 });
320
321 let finalKey = key;
322 if (typeof require === 'function' && !opts.emitFullSignatures) {
323 // Prefer to hash when we can (e.g. outside of ASTExplorer).
324 // This makes it deterministically compact, even if there's
325 // e.g. a useState initializer with some code inside.
326 // We also need it for www that has transforms like cx()
327 // that don't understand if something is part of a string.
328 finalKey = require('crypto')
329 .createHash('sha1')
330 .update(key)
331 .digest('base64');
332 }
333
334 const args = [node, t.stringLiteral(finalKey)];
335 if (forceReset || customHooksInScope.length > 0) {
336 args.push(t.booleanLiteral(forceReset));
337 }
338 if (customHooksInScope.length > 0) {
339 args.push(
340 // TODO: We could use an arrow here to be more compact.
341 // However, don't do it until AMA can run them natively.
342 t.functionExpression(
343 null,
344 [],
345 t.blockStatement([
346 t.returnStatement(t.arrayExpression(customHooksInScope)),
347 ]),
348 ),
349 );
350 }
351 return args;
352 }
353
354 function findHOCCallPathsAbove(path) {
355 const calls = [];
356 while (true) {
357 if (!path) {
358 return calls;
359 }
360 const parentPath = path.parentPath;
361 if (!parentPath) {
362 return calls;
363 }
364 if (
365 // hoc(_c = function() { })
366 parentPath.node.type === 'AssignmentExpression' &&
367 path.node === parentPath.node.right
368 ) {
369 // Ignore registrations.
370 path = parentPath;
371 continue;
372 }
373 if (
374 // hoc1(hoc2(...))
375 parentPath.node.type === 'CallExpression' &&
376 path.node !== parentPath.node.callee
377 ) {
378 calls.push(parentPath);
379 path = parentPath;
380 continue;
381 }
382 return calls; // Stop at other types.
383 }
384 }
385
386 const seenForRegistration = new WeakSet();
387 const seenForSignature = new WeakSet();
388 const seenForOutro = new WeakSet();
389
390 const hookCalls = new WeakMap();
391 const HookCallsVisitor = {
392 CallExpression(path) {
393 const node = path.node;
394 const callee = node.callee;
395
396 // Note: this visitor MUST NOT mutate the tree in any way.
397 // It runs early in a separate traversal and should be very fast.
398
399 let name = null;
400 switch (callee.type) {
401 case 'Identifier':
402 name = callee.name;
403 break;
404 case 'MemberExpression':
405 name = callee.property.name;
406 break;
407 }
408 if (name === null || !/^use[A-Z]/.test(name)) {
409 return;
410 }
411 const fnScope = path.scope.getFunctionParent();
412 if (fnScope === null) {
413 return;
414 }
415
416 // This is a Hook call. Record it.
417 const fnNode = fnScope.block;
418 if (!hookCalls.has(fnNode)) {
419 hookCalls.set(fnNode, []);
420 }
421 const hookCallsForFn = hookCalls.get(fnNode);
422 let key = '';
423 if (path.parent.type === 'VariableDeclarator') {
424 // TODO: if there is no LHS, consider some other heuristic.
425 key = path.parentPath.get('id').getSource();
426 }
427
428 // Some built-in Hooks reset on edits to arguments.
429 const args = path.get('arguments');
430 if (name === 'useState' && args.length > 0) {
431 // useState second argument is initial state.
432 key += '(' + args[0].getSource() + ')';
433 } else if (name === 'useReducer' && args.length > 1) {
434 // useReducer second argument is initial state.
435 key += '(' + args[1].getSource() + ')';
436 }
437
438 hookCallsForFn.push({
439 callee: path.node.callee,
440 name,
441 key,
442 });
443 },
444 };
445
446 return {
447 visitor: {
448 ExportDefaultDeclaration(path) {
449 const node = path.node;
450 const decl = node.declaration;
451 const declPath = path.get('declaration');
452 if (decl.type !== 'CallExpression') {
453 // For now, we only support possible HOC calls here.
454 // Named function declarations are handled in FunctionDeclaration.
455 // Anonymous direct exports like export default function() {}
456 // are currently ignored.
457 return;
458 }
459
460 // Make sure we're not mutating the same tree twice.
461 // This can happen if another Babel plugin replaces parents.
462 if (seenForRegistration.has(node)) {
463 return;
464 }
465 seenForRegistration.add(node);
466 // Don't mutate the tree above this point.
467
468 // This code path handles nested cases like:
469 // export default memo(() => {})
470 // In those cases it is more plausible people will omit names
471 // so they're worth handling despite possible false positives.
472 // More importantly, it handles the named case:
473 // export default memo(function Named() {})
474 const inferredName = '%default%';
475 const programPath = path.parentPath;
476 findInnerComponents(
477 inferredName,
478 declPath,
479 (persistentID, targetExpr, targetPath) => {
480 if (targetPath === null) {
481 // For case like:
482 // export default hoc(Foo)
483 // we don't want to wrap Foo inside the call.
484 // Instead we assume it's registered at definition.
485 return;
486 }
487 const handle = createRegistration(programPath, persistentID);
488 targetPath.replaceWith(
489 t.assignmentExpression('=', handle, targetExpr),
490 );
491 },
492 );
493 },
494 FunctionDeclaration: {
495 enter(path) {
496 const node = path.node;
497 let programPath;
498 let insertAfterPath;
499 let modulePrefix = '';
500 switch (path.parent.type) {
501 case 'Program':
502 insertAfterPath = path;
503 programPath = path.parentPath;
504 break;
505 case 'TSModuleBlock':
506 insertAfterPath = path;
507 programPath = insertAfterPath.parentPath.parentPath;
508 break;
509 case 'ExportNamedDeclaration':
510 insertAfterPath = path.parentPath;
511 programPath = insertAfterPath.parentPath;
512 break;
513 case 'ExportDefaultDeclaration':
514 insertAfterPath = path.parentPath;
515 programPath = insertAfterPath.parentPath;
516 break;
517 default:
518 return;
519 }
520
521 // These types can be nested in typescript namespace
522 // We need to find the export chain
523 // Or return if it stays local
524 if (
525 path.parent.type === 'TSModuleBlock' ||
526 path.parent.type === 'ExportNamedDeclaration'
527 ) {
528 while (programPath.type !== 'Program') {
529 if (programPath.type === 'TSModuleDeclaration') {
530 if (
531 programPath.parentPath.type !== 'Program' &&
532 programPath.parentPath.type !== 'ExportNamedDeclaration'
533 ) {
534 return;
535 }
536 modulePrefix = programPath.node.id.name + '$' + modulePrefix;
537 }
538 programPath = programPath.parentPath;
539 }
540 }
541
542 const id = node.id;
543 if (id === null) {
544 // We don't currently handle anonymous default exports.
545 return;
546 }
547 const inferredName = id.name;
548 if (!isComponentishName(inferredName)) {
549 return;
550 }
551
552 // Make sure we're not mutating the same tree twice.
553 // This can happen if another Babel plugin replaces parents.
554 if (seenForRegistration.has(node)) {
555 return;
556 }
557 seenForRegistration.add(node);
558 // Don't mutate the tree above this point.
559
560 const innerName = modulePrefix + inferredName;
561 // export function Named() {}
562 // function Named() {}
563 findInnerComponents(innerName, path, (persistentID, targetExpr) => {
564 const handle = createRegistration(programPath, persistentID);
565 insertAfterPath.insertAfter(
566 t.expressionStatement(
567 t.assignmentExpression('=', handle, targetExpr),
568 ),
569 );
570 });
571 },
572 exit(path) {
573 const node = path.node;
574 const id = node.id;
575 if (id === null) {
576 return;
577 }
578 const signature = getHookCallsSignature(node);
579 if (signature === null) {
580 return;
581 }
582
583 // Make sure we're not mutating the same tree twice.
584 // This can happen if another Babel plugin replaces parents.
585 if (seenForSignature.has(node)) {
586 return;
587 }
588 seenForSignature.add(node);
589 // Don't mutate the tree above this point.
590
591 const sigCallID = path.scope.generateUidIdentifier('_s');
592 path.scope.parent.push({
593 id: sigCallID,
594 init: t.callExpression(refreshSig, []),
595 });
596
597 // The signature call is split in two parts. One part is called inside the function.
598 // This is used to signal when first render happens.
599 path
600 .get('body')
601 .unshiftContainer(
602 'body',
603 t.expressionStatement(t.callExpression(sigCallID, [])),
604 );
605
606 // The second call is around the function itself.
607 // This is used to associate a type with a signature.
608
609 // Unlike with $RefreshReg$, this needs to work for nested
610 // declarations too. So we need to search for a path where
611 // we can insert a statement rather than hard coding it.
612 let insertAfterPath = null;
613 path.find(p => {
614 if (p.parentPath.isBlock()) {
615 insertAfterPath = p;
616 return true;
617 }
618 });
619 if (insertAfterPath === null) {
620 return;
621 }
622
623 insertAfterPath.insertAfter(
624 t.expressionStatement(
625 t.callExpression(
626 sigCallID,
627 createArgumentsForSignature(
628 id,
629 signature,
630 insertAfterPath.scope,
631 ),
632 ),
633 ),
634 );
635 },
636 },
637 'ArrowFunctionExpression|FunctionExpression': {
638 exit(path) {
639 const node = path.node;
640 const signature = getHookCallsSignature(node);
641 if (signature === null) {
642 return;
643 }
644
645 // Make sure we're not mutating the same tree twice.
646 // This can happen if another Babel plugin replaces parents.
647 if (seenForSignature.has(node)) {
648 return;
649 }
650 seenForSignature.add(node);
651 // Don't mutate the tree above this point.
652
653 const sigCallID = path.scope.generateUidIdentifier('_s');
654 path.scope.parent.push({
655 id: sigCallID,
656 init: t.callExpression(refreshSig, []),
657 });
658
659 // The signature call is split in two parts. One part is called inside the function.
660 // This is used to signal when first render happens.
661 if (path.node.body.type !== 'BlockStatement') {
662 path.node.body = t.blockStatement([
663 t.returnStatement(path.node.body),
664 ]);
665 }
666 path
667 .get('body')
668 .unshiftContainer(
669 'body',
670 t.expressionStatement(t.callExpression(sigCallID, [])),
671 );
672
673 // The second call is around the function itself.
674 // This is used to associate a type with a signature.
675
676 if (path.parent.type === 'VariableDeclarator') {
677 let insertAfterPath = null;
678 path.find(p => {
679 if (p.parentPath.isBlock()) {
680 insertAfterPath = p;
681 return true;
682 }
683 });
684 if (insertAfterPath === null) {
685 return;
686 }
687 // Special case when a function would get an inferred name:
688 // let Foo = () => {}
689 // let Foo = function() {}
690 // We'll add signature it on next line so that
691 // we don't mess up the inferred 'Foo' function name.
692 insertAfterPath.insertAfter(
693 t.expressionStatement(
694 t.callExpression(
695 sigCallID,
696 createArgumentsForSignature(
697 path.parent.id,
698 signature,
699 insertAfterPath.scope,
700 ),
701 ),
702 ),
703 );
704 // Result: let Foo = () => {}; __signature(Foo, ...);
705 } else {
706 // let Foo = hoc(() => {})
707 const paths = [path, ...findHOCCallPathsAbove(path)];
708 paths.forEach(p => {
709 p.replaceWith(
710 t.callExpression(
711 sigCallID,
712 createArgumentsForSignature(p.node, signature, p.scope),
713 ),
714 );
715 });
716 // Result: let Foo = __signature(hoc(__signature(() => {}, ...)), ...)
717 }
718 },
719 },
720 VariableDeclaration(path) {
721 const node = path.node;
722 let programPath;
723 let insertAfterPath;
724 let modulePrefix = '';
725 switch (path.parent.type) {
726 case 'Program':
727 insertAfterPath = path;
728 programPath = path.parentPath;
729 break;
730 case 'TSModuleBlock':
731 insertAfterPath = path;
732 programPath = insertAfterPath.parentPath.parentPath;
733 break;
734 case 'ExportNamedDeclaration':
735 insertAfterPath = path.parentPath;
736 programPath = insertAfterPath.parentPath;
737 break;
738 case 'ExportDefaultDeclaration':
739 insertAfterPath = path.parentPath;
740 programPath = insertAfterPath.parentPath;
741 break;
742 default:
743 return;
744 }
745
746 // These types can be nested in typescript namespace
747 // We need to find the export chain
748 // Or return if it stays local
749 if (
750 path.parent.type === 'TSModuleBlock' ||
751 path.parent.type === 'ExportNamedDeclaration'
752 ) {
753 while (programPath.type !== 'Program') {
754 if (programPath.type === 'TSModuleDeclaration') {
755 if (
756 programPath.parentPath.type !== 'Program' &&
757 programPath.parentPath.type !== 'ExportNamedDeclaration'
758 ) {
759 return;
760 }
761 modulePrefix = programPath.node.id.name + '$' + modulePrefix;
762 }
763 programPath = programPath.parentPath;
764 }
765 }
766
767 // Make sure we're not mutating the same tree twice.
768 // This can happen if another Babel plugin replaces parents.
769 if (seenForRegistration.has(node)) {
770 return;
771 }
772 seenForRegistration.add(node);
773 // Don't mutate the tree above this point.
774
775 const declPaths = path.get('declarations');
776 if (declPaths.length !== 1) {
777 return;
778 }
779 const declPath = declPaths[0];
780 const inferredName = declPath.node.id.name;
781 const innerName = modulePrefix + inferredName;
782 findInnerComponents(
783 innerName,
784 declPath,
785 (persistentID, targetExpr, targetPath) => {
786 if (targetPath === null) {
787 // For case like:
788 // export const Something = hoc(Foo)
789 // we don't want to wrap Foo inside the call.
790 // Instead we assume it's registered at definition.
791 return;
792 }
793 const handle = createRegistration(programPath, persistentID);
794 if (targetPath.parent.type === 'VariableDeclarator') {
795 // Special case when a variable would get an inferred name:
796 // let Foo = () => {}
797 // let Foo = function() {}
798 // let Foo = styled.div``;
799 // We'll register it on next line so that
800 // we don't mess up the inferred 'Foo' function name.
801 // (eg: with @babel/plugin-transform-react-display-name or
802 // babel-plugin-styled-components)
803 insertAfterPath.insertAfter(
804 t.expressionStatement(
805 t.assignmentExpression('=', handle, declPath.node.id),
806 ),
807 );
808 // Result: let Foo = () => {}; _c1 = Foo;
809 } else {
810 // let Foo = hoc(() => {})
811 targetPath.replaceWith(
812 t.assignmentExpression('=', handle, targetExpr),
813 );
814 // Result: let Foo = hoc(_c1 = () => {})
815 }
816 },
817 );
818 },
819 Program: {
820 enter(path) {
821 // This is a separate early visitor because we need to collect Hook calls
822 // and "const [foo, setFoo] = ..." signatures before the destructuring
823 // transform mangles them. This extra traversal is not ideal for perf,
824 // but it's the best we can do until we stop transpiling destructuring.
825 path.traverse(HookCallsVisitor);
826 },
827 exit(path) {
828 const registrations = registrationsByProgramPath.get(path);
829 if (registrations === undefined) {
830 return;
831 }
832
833 // Make sure we're not mutating the same tree twice.
834 // This can happen if another Babel plugin replaces parents.
835 const node = path.node;
836 if (seenForOutro.has(node)) {
837 return;
838 }
839 seenForOutro.add(node);
840 // Don't mutate the tree above this point.
841
842 registrationsByProgramPath.delete(path);
843 const declarators = [];
844 path.pushContainer('body', t.variableDeclaration('var', declarators));
845 registrations.forEach(({handle, persistentID}) => {
846 path.pushContainer(
847 'body',
848 t.expressionStatement(
849 t.callExpression(refreshReg, [
850 handle,
851 t.stringLiteral(persistentID),
852 ]),
853 ),
854 );
855 declarators.push(t.variableDeclarator(handle));
856 });
857 },
858 },
859 },
860 };
861 }