From 1308e49a6923d0dfd935dcd12cc420ec57239981 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 30 Mar 2023 22:05:03 +0100 Subject: [PATCH] [Flight Plugin] Scan for "use client" (#26474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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.) --- .../react-server-dom-webpack/package.json | 2 +- .../src/ReactFlightWebpackNodeLoader.js | 2 +- .../src/ReactFlightWebpackNodeRegister.js | 2 +- .../src/ReactFlightWebpackPlugin.js | 71 +++++++++++++++++-- yarn.lock | 11 ++- 5 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/react-server-dom-webpack/package.json b/packages/react-server-dom-webpack/package.json index f32d8cdeefd7d..4075fdec420dd 100644 --- a/packages/react-server-dom-webpack/package.json +++ b/packages/react-server-dom-webpack/package.json @@ -84,7 +84,7 @@ "webpack": "^5.59.0" }, "dependencies": { - "acorn": "^6.2.1", + "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "loose-envify": "^1.1.0" }, diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js index dae810af0a765..1e03a453474c1 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -7,7 +7,7 @@ * @flow */ -import * as acorn from 'acorn'; +import * as acorn from 'acorn-loose'; type ResolveContext = { conditions: Array, diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js index fb7cbfd271968..3b4dec28aced4 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -7,7 +7,7 @@ * @flow */ -const acorn = require('acorn'); +const acorn = require('acorn-loose'); const url = require('url'); diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js index 5b19ee7d6f9a2..298bd4d9b7250 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js @@ -9,8 +9,8 @@ import {join} from 'path'; import {pathToFileURL} from 'url'; - import asyncLib from 'neo-async'; +import * as acorn from 'acorn-loose'; import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency'; import NullDependency from 'webpack/lib/dependencies/NullDependency'; @@ -117,10 +117,12 @@ export default class ReactFlightWebpackPlugin { PLUGIN_NAME, ({contextModuleFactory}, callback) => { const contextResolver = compiler.resolverFactory.get('context', {}); + const normalResolver = compiler.resolverFactory.get('normal'); _this.resolveAllClientFiles( compiler.context, contextResolver, + normalResolver, compiler.inputFileSystem, contextModuleFactory, function (err, resolvedClientRefs) { @@ -219,6 +221,10 @@ export default class ReactFlightWebpackPlugin { return; } + const resolvedClientFiles = new Set( + (resolvedClientReferences || []).map(ref => ref.request), + ); + const clientManifest: { [string]: {chunks: $FlowFixMe, id: string, name: string}, } = {}; @@ -237,8 +243,7 @@ export default class ReactFlightWebpackPlugin { // TODO: Hook into deps instead of the target module. // That way we know by the type of dep whether to include. // It also resolves conflicts when the same module is in multiple chunks. - - if (!/\.(js|ts)x?$/.test(module.resource)) { + if (!resolvedClientFiles.has(module.resource)) { return; } @@ -328,6 +333,7 @@ export default class ReactFlightWebpackPlugin { resolveAllClientFiles( context: string, contextResolver: any, + normalResolver: any, fs: any, contextModuleFactory: any, callback: ( @@ -335,6 +341,31 @@ export default class ReactFlightWebpackPlugin { result?: $ReadOnlyArray, ) => void, ) { + function hasUseClientDirective(source: string): boolean { + if (source.indexOf('use client') === -1) { + return false; + } + let body; + try { + body = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + return false; + } + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + return true; + } + } + return false; + } + asyncLib.map( this.clientReferences, ( @@ -373,6 +404,7 @@ export default class ReactFlightWebpackPlugin { options, (err2: null | Error, deps: Array) => { if (err2) return cb(err2); + const clientRefDeps = deps.map(dep => { // use userRequest instead of request. request always end with undefined which is wrong const request = join(resolvedDirectory, dep.userRequest); @@ -380,7 +412,38 @@ export default class ReactFlightWebpackPlugin { clientRefDep.userRequest = dep.userRequest; return clientRefDep; }); - cb(null, clientRefDeps); + + asyncLib.filter( + clientRefDeps, + ( + clientRefDep: ClientReferenceDependency, + filterCb: (err: null | Error, truthValue: boolean) => void, + ) => { + normalResolver.resolve( + {}, + context, + clientRefDep.request, + {}, + (err3: null | Error, resolvedPath: mixed) => { + if (err3 || typeof resolvedPath !== 'string') { + return filterCb(null, false); + } + fs.readFile( + resolvedPath, + 'utf-8', + (err4: null | Error, content: string) => { + if (err4 || typeof content !== 'string') { + return filterCb(null, false); + } + const useClient = hasUseClientDirective(content); + filterCb(null, useClient); + }, + ); + }, + ); + }, + cb, + ); }, ); }, diff --git a/yarn.lock b/yarn.lock index faff86c0e5713..3c8628af7f140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3434,12 +3434,19 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-loose@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.3.0.tgz#0cd62461d21dce4f069785f8d3de136d5525029a" + integrity sha512-75lAs9H19ldmW+fAbyqHdjgdCrz0pWGXKmnqFoh8PyVd1L2RIb4RzYrSjmopeqv3E1G3/Pimu6GgLlrGbrkF7w== + dependencies: + acorn "^8.5.0" + acorn-walk@^8.0.2: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^6.0.7, acorn@^6.2.1, acorn@^6.4.1: +acorn@^6.0.7, acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== @@ -3449,7 +3456,7 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.1.0, acorn@^8.8.1: +acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==