'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

// the same as before
var PREFIX = '__reactstandin__';
var REGENERATE_METHOD = PREFIX + 'regenerateByEval';

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

var SIGNATURE = '__signature__';

function fresh (babel) {
  var t = babel.types;


  var registrationsByProgramPath = new Map();

  function createRegistration(programPath, persistentID) {
    var handle = programPath.scope.generateUidIdentifier('c');
    if (!registrationsByProgramPath.has(programPath)) {
      registrationsByProgramPath.set(programPath, []);
    }
    var registrations = registrationsByProgramPath.get(programPath);
    registrations.push({
      handle: handle,
      persistentID: persistentID
    });
    return handle;
  }

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

  function findInnerComponents(inferredName, path, callback) {
    var node = path.node;
    switch (node.type) {
      case 'Identifier':
        {
          if (!isComponentishName(node.name)) {
            return false;
          }
          // export default hoc(Foo)
          // const X = hoc(Foo)
          callback(inferredName, node, null);
          return true;
        }
      case 'FunctionDeclaration':
        {
          // function Foo() {}
          // export function Foo() {}
          // export default function Foo() {}
          callback(inferredName, node.id, null);
          return true;
        }
      case 'ArrowFunctionExpression':
        {
          if (node.body.type === 'ArrowFunctionExpression') {
            return false;
          }
          // let Foo = () => {}
          // export default hoc1(hoc2(() => {}))
          callback(inferredName, node, path);
          return true;
        }
      case 'FunctionExpression':
        {
          // let Foo = function() {}
          // const Foo = hoc1(forwardRef(function renderFoo() {}))
          // export default memo(function() {})
          callback(inferredName, node, path);
          return true;
        }
      case 'CallExpression':
        {
          var argsPath = path.get('arguments');
          if (argsPath === undefined || argsPath.length === 0) {
            return false;
          }
          var calleePath = path.get('callee');
          switch (calleePath.node.type) {
            case 'MemberExpression':
            case 'Identifier':
              {
                var calleeSource = calleePath.getSource();
                var firstArgPath = argsPath[0];
                var innerName = inferredName + '$' + calleeSource;
                var foundInside = findInnerComponents(innerName, firstArgPath, callback);
                if (!foundInside) {
                  return false;
                }
                // const Foo = hoc1(hoc2(() => {}))
                // export default memo(React.forwardRef(function() {}))
                callback(inferredName, node, path);
                return true;
              }
            default:
              {
                return false;
              }
          }
        }
      case 'VariableDeclarator':
        {
          var init = node.init;
          if (init === null) {
            return false;
          }
          var name = node.id.name;
          if (!isComponentishName(name)) {
            return false;
          }
          if (init.type === 'Identifier' || init.type === 'MemberExpression') {
            return false;
          }
          var initPath = path.get('init');
          var _foundInside = findInnerComponents(inferredName, initPath, callback);
          if (_foundInside) {
            return true;
          }
          // See if this identifier is used in JSX. Then it's a component.
          var binding = path.scope.getBinding(name);
          if (binding === undefined) {
            return;
          }
          var isLikelyUsedAsType = false;
          var referencePaths = binding.referencePaths;
          for (var i = 0; i < referencePaths.length; i++) {
            var ref = referencePaths[i];
            if (ref.node.type !== 'JSXIdentifier' && ref.node.type !== 'Identifier') {
              continue;
            }
            var refParent = ref.parent;
            if (refParent.type === 'JSXOpeningElement') {
              isLikelyUsedAsType = true;
            } else if (refParent.type === 'CallExpression') {
              var callee = refParent.callee;
              var fnName = void 0;
              switch (callee.type) {
                case 'Identifier':
                  fnName = callee.name;
                  break;
                case 'MemberExpression':
                  fnName = callee.property.name;
                  break;
              }
              switch (fnName) {
                case 'createElement':
                case 'jsx':
                case 'jsxDEV':
                case 'jsxs':
                  isLikelyUsedAsType = true;
                  break;
              }
            }
            if (isLikelyUsedAsType) {
              // const X = ... + later <X />
              callback(inferredName, init, initPath);
              return true;
            }
          }
        }
    }
    return false;
  }

  function isBuiltinHook(hookName) {
    switch (hookName) {
      case 'useState':
      case 'React.useState':
      case 'useReducer':
      case 'React.useReducer':
      case 'useEffect':
      case 'React.useEffect':
      case 'useLayoutEffect':
      case 'React.useLayoutEffect':
      case 'useMemo':
      case 'React.useMemo':
      case 'useCallback':
      case 'React.useCallback':
      case 'useRef':
      case 'React.useRef':
      case 'useContext':
      case 'React.useContext':
      case 'useImperativeMethods':
      case 'React.useImperativeMethods':
      case 'useDebugValue':
      case 'React.useDebugValue':
        return true;
      default:
        return false;
    }
  }

  function getCalleeName(callee) {
    if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
      return callee.object.name;
    }

    return callee.name;
  }

  function getHookCallsSignature(functionNode, scope) {
    var fnHookCalls = hookCalls.get(functionNode);
    if (fnHookCalls === undefined) {
      return null;
    }
    return {
      key: fnHookCalls.map(function (call) {
        return call.name + '{' + call.key + '}';
      }).join('\n'),
      customHooks: fnHookCalls.filter(function (call) {
        return !isBuiltinHook(call.name);
      }).filter(function (call) {
        return scope.parent.hasBinding(call.name);
      }).map(function (call) {
        return t.cloneDeep(call.callee);
      })
    };
  }

  function createArgumentsForSignature(node, signature, scope) {
    var key = signature.key,
        customHooks = signature.customHooks;

    var args = [node, t.stringLiteral(key)];
    var hooksInScope = customHooks.filter(function (call) {
      return scope.hasBinding(getCalleeName(call));
    });
    if (hooksInScope.length > 0) {
      args.push(t.arrowFunctionExpression([], t.arrayExpression(hooksInScope)));
    }
    return args;
  }

  var seenForRegistration = new WeakSet();
  var seenForSignature = new WeakSet();

  var hookCalls = new WeakMap();
  var HookCallsVisitor = {
    CallExpression: function CallExpression(path) {
      var node = path.node;
      var callee = node.callee;

      // Note: this visitor MUST NOT mutate the tree in any way.
      // It runs early in a separate traversal and should be very fast.

      var name = null;
      switch (callee.type) {
        case 'Identifier':
          name = callee.name;
          break;
        case 'MemberExpression':
          name = callee.property.name;
          break;
      }
      if (name === null || !/^use[A-Z]/.test(name)) {
        return;
      }
      var fnScope = path.scope.getFunctionParent();

      if (fnScope === null) {
        return;
      }

      // This is a Hook call. Record it.
      var fnNode = fnScope.block;
      if (!hookCalls.has(fnNode)) {
        hookCalls.set(fnNode, []);
      }

      var hookCallsForFn = hookCalls.get(fnNode);
      var key = '';
      if (path.parent.type === 'VariableDeclarator') {
        // TODO: if there is no LHS, consider some other heuristic.
        key = path.parentPath.get('id').getSource();
      }

      // Some built-in Hooks reset on edits to arguments.
      var args = path.get('arguments');
      if (name === 'useState' && args.length > 0) {
        // useState second argument is initial state.
        key += '(' + args[0].getSource() + ')';
      } else if (name === 'useReducer' && args.length > 1) {
        // useReducer second argument is initial state.
        key += '(' + args[1].getSource() + ')';
      }

      hookCallsForFn.push({
        callee: path.node.callee,
        name: name,
        key: key
      });
    }
  };

  return {
    visitor: {
      ExportDefaultDeclaration: function ExportDefaultDeclaration(path) {
        var node = path.node;
        var decl = node.declaration;
        var declPath = path.get('declaration');
        if (decl.type !== 'CallExpression') {
          // For now, we only support possible HOC calls here.
          // Named function declarations are handled in FunctionDeclaration.
          // Anonymous direct exports like export default function() {}
          // are currently ignored.
          return;
        }

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

        // This code path handles nested cases like:
        // export default memo(() => {})
        // In those cases it is more plausible people will omit names
        // so they're worth handling despite possible false positives.
        // More importantly, it handles the named case:
        // export default memo(function Named() {})
        var inferredName = '%default%';
        var programPath = path.parentPath;
        findInnerComponents(inferredName, declPath, function (persistentID, targetExpr, targetPath) {
          if (targetPath === null) {
            // For case like:
            // export default hoc(Foo)
            // we don't want to wrap Foo inside the call.
            // Instead we assume it's registered at definition.
            return;
          }
          var handle = createRegistration(programPath, persistentID);
          targetPath.replaceWith(t.assignmentExpression('=', handle, targetExpr));
        });
      },

      FunctionDeclaration: {
        enter: function enter(path) {
          return;
          var node = path.node;
          var programPath = void 0;
          var insertAfterPath = void 0;
          switch (path.parent.type) {
            case 'Program':
              insertAfterPath = path;
              programPath = path.parentPath;
              break;
            case 'ExportNamedDeclaration':
              insertAfterPath = path.parentPath;
              programPath = insertAfterPath.parentPath;
              break;
            case 'ExportDefaultDeclaration':
              insertAfterPath = path.parentPath;
              programPath = insertAfterPath.parentPath;
              break;
            default:
              return;
          }
          var id = node.id;
          if (id === null) {
            // We don't currently handle anonymous default exports.
            return;
          }
          var inferredName = id.name;
          if (!isComponentishName(inferredName)) {
            return;
          }

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

          // export function Named() {}
          // function Named() {}
          findInnerComponents(inferredName, path, function (persistentID, targetExpr) {
            var handle = createRegistration(programPath, persistentID);
            insertAfterPath.insertAfter(t.expressionStatement(t.assignmentExpression('=', handle, targetExpr)));
          });
        },
        exit: function exit(path) {
          //return;
          var node = path.node;
          var id = node.id;
          if (id === null) {
            return;
          }
          var signature = getHookCallsSignature(node, path.scope);
          if (signature === null) {
            return;
          }

          // Make sure we're not mutating the same tree twice.
          // This can happen if another Babel plugin replaces parents.
          if (seenForSignature.has(node)) {
            return;
          }
          seenForSignature.add(node);

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

          insertAfterPath.insertAfter(t.expressionStatement(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(id, signature, insertAfterPath.scope))));
        }
      },
      'ArrowFunctionExpression|FunctionExpression': {
        exit: function exit(path) {
          var node = path.node;
          var signature = getHookCallsSignature(node, path.scope);
          if (signature === null) {
            return;
          }

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

          if (path.parent.type === 'VariableDeclarator') {
            var insertAfterPath = null;
            path.find(function (p) {
              if (p.parentPath.isBlock()) {
                insertAfterPath = p;
                return true;
              }
            });
            if (insertAfterPath === null) {
              return;
            }
            // Special case when a function would get an inferred name:
            // let Foo = () => {}
            // let Foo = function() {}
            // We'll add signature it on next line so that
            // we don't mess up the inferred 'Foo' function name.
            insertAfterPath.insertAfter(t.expressionStatement(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(path.parent.id, signature, insertAfterPath.scope))));
            // Result: let Foo = () => {}; __signature(Foo, ...);
          } else {
            // let Foo = hoc(() => {})
            path.replaceWith(t.callExpression(t.identifier(SIGNATURE), createArgumentsForSignature(node, signature, path.scope)));
            // Result: let Foo = hoc(__signature(() => {}, ...))
          }
        }
      },
      Program: {
        enter: function enter(path) {
          // This is a separate early visitor because we need to collect Hook calls
          // and "const [foo, setFoo] = ..." signatures before the destructuring
          // transform mangles them. This extra traversal is not ideal for perf,
          // but it's the best we can do until we stop transpiling destructuring.
          path.traverse(HookCallsVisitor);
        }
      }
    }
  };
}

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
  return typeof obj;
} : function (obj) {
  return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};

