Skip to content

Commit 92d7fd6

Browse files
authored
fix: out of order cleanup with top-level await (#898)
* Replace global __webpack_require__.$Refresh$ with module scoped one * Update runtime chunk snapshots
1 parent 781ef6b commit 92d7fd6

File tree

2 files changed

+137
-158
lines changed

2 files changed

+137
-158
lines changed

lib/utils/makeRefreshRuntimeModule.js

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
const { getRefreshGlobalScope } = require('../globals');
2-
const getRefreshGlobal = require('./getRefreshGlobal');
3-
41
/**
52
* Makes a runtime module to intercept module execution for React Refresh.
3+
* This module creates an isolated __webpak_require__ function for each module
4+
* and injects a $Refresh$ object into it for use by react-refresh.
65
* @param {import('webpack')} webpack The Webpack exports.
76
* @returns {ReactRefreshRuntimeModule} The runtime module class.
87
*/
@@ -19,56 +18,72 @@ function makeRefreshRuntimeModule(webpack) {
1918
*/
2019
generate() {
2120
const { runtimeTemplate } = this.compilation;
22-
const refreshGlobal = getRefreshGlobalScope(webpack.RuntimeGlobals);
21+
const declareVar = runtimeTemplate.supportsConst() ? 'const' : 'var';
2322
return webpack.Template.asString([
23+
`${declareVar} setup = ${runtimeTemplate.basicFunction('moduleId', [
24+
`${declareVar} refresh = {`,
25+
webpack.Template.indent([
26+
`moduleId: moduleId,`,
27+
`register: ${runtimeTemplate.basicFunction('type, id', [
28+
`${declareVar} typeId = moduleId + " " + id;`,
29+
`refresh.runtime.register(type, typeId);`,
30+
])},`,
31+
`signature: ${runtimeTemplate.returningFunction(
32+
'refresh.runtime.createSignatureFunctionForTransform()'
33+
)},`,
34+
`runtime: {`,
35+
webpack.Template.indent([
36+
`createSignatureFunctionForTransform: ${runtimeTemplate.returningFunction(
37+
runtimeTemplate.returningFunction('type', 'type')
38+
)},`,
39+
`register: ${runtimeTemplate.emptyFunction()}`,
40+
]),
41+
`},`,
42+
]),
43+
`};`,
44+
`return refresh;`,
45+
])}`,
46+
'',
2447
`${webpack.RuntimeGlobals.interceptModuleExecution}.push(${runtimeTemplate.basicFunction(
2548
'options',
2649
[
27-
`${
28-
runtimeTemplate.supportsConst() ? 'const' : 'var'
29-
} originalFactory = options.factory;`,
30-
`options.factory = function (moduleObject, moduleExports, webpackRequire) {`,
31-
webpack.Template.indent([
32-
`${refreshGlobal}.setup(options.id);`,
33-
'try {',
34-
webpack.Template.indent(
35-
'originalFactory.call(this, moduleObject, moduleExports, webpackRequire);'
36-
),
37-
'} finally {',
38-
webpack.Template.indent([
39-
`if (typeof Promise !== 'undefined' && moduleObject.exports instanceof Promise) {`,
40-
webpack.Template.indent([
41-
// The exports of the module are re-assigned -
42-
// this ensures anything coming after us would wait for `cleanup` to fire.
43-
// This is particularly important because `cleanup` restores the refresh global,
44-
// maintaining consistency for mutable variables like `moduleId`.
45-
// This `.then` clause is a ponyfill of the `Promise.finally` API -
46-
// it is only part of the spec after ES2018,
47-
// but Webpack's top level await implementation support engines down to ES2015.
48-
'options.module.exports = options.module.exports.then(',
50+
`${declareVar} originalFactory = options.factory;`,
51+
`options.factory = ${runtimeTemplate.basicFunction(
52+
['moduleObject', 'moduleExports', 'webpackRequire'],
53+
[
54+
// Our require function delegates to the original require function
55+
`${declareVar} hotRequire = ${runtimeTemplate.returningFunction(
56+
'webpackRequire(request)',
57+
'request'
58+
)};`,
59+
// The propery descriptor factory below ensures that all properties but $Refresh$ are proxied through to the original require function
60+
`${declareVar} createPropertyDescriptor = ${runtimeTemplate.basicFunction('name', [
61+
`return {`,
4962
webpack.Template.indent([
50-
`${runtimeTemplate.basicFunction('result', [
51-
`${refreshGlobal}.cleanup(options.id);`,
52-
'return result;',
63+
`configurable: true,`,
64+
`enumerable: true,`,
65+
`get: ${runtimeTemplate.returningFunction('webpackRequire[name]')},`,
66+
`set: ${runtimeTemplate.basicFunction('value', [
67+
'webpackRequire[name] = value;',
5368
])},`,
54-
runtimeTemplate.basicFunction('reason', [
55-
`${refreshGlobal}.cleanup(options.id);`,
56-
'return Promise.reject(reason);',
57-
]),
5869
]),
59-
`);`,
70+
`};`,
71+
])};`,
72+
`for (${declareVar} name in webpackRequire) {`,
73+
webpack.Template.indent([
74+
`if (Object.prototype.hasOwnProperty.call(webpackRequire, name) && name !== "$Refresh$") {`,
75+
webpack.Template.indent([
76+
`Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));`,
77+
]),
78+
`}`,
6079
]),
61-
'} else {',
62-
webpack.Template.indent(`${refreshGlobal}.cleanup(options.id)`),
63-
'}',
64-
]),
65-
'}',
66-
]),
67-
`};`,
80+
`}`,
81+
`hotRequire.$Refresh$ = setup(options.id);`,
82+
`originalFactory.call(this, moduleObject, moduleExports, hotRequire);`,
83+
]
84+
)};`,
6885
]
69-
)})`,
70-
'',
71-
getRefreshGlobal(webpack.Template, webpack.RuntimeGlobals, runtimeTemplate),
86+
)});`,
7287
]);
7388
}
7489
};

