From 86cbf6c97de5524ee19187780cffd2fc7630a8e7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 20:28:32 -0500 Subject: [PATCH 1/8] Rename "name"->"filepath" field on Webpack module references This field name will get confused with the imported name or the module id. --- .../src/ReactFlightServerWebpackBundlerConfig.js | 6 +++--- .../src/ReactFlightWebpackNodeLoader.js | 2 +- .../src/ReactFlightWebpackNodeRegister.js | 2 +- .../src/__tests__/ReactFlightDOM-test.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js index f691809522ca1..0aa0d0ba6b904 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -16,7 +16,7 @@ export type BundlerConfig = WebpackMap; // eslint-disable-next-line no-unused-vars export type ModuleReference = { $$typeof: Symbol, - name: string, + filepath: string, }; export type ModuleMetaData = { @@ -30,7 +30,7 @@ export type ModuleKey = string; const MODULE_TAG = Symbol.for('react.module.reference'); export function getModuleKey(reference: ModuleReference): ModuleKey { - return reference.name; + return reference.filepath; } export function isModuleReference(reference: Object): boolean { @@ -41,5 +41,5 @@ export function resolveModuleMetaData( config: BundlerConfig, moduleReference: ModuleReference, ): ModuleMetaData { - return config[moduleReference.name]; + return config[moduleReference.filepath]; } diff --git a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js index 102da33028765..48603389c2b11 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -75,7 +75,7 @@ export async function getSource( if (url.endsWith('.client.js')) { // TODO: Named exports. const src = - "export default { $$typeof: Symbol.for('react.module.reference'), name: " + + "export default { $$typeof: Symbol.for('react.module.reference'), filepath: " + JSON.stringify(url) + '}'; return {source: src}; diff --git a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js index 17c3b8ef9d9c6..fcfab41711956 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -16,7 +16,7 @@ module.exports = function register() { (require: any).extensions['.client.js'] = function(module, path) { module.exports = { $$typeof: Symbol.for('react.module.reference'), - name: url.pathToFileURL(path).href, + filepath: url.pathToFileURL(path).href, }; }; diff --git a/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js index d8568c661c2af..66c20958b306f 100644 --- a/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -73,7 +73,7 @@ describe('ReactFlightDOM', () => { name: 'd', }; const MODULE_TAG = Symbol.for('react.module.reference'); - return {$$typeof: MODULE_TAG, name: 'path/' + idx}; + return {$$typeof: MODULE_TAG, filepath: 'path/' + idx}; } async function waitForSuspense(fn) { From aac1d3e4e46ae04494934e3a20abbe643247b0fe Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 15:30:38 -0500 Subject: [PATCH 2/8] Switch back to transformSource instead of getSource getSource would be more efficient in the cases where we don't need to read the original file but we'll need to most of the time. Even then, we can't return a JS file if we're trying to support non-JS loader because it'll end up being transformed. Similarly, we'll need to parse the file and we can't parse it before it's transformed. So we need to chain with other loaders that know how. --- fixtures/flight/loader/index.js | 14 ++++++++-- .../src/ReactFlightWebpackNodeLoader.js | 26 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/fixtures/flight/loader/index.js b/fixtures/flight/loader/index.js index b9cfa5b73eee0..04960e6dcc9e1 100644 --- a/fixtures/flight/loader/index.js +++ b/fixtures/flight/loader/index.js @@ -1,4 +1,8 @@ -import {resolve, getSource} from 'react-transport-dom-webpack/node-loader'; +import { + resolve, + getSource, + transformSource as reactTransformSource, +} from 'react-transport-dom-webpack/node-loader'; export {resolve, getSource}; @@ -13,7 +17,7 @@ const babelOptions = { ], }; -export async function transformSource(source, context, defaultTransformSource) { +async function babelTransformSource(source, context, defaultTransformSource) { const {format} = context; if (format === 'module') { const opt = Object.assign({filename: context.url}, babelOptions); @@ -22,3 +26,9 @@ export async function transformSource(source, context, defaultTransformSource) { } return defaultTransformSource(source, context, defaultTransformSource); } + +export async function transformSource(source, context, defaultTransformSource) { + return reactTransformSource(source, context, (s, c) => { + return babelTransformSource(s, c, defaultTransformSource); + }); +} diff --git a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js index 48603389c2b11..b71580423784c 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -29,6 +29,17 @@ type GetSourceFunction = ( GetSourceFunction, ) => Promise<{source: Source}>; +type TransformSourceContext = { + format: string, + url: string, +}; + +type TransformSourceFunction = ( + Source, + TransformSourceContext, + TransformSourceFunction, +) => Promise<{source: Source}>; + type Source = string | ArrayBuffer | Uint8Array; let warnedAboutConditionsFlag = false; @@ -71,14 +82,23 @@ export async function getSource( url: string, context: GetSourceContext, defaultGetSource: GetSourceFunction, +) { + return defaultGetSource(url, context, defaultGetSource); +} + +export async function transformSource( + source: Source, + context: TransformSourceContext, + defaultTransformSource: TransformSourceFunction, ): Promise<{source: Source}> { - if (url.endsWith('.client.js')) { + if (context.url.endsWith('.client.js')) { // TODO: Named exports. const src = "export default { $$typeof: Symbol.for('react.module.reference'), filepath: " + - JSON.stringify(url) + + JSON.stringify(context.url) + '}'; return {source: src}; } - return defaultGetSource(url, context, defaultGetSource); + + return defaultTransformSource(source, context, defaultTransformSource); } From ac4762d8148ab3baf3d9ee03c3d88b745d1f127a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 15:52:09 -0500 Subject: [PATCH 3/8] Add acorn dependency This should be the version used by Webpack since we have a dependency on Webpack anyway. --- packages/react-transport-dom-webpack/package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/react-transport-dom-webpack/package.json b/packages/react-transport-dom-webpack/package.json index a71a558f5160f..8e2db02610eff 100644 --- a/packages/react-transport-dom-webpack/package.json +++ b/packages/react-transport-dom-webpack/package.json @@ -50,6 +50,7 @@ "webpack": "^4.43.0" }, "dependencies": { + "acorn": "^6.2.1", "loose-envify": "^1.1.0", "object-assign": "^4.1.1" }, diff --git a/yarn.lock b/yarn.lock index d44e85f4e47d7..857a6c0b175a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2642,6 +2642,11 @@ acorn@^6.0.1, acorn@^6.0.7: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== +acorn@^6.2.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + acorn@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" From cded3bbbac58ce607bde35f00403268af79804ad Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 17:26:20 -0500 Subject: [PATCH 4/8] Parse exported names of ESM modules We need to statically resolve the names that a client component will export so that we can export a module reference for each of the names. For export * from, this gets tricky because we need to also load the source of the next file to parse that. We don't know exactly how the client is built so we guess it's somewhat default. --- fixtures/flight/server/handler.server.js | 9 +- fixtures/flight/src/App.server.js | 4 +- fixtures/flight/src/Counter.client.js | 2 +- fixtures/flight/src/Counter2.client.js | 1 + .../src/ReactFlightWebpackNodeLoader.js | 182 ++++++++++++++++-- scripts/rollup/bundles.js | 2 +- 6 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 fixtures/flight/src/Counter2.client.js diff --git a/fixtures/flight/server/handler.server.js b/fixtures/flight/server/handler.server.js index d7715af9cc757..196283a10926b 100644 --- a/fixtures/flight/server/handler.server.js +++ b/fixtures/flight/server/handler.server.js @@ -18,12 +18,17 @@ module.exports = async function(req, res) { // TODO: Read from a map on the disk. [resolve('../src/Counter.client.js')]: { id: './src/Counter.client.js', + chunks: ['2'], + name: 'Counter', + }, + [resolve('../src/Counter2.client.js')]: { + id: './src/Counter2.client.js', chunks: ['1'], - name: 'default', + name: 'Counter', }, [resolve('../src/ShowMore.client.js')]: { id: './src/ShowMore.client.js', - chunks: ['2'], + chunks: ['3'], name: 'default', }, }); diff --git a/fixtures/flight/src/App.server.js b/fixtures/flight/src/App.server.js index 54a644dc48d49..35a223dce8b07 100644 --- a/fixtures/flight/src/App.server.js +++ b/fixtures/flight/src/App.server.js @@ -2,7 +2,8 @@ import * as React from 'react'; import Container from './Container.js'; -import Counter from './Counter.client.js'; +import {Counter} from './Counter.client.js'; +import {Counter as Counter2} from './Counter2.client.js'; import ShowMore from './ShowMore.client.js'; @@ -11,6 +12,7 @@ export default function App() {

