index.js 3.49 KB
// This is a patch for mozilla/source-map#349 -
// internally, it uses the existence of the `fetch` global to toggle browser behaviours.
// That check, however, will break when `fetch` polyfills are used for SSR setups.
// We "reset" the polyfill here to ensure it won't interfere with source-map generation.
const originalFetch = global.fetch;
delete global.fetch;

const { getOptions } = require('loader-utils');
const { validate: validateOptions } = require('schema-utils');
const { SourceMapConsumer, SourceNode } = require('source-map');
const {
  getIdentitySourceMap,
  getModuleSystem,
  getRefreshModuleRuntime,
  normalizeOptions,
} = require('./utils');
const schema = require('./options.json');

const RefreshRuntimePath = require
  .resolve('react-refresh')
  .replace(/\\/g, '/')
  .replace(/'/g, "\\'");

/**
 * A simple Webpack loader to inject react-refresh HMR code into modules.
 *
 * [Reference for Loader API](https://webpack.js.org/api/loaders/)
 * @this {import('webpack').LoaderContext<import('./types').ReactRefreshLoaderOptions>}
 * @param {string} source The original module source code.
 * @param {import('source-map').RawSourceMap} [inputSourceMap] The source map of the module.
 * @param {*} [meta] The loader metadata passed in.
 * @returns {void}
 */
function ReactRefreshLoader(source, inputSourceMap, meta) {
  let options = getOptions(this);
  validateOptions(schema, options, {
    baseDataPath: 'options',
    name: 'React Refresh Loader',
  });

  options = normalizeOptions(options);

  const callback = this.async();

  const { ModuleFilenameHelpers, Template } = this._compiler.webpack || require('webpack');

  const RefreshSetupRuntimes = {
    cjs: Template.asString(
      `__webpack_require__.$Refresh$.runtime = require('${RefreshRuntimePath}');`
    ),
    esm: Template.asString([
      `import * as __react_refresh_runtime__ from '${RefreshRuntimePath}';`,
      `__webpack_require__.$Refresh$.runtime = __react_refresh_runtime__;`,
    ]),
  };

  /**
   * @this {import('webpack').loader.LoaderContext}
   * @param {string} source
   * @param {import('source-map').RawSourceMap} [inputSourceMap]
   * @returns {Promise<[string, import('source-map').RawSourceMap]>}
   */
  async function _loader(source, inputSourceMap) {
    const moduleSystem = await getModuleSystem.call(this, ModuleFilenameHelpers, options);

    const RefreshSetupRuntime = RefreshSetupRuntimes[moduleSystem];
    const RefreshModuleRuntime = getRefreshModuleRuntime(Template, {
      const: options.const,
      moduleSystem,
    });

    if (this.sourceMap) {
      let originalSourceMap = inputSourceMap;
      if (!originalSourceMap) {
        originalSourceMap = getIdentitySourceMap(source, this.resourcePath);
      }

      return SourceMapConsumer.with(originalSourceMap, undefined, (consumer) => {
        const node = SourceNode.fromStringWithSourceMap(source, consumer);

        node.prepend([RefreshSetupRuntime, '\n\n']);
        node.add(['\n\n', RefreshModuleRuntime]);

        const { code, map } = node.toStringWithSourceMap();
        return [code, map.toJSON()];
      });
    } else {
      return [[RefreshSetupRuntime, source, RefreshModuleRuntime].join('\n\n'), inputSourceMap];
    }
  }

  _loader.call(this, source, inputSourceMap).then(
    ([code, map]) => {
      callback(null, code, map, meta);
    },
    (error) => {
      callback(error);
    }
  );
}

module.exports = ReactRefreshLoader;

// Restore the original value of the `fetch` global, if it exists
if (originalFetch) {
  global.fetch = originalFetch;
}