From 566e982c36b14c79a0006d5eb882982b2c7999b2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 25 Mar 2023 00:06:04 +0000 Subject: [PATCH] [Flight Plugin] Scan for "use client" --- .../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==