Skip to content

Commit 0cc724c

Browse files
authored
update ReactFlightWebpackPlugin to be compatiable with webpack v5 (#22739)
1 parent ca106a0 commit 0cc724c

File tree

2 files changed

+156
-93
lines changed

2 files changed

+156
-93
lines changed

packages/react-server-dom-webpack/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"peerDependencies": {
5151
"react": "^17.0.0",
5252
"react-dom": "^17.0.0",
53-
"webpack": "^4.43.0"
53+
"webpack": "^5.59.0"
5454
},
5555
"dependencies": {
5656
"acorn": "^6.2.1",

packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js

+155-92
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ import asyncLib from 'neo-async';
1414

1515
import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
1616
import NullDependency from 'webpack/lib/dependencies/NullDependency';
17-
import AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock';
1817
import Template from 'webpack/lib/Template';
18+
import {
19+
sources,
20+
WebpackError,
21+
Compilation,
22+
AsyncDependenciesBlock,
23+
} from 'webpack';
1924

2025
import isArray from 'shared/isArray';
2126

@@ -34,6 +39,7 @@ class ClientReferenceDependency extends ModuleDependency {
3439
// We use the Flight client implementation because you can't get to these
3540
// without the client runtime so it's the first time in the loading sequence
3641
// you might want them.
42+
const clientImportName = 'react-server-dom-webpack';
3743
const clientFileName = require.resolve('../');
3844

3945
type ClientReferenceSearchPath = {
@@ -97,33 +103,35 @@ export default class ReactFlightWebpackPlugin {
97103
}
98104

99105
apply(compiler: any) {
106+
const _this = this;
100107
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+
);
123133

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(
127135
PLUGIN_NAME,
128136
(compilation, {normalModuleFactory}) => {
129137
compilation.dependencyFactories.set(
@@ -135,86 +143,140 @@ export default class ReactFlightWebpackPlugin {
135143
new NullDependency.Template(),
136144
);
137145

138-
compilation.hooks.buildModule.tap(PLUGIN_NAME, module => {
146+
const handler = parser => {
139147
// We need to add all client references as dependency of something in the graph so
140148
// Webpack knows which entries need to know about the relevant chunks and include the
141149
// map in their runtime. The things that actually resolves the dependency is the Flight
142150
// client runtime. So we add them as a dependency of the Flight client runtime.
143151
// 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;
165157
}
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);
168195
},
169196
);
170197

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+
);
181211
return;
182212
}
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-
}
196213

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+
}
205252
}
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+
});
206271
});
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+
);
216278
},
217-
};
279+
);
218280
});
219281
}
220282

@@ -268,7 +330,8 @@ export default class ReactFlightWebpackPlugin {
268330
(err2: null | Error, deps: Array<ModuleDependency>) => {
269331
if (err2) return cb(err2);
270332
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);
272335
const clientRefDep = new ClientReferenceDependency(request);
273336
clientRefDep.userRequest = dep.userRequest;
274337
return clientRefDep;

0 commit comments

Comments
 (0)