123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- const validateOptions = require('schema-utils');
- const { DefinePlugin, ModuleFilenameHelpers, ProvidePlugin, Template } = require('webpack');
- const ConstDependency = require('webpack/lib/dependencies/ConstDependency');
- const { refreshGlobal, webpackRequire, webpackVersion } = require('./globals');
- const {
- createError,
- getParserHelpers,
- getRefreshGlobal,
- getSocketIntegration,
- injectRefreshEntry,
- injectRefreshLoader,
- normalizeOptions,
- } = require('./utils');
- const schema = require('./options.json');
- // Mapping of react-refresh globals to Webpack runtime globals
- const REPLACEMENTS = {
- $RefreshRuntime$: {
- expr: `${refreshGlobal}.runtime`,
- req: [webpackRequire, `${refreshGlobal}.runtime`],
- type: 'object',
- },
- $RefreshSetup$: {
- expr: `${refreshGlobal}.setup`,
- req: [webpackRequire, `${refreshGlobal}.setup`],
- type: 'function',
- },
- $RefreshCleanup$: {
- expr: `${refreshGlobal}.cleanup`,
- req: [webpackRequire, `${refreshGlobal}.cleanup`],
- type: 'function',
- },
- $RefreshReg$: {
- expr: `${refreshGlobal}.register`,
- req: [webpackRequire, `${refreshGlobal}.register`],
- type: 'function',
- },
- $RefreshSig$: {
- expr: `${refreshGlobal}.signature`,
- req: [webpackRequire, `${refreshGlobal}.signature`],
- type: 'function',
- },
- };
- class ReactRefreshPlugin {
- /**
- * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
- */
- constructor(options = {}) {
- validateOptions(schema, options, {
- name: 'React Refresh Plugin',
- baseDataPath: 'options',
- });
- /**
- * @readonly
- * @type {import('./types').NormalizedPluginOptions}
- */
- this.options = normalizeOptions(options);
- }
- /**
- * Applies the plugin.
- * @param {import('webpack').Compiler} compiler A webpack compiler object.
- * @returns {void}
- */
- apply(compiler) {
- // Throw if we encounter an unsupported Webpack version,
- // since things will most likely not work.
- if (webpackVersion !== 4 && webpackVersion !== 5) {
- throw createError(`Webpack v${webpackVersion} is not supported!`);
- }
- // Skip processing in non-development mode, but allow manual force-enabling
- if (
- // Webpack do not set process.env.NODE_ENV, so we need to check for mode.
- // Ref: https://github.com/webpack/webpack/issues/7074
- (compiler.options.mode !== 'development' ||
- // We also check for production process.env.NODE_ENV,
- // in case it was set and mode is non-development (e.g. 'none')
- (process.env.NODE_ENV && process.env.NODE_ENV === 'production')) &&
- !this.options.forceEnable
- ) {
- return;
- }
- // Inject react-refresh context to all Webpack entry points
- compiler.options.entry = injectRefreshEntry(compiler.options.entry, this.options);
- // Inject necessary modules to bundle's global scope
- /** @type {Record<string, string>} */
- let providedModules = {
- __react_refresh_utils__: require.resolve('./runtime/RefreshUtils'),
- };
- if (this.options.overlay === false) {
- // Stub errorOverlay module so calls to it can be erased
- const definePlugin = new DefinePlugin({
- __react_refresh_error_overlay__: false,
- __react_refresh_init_socket__: false,
- });
- definePlugin.apply(compiler);
- } else {
- providedModules = {
- ...providedModules,
- ...(this.options.overlay.module && {
- __react_refresh_error_overlay__: require.resolve(this.options.overlay.module),
- }),
- ...(this.options.overlay.sockIntegration && {
- __react_refresh_init_socket__: getSocketIntegration(this.options.overlay.sockIntegration),
- }),
- };
- }
- const providePlugin = new ProvidePlugin(providedModules);
- providePlugin.apply(compiler);
- const matchObject = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
- const { evaluateToString, toConstantDependency } = getParserHelpers();
- compiler.hooks.compilation.tap(
- this.constructor.name,
- (compilation, { normalModuleFactory }) => {
- // Only hook into the current compiler
- if (compilation.compiler !== compiler) {
- return;
- }
- // Set template for ConstDependency which is used by parser hooks
- compilation.dependencyTemplates.set(ConstDependency, new ConstDependency.Template());
- // Tap into version-specific compilation hooks
- switch (webpackVersion) {
- case 4: {
- const outputOptions = compilation.mainTemplate.outputOptions;
- compilation.mainTemplate.hooks.require.tap(
- this.constructor.name,
- // Constructs the module template for react-refresh
- (source, chunk, hash) => {
- // Check for the output filename
- // This is to ensure we are processing a JS-related chunk
- let filename = outputOptions.filename;
- if (typeof filename === 'function') {
- // Only usage of the `chunk` property is documented by Webpack.
- // However, some internal Webpack plugins uses other properties,
- // so we also pass them through to be on the safe side.
- filename = filename({
- contentHashType: 'javascript',
- chunk,
- hash,
- });
- }
- // Check whether the current compilation is outputting to JS,
- // since other plugins can trigger compilations for other file types too.
- // If we apply the transform to them, their compilation will break fatally.
- // One prominent example of this is the HTMLWebpackPlugin.
- // If filename is falsy, something is terribly wrong and there's nothing we can do.
- if (!filename || !filename.includes('.js')) {
- return source;
- }
- // Split template source code into lines for easier processing
- const lines = source.split('\n');
- // Webpack generates this line when the MainTemplate is called
- const moduleInitializationLineNumber = lines.findIndex((line) =>
- line.includes('modules[moduleId].call(')
- );
- // Unable to find call to module execution -
- // this happens if the current module does not call MainTemplate.
- // In this case, we will return the original source and won't mess with it.
- if (moduleInitializationLineNumber === -1) {
- return source;
- }
- const moduleInterceptor = Template.asString([
- `${refreshGlobal}.init();`,
- 'try {',
- Template.indent(lines[moduleInitializationLineNumber]),
- '} finally {',
- Template.indent(`${refreshGlobal}.cleanup(moduleId);`),
- '}',
- ]);
- return Template.asString([
- ...lines.slice(0, moduleInitializationLineNumber),
- '',
- outputOptions.strictModuleExceptionHandling
- ? Template.indent(moduleInterceptor)
- : moduleInterceptor,
- '',
- ...lines.slice(moduleInitializationLineNumber + 1, lines.length),
- ]);
- }
- );
- compilation.mainTemplate.hooks.requireExtensions.tap(
- this.constructor.name,
- // Setup react-refresh globals as extensions to Webpack's require function
- (source) => {
- return Template.asString([source, '', getRefreshGlobal()]);
- }
- );
- normalModuleFactory.hooks.afterResolve.tap(
- this.constructor.name,
- // Add react-refresh loader to process files that matches specified criteria
- (data) => {
- return injectRefreshLoader(data, matchObject);
- }
- );
- compilation.hooks.normalModuleLoader.tap(
- // `Infinity` ensures this check will run only after all other taps
- { name: this.constructor.name, stage: Infinity },
- // Check for existence of the HMR runtime -
- // it is the foundation to this plugin working correctly
- (context) => {
- if (!context.hot) {
- throw createError(
- [
- 'Hot Module Replacement (HMR) is not enabled!',
- 'React Refresh requires HMR to function properly.',
- ].join(' ')
- );
- }
- }
- );
- break;
- }
- case 5: {
- const NormalModule = require('webpack/lib/NormalModule');
- const RuntimeGlobals = require('webpack/lib/RuntimeGlobals');
- const ReactRefreshRuntimeModule = require('./runtime/RefreshRuntimeModule');
- compilation.hooks.additionalTreeRuntimeRequirements.tap(
- this.constructor.name,
- // Setup react-refresh globals with a Webpack runtime module
- (chunk, runtimeRequirements) => {
- runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
- compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule());
- }
- );
- normalModuleFactory.hooks.afterResolve.tap(
- this.constructor.name,
- // Add react-refresh loader to process files that matches specified criteria
- (resolveData) => {
- injectRefreshLoader(resolveData.createData, matchObject);
- }
- );
- NormalModule.getCompilationHooks(compilation).loader.tap(
- // `Infinity` ensures this check will run only after all other taps
- { name: this.constructor.name, stage: Infinity },
- // Check for existence of the HMR runtime -
- // it is the foundation to this plugin working correctly
- (context) => {
- if (!context.hot) {
- throw createError(
- [
- 'Hot Module Replacement (HMR) is not enabled!',
- 'React Refresh requires HMR to function properly.',
- ].join(' ')
- );
- }
- }
- );
- break;
- }
- default: {
- throw createError(`Encountered unexpected Webpack version (v${webpackVersion})`);
- }
- }
- /**
- * Transform global calls into Webpack runtime calls.
- * @param {*} parser
- * @returns {void}
- */
- const parserHandler = (parser) => {
- Object.entries(REPLACEMENTS).forEach(([key, info]) => {
- parser.hooks.expression
- .for(key)
- .tap(this.constructor.name, toConstantDependency(parser, info.expr, info.req));
- if (info.type) {
- parser.hooks.evaluateTypeof
- .for(key)
- .tap(this.constructor.name, evaluateToString(info.type));
- }
- });
- };
- normalModuleFactory.hooks.parser
- .for('javascript/auto')
- .tap(this.constructor.name, parserHandler);
- normalModuleFactory.hooks.parser
- .for('javascript/dynamic')
- .tap(this.constructor.name, parserHandler);
- normalModuleFactory.hooks.parser
- .for('javascript/esm')
- .tap(this.constructor.name, parserHandler);
- }
- );
- }
- }
- module.exports.ReactRefreshPlugin = ReactRefreshPlugin;
- module.exports = ReactRefreshPlugin;
|