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..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,6 +6,7 @@ */ import { parse } from '../../swc' +import { buildExports } from './utils' function addExportNames(names: string[], node: any) { switch (node.type) { @@ -39,7 +40,7 @@ function addExportNames(names: string[], node: any) { } } -async function parseExportNamesInto( +async function parseModuleInfo( resourcePath: string, transformedSource: string, names: Array @@ -56,7 +57,7 @@ async function parseExportNamesInto( case 'ExportDefaultExpression': case 'ExportDefaultDeclaration': names.push('default') - continue + break case 'ExportNamedDeclaration': if (node.declaration) { if (node.declaration.type === 'VariableDeclaration') { @@ -74,12 +75,26 @@ async function parseExportNamesInto( 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 }, + } = node + // exports.xxx = xxx + if ( + left.type === 'MemberExpression' && + left?.object.type === 'Identifier' && + left.object?.value === 'exports' + ) { + addExportNames(names, left.property) + } + break + } default: break } @@ -98,28 +113,28 @@ export default async function transformSource( } const names: string[] = [] - await parseExportNamesInto(resourcePath, transformedSource, names) + 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 + }, {}) + + // 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 dc8884da6fc3..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,5 +1,6 @@ import { parse } from '../../swc' import { getRawPageExtensions } from '../../utils' +import { buildExports, isEsmNodeType } 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 || isEsmNodeType(node.type) 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__, + _: () => {\n${imports}\n} + }`, + } 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..c0628c568154 --- /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 isEsmNodeType = (type: string) => esmNodeTypes.includes(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 () => {