function hash(str) {
  let hash = 5381,
    i = str.length;
  while (i) {
    hash = (hash * 33) ^ str.charCodeAt(--i);
  }

  return (hash >>> 0).toString(36);
}

/**
 *
 * Majority copied from https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js
 */
module.exports=function (babel, opts = {}) {
  if (typeof babel.env === 'function') {
    const env = babel.env();
    if (env !== 'development' && !opts.skipEnvCheck) {
      throw new Error(
        'Prefresh Babel transform should only be enabled in development environment. ' +
          'Instead, the environment is: "' +
          env +
          '". If you want to override this check, pass {skipEnvCheck: true} as plugin options.'
      );
    }
  }

  const { types: t, template } = babel;
  const refreshReg = t.identifier(opts.refreshReg || '$RefreshReg$');
  const refreshSig = t.identifier(opts.refreshSig || '$RefreshSig$');

  const registrationsByProgramPath = new Map();
  function createRegistration(programPath, persistentID) {
    const handle = programPath.scope.generateUidIdentifier('c');
    if (!registrationsByProgramPath.has(programPath)) {
      registrationsByProgramPath.set(programPath, []);
    }

    const registrations = registrationsByProgramPath.get(programPath);
    registrations.push({
      handle,
      persistentID,
    });

    return handle;
  }

  function isComponentishName(name) {
    return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z';
  }

  function findInnerComponents(inferredName, path, callback) {
    const 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() {}
        // function Foo() {}
        // module.exports=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': {
        const argsPath = path.get('arguments');
        if (argsPath === undefined || argsPath.length === 0) {
          return false;
        }
        const calleePath = path.get('callee');
        switch (calleePath.node.type) {
          case 'CallExpression':
          case 'MemberExpression':
          case 'Identifier': {
            const calleeSource = calleePath.getSource().split('(')[0];
            const innerName = inferredName + '$' + calleeSource;
            const foundInside = argsPath.some(argPath =>
              findInnerComponents(innerName, argPath, 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': {
        const init = node.init;
        if (init === null) {
          return false;
        }
        const name = node.id.name;
        if (!isComponentishName(name)) {
          return false;
        }
        switch (init.type) {
          case 'ArrowFunctionExpression':
          case 'FunctionExpression':
            // Likely component definitions.
            break;
          case 'CallExpression': {
            // Maybe a HOC.
            // Try to determine if this is some form of import.
            const callee = init.callee;
            const calleeType = callee.type;
            if (calleeType === 'Import') {
              return false;
            } else if (calleeType === 'Identifier') {
              if (callee.name.indexOf('require') === 0) {
                return false;
              } else if (callee.name.indexOf('import') === 0) {
                return false;
              }
              // Neither require nor import. Might be a HOC.
              // Pass through.
            } else if (calleeType === 'MemberExpression') {
              // Could be something like React.forwardRef(...)
              // Pass through.
            }
            break;
          }
          case 'TaggedTemplateExpression':
            // Maybe something like styled.div`...`
            break;
          default:
            return false;
        }
        const initPath = path.get('init');
        const foundInside = findInnerComponents(
          inferredName,
          initPath,
          callback
        );
        if (foundInside) {
          return true;
        }
        // See if this identifier is used in JSX. Then it's a component.
        const binding = path.scope.getBinding(name);
        if (binding === undefined) {
          return;
        }
        let isLikelyUsedAsType = false;
        const referencePaths = binding.referencePaths;
        for (let i = 0; i < referencePaths.length; i++) {
          const ref = referencePaths[i];
          if (
            ref.node &&
            ref.node.type !== 'JSXIdentifier' &&
            ref.node.type !== 'Identifier'
          ) {
            continue;
          }
          const refParent = ref.parent;
          if (refParent.type === 'JSXOpeningElement') {
            isLikelyUsedAsType = true;
          } else if (refParent.type === 'CallExpression') {
            const callee = refParent.callee;
            let fnName;
            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 'useErrorBoundary':
      case 'React.useErrorBoundary':
      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 getHookCallsSignature(functionNode) {
    const fnHookCalls = hookCalls.get(functionNode);
    if (fnHookCalls === undefined) {
      return null;
    }
    return {
      key: fnHookCalls.map(call => call.name + '{' + call.key + '}').join('\n'),
      customHooks: fnHookCalls
        .filter(call => !isBuiltinHook(call.name))
        .map(call => t.cloneDeep(call.callee)),
    };
  }

  const hasForceResetCommentByFile = new WeakMap();

  // We let user do /* @refresh reset */ to reset state in the whole file.
  function hasForceResetComment(path) {
    const file = path.hub.file;
    let hasForceReset = hasForceResetCommentByFile.get(file);
    if (hasForceReset !== undefined) {
      return hasForceReset;
    }

    hasForceReset = false;
    const comments = file.ast.comments;
    for (let i = 0; i < comments.length; i++) {
      const cmt = comments[i];
      if (cmt.value.indexOf('@refresh reset') !== -1) {
        hasForceReset = true;
        break;
      }
    }

    hasForceResetCommentByFile.set(file, hasForceReset);
    return hasForceReset;
  }

  function createArgumentsForSignature(node, signature, scope) {
    const { key, customHooks } = signature;

    let forceReset = hasForceResetComment(scope.path);
    const customHooksInScope = [];
    customHooks.forEach(callee => {
      // Check if a corresponding binding exists where we emit the signature.
      let bindingName;
      switch (callee.type) {
        case 'MemberExpression':
          if (callee.object.type === 'Identifier') {
            bindingName = callee.object.name;
          }
          break;
        case 'Identifier':
          bindingName = callee.name;
          break;
      }
      if (scope.hasBinding(bindingName)) {
        customHooksInScope.push(callee);
      } else {
        // We don't have anything to put in the array because Hook is out of scope.
        // Since it could potentially have been edited, remount the component.
        forceReset = true;
      }
    });

    let finalKey = key;
    if (typeof require === 'function' && !opts.emitFullSignatures) {
      // Prefer to hash when we can (e.g. outside of ASTExplorer).
      // This makes it deterministically compact, even if there's
      // e.g. a useState initializer with some code inside.
      // We also need it for www that has transforms like cx()
      // that don't understand if something is part of a string.
      finalKey = require('crypto')
        .createHash('sha1')
        .update(key)
        .digest('base64');
    }

    const args = [node, t.stringLiteral(finalKey)];
    if (forceReset || customHooksInScope.length > 0) {
      args.push(t.booleanLiteral(forceReset));
    }
    if (customHooksInScope.length > 0) {
      args.push(
        // TODO: We could use an arrow here to be more compact.
        // However, don't do it until AMA can run them natively.
        t.functionExpression(
          null,
          [],
          t.blockStatement([
            t.returnStatement(t.arrayExpression(customHooksInScope)),
          ])
        )
      );
    }
    return args;
  }

  const seenForRegistration = new WeakSet();
  const seenForSignature = new WeakSet();
  const seenForOutro = new WeakSet();

  const hookCalls = new WeakMap();
  const HookCallsVisitor = {
    CallExpression(path) {
      const node = path.node;
      const 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.

      let 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;
      }
      const fnScope = path.scope.getFunctionParent();
      if (fnScope === null) {
        return;
      }

      // This is a Hook call. Record it.
      const fnNode = fnScope.block;
      if (!hookCalls.has(fnNode)) {
        hookCalls.set(fnNode, []);
      }
      const hookCallsForFn = hookCalls.get(fnNode);
      let 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.
      const args = path.get('arguments');
      if (name === 'useState' && args.length > 0) {
        // useState first 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,
        key,
      });
    },
  };

  const createContextTemplate = template(
    `
    Object.assign((CREATECONTEXT[IDENT] || (CREATECONTEXT[IDENT]=CREATECONTEXT(VALUE))), {__:VALUE});
  `,
    { placeholderPattern: /^[A-Z]+$/ }
  );

  const emptyTemplate = template(`
    (CREATECONTEXT[IDENT] || (CREATECONTEXT[IDENT]=CREATECONTEXT()));
	`);

  const getFirstNonTsExpression = expression =>
    expression.type === 'TSAsExpression'
      ? getFirstNonTsExpression(expression.expression)
      : expression;

  return {
    visitor: {
      ClassDeclaration: {
        enter(path) {
          const node = path.node;
          let programPath;
          let insertAfterPath;
          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;
          }
          const id = node.id;
          if (id === null) {
            // We don't currently handle anonymous default exports.
            return;
          }
          const 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.

          const handle = createRegistration(programPath, inferredName);
          insertAfterPath.insertAfter(
            t.expressionStatement(
              t.assignmentExpression('=', handle, path.node.id)
            )
          );
        },
      },
      CallExpression(path, state) {
        if (
          !path.get('callee').referencesImport('preact', 'createContext') &&
          !path.get('callee').referencesImport('react', 'createContext') &&
          !path.get('callee').referencesImport('preact/compat', 'createContext')
        )
          return;

        let id = '';
        if (t.isObjectProperty(path.parentPath)) {
          id += '__' + path.parent.key.name;
        } else if (t.isVariableDeclarator(path.parentPath)) {
          id += '$' + path.parent.id.name;
        } else if (t.isAssignmentExpression(path.parentPath)) {
          if (t.isIdentifier(path.parent.left)) {
            id += '_' + path.parent.left.name;
          } else {
            id += '_' + hash(path.parentPath.get('left').getSource());
          }
        }

        const getFirstParent = parentPath => {
          if (
            t.isProgram(parentPath) ||
            t.isFunctionDeclaration(parentPath) ||
            t.isArrowFunctionExpression(parentPath)
          )
            return parentPath;
          return getFirstParent(parentPath.parentPath);
        };

        const closestClosurePath = getFirstParent(path.parentPath);

        const contexts = state.get('contexts');
        let counter = (contexts.get(id) || -1) + 1;
        contexts.set(id, counter);
        if (counter) id += counter;
        id = '_' + state.get('filehash') + id;
        path.skip();
        if (!t.isProgram(closestClosurePath)) {
          const params = closestClosurePath.node.params;
          params.forEach(param => {
            if (t.isIdentifier(param)) {
              id += '_PARAM' + param.name;
            }
          });
        }

        // TODO: maybe wrap with JSON.stringify
        if (path.node.arguments[0]) {
          const [quasi, ...expressions] = id.split('_PARAM');
          const first = t.templateElement({ raw: quasi, cooked: '' });
          const expr = expressions.map(x => t.identifier(x.replace('}', '')));
          path.replaceWith(
            createContextTemplate({
              CREATECONTEXT: path.get('callee').node,
              IDENT: t.templateLiteral(
                [
                  first,
                  ...expressions.map(() =>
                    t.templateElement({ raw: '', cooked: '' }, true)
                  ),
                ],
                expr
              ),
              VALUE: t.clone(getFirstNonTsExpression(path.node.arguments[0])),
            })
          );
        } else {
          const [quasi, ...expressions] = id.split('_PARAM');
          const first = t.templateElement({ raw: quasi, cooked: '' });
          const expr = expressions.map(x => t.identifier(x.replace('}', '')));
          path.replaceWith(
            emptyTemplate({
              CREATECONTEXT: path.get('callee').node,
              IDENT: t.templateLiteral(
                [
                  first,
                  ...expressions.map(() =>
                    t.templateElement({ raw: '', cooked: '' }, true)
                  ),
                ],
                expr
              ),
            })
          );
        }
      },
      ExportDefaultDeclaration(path) {
        const node = path.node;
        const decl = node.declaration;
        const 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 module.exports=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() {})
        const inferredName = '%default%';
        const programPath = path.parentPath;
        findInnerComponents(
          inferredName,
          declPath,
          (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;
            }
            const handle = createRegistration(programPath, persistentID);
            targetPath.replaceWith(
              t.assignmentExpression('=', handle, targetExpr)
            );
          }
        );
      },
      FunctionDeclaration: {
        enter(path) {
          const node = path.node;
          let programPath;
          let insertAfterPath;
          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;
          }
          const id = node.id;
          if (id === null) {
            // We don't currently handle anonymous default exports.
            return;
          }
          const 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.

          // function Named() {}
          // function Named() {}
          findInnerComponents(
            inferredName,
            path,
            (persistentID, targetExpr) => {
              const handle = createRegistration(programPath, persistentID);
              insertAfterPath.insertAfter(
                t.expressionStatement(
                  t.assignmentExpression('=', handle, targetExpr)
                )
              );
            }
          );
        },
        exit(path) {
          const node = path.node;
          const id = node.id;
          if (id === null) {
            return;
          }
          const signature = getHookCallsSignature(node);
          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.

          const sigCallID = path.scope.generateUidIdentifier('_s');
          path.scope.parent.push({
            id: sigCallID,
            init: t.callExpression(refreshSig, []),
          });

          // The signature call is split in two parts. One part is called inside the function.
          // This is used to signal when first render happens.
          path
            .get('body')
            .unshiftContainer(
              'body',
              t.expressionStatement(t.callExpression(sigCallID, []))
            );

          // The second call is around the function itself.
          // This is used to associate a type with a signature.

          // Unlike with $RefreshReg$, this needs to work for nested
          // declarations too. So we need to search for a path where
          // we can insert a statement rather than hard coding it.
          let insertAfterPath = null;
          path.find(p => {
            if (p.parentPath.isBlock()) {
              insertAfterPath = p;
              return true;
            }
          });
          if (insertAfterPath === null) {
            return;
          }

          insertAfterPath.insertAfter(
            t.expressionStatement(
              t.callExpression(
                sigCallID,
                createArgumentsForSignature(
                  id,
                  signature,
                  insertAfterPath.scope
                )
              )
            )
          );
        },
      },
      'ArrowFunctionExpression|FunctionExpression': {
        exit(path) {
          const node = path.node;
          const signature = getHookCallsSignature(node);
          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.

          const sigCallID = path.scope.generateUidIdentifier('_s');
          path.scope.parent.push({
            id: sigCallID,
            init: t.callExpression(refreshSig, []),
          });

          // The signature call is split in two parts. One part is called inside the function.
          // This is used to signal when first render happens.
          if (path.node.body.type !== 'BlockStatement') {
            path.node.body = t.blockStatement([
              t.returnStatement(path.node.body),
            ]);
          }
          path
            .get('body')
            .unshiftContainer(
              'body',
              t.expressionStatement(t.callExpression(sigCallID, []))
            );

          // The second call is around the function itself.
          // This is used to associate a type with a signature.

          if (path.parent.type === 'VariableDeclarator') {
            let insertAfterPath = null;
            path.find(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(
                  sigCallID,
                  createArgumentsForSignature(
                    path.parent.id,
                    signature,
                    insertAfterPath.scope
                  )
                )
              )
            );
            // Result: let Foo = () => {}; __signature(Foo, ...);
          } else {
            // let Foo = hoc(() => {})
            path.replaceWith(
              t.callExpression(
                sigCallID,
                createArgumentsForSignature(node, signature, path.scope)
              )
            );
            // Result: let Foo = hoc(__signature(() => {}, ...))
          }
        },
      },
      VariableDeclaration(path) {
        const node = path.node;
        let programPath;
        let insertAfterPath;
        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;
        }

        // 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.

        const declPaths = path.get('declarations');
        if (declPaths.length !== 1) {
          return;
        }
        const declPath = declPaths[0];
        const inferredName = declPath.node.id.name;
        findInnerComponents(
          inferredName,
          declPath,
          (persistentID, targetExpr, targetPath) => {
            if (targetPath === null) {
              // For case like:
              // const Something = hoc(Foo)
              // we don't want to wrap Foo inside the call.
              // Instead we assume it's registered at definition.
              return;
            }
            const handle = createRegistration(programPath, persistentID);
            if (targetPath.parent.type === 'VariableDeclarator') {
              // Special case when a variable would get an inferred name:
              // let Foo = () => {}
              // let Foo = function() {}
              // let Foo = styled.div``;
              // We'll register it on next line so that
              // we don't mess up the inferred 'Foo' function name.
              // (eg: with @babel/plugin-transform-react-display-name or
              // babel-plugin-styled-components)
              insertAfterPath.insertAfter(
                t.expressionStatement(
                  t.assignmentExpression('=', handle, declPath.node.id)
                )
              );
              // Result: let Foo = () => {}; _c1 = Foo;
            } else {
              // let Foo = hoc(() => {})
              targetPath.replaceWith(
                t.assignmentExpression('=', handle, targetExpr)
              );
              // Result: let Foo = hoc(_c1 = () => {})
            }
          }
        );
      },
      Program: {
        enter(path, state) {
          state.set(
            'filehash',
            hash(
              path.hub.file.opts.filename ||
                path.hub.file.opts.sourceFileName ||
                path.hub.file.opts.generatorOpts?.sourceFileName ||
                'unnamed'
            )
          );
          state.set('contexts', new Map());
          // 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);
        },
        exit(path) {
          const registrations = registrationsByProgramPath.get(path);
          if (registrations === undefined) {
            return;
          }

          // Make sure we're not mutating the same tree twice.
          // This can happen if another Babel plugin replaces parents.
          const node = path.node;
          if (seenForOutro.has(node)) {
            return;
          }
          seenForOutro.add(node);
          // Don't mutate the tree above this point.

          registrationsByProgramPath.delete(path);
          const declarators = [];
          path.pushContainer('body', t.variableDeclaration('var', declarators));
          registrations.forEach(({ handle, persistentID }) => {
            path.pushContainer(
              'body',
              t.expressionStatement(
                t.callExpression(refreshReg, [
                  handle,
                  t.stringLiteral(persistentID),
                ])
              )
            );
            declarators.push(t.variableDeclarator(handle));
          });
        },
      },
    },
  };
}
