From 86d53401e5ad2b44c5fe4d6cf3f9661fba5b5665 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 24 Mar 2022 08:05:30 +0100 Subject: [PATCH 1/3] RSC: handle commonjs in flight loader --- .../loaders/next-flight-client-loader.ts | 56 +++++++++++++------ .../loaders/next-flight-server-loader.ts | 30 ++++++---- packages/next/build/webpack/loaders/utils.ts | 21 +++++++ .../app/components/cjs.client.js | 3 + .../app/components/cjs.js | 3 + .../app/pages/various-exports.server.js | 8 +++ .../test/rsc.js | 2 + 7 files changed, 95 insertions(+), 28 deletions(-) create mode 100644 packages/next/build/webpack/loaders/utils.ts create mode 100644 test/integration/react-streaming-and-server-components/app/components/cjs.client.js create mode 100644 test/integration/react-streaming-and-server-components/app/components/cjs.js diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts index c08689af661e..0b0cd01e865f 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -6,6 +6,7 @@ */ import { parse } from '../../swc' +import { buildExports, isEsmNode } from './utils' function addExportNames(names: string[], node: any) { switch (node.type) { @@ -39,17 +40,19 @@ function addExportNames(names: string[], node: any) { } } -async function parseExportNamesInto( +async function parseModuleInfo( resourcePath: string, transformedSource: string, names: Array -): Promise { +): Promise<{ isEsm: boolean }> { const { body } = await parse(transformedSource, { filename: resourcePath, isModule: true, }) + let isEsm = false for (let i = 0; i < body.length; i++) { const node = body[i] + isEsm = isEsm || isEsmNode(node) switch (node.type) { // TODO: support export * from module path // case 'ExportAllDeclaration': @@ -80,10 +83,24 @@ async function parseExportNamesInto( addExportNames(names, node.declaration.identifier) } continue + case 'ExpressionStatement': { + const { + expression: { left }, + } = node + // exports.xxx = xxx + if ( + left.type === 'MemberExpression' && + left?.object.type === 'Identifier' && + left.object?.value === 'exports' + ) { + addExportNames(names, left.property) + } + } default: break } } + return { isEsm } } export default async function transformSource( @@ -98,28 +115,31 @@ export default async function transformSource( } const names: string[] = [] - await parseExportNamesInto(resourcePath, transformedSource, names) + const { isEsm } = await parseModuleInfo( + resourcePath, + transformedSource, + names + ) // next.js/packages/next/.js if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) { names.push('default') } - let newSrc = + const moduleRefDef = "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(resourcePath) - newSrc += ', name: ' - newSrc += JSON.stringify(name) - newSrc += '};\n' - } - return newSrc + const clientRefsExports = names.reduce((res: any, name) => { + const moduleRef = + '{ $$typeof: MODULE_REFERENCE, filepath: ' + + JSON.stringify(resourcePath) + + ', name: ' + + JSON.stringify(name) + + '};\n' + res[name] = moduleRef + return res + }, {}) + + const output = moduleRefDef + buildExports(clientRefsExports, isEsm) + return output } diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index dc8884da6fc3..afda6ae7c9b7 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -1,5 +1,6 @@ import { parse } from '../../swc' import { getRawPageExtensions } from '../../utils' +import { buildExports, isEsmNode } from './utils' const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] @@ -24,7 +25,7 @@ const createServerComponentFilter = (pageExtensions: string[]) => { return (importSource: string) => regex.test(importSource) } -async function parseImportsInfo({ +async function parseModuleInfo({ resourcePath, source, isClientCompilation, @@ -39,16 +40,18 @@ async function parseImportsInfo({ }): Promise<{ source: string imports: string + isEsm: boolean }> { const ast = await parse(source, { filename: resourcePath, isModule: true }) const { body } = ast - let transformedSource = '' let lastIndex = 0 let imports = '' + let isEsm = false for (let i = 0; i < body.length; i++) { const node = body[i] + isEsm = isEsm || isEsmNode(node) switch (node.type) { case 'ImportDeclaration': { const importSource = node.source.value @@ -117,7 +120,7 @@ async function parseImportsInfo({ transformedSource += source.substring(lastIndex) } - return { source: transformedSource, imports } + return { source: transformedSource, imports, isEsm } } export default async function transformSource( @@ -152,7 +155,11 @@ export default async function transformSource( } } - const { source: transformedSource, imports } = await parseImportsInfo({ + const { + source: transformedSource, + imports, + isEsm, + } = await parseModuleInfo({ resourcePath, source, isClientCompilation, @@ -172,14 +179,17 @@ export default async function transformSource( * export const __next_rsc__ = { __webpack_require__, _: () => { ... } } */ - let rscExports = `export const __next_rsc__={ - __webpack_require__, - _: () => {${imports}} - }` + const rscExports: any = { + __next_rsc__: `{ + __webpack_require__, + _: () => {${imports}} + }`, + } if (isClientCompilation) { - rscExports += '\nexport default function RSC () {}' + rscExports['default'] = 'function RSC() {}' } - return transformedSource + '\n' + rscExports + const output = transformedSource + '\n' + buildExports(rscExports, isEsm) + return output } diff --git a/packages/next/build/webpack/loaders/utils.ts b/packages/next/build/webpack/loaders/utils.ts new file mode 100644 index 000000000000..f3e83553ad7a --- /dev/null +++ b/packages/next/build/webpack/loaders/utils.ts @@ -0,0 +1,21 @@ +export function buildExports(moduleExports: any, isESM: boolean) { + let ret = '' + Object.keys(moduleExports).forEach((key) => { + const exportExpression = isESM + ? `export ${key === 'default' ? key : `const ${key} =`} ${ + moduleExports[key] + }` + : `exports.${key} = ${moduleExports[key]}` + + ret += exportExpression + '\n' + }) + return ret +} + +const esmNodeTypes = [ + 'ImportDeclaration', + 'ExportNamedDeclaration', + 'ExportDefaultExpression', + 'ExportDefaultDeclaration', +] +export const isEsmNode = (node: any) => esmNodeTypes.includes(node.type) diff --git a/test/integration/react-streaming-and-server-components/app/components/cjs.client.js b/test/integration/react-streaming-and-server-components/app/components/cjs.client.js new file mode 100644 index 000000000000..d3c078184ab6 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/components/cjs.client.js @@ -0,0 +1,3 @@ +exports.Cjs = function Cjs() { + return 'cjs-client' +} diff --git a/test/integration/react-streaming-and-server-components/app/components/cjs.js b/test/integration/react-streaming-and-server-components/app/components/cjs.js new file mode 100644 index 000000000000..444f1229a227 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/components/cjs.js @@ -0,0 +1,3 @@ +exports.Cjs = function Cjs() { + return 'cjs-shared' +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/various-exports.server.js b/test/integration/react-streaming-and-server-components/app/pages/various-exports.server.js index b89234a14c6b..5a9b4ce9acb5 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/various-exports.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/various-exports.server.js @@ -4,6 +4,8 @@ import { a, b, c, d, e } from '../components/shared-exports' import DefaultArrow, { Named as ClientNamed, } from '../components/client-exports.client' +import { Cjs as CjsShared } from '../components/cjs' +import { Cjs as CjsClient } from '../components/cjs.client' export default function Page() { return ( @@ -21,6 +23,12 @@ export default function Page() {
+ + + + + + ) } diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index 472e03559bc7..0078f7056f64 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -206,6 +206,8 @@ export default function (context, { runtime, env }) { expect(hydratedContent).toContain('abcde') expect(hydratedContent).toContain('default-export-arrow.client') expect(hydratedContent).toContain('named.client') + expect(hydratedContent).toContain('cjs-shared') + expect(hydratedContent).toContain('cjs-client') }) it('should handle 404 requests and missing routes correctly', async () => { From eeec5d4da5d0f917e7d1de0a7fa212aaff5e7250 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 24 Mar 2022 08:36:37 +0100 Subject: [PATCH 2/3] fix lint --- .../build/webpack/loaders/next-flight-client-loader.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts index 0b0cd01e865f..b1b9fdf4ff2b 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -59,7 +59,7 @@ async function parseModuleInfo( case 'ExportDefaultExpression': case 'ExportDefaultDeclaration': names.push('default') - continue + break case 'ExportNamedDeclaration': if (node.declaration) { if (node.declaration.type === 'VariableDeclaration') { @@ -77,12 +77,12 @@ async function parseModuleInfo( addExportNames(names, specificers[j].exported) } } - continue + break case 'ExportDeclaration': if (node.declaration?.identifier) { addExportNames(names, node.declaration.identifier) } - continue + break case 'ExpressionStatement': { const { expression: { left }, @@ -95,6 +95,7 @@ async function parseModuleInfo( ) { addExportNames(names, left.property) } + break } default: break From 543d00bbe0bfbf3eab4075002a6153a64cb94780 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 24 Mar 2022 09:39:37 +0100 Subject: [PATCH 3/3] generate esm for module references --- .../loaders/next-flight-client-loader.ts | 18 ++++++------------ .../loaders/next-flight-server-loader.ts | 6 +++--- packages/next/build/webpack/loaders/utils.ts | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts index b1b9fdf4ff2b..ee60abb516d1 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -6,7 +6,7 @@ */ import { parse } from '../../swc' -import { buildExports, isEsmNode } from './utils' +import { buildExports } from './utils' function addExportNames(names: string[], node: any) { switch (node.type) { @@ -44,15 +44,13 @@ async function parseModuleInfo( resourcePath: string, transformedSource: string, names: Array -): Promise<{ isEsm: boolean }> { +): Promise { const { body } = await parse(transformedSource, { filename: resourcePath, isModule: true, }) - let isEsm = false for (let i = 0; i < body.length; i++) { const node = body[i] - isEsm = isEsm || isEsmNode(node) switch (node.type) { // TODO: support export * from module path // case 'ExportAllDeclaration': @@ -101,7 +99,6 @@ async function parseModuleInfo( break } } - return { isEsm } } export default async function transformSource( @@ -116,11 +113,7 @@ export default async function transformSource( } const names: string[] = [] - const { isEsm } = await parseModuleInfo( - resourcePath, - transformedSource, - names - ) + await parseModuleInfo(resourcePath, transformedSource, names) // next.js/packages/next/.js if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) { @@ -136,11 +129,12 @@ export default async function transformSource( JSON.stringify(resourcePath) + ', name: ' + JSON.stringify(name) + - '};\n' + ' };\n' res[name] = moduleRef return res }, {}) - const output = moduleRefDef + buildExports(clientRefsExports, isEsm) + // still generate module references in ESM + const output = moduleRefDef + buildExports(clientRefsExports, true) return output } diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index afda6ae7c9b7..b87f234690b1 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -1,6 +1,6 @@ import { parse } from '../../swc' import { getRawPageExtensions } from '../../utils' -import { buildExports, isEsmNode } from './utils' +import { buildExports, isEsmNodeType } from './utils' const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] @@ -51,7 +51,7 @@ async function parseModuleInfo({ for (let i = 0; i < body.length; i++) { const node = body[i] - isEsm = isEsm || isEsmNode(node) + isEsm = isEsm || isEsmNodeType(node.type) switch (node.type) { case 'ImportDeclaration': { const importSource = node.source.value @@ -182,7 +182,7 @@ export default async function transformSource( const rscExports: any = { __next_rsc__: `{ __webpack_require__, - _: () => {${imports}} + _: () => {\n${imports}\n} }`, } diff --git a/packages/next/build/webpack/loaders/utils.ts b/packages/next/build/webpack/loaders/utils.ts index f3e83553ad7a..c0628c568154 100644 --- a/packages/next/build/webpack/loaders/utils.ts +++ b/packages/next/build/webpack/loaders/utils.ts @@ -18,4 +18,4 @@ const esmNodeTypes = [ 'ExportDefaultExpression', 'ExportDefaultDeclaration', ] -export const isEsmNode = (node: any) => esmNodeTypes.includes(node.type) +export const isEsmNodeType = (type: string) => esmNodeTypes.includes(type)