test/unit/makeRefreshRuntimeModule.test.js

Lines changed: 78 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -38,63 +38,45 @@ describe.skipIf(WEBPACK_VERSION !== 5, 'makeRefreshRuntimeModule', () => {
3838

3939
const runtime = instance.generate();
4040
expect(runtime).toMatchInlineSnapshot(`
41-
"__webpack_require__.i.push(function(options) {
42-
var originalFactory = options.factory;
43-
options.factory = function (moduleObject, moduleExports, webpackRequire) {
44-
__webpack_require__.$Refresh$.setup(options.id);
45-
try {
46-
originalFactory.call(this, moduleObject, moduleExports, webpackRequire);
47-
} finally {
48-
if (typeof Promise !== 'undefined' && moduleObject.exports instanceof Promise) {
49-
options.module.exports = options.module.exports.then(
50-
function(result) {
51-
__webpack_require__.$Refresh$.cleanup(options.id);
52-
return result;
53-
},
54-
function(reason) {
55-
__webpack_require__.$Refresh$.cleanup(options.id);
56-
return Promise.reject(reason);
57-
}
58-
);
59-
} else {
60-
__webpack_require__.$Refresh$.cleanup(options.id)
61-
}
62-
}
63-
};
64-
})
65-
66-
__webpack_require__.$Refresh$ = {
67-
register: function() { return undefined; },
68-
signature: function() { return function(type) { return type; }; },
69-
runtime: {
70-
createSignatureFunctionForTransform: function() { return function(type) { return type; }; },
71-
register: function() { return undefined; }
72-
},
73-
setup: function(currentModuleId) {
74-
var prevModuleId = __webpack_require__.$Refresh$.moduleId;
75-
var prevRegister = __webpack_require__.$Refresh$.register;
76-
var prevSignature = __webpack_require__.$Refresh$.signature;
77-
var prevCleanup = __webpack_require__.$Refresh$.cleanup;
78-
79-
__webpack_require__.$Refresh$.moduleId = currentModuleId;
80-
81-
__webpack_require__.$Refresh$.register = function(type, id) {
82-
var typeId = currentModuleId + " " + id;
83-
__webpack_require__.$Refresh$.runtime.register(type, typeId);
84-
}
85-
86-
__webpack_require__.$Refresh$.signature = function() { return __webpack_require__.$Refresh$.runtime.createSignatureFunctionForTransform(); };
87-
88-
__webpack_require__.$Refresh$.cleanup = function(cleanupModuleId) {
89-
if (currentModuleId === cleanupModuleId) {
90-
__webpack_require__.$Refresh$.moduleId = prevModuleId;
91-
__webpack_require__.$Refresh$.register = prevRegister;
92-
__webpack_require__.$Refresh$.signature = prevSignature;
93-
__webpack_require__.$Refresh$.cleanup = prevCleanup;
94-
}
95-
}
96-
}
97-
};"
41+
"var setup = function(moduleId) {
42+
var refresh = {
43+
moduleId: moduleId,
44+
register: function(type, id) {
45+
var typeId = moduleId + " " + id;
46+
refresh.runtime.register(type, typeId);
47+
},
48+
signature: function() { return refresh.runtime.createSignatureFunctionForTransform(); },
49+
runtime: {
50+
createSignatureFunctionForTransform: function() { return function(type) { return type; }; },
51+
register: function() {}
52+
},
53+
};
54+
return refresh;
55+
}
56+
57+
__webpack_require__.i.push(function(options) {
58+
var originalFactory = options.factory;
59+
options.factory = function(moduleObject,moduleExports,webpackRequire) {
60+
var hotRequire = function(request) { return webpackRequire(request); };
61+
var createPropertyDescriptor = function(name) {
62+
return {
63+
configurable: true,
64+
enumerable: true,
65+
get: function() { return webpackRequire[name]; },
66+
set: function(value) {
67+
webpackRequire[name] = value;
68+
},
69+
};
70+
};
71+
for (var name in webpackRequire) {
72+
if (Object.prototype.hasOwnProperty.call(webpackRequire, name) && name !== "$Refresh$") {
73+
Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
74+
}
75+
}
76+
hotRequire.$Refresh$ = setup(options.id);
77+
originalFactory.call(this, moduleObject, moduleExports, hotRequire);
78+
};
79+
});"
9880
`);
9981
expect(() => {
10082
eval(runtime);
@@ -117,63 +99,45 @@ describe.skipIf(WEBPACK_VERSION !== 5, 'makeRefreshRuntimeModule', () => {
11799

118100
const runtime = instance.generate();
119101
expect(runtime).toMatchInlineSnapshot(`
120-
"__webpack_require__.i.push((options) => {
121-
const originalFactory = options.factory;
122-
options.factory = function (moduleObject, moduleExports, webpackRequire) {
123-
__webpack_require__.$Refresh$.setup(options.id);
124-
try {
125-
originalFactory.call(this, moduleObject, moduleExports, webpackRequire);
126-
} finally {
127-
if (typeof Promise !== 'undefined' && moduleObject.exports instanceof Promise) {
128-
options.module.exports = options.module.exports.then(
129-
(result) => {
130-
__webpack_require__.$Refresh$.cleanup(options.id);
131-
return result;
132-
},
133-
(reason) => {
134-
__webpack_require__.$Refresh$.cleanup(options.id);
135-
return Promise.reject(reason);
136-
}
137-
);
138-
} else {
139-
__webpack_require__.$Refresh$.cleanup(options.id)
140-
}
141-
}
142-
};
143-
})
144-
145-
__webpack_require__.$Refresh$ = {
146-
register: () => (undefined),
147-
signature: () => ((type) => (type)),
148-
runtime: {
149-
createSignatureFunctionForTransform: () => ((type) => (type)),
150-
register: () => (undefined)
151-
},
152-
setup: (currentModuleId) => {
153-
const prevModuleId = __webpack_require__.$Refresh$.moduleId;
154-
const prevRegister = __webpack_require__.$Refresh$.register;
155-
const prevSignature = __webpack_require__.$Refresh$.signature;
156-
const prevCleanup = __webpack_require__.$Refresh$.cleanup;
157-
158-
__webpack_require__.$Refresh$.moduleId = currentModuleId;
159-
160-
__webpack_require__.$Refresh$.register = (type, id) => {
161-
const typeId = currentModuleId + " " + id;
162-
__webpack_require__.$Refresh$.runtime.register(type, typeId);
163-
}
164-
165-
__webpack_require__.$Refresh$.signature = () => (__webpack_require__.$Refresh$.runtime.createSignatureFunctionForTransform());
166-
167-
__webpack_require__.$Refresh$.cleanup = (cleanupModuleId) => {
168-
if (currentModuleId === cleanupModuleId) {
169-
__webpack_require__.$Refresh$.moduleId = prevModuleId;
170-
__webpack_require__.$Refresh$.register = prevRegister;
171-
__webpack_require__.$Refresh$.signature = prevSignature;
172-
__webpack_require__.$Refresh$.cleanup = prevCleanup;
173-
}
174-
}
175-
}
176-
};"
102+
"const setup = (moduleId) => {
103+
const refresh = {
104+
moduleId: moduleId,
105+
register: (type, id) => {
106+
const typeId = moduleId + " " + id;
107+
refresh.runtime.register(type, typeId);
108+
},
109+
signature: () => (refresh.runtime.createSignatureFunctionForTransform()),
110+
runtime: {
111+
createSignatureFunctionForTransform: () => ((type) => (type)),
112+
register: x => {}
113+
},
114+
};
115+
return refresh;
116+
}
117+
118+
__webpack_require__.i.push((options) => {
119+
const originalFactory = options.factory;
120+
options.factory = (moduleObject,moduleExports,webpackRequire) => {
121+
const hotRequire = (request) => (webpackRequire(request));
122+
const createPropertyDescriptor = (name) => {
123+
return {
124+
configurable: true,
125+
enumerable: true,
126+
get: () => (webpackRequire[name]),
127+
set: (value) => {
128+
webpackRequire[name] = value;
129+
},
130+
};
131+
};
132+
for (const name in webpackRequire) {
133+
if (Object.prototype.hasOwnProperty.call(webpackRequire, name) && name !== "$Refresh$") {
134+
Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
135+
}
136+
}
137+
hotRequire.$Refresh$ = setup(options.id);
138+
originalFactory.call(this, moduleObject, moduleExports, hotRequire);
139+
};
140+
});"
177141
`);
178142
expect(() => {
179143
eval(runtime);

0 commit comments

Comments
 (0)