var templateOptions = {
  placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/
};

/* eslint-disable */
var shouldIgnoreFile = function shouldIgnoreFile(file) {
  return !!file.split('\\').join('/').match(/node_modules\/(react|react-dom|react-hot-loader)([\/]|$)/);
};

/* eslint-enable */

function plugin(args) {
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  // This is a Babel plugin, but the user put it in the Webpack config.
  if (this && this.callback) {
    throw new Error('React Hot Loader: You are erroneously trying to use a Babel plugin ' + 'as a Webpack loader. We recommend that you use Babel, ' + 'remove "react-hot-loader/babel" from the "loaders" section ' + 'of your Webpack configuration, and instead add ' + '"react-hot-loader/babel" to the "plugins" section of your .babelrc file. ' + 'If you prefer not to use Babel, replace "react-hot-loader/babel" with ' + '"react-hot-loader/webpack" in the "loaders" section of your Webpack configuration. ');
  }
  var t = args.types,
      template = args.template;
  var _options$safetyNet = options.safetyNet,
      safetyNet = _options$safetyNet === undefined ? true : _options$safetyNet;


  var buildRegistration = template('reactHotLoader.register(ID, NAME, FILENAME);', templateOptions);

  var signatureHeader = template('var __signature__ = typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.default.signature : function (a) {return a;}', templateOptions);

  var headerTemplate = template('(function () {\n       var enterModule = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.enterModule : undefined);\n       enterModule && enterModule(module);\n     }())', templateOptions);
  var footerTemplate = template('(function () {\n       var leaveModule = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.leaveModule : undefined);\n       leaveModule && leaveModule(module);\n     }())', templateOptions);
  var evalTemplate = template('this[key]=eval(code);', templateOptions);

  // We're making the IIFE we insert at the end of the file an unused variable
  // because it otherwise breaks the output of the babel-node REPL (#359).

  var buildTagger = template('    \n(function () {  \n  \n  var reactHotLoader = (typeof reactHotLoaderGlobal !== \'undefined\' ? reactHotLoaderGlobal.default : undefined);\n  \n  if (!reactHotLoader) {\n    return;\n  }\n\n  REGISTRATIONS  \n}());\n  ', templateOptions);

  // Gather top-level variables, functions, and classes.
  // Try our best to avoid variables from require().
  // Ideally we only want to find components defined by the user.
  function shouldRegisterBinding(binding) {
    var _binding$path = binding.path,
        type = _binding$path.type,
        node = _binding$path.node,
        parent = _binding$path.parent;

    switch (type) {
      case 'FunctionDeclaration':
      case 'ClassDeclaration':
      case 'VariableDeclaration':
        return true;
      case 'VariableDeclarator':
        {
          var init = node.init;

          if (t.isCallExpression(init) && init.callee.name === 'require') {
            return false;
          }
          if (parent.declare) {
            return false;
          }
          return true;
        }
      default:
        return false;
    }
  }

  var REGISTRATIONS = Symbol('registrations');
  return {
    visitor: {
      ExportDefaultDeclaration: function ExportDefaultDeclaration(path, state) {
        var file = state.file;
        // Default exports with names are going
        // to be in scope anyway so no need to bother.

        if (path.node.declaration.id) {
          return;
        }

        // Move export default right hand side to a variable
        // so we can later refer to it and tag it with __source.
        var id = path.scope.generateUidIdentifier('default');
        var expression = t.isExpression(path.node.declaration) ? path.node.declaration : t.toExpression(path.node.declaration);
        path.scope.registerDeclaration(path.insertBefore(t.variableDeclaration('const', [t.variableDeclarator(id, expression)]))[0]);
        path.node.declaration = id; // eslint-disable-line no-param-reassign

        // It won't appear in scope.bindings
        // so we'll manually remember it exists.
        state[REGISTRATIONS].push(buildRegistration({
          ID: id,
          NAME: t.stringLiteral('default'),
          FILENAME: t.stringLiteral(file.opts.filename)
        }));
      },


      Program: {
        enter: function enter(_ref, state) {
          var scope = _ref.scope,
              node = _ref.node;
          var file = state.file;

          state[REGISTRATIONS] = []; // eslint-disable-line no-param-reassign

          node.body.unshift(signatureHeader());

          // Everything in the top level scope, when reasonable,
          // is going to get tagged with __source.
          /* eslint-disable guard-for-in,no-restricted-syntax */
          for (var id in scope.bindings) {
            var binding = scope.bindings[id];
            if (shouldRegisterBinding(binding)) {
              state[REGISTRATIONS].push(buildRegistration({
                ID: binding.identifier,
                NAME: t.stringLiteral(id),
                FILENAME: t.stringLiteral(file.opts.filename)
              }));
            }
          }
          /* eslint-enable */
        },
        exit: function exit(_ref2, state) {
          var node = _ref2.node;
          var file = state.file;

          var registrations = state[REGISTRATIONS];
          state[REGISTRATIONS] = [];

          // inject the code only if applicable
          if (registrations && registrations.length && !shouldIgnoreFile(file.opts.filename)) {
            if (safetyNet) {
              node.body.unshift(headerTemplate());
            }
            // Inject the generated tagging code at the very end
            // so that it is as minimally intrusive as possible.
            node.body.push(t.emptyStatement());
            node.body.push(buildTagger({ REGISTRATIONS: registrations }));
            node.body.push(t.emptyStatement());

            if (safetyNet) {
              node.body.push(footerTemplate());
            }
          }
        }
      },
      Class: function Class(classPath) {
        var classBody = classPath.get('body');
        var hasRegenerateMethod = false;
        var hasMethods = false;

        classBody.get('body').forEach(function (path) {
          var node = path.node;

          // don't apply transform to static class properties

          if (node.static) {
            return;
          }

          if (node.key.name !== REGENERATE_METHOD) {
            hasMethods = true;
          } else {
            hasRegenerateMethod = true;
          }
        });

        if (hasMethods && !hasRegenerateMethod) {
          var regenerateMethod = t.classMethod('method', t.identifier(REGENERATE_METHOD), [t.identifier('key'), t.identifier('code')], t.blockStatement([evalTemplate()]));

          classBody.pushContainer('body', regenerateMethod);

          classBody.get('body').forEach(function (path) {
            var node = path.node;


            if (node.key.name === REGENERATE_METHOD) {
              path.addComment('leading', ' @ts-ignore', true);
              path.get('body').get('body')[0].addComment('leading', ' @ts-ignore', true);
            }
          });
        }
      }
    }
  };
}

