Skip to content

Commit ca31cb0

Browse files
authored
feat: use entry hooks for injection in Webpack 5 (#319)
1 parent 2c268ab commit ca31cb0

7 files changed

+177
-72
lines changed

lib/index.js

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
const { validate: validateOptions } = require('schema-utils');
2-
const { DefinePlugin, ModuleFilenameHelpers, ProvidePlugin, Template } = require('webpack');
2+
const {
3+
DefinePlugin,
4+
EntryPlugin,
5+
ModuleFilenameHelpers,
6+
ProvidePlugin,
7+
Template,
8+
} = require('webpack');
39
const ConstDependency = require('webpack/lib/dependencies/ConstDependency');
410
const { refreshGlobal, webpackRequire, webpackVersion } = require('./globals');
511
const {
12+
getAdditionalEntries,
13+
getIntegrationEntry,
614
getParserHelpers,
715
getRefreshGlobal,
816
getSocketIntegration,
917
injectRefreshEntry,
1018
injectRefreshLoader,
1119
normalizeOptions,
12-
getAdditionalEntries,
1320
} = require('./utils');
1421
const schema = require('./options.json');
1522

@@ -72,11 +79,83 @@ class ReactRefreshPlugin {
7279
const logger = compiler.getInfrastructureLogger(this.constructor.name);
7380
let loggedHotWarning = false;
7481

75-
// Inject react-refresh context to all Webpack entry points
76-
compiler.options.entry = injectRefreshEntry(
77-
compiler.options.entry,
78-
getAdditionalEntries({ options: this.options, devServer: compiler.options.devServer })
79-
);
82+
// Inject react-refresh context to all Webpack entry points.
83+
// This should create `EntryDependency` objects when available,
84+
// and fallback to patching the `entry` object for legacy workflows.
85+
const additional = getAdditionalEntries({
86+
devServer: compiler.options.devServer,
87+
options: this.options,
88+
});
89+
if (EntryPlugin) {
90+
// Prepended entries does not care about injection order,
91+
// so we can utilise EntryPlugin for simpler logic.
92+
additional.prependEntries.forEach((entry) => {
93+
new EntryPlugin(compiler.context, entry, { name: undefined }).apply(compiler);
94+
});
95+
96+
const integrationEntry = getIntegrationEntry(this.options.overlay.sockIntegration);
97+
const socketEntryData = [];
98+
compiler.hooks.make.tap(
99+
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
100+
(compilation) => {
101+
// Exhaustively search all entries for `integrationEntry`.
102+
// If found, mark those entries and the index of `integrationEntry`.
103+
for (const [name, entryData] of compilation.entries.entries()) {
104+
const index = entryData.dependencies.findIndex((dep) =>
105+
dep.request.includes(integrationEntry)
106+
);
107+
if (index !== -1) {
108+
socketEntryData.push({ name, index });
109+
}
110+
}
111+
}
112+
);
113+
114+
// Overlay entries need to be injected AFTER integration's entry,
115+
// so we will loop through everything in `finishMake` instead of `make`.
116+
// This ensures we can traverse all entry points and inject stuff with the correct order.
117+
additional.overlayEntries.forEach((entry, idx, arr) => {
118+
compiler.hooks.finishMake.tapPromise(
119+
{ name: this.constructor.name, stage: Number.MIN_SAFE_INTEGER + (arr.length - idx - 1) },
120+
(compilation) => {
121+
// Only hook into the current compiler
122+
if (compilation.compiler !== compiler) {
123+
return Promise.resolve();
124+
}
125+
126+
const injectData = socketEntryData.length ? socketEntryData : [{ name: undefined }];
127+
return Promise.all(
128+
injectData.map(({ name, index }) => {
129+
return new Promise((resolve, reject) => {
130+
const options = { name };
131+
const dep = EntryPlugin.createDependency(entry, options);
132+
compilation.addEntry(compiler.context, dep, options, (err) => {
133+
if (err) return reject(err);
134+
135+
// If the entry is not a global one,
136+
// and we have registered the index for integration entry,
137+
// we will reorder all entry dependencies to our desired order.
138+
// That is, to have additional entries DIRECTLY behind integration entry.
139+
if (name && typeof index !== 'undefined') {
140+
const entryData = compilation.entries.get(name);
141+
entryData.dependencies.splice(
142+
index + 1,
143+
0,
144+
entryData.dependencies.splice(entryData.dependencies.length - 1, 1)[0]
145+
);
146+
}
147+
148+
resolve();
149+
});
150+
});
151+
})
152+
).then(() => {});
153+
}
154+
);
155+
});
156+
} else {
157+
compiler.options.entry = injectRefreshEntry(compiler.options.entry, additional);
158+
}
80159

81160
// Inject necessary modules to bundle's global scope
82161
/** @type {Record<string, string | boolean>}*/
@@ -114,10 +193,8 @@ class ReactRefreshPlugin {
114193
}
115194
}
116195

117-
const definePlugin = new DefinePlugin(definedModules);
118-
definePlugin.apply(compiler);
119-
const providePlugin = new ProvidePlugin(providedModules);
120-
providePlugin.apply(compiler);
196+
new DefinePlugin(definedModules).apply(compiler);
197+
new ProvidePlugin(providedModules).apply(compiler);
121198

122199
const match = ModuleFilenameHelpers.matchObject.bind(undefined, this.options);
123200
const { evaluateToString, toConstantDependency } = getParserHelpers();
@@ -217,8 +294,8 @@ class ReactRefreshPlugin {
217294
);
218295

219296
compilation.hooks.normalModuleLoader.tap(
220-
// `Infinity` ensures this check will run only after all other taps
221-
{ name: this.constructor.name, stage: Infinity },
297+
// `Number.POSITIVE_INFINITY` ensures this check will run only after all other taps
298+
{ name: this.constructor.name, stage: Number.POSITIVE_INFINITY },
222299
// Check for existence of the HMR runtime -
223300
// it is the foundation to this plugin working correctly
224301
(context) => {
@@ -237,10 +314,14 @@ class ReactRefreshPlugin {
237314
break;
238315
}
239316
case 5: {
317+
const EntryDependency = require('webpack/lib/dependencies/EntryDependency');
240318
const NormalModule = require('webpack/lib/NormalModule');
241319
const RuntimeGlobals = require('webpack/lib/RuntimeGlobals');
242320
const ReactRefreshRuntimeModule = require('./RefreshRuntimeModule');
243321

322+
// Set factory for EntryDependency which is used to initialise the module
323+
compilation.dependencyFactories.set(EntryDependency, normalModuleFactory);
324+
244325
compilation.hooks.additionalTreeRuntimeRequirements.tap(
245326
this.constructor.name,
246327
// Setup react-refresh globals with a Webpack runtime module

lib/utils/getAdditionalEntries.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ const querystring = require('querystring');
22

33
/**
44
* @typedef {Object} AdditionalEntries
5-
* @property {import('./injectRefreshEntry').WebpackEntry} prependEntries
6-
* @property {import('./injectRefreshEntry').WebpackEntry} overlayEntries
5+
* @property {string[]} prependEntries
6+
* @property {string[]} overlayEntries
77
*/
88

99
/**
@@ -57,7 +57,7 @@ function getAdditionalEntries({ devServer, options }) {
5757
// Error overlay runtime
5858
options.overlay &&
5959
options.overlay.entry &&
60-
`${options.overlay.entry}${queryString ? `?${queryString}` : ''}`,
60+
`${require.resolve(options.overlay.entry)}${queryString ? `?${queryString}` : ''}`,
6161
].filter(Boolean);
6262

6363
return { prependEntries, overlayEntries };

lib/utils/getIntegrationEntry.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Gets entry point of a supported socket integration.
3+
* @param {'wds' | 'whm' | 'wps' | string} integrationType A valid socket integration type or a path to a module.
4+
* @returns {string | undefined} Path to the resolved integration entry point.
5+
*/
6+
function getIntegrationEntry(integrationType) {
7+
let resolvedEntry;
8+
switch (integrationType) {
9+
case 'whm': {
10+
resolvedEntry = 'webpack-hot-middleware/client';
11+
break;
12+
}
13+
case 'wps': {
14+
resolvedEntry = 'webpack-plugin-serve/client';
15+
break;
16+
}
17+
}
18+
19+
return resolvedEntry;
20+
}
21+
22+
module.exports = getIntegrationEntry;

lib/utils/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
const getAdditionalEntries = require('./getAdditionalEntries');
2+
const getIntegrationEntry = require('./getIntegrationEntry');
13
const getParserHelpers = require('./getParserHelpers');
24
const getRefreshGlobal = require('./getRefreshGlobal');
35
const getSocketIntegration = require('./getSocketIntegration');
46
const injectRefreshEntry = require('./injectRefreshEntry');
57
const injectRefreshLoader = require('./injectRefreshLoader');
68
const normalizeOptions = require('./normalizeOptions');
7-
const getAdditionalEntries = require('./getAdditionalEntries');
89

910
module.exports = {
11+
getAdditionalEntries,
12+
getIntegrationEntry,
1013
getParserHelpers,
1114
getRefreshGlobal,
1215
getSocketIntegration,
1316
injectRefreshEntry,
1417
injectRefreshLoader,
1518
normalizeOptions,
16-
getAdditionalEntries,
1719
};

lib/utils/injectRefreshEntry.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@ const EntryParseError = new Error(
99
].join(' ')
1010
);
1111

12+
/**
13+
* Webpack entries related to socket integrations.
14+
* They have to run before any code that sets up the error overlay.
15+
* @type {string[]}
16+
*/
17+
const socketEntries = [
18+
'webpack-dev-server/client',
19+
'webpack-hot-middleware/client',
20+
'webpack-plugin-serve/client',
21+
'react-dev-utils/webpackHotDevClient',
22+
];
23+
1224
/**
1325
* Checks if a Webpack entry string is related to socket integrations.
1426
* @param {string} entry A Webpack entry string.
1527
* @returns {boolean} Whether the entry is related to socket integrations.
1628
*/
1729
function isSocketEntry(entry) {
18-
/**
19-
* Webpack entries related to socket integrations.
20-
* They have to run before any code that sets up the error overlay.
21-
* @type {string[]}
22-
*/
23-
const socketEntries = [
24-
'webpack-dev-server/client',
25-
'webpack-hot-middleware/client',
26-
'webpack-plugin-serve/client',
27-
'react-dev-utils/webpackHotDevClient',
28-
];
29-
3030
return socketEntries.some((socketEntry) => entry.includes(socketEntry));
3131
}
3232

@@ -37,7 +37,7 @@ function isSocketEntry(entry) {
3737
* @returns {WebpackEntry} An injected entry object.
3838
*/
3939
function injectRefreshEntry(originalEntry, additionalEntries) {
40-
const { prependEntries = [], overlayEntries = [] } = additionalEntries;
40+
const { prependEntries, overlayEntries } = additionalEntries;
4141

4242
// Single string entry point
4343
if (typeof originalEntry === 'string') {
@@ -95,3 +95,4 @@ function injectRefreshEntry(originalEntry, additionalEntries) {
9595
}
9696

9797
module.exports = injectRefreshEntry;
98+
module.exports.socketEntries = socketEntries;

test/unit/injectRefreshEntry.test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ describe('injectRefreshEntry', () => {
101101
});
102102

103103
it('should not append overlay entry when unused', () => {
104-
expect(injectRefreshEntry('test.js', {})).toStrictEqual(['test.js']);
104+
expect(
105+
injectRefreshEntry('test.js', {
106+
prependEntries: [],
107+
overlayEntries: [],
108+
})
109+
).toStrictEqual(['test.js']);
105110
});
106111

107112
it('should append overlay entry for a string after socket-related entries', () => {

0 commit comments

Comments
 (0)