Skip to content

Commit 4470b37

Browse files
gaearonAndyPengc12
authored andcommitted
[Flight Plugin] Scan for "use client" (facebook#26474)
## Summary Our toy webpack plugin for Server Components is pretty broken right now because, now that `.client.js` convention is gone, it ends up adding every single JS file it can find (including `node_modules`) as a potential async dependency. Instead, it should only look for files with the `'use client'` directive. The ideal way is to implement this by bundling the RSC graph first. Then, we would know which `'use client'` files were actually discovered — and so there would be no point to scanning the disk for them. That's how Next.js bundler does it. We're not doing that here. This toy plugin is very simple, and I'm not planning to do heavy lifting. I'm just bringing it up to date with the convention. The change is that we now read every file we discover (alas), bail if it has no `'use client'`, and parse it if it does (to verify it's actually used as a directive). I've changed to use `acorn-loose` because it's forgiving of JSX (and likely TypeScript/Flow). Otherwise, this wouldn't work on uncompiled source. ## Test plan Verified I can get our initial Server Components Demo running after this change. Previously, it would get stuck compiling and then emit thousands of errors. Also confirmed the fixture still works. (It doesn’t work correctly on the first load after dev server starts, but that’s already the case on main so seems unrelated.)
1 parent 8ba5925 commit 4470b37

File tree

5 files changed

+79
-9
lines changed

5 files changed

+79
-9
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
"webpack": "^5.59.0"
8585
},
8686
"dependencies": {
87-
"acorn": "^6.2.1",
87+
"acorn-loose": "^8.3.0",
8888
"neo-async": "^2.6.1",
8989
"loose-envify": "^1.1.0"
9090
},

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import * as acorn from 'acorn';
10+
import * as acorn from 'acorn-loose';
1111

