UNPKG

5.84 kBJavaScriptView Raw
1const path = require('path');
2const validateOptions = require('schema-utils');
3const webpack = require('webpack');
4const {
5 createRefreshTemplate,
6 getSocketIntegration,
7 injectRefreshEntry,
8 normalizeOptions,
9} = require('./helpers');
10const { errorOverlay, initSocket, refreshUtils } = require('./runtime/globals');
11const schema = require('./options.json');
12
13class ReactRefreshPlugin {
14 /**
15 * @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
16 */
17 constructor(options = {}) {
18 validateOptions(schema, options, {
19 name: 'React Refresh Plugin',
20 baseDataPath: 'options',
21 });
22
23 /**
24 * @readonly
25 * @type {import('./types').NormalizedPluginOptions}
26 */
27 this.options = normalizeOptions(options);
28 }
29
30 /**
31 * Applies the plugin.
32 * @param {import('webpack').Compiler} compiler A webpack compiler object.
33 * @returns {void}
34 */
35 apply(compiler) {
36 // Skip processing on non-development mode, but allow manual force-enabling
37 if (
38 // Webpack do not set process.env.NODE_ENV, so we need to check for mode.
39 // Ref: https://github.com/webpack/webpack/issues/7074
40 (compiler.options.mode !== 'development' ||
41 // We also check for production process.env.NODE_ENV,
42 // in case it was set and mode is non-development (e.g. 'none')
43 (process.env.NODE_ENV && process.env.NODE_ENV === 'production')) &&
44 !this.options.forceEnable
45 ) {
46 return;
47 }
48
49 // Inject react-refresh context to all Webpack entry points
50 compiler.options.entry = injectRefreshEntry(compiler.options.entry, this.options);
51
52 // Inject necessary modules to Webpack's global scope
53 let providedModules = {
54 [refreshUtils]: require.resolve('./runtime/refreshUtils'),
55 };
56
57 if (this.options.overlay === false) {
58 // Stub errorOverlay module so calls to it will be erased
59 const definePlugin = new webpack.DefinePlugin({ [errorOverlay]: false });
60 definePlugin.apply(compiler);
61 } else {
62 providedModules = {
63 ...providedModules,
64 [errorOverlay]: require.resolve(this.options.overlay.module),
65 [initSocket]: getSocketIntegration(this.options.overlay.sockIntegration),
66 };
67 }
68
69 const providePlugin = new webpack.ProvidePlugin(providedModules);
70 providePlugin.apply(compiler);
71
72 compiler.hooks.beforeRun.tap(this.constructor.name, (compiler) => {
73 // Check for existence of HotModuleReplacementPlugin in the plugin list
74 // It is the foundation to this plugin working correctly
75 if (
76 !compiler.options.plugins ||
77 !compiler.options.plugins.find(
78 // It's validated with the name rather than the constructor reference
79 // because a project might contain multiple references to Webpack
80 (plugin) => plugin.constructor.name === 'HotModuleReplacementPlugin'
81 )
82 ) {
83 throw new Error(
84 'Hot Module Replacement (HMR) is not enabled! React-refresh requires HMR to function properly.'
85 );
86 }
87 });
88
89 const matchObject = webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
90 compiler.hooks.normalModuleFactory.tap(this.constructor.name, (nmf) => {
91 nmf.hooks.afterResolve.tap(this.constructor.name, (data) => {
92 // Inject refresh loader to all JavaScript-like files
93 if (
94 // Include and exclude user-specified files
95 matchObject(data.resource) &&
96 // Skip files related to the plugin's runtime to prevent self-referencing.
97 // This is particularly useful when using the plugin as a direct dependency.
98 !data.resource.includes(path.join(__dirname, './overlay')) &&
99 !data.resource.includes(path.join(__dirname, './runtime'))
100 ) {
101 data.loaders.unshift({
102 loader: require.resolve('./loader'),
103 options: undefined,
104 });
105 }
106
107 return data;
108 });
109 });
110
111 compiler.hooks.compilation.tap(this.constructor.name, (compilation) => {
112 compilation.mainTemplate.hooks.require.tap(
113 this.constructor.name,
114 // Constructs the correct module template for react-refresh
115 (source, chunk, hash) => {
116 const mainTemplate = compilation.mainTemplate;
117
118 // Check for the output filename
119 // This is to ensure we are processing a JS-related chunk
120 let filename = mainTemplate.outputOptions.filename;
121 if (typeof filename === 'function') {
122 // Only usage of the `chunk` property is documented by Webpack.
123 // However, some internal Webpack plugins uses other properties,
124 // so we also pass them through to be on the safe side.
125 filename = filename({
126 chunk,
127 hash,
128 // TODO: Figure out whether we need to stub the following properties, probably no
129 contentHashType: 'javascript',
130 hashWithLength: (length) => mainTemplate.renderCurrentHashCode(hash, length),
131 noChunkHash: mainTemplate.useChunkHash(chunk),
132 });
133 }
134
135 // Check whether the current compilation is outputting to JS,
136 // since other plugins can trigger compilations for other file types too.
137 // If we apply the transform to them, their compilation will break fatally.
138 // One prominent example of this is the HTMLWebpackPlugin.
139 // If filename is falsy, something is terribly wrong and there's nothing we can do.
140 if (!filename || !filename.includes('.js')) {
141 return source;
142 }
143
144 return createRefreshTemplate(source);
145 }
146 );
147 });
148 }
149}
150
151module.exports.ReactRefreshPlugin = ReactRefreshPlugin;
152module.exports = ReactRefreshPlugin;