@samitouri / QOS-React-2 / commits / 0cc724c777

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;