Hello, world

+

Lorem ipsum

diff --git a/fixtures/flight/src/Counter.client.js b/fixtures/flight/src/Counter.client.js index 00a1f2cbe440d..676280f0542f6 100644 --- a/fixtures/flight/src/Counter.client.js +++ b/fixtures/flight/src/Counter.client.js @@ -2,7 +2,7 @@ import * as React from 'react'; import Container from './Container.js'; -export default function Counter() { +export function Counter() { const [count, setCount] = React.useState(0); return ( diff --git a/fixtures/flight/src/Counter2.client.js b/fixtures/flight/src/Counter2.client.js new file mode 100644 index 0000000000000..084f7bc5f071d --- /dev/null +++ b/fixtures/flight/src/Counter2.client.js @@ -0,0 +1 @@ +export * from './Counter.client.js'; diff --git a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js index b71580423784c..d716cda4653cc 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -7,6 +7,8 @@ * @flow */ +import acorn from 'acorn'; + type ResolveContext = { conditions: Array, parentURL: string | void, @@ -16,11 +18,10 @@ type ResolveFunction = ( string, ResolveContext, ResolveFunction, -) => Promise; +) => Promise<{url: string}>; type GetSourceContext = { format: string, - url: string, }; type GetSourceFunction = ( @@ -44,11 +45,17 @@ type Source = string | ArrayBuffer | Uint8Array; let warnedAboutConditionsFlag = false; +let stashedGetSource: null | GetSourceFunction = null; +let stashedResolve: null | ResolveFunction = null; + export async function resolve( specifier: string, context: ResolveContext, defaultResolve: ResolveFunction, -): Promise { +): Promise<{url: string}> { + // We stash this in case we end up needing to resolve export * statements later. + stashedResolve = defaultResolve; + if (!context.conditions.includes('react-server')) { context = { ...context, @@ -83,22 +90,173 @@ export async function getSource( context: GetSourceContext, defaultGetSource: GetSourceFunction, ) { + // We stash this in case we end up needing to resolve export * statements later. + stashedGetSource = defaultGetSource; return defaultGetSource(url, context, defaultGetSource); } +function addExportNames(names, node) { + switch (node.type) { + case 'Identifier': + names.push(node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addExportNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addExportNames(names, element); + } + return; + case 'Property': + addExportNames(names, node.value); + return; + case 'AssignmentPattern': + addExportNames(names, node.left); + return; + case 'RestElement': + addExportNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addExportNames(names, node.expression); + return; + } +} + +function resolveClientImport( + specifier: string, + parentURL: string, +): Promise<{url: string}> { + // Resolve an import specifier as if it was loaded by the client. This doesn't use + // the overrides that this loader does but instead reverts to the default. + // This resolution algorithm will not necessarily have the same configuration + // as the actual client loader. It should mostly work and if it doesn't you can + // always convert to explicit exported names instead. + const conditions = ['node', 'import']; + if (stashedResolve === null) { + throw new Error( + 'Expected resolve to have been called before transformSource', + ); + } + return stashedResolve(specifier, {conditions, parentURL}, stashedResolve); +} + +async function loadClientImport( + url: string, + defaultTransformSource: TransformSourceFunction, +): Promise<{source: Source}> { + if (stashedGetSource === null) { + throw new Error( + 'Expected getSource to have been called before transformSource', + ); + } + // TODO: Validate that this is another module by calling getFormat. + const {source} = await stashedGetSource( + url, + {format: 'module'}, + stashedGetSource, + ); + return defaultTransformSource( + source, + {format: 'module', url}, + defaultTransformSource, + ); +} + +async function parseExportNamesInto( + transformedSource: string, + names: Array, + parentURL: string, + defaultTransformSource, +): Promise { + const {body} = acorn.parse(transformedSource, { + ecmaVersion: '2019', + sourceType: 'module', + }); + for (let i = 0; i < body.length; i++) { + const node = body[i]; + switch (node.type) { + case 'ExportAllDeclaration': + if (node.exported) { + addExportNames(names, node.exported); + continue; + } else { + const {url} = await resolveClientImport(node.source.value, parentURL); + const {source} = await loadClientImport(url, defaultTransformSource); + if (typeof source !== 'string') { + throw new Error('Expected the transformed source to be a string.'); + } + parseExportNamesInto(source, names, url, defaultTransformSource); + continue; + } + case 'ExportDefaultDeclaration': + names.push('default'); + continue; + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addExportNames(names, declarations[j].id); + } + } else { + addExportNames(names, node.declaration.id); + } + } + if (node.specificers) { + const specificers = node.specificers; + for (let j = 0; j < specificers.length; j++) { + addExportNames(names, specificers[j].exported); + } + } + continue; + } + } +} + export async function transformSource( source: Source, context: TransformSourceContext, defaultTransformSource: TransformSourceFunction, ): Promise<{source: Source}> { - if (context.url.endsWith('.client.js')) { - // TODO: Named exports. - const src = - "export default { $$typeof: Symbol.for('react.module.reference'), filepath: " + - JSON.stringify(context.url) + - '}'; - return {source: src}; - } + const transformed = await defaultTransformSource( + source, + context, + defaultTransformSource, + ); + if (context.format === 'module' && context.url.endsWith('.client.js')) { + const transformedSource = transformed.source; + if (typeof transformedSource !== 'string') { + throw new Error('Expected source to have been transformed to a string.'); + } + + const names = []; + await parseExportNamesInto( + transformedSource, + names, + context.url, + defaultTransformSource, + ); - return defaultTransformSource(source, context, defaultTransformSource); + let newSrc = + "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (name === 'default') { + newSrc += 'export default '; + } else { + newSrc += 'export const ' + name + ' = '; + } + newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '; + newSrc += JSON.stringify(context.url); + newSrc += ', name: '; + newSrc += JSON.stringify(name); + newSrc += '};\n'; + } + + return {source: newSrc}; + } + return transformed; } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 641e6dd260271..6f0f88e04e00b 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -301,7 +301,7 @@ const bundles = [ moduleType: RENDERER_UTILS, entry: 'react-transport-dom-webpack/node-loader', global: 'ReactFlightWebpackNodeLoader', - externals: [], + externals: ['acorn'], }, /******* React Transport DOM Webpack Node.js CommonJS Loader *******/ From 403501fda0b3590fd0604fc926a6538ce7b43793 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 20:02:53 -0500 Subject: [PATCH 5/8] Handle imported names one level deep in CommonJS using a Proxy We use a proxy to see what property the server access and that will tell us which property we'll want to import on the client. --- .../src/ReactFlightWebpackNodeRegister.js | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js index fcfab41711956..26a92b8323d31 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -13,11 +13,58 @@ const url = require('url'); const Module = require('module'); module.exports = function register() { + const MODULE_REFERENCE = Symbol.for('react.module.reference'); + const proxyHandlers = { + get: function(target, name, receiver) { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + // These names are a little too common. We should probably have a way to + // have the Flight runtime extract the inner target instead. + return target.$$typeof; + case 'filepath': + return target.filepath; + case 'name': + return target.name; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + case '__esModule': + // Something is conditionally checking which export to use. We'll pretend to be + // an ESM compat module but then we'll check again on the client. + target.default = { + $$typeof: MODULE_REFERENCE, + filepath: target.filepath, + // This a placeholder value that tells the client to conditionally use the + // whole object or just the default export. + name: '', + }; + return true; + } + let cachedReference = target[name]; + if (!cachedReference) { + cachedReference = target[name] = { + $$typeof: MODULE_REFERENCE, + filepath: target.filepath, + name: name, + }; + } + return cachedReference; + }, + set: function() { + throw new Error('Cannot assign to a client module from a server module.'); + }, + }; + (require: any).extensions['.client.js'] = function(module, path) { - module.exports = { - $$typeof: Symbol.for('react.module.reference'), - filepath: url.pathToFileURL(path).href, + const moduleId = url.pathToFileURL(path).href; + const moduleReference: {[string]: any} = { + $$typeof: MODULE_REFERENCE, + filepath: moduleId, + name: '*', // Represents the whole object instead of a particular import. }; + module.exports = new Proxy(moduleReference, proxyHandlers); }; const originalResolveFilename = Module._resolveFilename; From 0afb1b4fe6b8665fc920eabe62e6d5ec1d0465d8 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 20:53:40 -0500 Subject: [PATCH 6/8] Add export name to module reference and Webpack map To support named exports each name needs to be encoded as a separate reference. It's possible with module splitting that different exports end up in different chunks. It's also possible that the export is renamed as part of minification. So the map also includes a map from the original to the bundled name. --- fixtures/flight/server/handler.server.js | 24 ++++++++++++------- .../ReactFlightServerWebpackBundlerConfig.js | 9 ++++--- .../src/__tests__/ReactFlightDOM-test.js | 10 ++++---- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/fixtures/flight/server/handler.server.js b/fixtures/flight/server/handler.server.js index 196283a10926b..3a9246ac467c1 100644 --- a/fixtures/flight/server/handler.server.js +++ b/fixtures/flight/server/handler.server.js @@ -17,19 +17,25 @@ module.exports = async function(req, res) { pipeToNodeWritable(, res, { // TODO: Read from a map on the disk. [resolve('../src/Counter.client.js')]: { - id: './src/Counter.client.js', - chunks: ['2'], - name: 'Counter', + Counter: { + id: './src/Counter.client.js', + chunks: ['2'], + name: 'Counter', + }, }, [resolve('../src/Counter2.client.js')]: { - id: './src/Counter2.client.js', - chunks: ['1'], - name: 'Counter', + Counter: { + id: './src/Counter2.client.js', + chunks: ['1'], + name: 'Counter', + }, }, [resolve('../src/ShowMore.client.js')]: { - id: './src/ShowMore.client.js', - chunks: ['3'], - name: 'default', + default: { + id: './src/ShowMore.client.js', + chunks: ['3'], + name: 'default', + }, }, }); }; diff --git a/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js index 0aa0d0ba6b904..c8469eeba8068 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -8,7 +8,9 @@ */ type WebpackMap = { - [filename: string]: ModuleMetaData, + [filepath: string]: { + [name: string]: ModuleMetaData, + }, }; export type BundlerConfig = WebpackMap; @@ -17,6 +19,7 @@ export type BundlerConfig = WebpackMap; export type ModuleReference = { $$typeof: Symbol, filepath: string, + name: string, }; export type ModuleMetaData = { @@ -30,7 +33,7 @@ export type ModuleKey = string; const MODULE_TAG = Symbol.for('react.module.reference'); export function getModuleKey(reference: ModuleReference): ModuleKey { - return reference.filepath; + return reference.filepath + '#' + reference.name; } export function isModuleReference(reference: Object): boolean { @@ -41,5 +44,5 @@ export function resolveModuleMetaData( config: BundlerConfig, moduleReference: ModuleReference, ): ModuleMetaData { - return config[moduleReference.filepath]; + return config[moduleReference.filepath][moduleReference.name]; } diff --git a/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 66c20958b306f..e437c7b20bc61 100644 --- a/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -68,12 +68,14 @@ describe('ReactFlightDOM', () => { d: moduleExport, }; webpackMap['path/' + idx] = { - id: '' + idx, - chunks: [], - name: 'd', + default: { + id: '' + idx, + chunks: [], + name: 'd', + }, }; const MODULE_TAG = Symbol.for('react.module.reference'); - return {$$typeof: MODULE_TAG, filepath: 'path/' + idx}; + return {$$typeof: MODULE_TAG, filepath: 'path/' + idx, name: 'default'}; } async function waitForSuspense(fn) { From d4e458555321fe1d6242c0c5042039c830d66b63 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 20 Nov 2020 21:20:27 -0500 Subject: [PATCH 7/8] Special case plain CJS requires and conditional imports using __esModule This models if the server tries to import .default or a plain require. We should replicate the same thing on the client when we load that module reference. --- fixtures/flight/server/handler.server.js | 10 ++++++++++ .../src/ReactFlightClientWebpackBundlerConfig.js | 13 ++++++++++++- scripts/flow/environment.js | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/fixtures/flight/server/handler.server.js b/fixtures/flight/server/handler.server.js index 3a9246ac467c1..86476bb2c2d71 100644 --- a/fixtures/flight/server/handler.server.js +++ b/fixtures/flight/server/handler.server.js @@ -36,6 +36,16 @@ module.exports = async function(req, res) { chunks: ['3'], name: 'default', }, + '': { + id: './src/ShowMore.client.js', + chunks: ['3'], + name: '', + }, + '*': { + id: './src/ShowMore.client.js', + chunks: ['3'], + name: '*', + }, }, }); }; diff --git a/packages/react-transport-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-transport-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js index 3e667960d2620..f3c4e1bf1c16d 100644 --- a/packages/react-transport-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js +++ b/packages/react-transport-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -59,5 +59,16 @@ export function requireModule(moduleData: ModuleReference): T { throw entry; } } - return __webpack_require__(moduleData.id)[moduleData.name]; + const moduleExports = __webpack_require__(moduleData.id); + if (moduleData.name === '*') { + // This is a placeholder value that represents that the caller imported this + // as a CommonJS module as is. + return moduleExports; + } + if (moduleData.name === '') { + // This is a placeholder value that represents that the caller accessed the + // default property of this if it was an ESM interop module. + return moduleExports.__esModule ? moduleExports.default : moduleExports; + } + return moduleExports[moduleData.name]; } diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 8fd7111ebf521..3efe41ff344d2 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -68,4 +68,4 @@ declare module 'EventListener' { } declare function __webpack_chunk_load__(id: string): Promise; -declare function __webpack_require__(id: string): {default: any}; +declare function __webpack_require__(id: string): any; From a5f7228fdef1262607ad4419f1e703f456706675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 23 Nov 2020 20:50:00 +0100 Subject: [PATCH 8/8] Dedupe acorn-related deps --- yarn.lock | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/yarn.lock b/yarn.lock index 857a6c0b175a7..7b6cb08909628 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2617,15 +2617,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" - integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== - -acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== +acorn-jsx@^5.0.0, acorn-jsx@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== acorn-walk@^6.0.1: version "6.2.0" @@ -2637,30 +2632,15 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^6.0.1, acorn@^6.0.7: - version "6.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" - integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== - -acorn@^6.2.1: +acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1, 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== -acorn@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== - -acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== - -acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== adbkit-logcat@^1.1.0: version "1.1.0"