var mergeRecord = function mergeRecord(sourceRecord, newRecord) {
  Object.keys(newRecord).forEach(function (key) {
    var action = newRecord[key];
    if (typeof action === 'function') {
      if (!sourceRecord[key]) {
        sourceRecord[key] = function () {
          return {};
        };
      }
      var prev = sourceRecord[key];
      sourceRecord[key] = function () {
        prev.apply(undefined, arguments);
        action.apply(undefined, arguments);
      };
    } else if ((typeof action === 'undefined' ? 'undefined' : _typeof(action)) === 'object') {
      if (!sourceRecord[key]) {
        sourceRecord[key] = {};
      }
      mergeRecord(sourceRecord[key], action);
    }
  });
};

var composePlugins = function composePlugins(plugins) {
  return function () {
    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    var result = {};
    plugins.forEach(function (creator) {
      var plugin = creator.apply(undefined, args);
      mergeRecord(result, plugin);
    });
    return result;
  };
};

module.exports = composePlugins([plugin, function () {
  var p = fresh.apply(undefined, arguments);
  // removing everything we dont want right now

  // registration
  // delete p.visitor.Program;
  // delete p.visitor.Program.exit;

  // registrations
  // delete p.visitor.FunctionDeclaration.enter;
  // delete p.visitor.FunctionDeclaration.leave;
  // delete p.visitor.VariableDeclaration;

  return p;
}]);

module.exports.shouldIgnoreFile = shouldIgnoreFile;
