update ReactFlightWebpackPlugin to be compatiable with webpack v5 (#22739)
Michelle Chen committed
Nov 25, 2021 at 17:43 UTC
0cc724c777930438d92727803826a44ec97bcc55
2 files changed
+156
-93
packages/react-server-dom-webpack/package.json
+1
-1
@@ -50,7 +50,7 @@
50
"peerDependencies": {
51
"react": "^17.0.0",
52
"react-dom": "^17.0.0",
53
- "webpack": "^4.43.0"
53
+ "webpack": "^5.59.0"
54
},
55
"dependencies": {
56
"acorn": "^6.2.1",
packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js
+155
-92
@@ -14,8 +14,13 @@ import asyncLib from 'neo-async';
14
15
import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
16
import NullDependency from 'webpack/lib/dependencies/NullDependency';
17
-import AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock';
17
import Template from 'webpack/lib/Template';
18
+import {
19
+ sources,
20
+ WebpackError,
21
+ Compilation,
22
+ AsyncDependenciesBlock,
23
+} from 'webpack';
24
25
import isArray from 'shared/isArray';
26
@@ -34,6 +39,7 @@ class ClientReferenceDependency extends ModuleDependency {
39
// We use the Flight client implementation because you can't get to these
40
// without the client runtime so it's the first time in the loading sequence
41
// you might want them.
42
+const clientImportName = 'react-server-dom-webpack';
43
const clientFileName = require.resolve('../');
44
45
type ClientReferenceSearchPath = {
@@ -97,33 +103,35 @@ export default class ReactFlightWebpackPlugin {
103
}
104
105
apply(compiler: any) {
106
+ const _this = this;
107
let resolvedClientReferences;
101
- const run = (params, callback) => {
102
- // First we need to find all client files on the file system. We do this early so
103
- // that we have them synchronously available later when we need them. This might
104
- // not be needed anymore since we no longer need to compile the module itself in
105
- // a special way. So it's probably better to do this lazily and in parallel with
106
- // other compilation.
107
- const contextResolver = compiler.resolverFactory.get('context', {});
108
- this.resolveAllClientFiles(
109
- compiler.context,
110
- contextResolver,
111
- compiler.inputFileSystem,
112
- compiler.createContextModuleFactory(),
113
- (err, resolvedClientRefs) => {
114
- if (err) {
115
- callback(err);
116
- return;
117
- }
118
- resolvedClientReferences = resolvedClientRefs;
119
- callback();
120
- },
121
- );
122
- };
108
+ let clientFileNameFound = false;
109
+
110
+ // Find all client files on the file system
111
+ compiler.hooks.beforeCompile.tapAsync(
112
+ PLUGIN_NAME,
113
+ ({contextModuleFactory}, callback) => {
114
+ const contextResolver = compiler.resolverFactory.get('context', {});
115
+
116
+ _this.resolveAllClientFiles(
117
+ compiler.context,
118
+ contextResolver,
119
+ compiler.inputFileSystem,
120
+ contextModuleFactory,
121
+ function(err, resolvedClientRefs) {
122
+ if (err) {
123
+ callback(err);
124
+ return;
125
+ }
126
+
127
+ resolvedClientReferences = resolvedClientRefs;
128
+ callback();
129
+ },
130
+ );
131
+ },
132
+ );
133
124
- compiler.hooks.run.tapAsync(PLUGIN_NAME, run);
125
- compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, run);
126
- compiler.hooks.compilation.tap(
134
+ compiler.hooks.thisCompilation.tap(
135
PLUGIN_NAME,
136
(compilation, {normalModuleFactory}) => {
137
compilation.dependencyFactories.set(
@@ -135,86 +143,140 @@ export default class ReactFlightWebpackPlugin {
143
new NullDependency.Template(),
144
);
145
138
- compilation.hooks.buildModule.tap(PLUGIN_NAME, module => {
146
+ const handler = parser => {
147
// We need to add all client references as dependency of something in the graph so
148
// Webpack knows which entries need to know about the relevant chunks and include the
149
// map in their runtime. The things that actually resolves the dependency is the Flight
150
// client runtime. So we add them as a dependency of the Flight client runtime.
151
// Anything that imports the runtime will be made aware of these chunks.
144
- // TODO: Warn if we don't find this file anywhere in the compilation.
145
- if (module.resource !== clientFileName) {
146
- return;
147
- }
148
- if (resolvedClientReferences) {
149
- for (let i = 0; i < resolvedClientReferences.length; i++) {
150
- const dep = resolvedClientReferences[i];
151
- const chunkName = this.chunkName
152
- .replace(/\[index\]/g, '' + i)
153
- .replace(/\[request\]/g, Template.toPath(dep.userRequest));
154
-
155
- const block = new AsyncDependenciesBlock(
156
- {
157
- name: chunkName,
158
- },
159
- module,
160
- null,
161
- dep.require,
162
- );
163
- block.addDependency(dep);
164
- module.addBlock(block);
152
+ parser.hooks.program.tap(PLUGIN_NAME, () => {
153
+ const module = parser.state.module;
154
+
155
+ if (module.resource !== clientFileName) {
156
+ return;
157
}
166
- }
167
- });
158
+
159
+ clientFileNameFound = true;
160
+
161
+ if (resolvedClientReferences) {
162
+ for (let i = 0; i < resolvedClientReferences.length; i++) {
163
+ const dep = resolvedClientReferences[i];
164
+
165
+ const chunkName = _this.chunkName
166
+ .replace(/\[index\]/g, '' + i)
167
+ .replace(/\[request\]/g, Template.toPath(dep.userRequest));
168
+
169
+ const block = new AsyncDependenciesBlock(
170
+ {
171
+ name: chunkName,
172
+ },
173
+ null,
174
+ dep.request,
175
+ );
176
+
177
+ block.addDependency(dep);
178
+ module.addBlock(block);
179
+ }
180
+ }
181
+ });
182
+ };
183
+
184
+ normalModuleFactory.hooks.parser
185
+ .for('javascript/auto')
186
+ .tap('HarmonyModulesPlugin', handler);
187
+
188
+ normalModuleFactory.hooks.parser
189
+ .for('javascript/esm')
190
+ .tap('HarmonyModulesPlugin', handler);
191
+
192
+ normalModuleFactory.hooks.parser
193
+ .for('javascript/dynamic')
194
+ .tap('HarmonyModulesPlugin', handler);
195
},
196
);
197
171
- compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
172
- const json = {};
173
- compilation.chunkGroups.forEach(chunkGroup => {
174
- const chunkIds = chunkGroup.chunks.map(c => c.id);
175
-
176
- function recordModule(id, mod) {
177
- // TODO: Hook into deps instead of the target module.
178
- // That way we know by the type of dep whether to include.
179
- // It also resolves conflicts when the same module is in multiple chunks.
180
- if (!/\.client\.js$/.test(mod.resource)) {
198
+ compiler.hooks.make.tap(PLUGIN_NAME, compilation => {
199
+ compilation.hooks.processAssets.tap(
200
+ {
201
+ name: PLUGIN_NAME,
202
+ stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
203
+ },
204
+ function() {
205
+ if (clientFileNameFound === false) {
206
+ compilation.warnings.push(
207
+ new WebpackError(
208
+ `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`,
209
+ ),
210
+ );
211
return;
212
}
183
- const moduleExports = {};
184
- ['', '*'].concat(mod.buildMeta.providedExports).forEach(name => {
185
- moduleExports[name] = {
186
- id: id,
187
- chunks: chunkIds,
188
- name: name,
189
- };
190
- });
191
- const href = pathToFileURL(mod.resource).href;
192
- if (href !== undefined) {
193
- json[href] = moduleExports;
194
- }
195
- }
213
197
- chunkGroup.chunks.forEach(chunk => {
198
- chunk.getModules().forEach(mod => {
199
- recordModule(mod.id, mod);
200
- // If this is a concatenation, register each child to the parent ID.
201
- if (mod.modules) {
202
- mod.modules.forEach(concatenatedMod => {
203
- recordModule(mod.id, concatenatedMod);
204
- });
214
+ const json = {};
215
+ compilation.chunkGroups.forEach(function(chunkGroup) {
216
+ const chunkIds = chunkGroup.chunks.map(function(c) {
217
+ return c.id;
218
+ });
219
+
220
+ function recordModule(id, module) {
221
+ // TODO: Hook into deps instead of the target module.
222
+ // That way we know by the type of dep whether to include.
223
+ // It also resolves conflicts when the same module is in multiple chunks.
224
+
225
+ if (!/\.client\.(js|ts)x?$/.test(module.resource)) {
226
+ return;
227
+ }
228
+
229
+ const moduleProvidedExports = compilation.moduleGraph
230
+ .getExportsInfo(module)
231
+ .getProvidedExports();
232
+
233
+ const moduleExports = {};
234
+ ['', '*']
235
+ .concat(
236
+ Array.isArray(moduleProvidedExports)
237
+ ? moduleProvidedExports
238
+ : [],
239
+ )
240
+ .forEach(function(name) {
241
+ moduleExports[name] = {
242
+ id,
243
+ chunks: chunkIds,
244
+ name: name,
245
+ };
246
+ });
247
+ const href = pathToFileURL(module.resource).href;
248
+
249
+ if (href !== undefined) {
250
+ json[href] = moduleExports;
251
+ }
252
}
253
+
254
+ chunkGroup.chunks.forEach(function(chunk) {
255
+ const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
256
+ chunk,
257
+ );
258
+
259
+ Array.from(chunkModules).forEach(function(module) {
260
+ const moduleId = compilation.chunkGraph.getModuleId(module);
261
+
262
+ recordModule(moduleId, module);
263
+ // If this is a concatenation, register each child to the parent ID.
264
+ if (module.modules) {
265
+ module.modules.forEach(concatenatedMod => {
266
+ recordModule(moduleId, concatenatedMod);
267
+ });
268
+ }
269
+ });
270
+ });
271
});
207
- });
208
- });
209
- const output = JSON.stringify(json, null, 2);
210
- compilation.assets[this.manifestFilename] = {
211
- source() {
212
- return output;
213
- },
214
- size() {
215
- return output.length;
272
+
273
+ const output = JSON.stringify(json, null, 2);
274
+ compilation.emitAsset(
275
+ _this.manifestFilename,
276
+ new sources.RawSource(output, false),
277
+ );
278
},
217
- };
279
+ );
280
});
281
}
282
@@ -268,7 +330,8 @@ export default class ReactFlightWebpackPlugin {
330
(err2: null | Error, deps: Array<ModuleDependency>) => {
331
if (err2) return cb(err2);
332
const clientRefDeps = deps.map(dep => {
271
- const request = join(resolvedDirectory, dep.request);
333
+ // use userRequest instead of request. request always end with undefined which is wrong
334
+ const request = join(resolvedDirectory, dep.userRequest);
335
const clientRefDep = new ClientReferenceDependency(request);
336
clientRefDep.userRequest = dep.userRequest;
337
return clientRefDep;