1212
type ResolveContext = {
1313
conditions: Array<string>,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
const acorn = require('acorn');
10+
const acorn = require('acorn-loose');
1111

1212
const url = require('url');
1313

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

+67-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
import {join} from 'path';
1111
import {pathToFileURL} from 'url';
12-
1312
import asyncLib from 'neo-async';
13+
import * as acorn from 'acorn-loose';
1414

1515
import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
1616
import NullDependency from 'webpack/lib/dependencies/NullDependency';
@@ -117,10 +117,12 @@ export default class ReactFlightWebpackPlugin {
117117
PLUGIN_NAME,
118118
({contextModuleFactory}, callback) => {
119119
const contextResolver = compiler.resolverFactory.get('context', {});
120+
const normalResolver = compiler.resolverFactory.get('normal');
120121

121122
_this.resolveAllClientFiles(
122123
compiler.context,
123124
contextResolver,
125+
normalResolver,
124126
compiler.inputFileSystem,
125127
contextModuleFactory,
126128
function (err, resolvedClientRefs) {
@@ -219,6 +221,10 @@ export default class ReactFlightWebpackPlugin {
219221
return;
220222
}
221223

224+
const resolvedClientFiles = new Set(
225+
(resolvedClientReferences || []).map(ref => ref.request),
226+
);
227+
222228
const clientManifest: {
223229
[string]: {chunks: $FlowFixMe, id: string, name: string},
224230
} = {};
@@ -237,8 +243,7 @@ export default class ReactFlightWebpackPlugin {
237243
// TODO: Hook into deps instead of the target module.
238244
// That way we know by the type of dep whether to include.
239245
// It also resolves conflicts when the same module is in multiple chunks.
240-
241-
if (!/\.(js|ts)x?$/.test(module.resource)) {
246+
if (!resolvedClientFiles.has(module.resource)) {
242247
return;
243248
}
244249

@@ -328,13 +333,39 @@ export default class ReactFlightWebpackPlugin {
328333
resolveAllClientFiles(
329334
context: string,
330335
contextResolver: any,
336+
normalResolver: any,
331337
fs: any,
332338
contextModuleFactory: any,
333339
callback: (
334340
err: null | Error,
335341
result?: $ReadOnlyArray<ClientReferenceDependency>,
336342
) => void,
337343
) {
344+
function hasUseClientDirective(source: string): boolean {
345+
if (source.indexOf('use client') === -1) {
346+
return false;
347+
}
348+
let body;
349+
try {
350+
body = acorn.parse(source, {
351+
ecmaVersion: '2024',
352+
sourceType: 'module',
353+
}).body;
354+
} catch (x) {
355+
return false;
356+
}
357+
for (let i = 0; i < body.length; i++) {
358+
const node = body[i];
359+
if (node.type !== 'ExpressionStatement' || !node.directive) {
360+
break;
361+
}
362+
if (node.directive === 'use client') {
363+
return true;
364+
}
365+
}
366+
return false;
367+
}
368+
338369
asyncLib.map(
339370
this.clientReferences,
340371
(
@@ -373,14 +404,46 @@ export default class ReactFlightWebpackPlugin {
373404
options,
374405
(err2: null | Error, deps: Array<any /*ModuleDependency*/>) => {
375406
if (err2) return cb(err2);
407+
376408
const clientRefDeps = deps.map(dep => {
377409
// use userRequest instead of request. request always end with undefined which is wrong
378410
const request = join(resolvedDirectory, dep.userRequest);
379411
const clientRefDep = new ClientReferenceDependency(request);
380412
clientRefDep.userRequest = dep.userRequest;
381413
return clientRefDep;
382414
});
383-
cb(null, clientRefDeps);
415+
416+
asyncLib.filter(
417+
clientRefDeps,
418+
(
419+
clientRefDep: ClientReferenceDependency,
420+
filterCb: (err: null | Error, truthValue: boolean) => void,
421+
) => {
422+
normalResolver.resolve(
423+
{},
424+
context,
425+
clientRefDep.request,
426+
{},
427+
(err3: null | Error, resolvedPath: mixed) => {
428+
if (err3 || typeof resolvedPath !== 'string') {
429+
return filterCb(null, false);
430+
}
431+
fs.readFile(
432+
resolvedPath,
433+
'utf-8',
434+
(err4: null | Error, content: string) => {
435+
if (err4 || typeof content !== 'string') {
436+
return filterCb(null, false);
437+
}
438+
const useClient = hasUseClientDirective(content);
439+
filterCb(null, useClient);
440+
},
441+
);
442+
},
443+
);
444+
},
445+
cb,
446+
);
384447
},
385448
);
386449
},

yarn.lock

+9-2
Original file line numberDiff line numberDiff line change
@@ -3434,12 +3434,19 @@ acorn-jsx@^5.3.1:
34343434
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
34353435
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
34363436

3437+
acorn-loose@^8.3.0:
3438+
version "8.3.0"
3439+
resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.3.0.tgz#0cd62461d21dce4f069785f8d3de136d5525029a"
3440+
integrity sha512-75lAs9H19ldmW+fAbyqHdjgdCrz0pWGXKmnqFoh8PyVd1L2RIb4RzYrSjmopeqv3E1G3/Pimu6GgLlrGbrkF7w==
3441+
dependencies:
3442+
acorn "^8.5.0"
3443+
34373444
acorn-walk@^8.0.2:
34383445
version "8.2.0"
34393446
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
34403447
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
34413448

3442-
acorn@^6.0.7, acorn@^6.2.1, acorn@^6.4.1:
3449+
acorn@^6.0.7, acorn@^6.4.1:
34433450
version "6.4.2"
34443451
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
34453452
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
@@ -3449,7 +3456,7 @@ acorn@^7.1.1, acorn@^7.4.0:
34493456
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
34503457
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
34513458

3452-
acorn@^8.1.0, acorn@^8.8.1:
3459+
acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.1:
34533460
version "8.8.2"
34543461
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
34553462
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==

0 commit comments

Comments
 (0)