diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 420a34f324074c7..9166fefdf48343e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -980,6 +980,12 @@ export default async function getBaseWebpackConfig( }, } + const rscCodeCondition = { + test: serverComponentsRegex, + // only apply to the pages as the begin process of rsc loaders + include: [dir, /next[\\/]dist[\\/]pages/], + } + let webpackConfig: webpack.Configuration = { parallelism: Number(process.env.NEXT_WEBPACK_PARALLELISM) || undefined, externals: targetWeb @@ -1202,32 +1208,24 @@ export default async function getBaseWebpackConfig( ? [ // RSC server compilation loaders { - ...codeCondition, + ...rscCodeCondition, use: { loader: 'next-flight-server-loader', options: { - pageExtensions: rawPageExtensions, + extensions: rawPageExtensions, }, }, }, - { - test: codeCondition.test, - resourceQuery: /__sc_client__/, - use: { - loader: 'next-flight-client-loader', - }, - }, ] : [ // RSC client compilation loaders { - ...codeCondition, - test: serverComponentsRegex, + ...rscCodeCondition, use: { loader: 'next-flight-server-loader', options: { client: 1, - pageExtensions: rawPageExtensions, + extensions: rawPageExtensions, }, }, }, 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 b3f99dedcc6585d..18b370e7a98dbb0 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -87,7 +87,6 @@ async function parseModuleInfo( } = node // exports.xxx = xxx if ( - left && left.object && left.type === 'MemberExpression' && left.object.type === 'Identifier' && 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 29bec7106707664..b3146902f79fc38 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -3,14 +3,14 @@ import { buildExports } from './utils' const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] -export const createClientComponentFilter = (pageExtensions: string[]) => { +export const createClientComponentFilter = (extensions: string[]) => { // Special cases for Next.js APIs that are considered as client components: // - .client.[ext] // - next built-in client components // - .[imageExt] const regex = new RegExp( '(' + - `\\.client(\\.(${pageExtensions.join('|')}))?|` + + `\\.client(\\.(${extensions.join('|')}))?|` + `next/(link|image)(\\.js)?|` + `\\.(${imageExtensions.join('|')})` + ')$' @@ -19,26 +19,40 @@ export const createClientComponentFilter = (pageExtensions: string[]) => { return (importSource: string) => regex.test(importSource) } -export const createServerComponentFilter = (pageExtensions: string[]) => { - const regex = new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?$`) +export const createServerComponentFilter = (extensions: string[]) => { + const regex = new RegExp(`\\.server(\\.(${extensions.join('|')}))?$`) return (importSource: string) => regex.test(importSource) } +function createFlightServerRequest(request: string, extensions: string[]) { + return `next-flight-server-loader?${JSON.stringify({ + extensions, + })}!${request}` +} + +function hasFlightLoader(request: string, type: 'client' | 'server') { + return request.includes(`next-flight-${type}-loader`) +} + async function parseModuleInfo({ resourcePath, source, + extensions, isClientCompilation, isServerComponent, isClientComponent, + resolver, }: { resourcePath: string source: string isClientCompilation: boolean + extensions: string[] isServerComponent: (name: string) => boolean isClientComponent: (name: string) => boolean + resolver: (req: string) => Promise }): Promise<{ source: string - imports: string + imports: string[] isEsm: boolean __N_SSP: boolean pageRuntime: 'edge' | 'nodejs' | null @@ -50,7 +64,7 @@ async function parseModuleInfo({ const { type, body } = ast let transformedSource = '' let lastIndex = 0 - let imports = '' + let imports = [] let __N_SSP = false let pageRuntime = null @@ -61,6 +75,16 @@ async function parseModuleInfo({ switch (node.type) { case 'ImportDeclaration': const importSource = node.source.value + const resolvedPath = await resolver(importSource) + const isNodeModuleImport = resolvedPath.includes('/node_modules/') + + // matching node_module package but excluding react cores since react is required to be shared + const isReactImports = [ + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + ].includes(importSource) + if (!isClientCompilation) { // Server compilation for .server.js. if (isServerComponent(importSource)) { @@ -73,45 +97,37 @@ async function parseModuleInfo({ ) if (isClientComponent(importSource)) { - // A client component. It should be loaded as module reference. transformedSource += importDeclarations - transformedSource += JSON.stringify(`${importSource}?__sc_client__`) - imports += `require(${JSON.stringify(importSource)})\n` + transformedSource += JSON.stringify( + `next-flight-client-loader!${importSource}` + ) + imports.push(importSource) } else { - // FIXME - // case: 'react' - // Avoid module resolution error like Cannot find `./?__rsc_server__` in react/package.json - - // cases: 'react/jsx-runtime', 'react/jsx-dev-runtime' - // This is a special case to avoid the Duplicate React error. - // Since we already include React in the SSR runtime, - // here we can't create a new module with the ?__rsc_server__ query. - if ( - ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime'].includes( - importSource - ) - ) { - continue - } - - // A shared component. It should be handled as a server - // component. + // A shared component. It should be handled as a server component. + const serverImportSource = isReactImports + ? importSource + : createFlightServerRequest(importSource, extensions) transformedSource += importDeclarations - transformedSource += JSON.stringify(`${importSource}?__sc_server__`) + transformedSource += JSON.stringify(serverImportSource) + + // TODO: support handling RSC components from node_modules + if (!isNodeModuleImport) { + imports.push(importSource) + } } } else { // For the client compilation, we skip all modules imports but - // always keep client components in the bundle. All client components + // always keep client/shared components in the bundle. All client components // have to be imported from either server or client components. if ( - !( - isClientComponent(importSource) || isServerComponent(importSource) - ) + isServerComponent(importSource) || + hasFlightLoader(importSource, 'server') || + // TODO: support handling RSC components from node_modules + isNodeModuleImport ) { continue } - - imports += `require(${JSON.stringify(importSource)})\n` + imports.push(importSource) } lastIndex = node.source.span.end @@ -158,23 +174,33 @@ export default async function transformSource( this: any, source: string ): Promise { - const { client: isClientCompilation, pageExtensions } = this.getOptions() - const { resourcePath, resourceQuery } = this + const { client: isClientCompilation, extensions } = this.getOptions() + const { resourcePath, resolve: resolveFn, context } = this + + const resolver = (req: string): Promise => { + return new Promise((resolve, reject) => { + resolveFn(context, req, (err: any, result: string) => { + if (err) return reject(err) + resolve(result) + }) + }) + } if (typeof source !== 'string') { throw new Error('Expected source to have been transformed to a string.') } - const isServerComponent = createServerComponentFilter(pageExtensions) - const isClientComponent = createClientComponentFilter(pageExtensions) + const isServerComponent = createServerComponentFilter(extensions) + const isClientComponent = createClientComponentFilter(extensions) + const hasAppliedFlightServerLoader = this.loaders.some((loader: any) => { + return hasFlightLoader(loader.path, 'server') + }) + const isServerExt = isServerComponent(resourcePath) if (!isClientCompilation) { // We only apply the loader to server components, or shared components that // are imported by a server component. - if ( - !isServerComponent(resourcePath) && - resourceQuery !== '?__sc_server__' - ) { + if (!isServerExt && !hasAppliedFlightServerLoader) { return source } } @@ -188,9 +214,11 @@ export default async function transformSource( } = await parseModuleInfo({ resourcePath, source, + extensions, isClientCompilation, isServerComponent, isClientComponent, + resolver, }) /** @@ -208,8 +236,12 @@ export default async function transformSource( const rscExports: any = { __next_rsc__: `{ __webpack_require__, - _: () => {\n${imports}\n}, - server: ${isServerComponent(resourcePath) ? 'true' : 'false'} + _: () => { + ${imports + .map((importSource) => `require('${importSource}');`) + .join('\n')} + }, + server: ${isServerExt ? 'true' : 'false'} }`, } diff --git a/test/integration/react-streaming-and-server-components/app/components/nav.server.js b/test/integration/react-streaming-and-server-components/app/components/nav.js similarity index 100% rename from test/integration/react-streaming-and-server-components/app/components/nav.server.js rename to test/integration/react-streaming-and-server-components/app/components/nav.js diff --git a/test/integration/react-streaming-and-server-components/app/pages/external-imports.server.js b/test/integration/react-streaming-and-server-components/app/pages/external-imports.server.js index 40a41e569a75e15..78698b9fa948917 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/external-imports.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/external-imports.server.js @@ -1,11 +1,9 @@ -import moment from 'moment' import nonIsomorphicText from 'non-isomorphic-text' export default function Page() { return (
-
date:{moment().toString()}
-
{nonIsomorphicText()}
+
date:{nonIsomorphicText()}
) } diff --git a/test/integration/react-streaming-and-server-components/app/pages/index.server.js b/test/integration/react-streaming-and-server-components/app/pages/index.server.js index 7ca7e5e30855df4..9383793d9513f12 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/index.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/index.server.js @@ -1,4 +1,4 @@ -import Nav from '../components/nav.server' +import Nav from '../components/nav' const envVar = process.env.ENV_VAR_TEST const headerKey = 'x-next-test-client' diff --git a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js index 1162cea6f999ca3..5e8a81e35c34993 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js @@ -1,5 +1,5 @@ import Link from 'next/link' -import Nav from '../../components/nav.server' +import Nav from '../../components/nav' export default function LinkPage({ router }) { const { query } = router diff --git a/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js b/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js index 7d7c0cacce37be5..e308d0eb4f9968d 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/streaming-rsc.server.js @@ -1,5 +1,5 @@ import { Suspense } from 'react' -import Nav from '../components/nav.server' +import Nav from '../components/nav' let result let promise 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 767c0543bc286f8..a7cc18df86b7f54 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -192,7 +192,7 @@ export default function (context, { runtime, env }) { .readFileSync(join(distServerDir, 'external-imports.js')) .toString() - expect(bundle).not.toContain('moment') + expect(bundle).not.toContain('non-isomorphic-text') }) }