@samitouri / QOS-React-2 / commits / 30dfb86025

[Flight] Basic scan of the file system to find Client modules (#20383)

* Basic scan of the file system to find Client modules This does a rudimentary merge of the plugins. It still uses the global scan and writes to file system. Now the plugin accepts a search path or a list of referenced client files. In prod, the best practice is to provide a list of files that are actually referenced rather than including everything possibly reachable. Probably in dev too since it's faster. This is using the same convention as the upstream ContextModule - which powers the require.context helpers. * Add neo-async to dependencies

Sebastian Markbåge committed Dec 7, 2020 at 12:10 UTC 30dfb860259dea126b3f43ab270ec51593824dd8
5 files changed +241 -9
fixtures/flight/config/webpack.config.js
+8 -1
@@ -664,7 +664,14 @@ module.exports = function(webpackEnv) {
664 formatter: isEnvProduction ? typescriptFormatter : undefined,
665 }),
666 // Fork Start
667 - new ReactFlightWebpackPlugin({isServer: false}),
667 + new ReactFlightWebpackPlugin({
668 + isServer: false,
669 + clientReferences: {
670 + directory: './src/',
671 + recursive: true,
672 + include: /\.client\.js$/,
673 + },
674 + }),
675 // Fork End
676 ].filter(Boolean),
677 // Some libraries import Node modules but don't use them in the browser.
fixtures/flight/src/index.js
-4
@@ -17,7 +17,3 @@ ReactDOM.render(
17 </Suspense>,
18 document.getElementById('root')
19 );
20 -
21 -// Create entry points for Client Components.
22 -// TODO: Webpack plugin should do this.
23 -require.context('./', true, /\.client\.js$/, 'lazy');
packages/react-transport-dom-webpack/package.json
+1
@@ -51,6 +51,7 @@
51 },
52 "dependencies": {
53 "acorn": "^6.2.1",
54 + "neo-async": "^2.6.1",
55 "loose-envify": "^1.1.0",
56 "object-assign": "^4.1.1"
57 },
packages/react-transport-dom-webpack/src/ReactFlightWebpackPlugin.js
+231 -3
@@ -8,17 +8,169 @@
8 */
9
10 import {mkdirSync, writeFileSync} from 'fs';
11 -import {dirname, resolve} from 'path';
11 +import {dirname, resolve, join} from 'path';
12 import {pathToFileURL} from 'url';
13
14 +import asyncLib from 'neo-async';
15 +
16 +import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
17 +import NullDependency from 'webpack/lib/dependencies/NullDependency';
18 +import AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock';
19 +import Template from 'webpack/lib/Template';
20 +
21 +class ClientReferenceDependency extends ModuleDependency {
22 + constructor(request) {
23 + super(request);
24 + }
25 +
26 + get type() {
27 + return 'client-reference';
28 + }
29 +}
30 +
31 +// This is the module that will be used to anchor all client references to.
32 +// I.e. it will have all the client files as async deps from this point on.
33 +// We use the Flight client implementation because you can't get to these
34 +// without the client runtime so it's the first time in the loading sequence
35 +// you might want them.
36 +const clientFileName = require.resolve('../');
37 +
38 +type ClientReferenceSearchPath = {
39 + directory: string,
40 + recursive?: boolean,
41 + include: RegExp,
42 + exclude?: RegExp,
43 +};
44 +
45 +type ClientReferencePath = string | ClientReferenceSearchPath;
46 +
47 +type Options = {
48 + isServer: boolean,
49 + clientReferences?: ClientReferencePath | $ReadOnlyArray<ClientReferencePath>,
50 + chunkName?: string,
51 +};
52 +
53 +const PLUGIN_NAME = 'React Transport Plugin';
54 +
55 export default class ReactFlightWebpackPlugin {
15 - constructor(options: {isServer: boolean}) {}
56 + clientReferences: $ReadOnlyArray<ClientReferencePath>;
57 + chunkName: string;
58 + constructor(options: Options) {
59 + if (!options || typeof options.isServer !== 'boolean') {
60 + throw new Error(
61 + PLUGIN_NAME + ': You must specify the isServer option as a boolean.',
62 + );
63 + }
64 + if (options.isServer) {
65 + throw new Error('TODO: Implement the server compiler.');
66 + }
67 + if (!options.clientReferences) {
68 + this.clientReferences = [
69 + {
70 + directory: '.',
71 + recursive: true,
72 + include: /\.client\.(js|ts|jsx|tsx)$/,
73 + },
74 + ];
75 + } else if (
76 + typeof options.clientReferences === 'string' ||
77 + !Array.isArray(options.clientReferences)
78 + ) {
79 + this.clientReferences = [(options.clientReferences: $FlowFixMe)];
80 + } else {
81 + this.clientReferences = options.clientReferences;
82 + }
83 + if (typeof options.chunkName === 'string') {
84 + this.chunkName = options.chunkName;
85 + if (!/\[(index|request)\]/.test(this.chunkName)) {
86 + this.chunkName += '[index]';
87 + }
88 + } else {
89 + this.chunkName = 'client[index]';
90 + }
91 + }
92
93 apply(compiler: any) {
18 - compiler.hooks.emit.tap('React Transport Plugin', compilation => {
94 + const run = (params, callback) => {
95 + // First we need to find all client files on the file system. We do this early so
96 + // that we have them synchronously available later when we need them. This might
97 + // not be needed anymore since we no longer need to compile the module itself in
98 + // a special way. So it's probably better to do this lazily and in parallel with
99 + // other compilation.
100 + const contextResolver = compiler.resolverFactory.get('context', {});
101 + this.resolveAllClientFiles(
102 + compiler.context,
103 + contextResolver,
104 + compiler.inputFileSystem,
105 + compiler.createContextModuleFactory(),
106 + (err, resolvedClientReferences) => {
107 + if (err) {
108 + callback(err);
109 + return;
110 + }
111 + compiler.hooks.compilation.tap(
112 + PLUGIN_NAME,
113 + (compilation, {normalModuleFactory}) => {
114 + compilation.dependencyFactories.set(
115 + ClientReferenceDependency,
116 + normalModuleFactory,
117 + );
118 + compilation.dependencyTemplates.set(
119 + ClientReferenceDependency,
120 + new NullDependency.Template(),
121 + );
122 +
123 + compilation.hooks.buildModule.tap(PLUGIN_NAME, module => {
124 + // We need to add all client references as dependency of something in the graph so
125 + // Webpack knows which entries need to know about the relevant chunks and include the
126 + // map in their runtime. The things that actually resolves the dependency is the Flight
127 + // client runtime. So we add them as a dependency of the Flight client runtime.
128 + // Anything that imports the runtime will be made aware of these chunks.
129 + // TODO: Warn if we don't find this file anywhere in the compilation.
130 + if (module.resource !== clientFileName) {
131 + return;
132 + }
133 + if (resolvedClientReferences) {
134 + for (let i = 0; i < resolvedClientReferences.length; i++) {
135 + const dep = resolvedClientReferences[i];
136 + const chunkName = this.chunkName
137 + .replace(/\[index\]/g, '' + i)
138 + .replace(
139 + /\[request\]/g,
140 + Template.toPath(dep.userRequest),
141 + );
142 +
143 + const block = new AsyncDependenciesBlock(
144 + {
145 + name: chunkName,
146 + },
147 + module,
148 + null,
149 + dep.require,
150 + );
151 + block.addDependency(dep);
152 + module.addBlock(block);
153 + }
154 + }
155 + });
156 + },
157 + );
158 +
159 + callback();
160 + },
161 + );
162 + };
163 +
164 + compiler.hooks.run.tapAsync(PLUGIN_NAME, run);
165 + compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, run);
166 +
167 + compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
168 const json = {};
169 compilation.chunks.forEach(chunk => {
170 chunk.getModules().forEach(mod => {
171 + // TOOD: Hook into deps instead of the target module.
172 + // That way we know by the type of dep whether to include.
173 + // It also resolves conflicts when the same module is in multiple chunks.
174 if (!/\.client\.js$/.test(mod.resource)) {
175 return;
176 }
@@ -42,7 +194,83 @@ export default class ReactFlightWebpackPlugin {
194 'react-transport-manifest.json',
195 );
196 mkdirSync(dirname(filename), {recursive: true});
197 + // TODO: Use webpack's emit API and read from the devserver.
198 writeFileSync(filename, output);
199 });
200 }
201 +
202 + // This attempts to replicate the dynamic file path resolution used for other wildcard
203 + // resolution in Webpack is using.
204 + resolveAllClientFiles(
205 + context: string,
206 + contextResolver: any,
207 + fs: any,
208 + contextModuleFactory: any,
209 + callback: (
210 + err: null | Error,
211 + result?: $ReadOnlyArray<ClientReferenceDependency>,
212 + ) => void,
213 + ) {
214 + asyncLib.map(
215 + this.clientReferences,
216 + (
217 + clientReferencePath: string | ClientReferenceSearchPath,
218 + cb: (
219 + err: null | Error,
220 + result?: $ReadOnlyArray<ClientReferenceDependency>,
221 + ) => void,
222 + ): void => {
223 + if (typeof clientReferencePath === 'string') {
224 + cb(null, [new ClientReferenceDependency(clientReferencePath)]);
225 + return;
226 + }
227 + const clientReferenceSearch: ClientReferenceSearchPath = clientReferencePath;
228 + contextResolver.resolve(
229 + {},
230 + context,
231 + clientReferencePath.directory,
232 + {},
233 + (err, resolvedDirectory) => {
234 + if (err) return cb(err);
235 + const options = {
236 + resource: resolvedDirectory,
237 + resourceQuery: '',
238 + recursive:
239 + clientReferenceSearch.recursive === undefined
240 + ? true
241 + : clientReferenceSearch.recursive,
242 + regExp: clientReferenceSearch.include,
243 + include: undefined,
244 + exclude: clientReferenceSearch.exclude,
245 + };
246 + contextModuleFactory.resolveDependencies(
247 + fs,
248 + options,
249 + (err2: null | Error, deps: Array<ModuleDependency>) => {
250 + if (err2) return cb(err2);
251 + const clientRefDeps = deps.map(dep => {
252 + const request = join(resolvedDirectory, dep.request);
253 + const clientRefDep = new ClientReferenceDependency(request);
254 + clientRefDep.userRequest = dep.userRequest;
255 + return clientRefDep;
256 + });
257 + cb(null, clientRefDeps);
258 + },
259 + );
260 + },
261 + );
262 + },
263 + (
264 + err: null | Error,
265 + result: $ReadOnlyArray<$ReadOnlyArray<ClientReferenceDependency>>,
266 + ): void => {
267 + if (err) return callback(err);
268 + const flat = [];
269 + for (let i = 0; i < result.length; i++) {
270 + flat.push.apply(flat, result[i]);
271 + }
272 + callback(null, flat);
273 + },
274 + );
275 + }
276 }
scripts/rollup/bundles.js
+1 -1
@@ -301,7 +301,7 @@ const bundles = [
301 moduleType: RENDERER_UTILS,
302 entry: 'react-transport-dom-webpack/plugin',
303 global: 'ReactFlightWebpackPlugin',
304 - externals: ['fs', 'path', 'url'],
304 + externals: ['fs', 'path', 'url', 'neo-async'],
305 },
306
307 /******* React Transport DOM Webpack Node.js Loader *******/