diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index c93a660e4dd9..2213d83bce63 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -31,6 +31,7 @@ import { parse } from '../build/swc' import { isServerComponentPage, withoutRSCExtensions } from './utils' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' +import { serverComponentRegex } from './webpack/loaders/utils' type ObjectValue = T extends { [key: string]: infer V } ? V : never @@ -127,19 +128,19 @@ export function createPagesMapping({ } } -const cachedPageRuntimeConfig = new Map() +type PageStaticInfo = { runtime?: PageRuntime; ssr?: boolean; ssg?: boolean } + +const cachedPageStaticInfo = new Map() // @TODO: We should limit the maximum concurrency of this function as there // could be thousands of pages existing. -export async function getPageRuntime( +export async function getPageStaticInfo( pageFilePath: string, nextConfig: Partial, isDev?: boolean -): Promise { - if (!nextConfig.experimental?.reactRoot) return undefined - +): Promise { const globalRuntime = nextConfig.experimental?.runtime - const cached = cachedPageRuntimeConfig.get(pageFilePath) + const cached = cachedPageStaticInfo.get(pageFilePath) if (cached) { return cached[1] } @@ -151,7 +152,7 @@ export async function getPageRuntime( }) } catch (err) { if (!isDev) throw err - return undefined + return {} } // When gSSP or gSP is used, this page requires an execution runtime. If the @@ -160,6 +161,8 @@ export async function getPageRuntime( // https://github.com/vercel/next.js/discussions/34179 let isRuntimeRequired: boolean = false let pageRuntime: PageRuntime = undefined + let ssr = false + let ssg = false // Since these configurations should always be static analyzable, we can // skip these cases that "runtime" and "gSP", "gSSP" are not included in the @@ -192,6 +195,8 @@ export async function getPageRuntime( identifier === 'getServerSideProps' ) { isRuntimeRequired = true + ssg = identifier === 'getStaticProps' + ssr = identifier === 'getServerSideProps' } } } else if (type === 'ExportNamedDeclaration') { @@ -206,6 +211,8 @@ export async function getPageRuntime( orig?.value === 'getServerSideProps') if (hasDataFetchingExports) { isRuntimeRequired = true + ssg = orig.value === 'getStaticProps' + ssr = orig.value === 'getServerSideProps' break } } @@ -218,19 +225,29 @@ export async function getPageRuntime( if (isRuntimeRequired) { pageRuntime = globalRuntime } + } else { + // For Node.js runtime, we do static optimization. + if (!isRuntimeRequired && pageRuntime === 'nodejs') { + pageRuntime = undefined + } } - cachedPageRuntimeConfig.set(pageFilePath, [Date.now(), pageRuntime]) - return pageRuntime + const info = { + runtime: pageRuntime, + ssr, + ssg, + } + cachedPageStaticInfo.set(pageFilePath, [Date.now(), info]) + return info } export function invalidatePageRuntimeCache( pageFilePath: string, safeTime: number ) { - const cached = cachedPageRuntimeConfig.get(pageFilePath) + const cached = cachedPageStaticInfo.get(pageFilePath) if (cached && cached[0] < safeTime) { - cachedPageRuntimeConfig.delete(pageFilePath) + cachedPageStaticInfo.delete(pageFilePath) } } @@ -254,9 +271,10 @@ export function getEdgeServerEntry(opts: { bundlePath: string config: NextConfigComplete isDev: boolean + isServerComponent: boolean page: string pages: { [page: string]: string } -}): ObjectValue { +}) { if (opts.page.match(MIDDLEWARE_ROUTE)) { const loaderParams: MiddlewareLoaderOptions = { absolutePagePath: opts.absolutePagePath, @@ -283,10 +301,14 @@ export function getEdgeServerEntry(opts: { stringifiedConfig: JSON.stringify(opts.config), } - return `next-middleware-ssr-loader?${stringify(loaderParams)}!` + return { + import: `next-middleware-ssr-loader?${stringify(loaderParams)}!`, + layer: opts.isServerComponent ? 'sc_server' : undefined, + } } export function getViewsEntry(opts: { + name: string pagePath: string viewsDir: string pageExtensions: string[] @@ -381,10 +403,10 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { isViews ? 'views' : 'pages', bundleFile ) + const absolutePagePath = mappings[page] // Handle paths that have aliases const pageFilePath = (() => { - const absolutePagePath = mappings[page] if (absolutePagePath.startsWith(PAGES_DIR_ALIAS)) { return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir) } @@ -396,18 +418,27 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { return require.resolve(absolutePagePath) })() + const isServerComponent = serverComponentRegex.test(absolutePagePath) + runDependingOnPageType({ page, - pageRuntime: await getPageRuntime(pageFilePath, config, isDev), + pageRuntime: (await getPageStaticInfo(pageFilePath, config, isDev)) + .runtime, onClient: () => { - client[clientBundlePath] = getClientEntry({ - absolutePagePath: mappings[page], - page, - }) + if (isServerComponent) { + // We skip the initial entries for server component pages and let the + // server compiler inject them instead. + } else { + client[clientBundlePath] = getClientEntry({ + absolutePagePath: mappings[page], + page, + }) + } }, onServer: () => { if (isViews && viewsDir) { server[serverBundlePath] = getViewsEntry({ + name: serverBundlePath, pagePath: mappings[page], viewsDir, pageExtensions, @@ -421,7 +452,12 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }) } } else { - server[serverBundlePath] = [mappings[page]] + server[serverBundlePath] = isServerComponent + ? { + import: mappings[page], + layer: 'sc_server', + } + : [mappings[page]] } }, onEdgeServer: () => { @@ -430,6 +466,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { absolutePagePath: mappings[page], bundlePath: clientBundlePath, isDev: false, + isServerComponent, page, }) }, @@ -481,10 +518,12 @@ export function finalizeEntrypoint({ name, compilerType, value, + isServerComponent, }: { compilerType?: 'client' | 'server' | 'edge-server' name: string value: ObjectValue + isServerComponent?: boolean }): ObjectValue { const entry = typeof value !== 'object' || Array.isArray(value) @@ -496,7 +535,7 @@ export function finalizeEntrypoint({ return { publicPath: isApi ? '' : undefined, runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime', - layer: isApi ? 'api' : undefined, + layer: isApi ? 'api' : isServerComponent ? 'sc_server' : undefined, ...entry, } } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 882861acee4b..a564e107a40e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -82,7 +82,7 @@ import { runCompiler } from './compiler' import { createEntrypoints, createPagesMapping, - getPageRuntime, + getPageStaticInfo, } from './entries' import { generateBuildId } from './generate-build-id' import { isWriteable } from './is-writeable' @@ -114,6 +114,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import { recursiveCopy } from '../lib/recursive-copy' import { recursiveReadDir } from '../lib/recursive-readdir' import { lockfilePatchPromise, teardownTraceSubscriber } from './swc' +import { injectedClientEntries } from './webpack/plugins/flight-manifest-plugin' export type SsgRoute = { initialRevalidateSeconds: number | false @@ -139,11 +140,13 @@ export type PrerenderManifest = { type CompilerResult = { errors: webpack.StatsError[] warnings: webpack.StatsError[] - stats: [ - webpack.Stats | undefined, - webpack.Stats | undefined, - webpack.Stats | undefined - ] + stats: (webpack.Stats | undefined)[] +} + +type SingleCompilerResult = { + errors: webpack.StatsError[] + warnings: webpack.StatsError[] + stats: webpack.Stats | undefined } export default async function build( @@ -665,7 +668,7 @@ export default async function build( let result: CompilerResult = { warnings: [], errors: [], - stats: [undefined, undefined, undefined], + stats: [], } let webpackBuildStart let telemetryPlugin @@ -724,42 +727,85 @@ export default async function build( // We run client and server compilation separately to optimize for memory usage await runWebpackSpan.traceAsyncFn(async () => { - const clientResult = await runCompiler(clientConfig, { - runWebpackSpan, - }) - // Fail build if clientResult contains errors - if (clientResult.errors.length > 0) { - result = { - warnings: [...clientResult.warnings], - errors: [...clientResult.errors], - stats: [clientResult.stats, undefined, undefined], + // If we are under the serverless build, we will have to run the client + // compiler first because the server compiler depends on the manifest + // files that are created by the client compiler. + // Otherwise, we run the server compilers first and then the client + // compiler to track the boundary of server/client components. + + let clientResult: SingleCompilerResult | null = null + let serverResult: SingleCompilerResult | null = null + let edgeServerResult: SingleCompilerResult | null = null + + if (isLikeServerless) { + if (config.experimental.serverComponents) { + throw new Error( + 'Server Components are not supported in serverless mode.' + ) + } + + // Build client first + clientResult = await runCompiler(clientConfig, { + runWebpackSpan, + }) + + // Only continue if there were no errors + if (!clientResult.errors.length) { + serverResult = await runCompiler(configs[1], { + runWebpackSpan, + }) + edgeServerResult = configs[2] + ? await runCompiler(configs[2], { runWebpackSpan }) + : null } } else { - const serverResult = await runCompiler(configs[1], { + // During the server compilations, entries of client components will be + // injected to this set and then will be consumed by the client compiler. + injectedClientEntries.clear() + + serverResult = await runCompiler(configs[1], { runWebpackSpan, }) - const edgeServerResult = configs[2] + edgeServerResult = configs[2] ? await runCompiler(configs[2], { runWebpackSpan }) : null - result = { - warnings: [ - ...clientResult.warnings, - ...serverResult.warnings, - ...(edgeServerResult?.warnings || []), - ], - errors: [ - ...clientResult.errors, - ...serverResult.errors, - ...(edgeServerResult?.errors || []), - ], - stats: [ - clientResult.stats, - serverResult.stats, - edgeServerResult?.stats, - ], + // Only continue if there were no errors + if ( + !serverResult.errors.length && + !edgeServerResult?.errors.length + ) { + injectedClientEntries.forEach((value, key) => { + ;(clientConfig.entry as webpack.EntryObject)[key] = value + }) + + clientResult = await runCompiler(clientConfig, { + runWebpackSpan, + }) } } + + result = { + warnings: ([] as any[]) + .concat( + clientResult?.warnings, + serverResult?.warnings, + edgeServerResult?.warnings + ) + .filter(nonNullable), + errors: ([] as any[]) + .concat( + clientResult?.errors, + serverResult?.errors, + edgeServerResult?.errors + ) + .filter(nonNullable), + stats: [ + clientResult?.stats, + serverResult?.stats, + edgeServerResult?.stats, + ], + } }) result = nextBuildSpan .traceChild('format-webpack-messages') @@ -1031,7 +1077,8 @@ export default async function build( p.startsWith(actualPage + '/index.') ) const pageRuntime = pagePath - ? await getPageRuntime(join(pagesDir, pagePath), config) + ? (await getPageStaticInfo(join(pagesDir, pagePath), config)) + .runtime : undefined if (hasServerComponents && pagePath) { diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index cb0a154d6df9..ad694301735e 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -46,7 +46,7 @@ import { Sema } from 'next/dist/compiled/async-sema' import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' -import { getPageRuntime } from './entries' +import { getPageStaticInfo } from './entries' const { builtinModules } = require('module') const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/ @@ -1298,7 +1298,7 @@ export async function isEdgeRuntimeCompiled( // Check the page runtime as well since we cannot detect the runtime from // compilation when it's for the client part of edge function - return (await getPageRuntime(module.resource, config)) === 'edge' + return (await getPageStaticInfo(module.resource, config)).runtime === 'edge' } export function getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage( diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 080cd18d485f..c1d8986edd0d 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -922,6 +922,7 @@ export default async function getBaseWebpackConfig( let webpackConfig: webpack.Configuration = { parallelism: Number(process.env.NEXT_WEBPACK_PARALLELISM) || undefined, + // @ts-ignore externals: isClient || isEdgeServer ? // make sure importing "next" is handled gracefully for client @@ -998,11 +999,30 @@ export default async function getBaseWebpackConfig( nodeEnv: false, ...(hasServerComponents ? { - // We have to use the names here instead of hashes to ensure the consistency between builds. + // We have to use the names here instead of hashes to ensure the consistency between compilers. moduleIds: 'named', } : {}), splitChunks: ((): webpack.Options.SplitChunksOptions | false => { + // For the edge runtime, we have to bundle all dependencies inside without dynamic `require`s. + // To make some dependencies like `react` to be shared between entrypoints, we use a special + // cache group here even under dev mode. + const edgeRSCCacheGroups = hasServerComponents + ? { + rscDeps: { + enforce: true, + name: 'rsc-runtime-deps', + filename: 'rsc-runtime-deps.js', + test: /(node_modules\/react\/|\/shared\/lib\/head-manager-context\.js)/, + }, + } + : undefined + if (isEdgeServer && edgeRSCCacheGroups) { + return { + cacheGroups: edgeRSCCacheGroups, + } + } + if (dev) { return false } @@ -1022,6 +1042,7 @@ export default async function getBaseWebpackConfig( filename: 'edge-chunks/[name].js', chunks: 'all', minChunks: 2, + cacheGroups: edgeRSCCacheGroups, } } @@ -1179,6 +1200,7 @@ export default async function getBaseWebpackConfig( 'next-style-loader', 'next-flight-client-loader', 'next-flight-server-loader', + 'next-flight-client-entry-loader', 'noop-loader', 'next-middleware-loader', 'next-middleware-ssr-loader', @@ -1216,23 +1238,30 @@ export default async function getBaseWebpackConfig( // RSC server compilation loaders { ...serverComponentCodeCondition, + issuerLayer: 'sc_server', use: { loader: 'next-flight-server-loader', }, }, - ] - : [ - // RSC client compilation loaders { - ...serverComponentCodeCondition, + test: /(\.client\.(js|cjs|mjs))$|\/next\/(link|image|head|script)/, + issuerLayer: 'sc_server', use: { - loader: 'next-flight-server-loader', - options: { - client: 1, - }, + loader: 'next-flight-client-loader', }, }, ] + : [] + : []), + ...(hasServerComponents && isEdgeServer + ? [ + // Move shared dependencies from sc_server and sc_client into the + // same layer. + { + test: /(node_modules\/react\/|\/shared\/lib\/head-manager-context\.js)/, + layer: 'rsc_shared_deps', + }, + ] : []), { test: /\.(js|cjs|mjs)$/, diff --git a/packages/next/build/webpack/loaders/get-module-build-info.ts b/packages/next/build/webpack/loaders/get-module-build-info.ts index 6f0be47f61dd..36e4380778c8 100644 --- a/packages/next/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/build/webpack/loaders/get-module-build-info.ts @@ -11,9 +11,15 @@ export function getModuleBuildInfo(webpackModule: webpack5.Module) { nextUsedEnvVars?: Set nextWasmMiddlewareBinding?: WasmBinding usingIndirectEval?: boolean | Set + route?: RouteMeta } } +export interface RouteMeta { + page: string + absolutePagePath: string +} + export interface EdgeMiddlewareMeta { page: string } diff --git a/packages/next/build/webpack/loaders/next-client-pages-loader.ts b/packages/next/build/webpack/loaders/next-client-pages-loader.ts index 96e2dca83235..b01a8ec6342a 100644 --- a/packages/next/build/webpack/loaders/next-client-pages-loader.ts +++ b/packages/next/build/webpack/loaders/next-client-pages-loader.ts @@ -3,6 +3,7 @@ import { stringifyRequest } from '../stringify-request' export type ClientPagesLoaderOptions = { absolutePagePath: string page: string + isServerComponent?: boolean } // this parameter: https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters @@ -12,19 +13,21 @@ function nextClientPagesLoader(this: any) { ) return pagesLoaderSpan.traceFn(() => { - const { absolutePagePath, page } = + const { absolutePagePath, page, isServerComponent } = this.getOptions() as ClientPagesLoaderOptions pagesLoaderSpan.setAttribute('absolutePagePath', absolutePagePath) - const stringifiedPagePath = stringifyRequest(this, absolutePagePath) + const stringifiedPageRequest = isServerComponent + ? JSON.stringify(absolutePagePath + '!') + : stringifyRequest(this, absolutePagePath) const stringifiedPage = JSON.stringify(page) return ` (window.__NEXT_P = window.__NEXT_P || []).push([ ${stringifiedPage}, function () { - return require(${stringifiedPagePath}); + return require(${stringifiedPageRequest}); } ]); if(module.hot) { diff --git a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts new file mode 100644 index 000000000000..90bfbc06d2c4 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -0,0 +1,27 @@ +export default async function transformSource(this: any): Promise { + let { modules, runtime, ssr } = this.getOptions() + if (!Array.isArray(modules)) { + modules = modules ? [modules] : [] + } + + return ( + modules + .map( + (request: string) => `import(/* webpackMode: "eager" */ '${request}')` + ) + .join(';') + + ` + export const __next_rsc__ = { + server: false, + __webpack_require__ + }; + export default function RSC() {}; + ` + + // Currently for the Edge runtime, we treat all RSC pages as SSR pages. + (runtime === 'edge' + ? 'export const __N_SSP = true;' + : ssr + ? `export const __N_SSP = true;` + : `export const __N_SSG = true;`) + ) +} 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 83c4ac670f88..ef0efd4001f1 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -1,283 +1,31 @@ -import { builtinModules } from 'module' - import { parse } from '../../swc' -import { - buildExports, - clientComponentRegex, - serverComponentRegex, - isNextBuiltinClientComponent, -} from './utils' - -function createFlightServerRequest( - request: string, - options?: { client: 1 | undefined } -) { - return `next-flight-server-loader${ - options ? '?' + JSON.stringify(options) : '' - }!${request}` -} - -function hasFlightLoader(request: string, type: 'client' | 'server') { - return request.includes(`next-flight-${type}-loader`) -} - -async function parseModuleInfo({ - resourcePath, - source, - isClientCompilation, - resolver, -}: { - resourcePath: string - source: string - isClientCompilation: boolean - resolver: (req: string) => Promise -}): Promise<{ - source: string - imports: string[] - isEsm: boolean - __N_SSP: boolean - pageRuntime: 'edge' | 'nodejs' | null -}> { - const ast = await parse(source, { - filename: resourcePath, - isModule: 'unknown', - }) - const { type, body } = ast - const beginPos = ast.span.start - let transformedSource = '' - let lastIndex = 0 - let imports = [] - let __N_SSP = false - let pageRuntime = null - let isBuiltinModule - let isNodeModuleImport - - const isEsm = type === 'Module' - - async function getModuleType(path: string) { - const isBuiltinModule_ = builtinModules.includes(path) - const resolvedPath = isBuiltinModule_ ? path : await resolver(path) - - const isNodeModuleImport_ = - /[\\/]node_modules[\\/]/.test(resolvedPath) && - // exclude next built-in modules - !isNextBuiltinClientComponent(resolvedPath) - - return [isBuiltinModule_, isNodeModuleImport_] as const - } - - function addClientImport(path: string) { - if (serverComponentRegex.test(path) || hasFlightLoader(path, 'server')) { - // If it's a server component, we recursively import its dependencies. - imports.push(path) - } else if (clientComponentRegex.test(path)) { - // Client component. - imports.push(path) - } else { - // Shared component. - imports.push(createFlightServerRequest(path, { client: 1 })) - } - } - - for (let i = 0; i < body.length; i++) { - const node = body[i] - switch (node.type) { - case 'ImportDeclaration': - const importSource = node.source.value - - ;[isBuiltinModule, isNodeModuleImport] = await getModuleType( - importSource - ) - - // 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 (serverComponentRegex.test(importSource)) { - continue - } - - const importDeclarations = source.substring( - lastIndex, - node.source.span.start - beginPos - ) - - if (clientComponentRegex.test(importSource)) { - transformedSource += importDeclarations - transformedSource += JSON.stringify( - `next-flight-client-loader!${importSource}` - ) - imports.push(importSource) - } else { - // A shared component. It should be handled as a server component. - const serverImportSource = - isReactImports || isBuiltinModule - ? importSource - : createFlightServerRequest(importSource) - transformedSource += importDeclarations - transformedSource += JSON.stringify(serverImportSource) - - // TODO: support handling RSC components from node_modules - if (!isNodeModuleImport) { - imports.push(importSource) - } - } - } else { - // For now we assume there is no .client.js inside node_modules. - // TODO: properly handle this. - if (isNodeModuleImport || isBuiltinModule) continue - addClientImport(importSource) - } - - lastIndex = node.source.span.end - beginPos - break - case 'ExportDeclaration': - if (isClientCompilation) { - // Keep `__N_SSG` and `__N_SSP` exports. - if (node.declaration?.type === 'VariableDeclaration') { - for (const declaration of node.declaration.declarations) { - if (declaration.type === 'VariableDeclarator') { - if (declaration.id?.type === 'Identifier') { - const value = declaration.id.value - if (value === '__N_SSP') { - __N_SSP = true - } else if (value === 'config') { - const props = declaration.init.properties - const runtimeKeyValue = props.find( - (prop: any) => prop.key.value === 'runtime' - ) - const runtime = runtimeKeyValue?.value?.value - if (runtime === 'nodejs' || runtime === 'edge') { - pageRuntime = runtime - } - } - } - } - } - } - } - break - case 'ExportNamedDeclaration': - if (isClientCompilation) { - if (node.source) { - // export { ... } from '...' - const path = node.source.value - ;[isBuiltinModule, isNodeModuleImport] = await getModuleType(path) - if (!isBuiltinModule && !isNodeModuleImport) { - addClientImport(path) - } - } - } - break - default: - break - } - } - - if (!isClientCompilation) { - transformedSource += source.substring(lastIndex) - } - - return { source: transformedSource, imports, isEsm, __N_SSP, pageRuntime } -} export default async function transformSource( this: any, source: string ): Promise { - const { client: isClientCompilation } = 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 hasAppliedFlightServerLoader = this.loaders.some((loader: any) => { - return hasFlightLoader(loader.path, 'server') - }) - const isServerExt = serverComponentRegex.test(resourcePath) + const { resourcePath } = this - if (!isClientCompilation) { - // We only apply the loader to server components, or shared components that - // are imported by a server component. - if (!isServerExt && !hasAppliedFlightServerLoader) { - return source - } - } - - const { - source: transformedSource, - imports, - isEsm, - __N_SSP, - pageRuntime, - } = await parseModuleInfo({ - resourcePath, - source, - isClientCompilation, - resolver, + const ast = await parse(source, { + filename: resourcePath, + isModule: 'unknown', }) - - /** - * For .server.js files, we handle this loader differently. - * - * Server compilation output: - * (The content of the Server Component module will be kept.) - * export const __next_rsc__ = { __webpack_require__, _: () => { ... }, server: true } - * - * Client compilation output: - * (The content of the Server Component module will be removed.) - * export const __next_rsc__ = { __webpack_require__, _: () => { ... }, server: false } - */ - - const rscExports: any = { - __next_rsc__: `{ - __webpack_require__, - _: () => { - ${imports - .map( - (importSource) => - `import(/* webpackMode: "eager" */ ${JSON.stringify( - importSource - )});` - ) - .join('\n')} - }, - server: ${isServerExt ? 'true' : 'false'} - }`, - } - - if (isClientCompilation) { - rscExports.default = 'function RSC() {}' - - if (pageRuntime === 'edge') { - // Currently for the Edge runtime, we treat all RSC pages as SSR pages. - rscExports.__N_SSP = 'true' - } else { - if (__N_SSP) { - rscExports.__N_SSP = 'true' - } else { - // Server component pages are always considered as SSG by default because - // the flight data is needed for client navigation. - rscExports.__N_SSG = 'true' + const isModule = ast.type === 'Module' + + return ( + source + + (isModule + ? ` + export const __next_rsc__ = { + __webpack_require__, + server: true } - } - } - - const output = transformedSource + '\n' + buildExports(rscExports, isEsm) - return output + ` + : ` + exports.__next_rsc__ = { + __webpack_require__, + server: true + } + `) + ) } diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index ff6f665b50fc..5ac54d0992ec 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -35,6 +35,10 @@ export default async function middlewareSSRLoader(this: any) { isServerComponent: isServerComponent === 'true', page: page, } + buildInfo.route = { + page, + absolutePagePath, + } const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const stringifiedAppPath = stringifyRequest(this, absoluteAppPath) @@ -66,7 +70,6 @@ export default async function middlewareSSRLoader(this: any) { stringified500Path ? `require(${stringified500Path})` : 'null' } - const buildManifest = self.__BUILD_MANIFEST const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST const rscManifest = self.__RSC_MANIFEST diff --git a/packages/next/build/webpack/loaders/next-view-loader.ts b/packages/next/build/webpack/loaders/next-view-loader.ts index 4374b9ff3673..d0c1370a2901 100644 --- a/packages/next/build/webpack/loaders/next-view-loader.ts +++ b/packages/next/build/webpack/loaders/next-view-loader.ts @@ -1,6 +1,7 @@ import path from 'path' import type webpack from 'webpack5' import { NODE_RESOLVE_OPTIONS } from '../../webpack-config' +import { getModuleBuildInfo } from './get-module-build-info' function pathToUrlPath(pathname: string) { let urlPath = pathname.replace(/^private-next-views-dir/, '') @@ -54,11 +55,19 @@ async function resolveLayoutPathsByPage({ } const nextViewLoader: webpack.LoaderDefinitionFunction<{ + name: string pagePath: string viewsDir: string pageExtensions: string[] }> = async function nextViewLoader() { - const { viewsDir, pagePath, pageExtensions } = this.getOptions() || {} + const { name, viewsDir, pagePath, pageExtensions } = this.getOptions() || {} + + const buildInfo = getModuleBuildInfo((this as any)._module) + buildInfo.route = { + page: name.replace(/^views/, ''), + absolutePagePath: + viewsDir + pagePath.replace(/^private-next-views-dir/, ''), + } const extensions = pageExtensions.map((extension) => `.${extension}`) const resolveOptions: any = { diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 1dea74472665..f2cf30ba3821 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -5,9 +5,20 @@ * LICENSE file in the root directory of this source tree. */ +import { stringify } from 'querystring' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' -import { MIDDLEWARE_FLIGHT_MANIFEST } from '../../../shared/lib/constants' +import { + MIDDLEWARE_FLIGHT_MANIFEST, + EDGE_RUNTIME_WEBPACK, +} from '../../../shared/lib/constants' import { clientComponentRegex } from '../loaders/utils' +import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' +import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path' +import { + getInvalidator, + entries, +} from '../../../server/dev/on-demand-entry-handler' +import { getPageStaticInfo } from '../../entries' // This is the module that will be used to anchor all client references to. // I.e. it will have all the client files as async deps from this point on. @@ -27,6 +38,8 @@ const PLUGIN_NAME = 'FlightManifestPlugin' let edgeFlightManifest = {} let nodeFlightManifest = {} +export const injectedClientEntries = new Map() + export class FlightManifestPlugin { dev: boolean = false pageExtensions: string[] @@ -41,6 +54,8 @@ export class FlightManifestPlugin { } apply(compiler: any) { + const context = (this as any).context + compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation: any, { normalModuleFactory }: any) => { @@ -56,6 +71,144 @@ export class FlightManifestPlugin { ) // Only for webpack 5 + compiler.hooks.finishMake.tapAsync( + PLUGIN_NAME, + async (compilation: any, callback: any) => { + const promises: any = [] + + // For each SC server compilation entry, we need to create its corresponding + // client component entry. + for (const [name, entry] of compilation.entries.entries()) { + if (name === 'pages/_app.server') continue + + // Check if the page entry is a server component or not. + const entryDependency = entry.dependencies?.[0] + const request = entryDependency?.request + + if (request && entry.options?.layer === 'sc_server') { + const visited = new Set() + const clientComponentImports: string[] = [] + + function filterClientComponents(dependency: any) { + const module = + compilation.moduleGraph.getResolvedModule(dependency) + if (!module) return + + if (visited.has(module.userRequest)) return + visited.add(module.userRequest) + + if (clientComponentRegex.test(module.userRequest)) { + clientComponentImports.push(module.userRequest) + } + + compilation.moduleGraph + .getOutgoingConnections(module) + .forEach((connection: any) => { + filterClientComponents(connection.dependency) + }) + } + + // Traverse the module graph to find all client components. + filterClientComponents(entryDependency) + + const entryModule = + compilation.moduleGraph.getResolvedModule(entryDependency) + const routeInfo = entryModule.buildInfo.route || { + page: denormalizePagePath(name.replace(/^pages/, '')), + absolutePagePath: entryModule.resource, + } + + // Parse gSSP and gSP exports from the page source. + const pageStaticInfo = this.isEdgeServer + ? {} + : await getPageStaticInfo( + routeInfo.absolutePagePath, + {}, + this.dev + ) + + const clientLoader = `next-flight-client-entry-loader?${stringify({ + modules: clientComponentImports, + runtime: this.isEdgeServer ? 'edge' : 'nodejs', + ssr: pageStaticInfo.ssr, + // Adding name here to make the entry key unique. + name, + })}!` + + const bundlePath = 'pages' + normalizePagePath(routeInfo.page) + + // Inject the entry to the client compiler. + if (this.dev) { + const pageKey = 'client' + routeInfo.page + if (!entries[pageKey]) { + entries[pageKey] = { + bundlePath, + absolutePagePath: routeInfo.absolutePagePath, + clientLoader, + dispose: false, + lastActiveTime: Date.now(), + } as any + const invalidator = getInvalidator() + if (invalidator) { + invalidator.invalidate() + } + } + } else { + injectedClientEntries.set( + bundlePath, + `next-client-pages-loader?${stringify({ + isServerComponent: true, + page: denormalizePagePath(bundlePath.replace(/^pages/, '')), + absolutePagePath: clientLoader, + })}!` + clientLoader + ) + } + + // Inject the entry to the server compiler. + const clientComponentEntryDep = ( + webpack as any + ).EntryPlugin.createDependency( + clientLoader, + name + '.__sc_client__' + ) + promises.push( + new Promise((res, rej) => { + compilation.addEntry( + context, + clientComponentEntryDep, + this.isEdgeServer + ? { + name: name + '.__sc_client__', + library: { + name: ['self._CLIENT_ENTRY'], + type: 'assign', + }, + runtime: EDGE_RUNTIME_WEBPACK, + asyncChunks: false, + } + : { + name: name + '.__sc_client__', + runtime: 'webpack-runtime', + }, + (err: any) => { + if (err) { + rej(err) + } else { + res() + } + } + ) + }) + ) + } + } + + Promise.all(promises) + .then(() => callback()) + .catch(callback) + } + ) + compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => { compilation.hooks.processAssets.tap( { @@ -102,7 +255,7 @@ export class FlightManifestPlugin { moduleExportedKeys.forEach((name) => { if (!moduleExports[name]) { moduleExports[name] = { - id, + id: id.replace(/^\(sc_server\)\//, ''), name, chunks: [], } diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 19d03032570c..2c2b0e66067e 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -391,6 +391,14 @@ function getEntryFiles(entryFiles: string[], meta: EntryMetadata) { if (meta.edgeSSR) { if (meta.edgeSSR.isServerComponent) { files.push(`server/${MIDDLEWARE_FLIGHT_MANIFEST}.js`) + files.push( + ...entryFiles + .filter( + (file) => + file.startsWith('pages/') && !file.endsWith('.hot-update.js') + ) + .map((file) => 'server/' + file.replace('.js', '.__sc_client__.js')) + ) } files.push( diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 8d8e7651f76a..f9d3f2b0c4bf 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -43,7 +43,9 @@ import { Span, trace } from '../../trace' import { getProperError } from '../../lib/is-error' import ws from 'next/dist/compiled/ws' import { promises as fs } from 'fs' -import { getPageRuntime } from '../../build/entries' +import { getPageStaticInfo } from '../../build/entries' +import { serverComponentRegex } from '../../build/webpack/loaders/utils' +import { stringify } from 'querystring' const wsServer = new ws.Server({ noServer: true }) @@ -541,6 +543,10 @@ export default class HotReloader { await Promise.all( Object.keys(entries).map(async (pageKey) => { const { bundlePath, absolutePagePath, dispose } = entries[pageKey] + + // @FIXME + const { clientLoader } = entries[pageKey] as any + const result = /^(client|server|edge-server)(.*)/g.exec(pageKey) const [, key, page] = result! // this match should always happen if (key === 'client' && !isClientCompilation) return @@ -554,29 +560,49 @@ export default class HotReloader { return } + const isServerComponent = + serverComponentRegex.test(absolutePagePath) + runDependingOnPageType({ page, - pageRuntime: await getPageRuntime(absolutePagePath, this.config), + pageRuntime: ( + await getPageStaticInfo(absolutePagePath, this.config) + ).runtime, onEdgeServer: () => { - if (isEdgeServerCompilation) { + if (!isEdgeServerCompilation) return + entries[pageKey].status = BUILDING + entrypoints[bundlePath] = finalizeEntrypoint({ + compilerType: 'edge-server', + name: bundlePath, + value: getEdgeServerEntry({ + absolutePagePath, + buildId: this.buildId, + bundlePath, + config: this.config, + isDev: true, + page, + pages: this.pagesMapping, + isServerComponent, + }), + }) + }, + onClient: () => { + if (!isClientCompilation) return + if (isServerComponent) { entries[pageKey].status = BUILDING entrypoints[bundlePath] = finalizeEntrypoint({ - compilerType: 'edge-server', name: bundlePath, - value: getEdgeServerEntry({ - absolutePagePath, - buildId: this.buildId, - bundlePath, - config: this.config, - isDev: true, - page, - pages: this.pagesMapping, - }), + compilerType: 'client', + value: + `next-client-pages-loader?${stringify({ + isServerComponent, + page: denormalizePagePath( + bundlePath.replace(/^pages/, '') + ), + absolutePagePath: clientLoader, + })}!` + clientLoader, }) - } - }, - onClient: () => { - if (isClientCompilation) { + } else { entries[pageKey].status = BUILDING entrypoints[bundlePath] = finalizeEntrypoint({ name: bundlePath, @@ -589,29 +615,30 @@ export default class HotReloader { } }, onServer: () => { - if (isNodeServerCompilation) { - entries[pageKey].status = BUILDING - let request = relative(config.context!, absolutePagePath) - if (!isAbsolute(request) && !request.startsWith('../')) { - request = `./${request}` - } - - entrypoints[bundlePath] = finalizeEntrypoint({ - compilerType: 'server', - name: bundlePath, - value: - this.viewsDir && bundlePath.startsWith('views/') - ? getViewsEntry({ - pagePath: join( - VIEWS_DIR_ALIAS, - relative(this.viewsDir!, absolutePagePath) - ), - viewsDir: this.viewsDir!, - pageExtensions: this.config.pageExtensions, - }) - : request, - }) + if (!isNodeServerCompilation) return + entries[pageKey].status = BUILDING + let request = relative(config.context!, absolutePagePath) + if (!isAbsolute(request) && !request.startsWith('../')) { + request = `./${request}` } + + entrypoints[bundlePath] = finalizeEntrypoint({ + compilerType: 'server', + name: bundlePath, + isServerComponent, + value: + this.viewsDir && bundlePath.startsWith('views/') + ? getViewsEntry({ + name: bundlePath, + pagePath: join( + VIEWS_DIR_ALIAS, + relative(this.viewsDir!, absolutePagePath) + ), + viewsDir: this.viewsDir!, + pageExtensions: this.config.pageExtensions, + }) + : request, + }) }, }) }) diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 91222ebabe7d..00661ac31ba2 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -64,7 +64,10 @@ import isError, { getProperError } from '../../lib/is-error' import { getMiddlewareRegex } from '../../shared/lib/router/utils/get-middleware-regex' import { isCustomErrorPage, isReservedPage } from '../../build/utils' import { NodeNextResponse, NodeNextRequest } from '../base-http/node' -import { getPageRuntime, invalidatePageRuntimeCache } from '../../build/entries' +import { + getPageStaticInfo, + invalidatePageRuntimeCache, +} from '../../build/entries' import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' import { normalizeViewPath } from '../../shared/lib/router/utils/view-paths' @@ -331,10 +334,9 @@ export default class DevServer extends Server { } invalidatePageRuntimeCache(fileName, safeTime) - const pageRuntimeConfig = await getPageRuntime( - fileName, - this.nextConfig - ) + const pageRuntimeConfig = ( + await getPageStaticInfo(fileName, this.nextConfig) + ).runtime const isEdgeRuntime = pageRuntimeConfig === 'edge' if ( diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 116136adb030..c6b1333005e9 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -3,7 +3,7 @@ import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack' import type { NextConfigComplete } from '../config-shared' import { EventEmitter } from 'events' import { findPageFile } from '../lib/find-page-file' -import { getPageRuntime, runDependingOnPageType } from '../../build/entries' +import { getPageStaticInfo, runDependingOnPageType } from '../../build/entries' import { join, posix } from 'path' import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' @@ -12,6 +12,7 @@ import { removePagePathTail } from '../../shared/lib/page-path/remove-page-path- import { pageNotFoundError } from '../require' import { reportTrigger } from '../../build/output' import getRouteFromEntrypoint from '../get-route-from-entrypoint' +import { serverComponentRegex } from '../../build/webpack/loaders/utils' export const ADDED = Symbol('added') export const BUILDING = Symbol('building') @@ -33,6 +34,10 @@ export const entries: { * For example: `pages/about/index` */ bundlePath: string + /** + * Client entry loader and query parameters when RSC is enabled. + */ + clientLoader?: string /** * Tells if a page is scheduled to be disposed. */ @@ -48,6 +53,9 @@ export const entries: { } } = {} +let invalidator: Invalidator +export const getInvalidator = () => invalidator + export function onDemandEntryHandler({ maxInactiveAge, multiCompiler, @@ -65,17 +73,15 @@ export function onDemandEntryHandler({ viewsDir?: string watcher: any }) { - const invalidator = new Invalidator(watcher) + invalidator = new Invalidator(watcher) const doneCallbacks: EventEmitter | null = new EventEmitter() const lastClientAccessPages = [''] + const startBuilding = (_compilation: webpack.Compilation) => { + invalidator.startBuilding() + } for (const compiler of multiCompiler.compilers) { - compiler.hooks.make.tap( - 'NextJsOnDemandEntries', - (_compilation: webpack.Compilation) => { - invalidator.startBuilding() - } - ) + compiler.hooks.make.tap('NextJsOnDemandEntries', startBuilding) } function getPagePathsFromEntrypoints( @@ -189,7 +195,12 @@ export function onDemandEntryHandler({ const addPageEntry = (type: 'client' | 'server' | 'edge-server') => { return new Promise((resolve, reject) => { + const isServerComponent = serverComponentRegex.test( + pagePathData.absolutePagePath + ) + const pageKey = `${type}${pagePathData.page}` + if (entries[pageKey]) { entries[pageKey].dispose = false entries[pageKey].lastActiveTime = Date.now() @@ -198,13 +209,17 @@ export function onDemandEntryHandler({ return } } else { - entryAdded = true - entries[pageKey] = { - absolutePagePath: pagePathData.absolutePagePath, - bundlePath: pagePathData.bundlePath, - dispose: false, - lastActiveTime: Date.now(), - status: ADDED, + if (type === 'client' && isServerComponent) { + // Skip adding the client entry here. + } else { + entryAdded = true + entries[pageKey] = { + absolutePagePath: pagePathData.absolutePagePath, + bundlePath: pagePathData.bundlePath, + dispose: false, + lastActiveTime: Date.now(), + status: ADDED, + } } } @@ -217,10 +232,9 @@ export function onDemandEntryHandler({ const promises = runDependingOnPageType({ page: pagePathData.page, - pageRuntime: await getPageRuntime( - pagePathData.absolutePagePath, - nextConfig - ), + pageRuntime: ( + await getPageStaticInfo(pagePathData.absolutePagePath, nextConfig) + ).runtime, onClient: () => addPageEntry('client'), onServer: () => addPageEntry('server'), onEdgeServer: () => addPageEntry('edge-server'), diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 1bb854461630..fea6d8ffe300 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -3,6 +3,12 @@ import type { DocumentType, NextComponentType, } from '../shared/lib/utils' +import type { + PageConfig, + GetStaticPaths, + GetServerSideProps, + GetStaticProps, +} from 'next/types' import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, @@ -12,12 +18,7 @@ import { join } from 'path' import { requirePage, getPagePath } from './require' import { BuildManifest } from './get-page-files' import { interopDefault } from '../lib/interop-default' -import { - PageConfig, - GetStaticPaths, - GetServerSideProps, - GetStaticProps, -} from 'next/types' +import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' export type ManifestItem = { id: number | string @@ -130,6 +131,20 @@ export async function loadComponents( : null, ]) + if (serverComponents) { + try { + // Make sure to also load the client entry in cache. + await requirePage( + normalizePagePath(pathname) + '.__sc_client__', + distDir, + serverless + ) + } catch (_) { + // This page might not be a server component page, so there is no __sc_client__ + // bundle to load. + } + } + const Component = interopDefault(ComponentMod) const Document = interopDefault(DocumentMod) const App = interopDefault(AppMod) diff --git a/test/integration/build-trace-extra-entries/app/next.config.js b/test/integration/build-trace-extra-entries/app/next.config.js index 5a9fa362be0f..ebde58dcfa8a 100644 --- a/test/integration/build-trace-extra-entries/app/next.config.js +++ b/test/integration/build-trace-extra-entries/app/next.config.js @@ -1,15 +1,13 @@ const path = require('path') -let handledServer = false module.exports = { - webpack(cfg, { isServer }) { + webpack(cfg, { isServer, nextRuntime }) { console.log(cfg.entry) const origEntry = cfg.entry cfg.entry = async () => { const origEntries = await origEntry() - if (isServer && !handledServer) { - handledServer = true + if (isServer && nextRuntime === 'nodejs') { const curEntry = origEntries['pages/_app'] origEntries['pages/_app'] = [ path.join(__dirname, 'lib/get-data.js'), diff --git a/test/integration/middleware/without-builtin-module/test/index.test.js b/test/integration/middleware/without-builtin-module/test/index.test.js index b31c60fc5671..a08d90215774 100644 --- a/test/integration/middleware/without-builtin-module/test/index.test.js +++ b/test/integration/middleware/without-builtin-module/test/index.test.js @@ -9,6 +9,7 @@ import { launchApp, nextBuild, waitFor, + File, } from 'next-test-utils' const context = {} @@ -16,6 +17,10 @@ const context = {} jest.setTimeout(1000 * 60 * 2) context.appDir = join(__dirname, '../') +const middleware = new File( + join(context.appDir, 'pages', 'using-not-exist', '_middleware.js') +) + describe('Middleware importing Node.js built-in module', () => { function getModuleNotFound(name) { return `Module not found: Can't resolve '${name}'` @@ -84,12 +89,20 @@ describe('Middleware importing Node.js built-in module', () => { let buildResult beforeAll(async () => { + // Make sure to only keep the child_process error in prod build. + middleware.replace(`import NotExist from 'not-exist'`, '') + middleware.replace(`new NotExist()`, '') + buildResult = await nextBuild(context.appDir, undefined, { stderr: true, stdout: true, }) }) + afterAll(() => { + middleware.restore() + }) + it('should not have middleware error during build', () => { expect(buildResult.stderr).toContain(getModuleNotFound('child_process')) expect(buildResult.stderr).not.toContain( diff --git a/test/integration/react-streaming-and-server-components/app/node_modules/non-isomorphic-text/package.json b/test/integration/react-streaming-and-server-components/app/node_modules/non-isomorphic-text/package.json index bf2f13567110..7ffb0bfd9657 100644 --- a/test/integration/react-streaming-and-server-components/app/node_modules/non-isomorphic-text/package.json +++ b/test/integration/react-streaming-and-server-components/app/node_modules/non-isomorphic-text/package.json @@ -1,5 +1,6 @@ { "name": "non-isomorphic-text", - "module": "./index.js", + "type": "module", + "main": "./index.js", "browser": "./browser.js" } 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 2feb39738167..81a3edb7d2ea 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -76,14 +76,16 @@ export default function (context, { runtime, env }) { expect(sharedClientModule[0][1]).toBe(sharedClientModule[1][1]) expect(sharedServerModule[0][1]).not.toBe(sharedClientModule[0][1]) + // Note: This is currently unsupported because packages from another layer + // will not be re-initialized by webpack. // Should import 2 module instances for node_modules too. - const modFromClient = main.match( - /node_modules instance from \.client\.js:(\d+)/ - ) - const modFromServer = main.match( - /node_modules instance from \.server\.js:(\d+)/ - ) - expect(modFromClient[1]).not.toBe(modFromServer[1]) + // const modFromClient = main.match( + // /node_modules instance from \.client\.js:(\d+)/ + // ) + // const modFromServer = main.match( + // /node_modules instance from \.server\.js:(\d+)/ + // ) + // expect(modFromClient[1]).not.toBe(modFromServer[1]) }) it('should support next/link in server components', async () => { diff --git a/test/unit/fixtures/page-runtime/nodejs-ssr.js b/test/unit/fixtures/page-runtime/nodejs-ssr.js new file mode 100644 index 000000000000..b2f997866bc3 --- /dev/null +++ b/test/unit/fixtures/page-runtime/nodejs-ssr.js @@ -0,0 +1,12 @@ +export default function Nodejs() { + return 'nodejs' +} + +export function getServerSideProps() { + return { props: {} } +} + +export const config = { + amp: false, + runtime: 'nodejs', +} diff --git a/test/unit/parse-page-runtime.test.ts b/test/unit/parse-page-runtime.test.ts index f37490d6b0c2..496eabd20f47 100644 --- a/test/unit/parse-page-runtime.test.ts +++ b/test/unit/parse-page-runtime.test.ts @@ -1,10 +1,9 @@ -import { getPageRuntime } from 'next/dist/build/entries' -import type { PageRuntime } from 'next/dist/server/config-shared' +import { getPageStaticInfo } from 'next/dist/build/entries' import { join } from 'path' const fixtureDir = join(__dirname, 'fixtures') -function createNextConfig(runtime?: PageRuntime) { +function createNextConfig(runtime?: 'edge' | 'nodejs') { return { experimental: { reactRoot: true, runtime }, } @@ -12,15 +11,23 @@ function createNextConfig(runtime?: PageRuntime) { describe('parse page runtime config', () => { it('should parse nodejs runtime correctly', async () => { - const runtime = await getPageRuntime( - join(fixtureDir, 'page-runtime/nodejs.js'), + const { runtime } = await getPageStaticInfo( + join(fixtureDir, 'page-runtime/nodejs-ssr.js'), createNextConfig() ) expect(runtime).toBe('nodejs') }) + it('should parse static runtime correctly', async () => { + const { runtime } = await getPageStaticInfo( + join(fixtureDir, 'page-runtime/nodejs.js'), + createNextConfig() + ) + expect(runtime).toBe(undefined) + }) + it('should parse edge runtime correctly', async () => { - const runtime = await getPageRuntime( + const { runtime } = await getPageStaticInfo( join(fixtureDir, 'page-runtime/edge.js'), createNextConfig() ) @@ -28,7 +35,7 @@ describe('parse page runtime config', () => { }) it('should return undefined if no runtime is specified', async () => { - const runtime = await getPageRuntime( + const { runtime } = await getPageStaticInfo( join(fixtureDir, 'page-runtime/static.js'), createNextConfig() ) @@ -38,7 +45,7 @@ describe('parse page runtime config', () => { describe('fallback to the global runtime configuration', () => { it('should fallback when gSP is defined and exported', async () => { - const runtime = await getPageRuntime( + const { runtime } = await getPageStaticInfo( join(fixtureDir, 'page-runtime/fallback-with-gsp.js'), createNextConfig('edge') ) @@ -46,7 +53,7 @@ describe('fallback to the global runtime configuration', () => { }) it('should fallback when gSP is re-exported from other module', async () => { - const runtime = await getPageRuntime( + const { runtime } = await getPageStaticInfo( join(fixtureDir, 'page-runtime/fallback-re-export-gsp.js'), createNextConfig('edge') )