diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 78d94e230de..9cd266fd953 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -15,12 +15,16 @@ import { ROOT_DIR_ALIAS, APP_DIR_ALIAS, SERVER_RUNTIME, + WEBPACK_LAYERS, } from '../lib/constants' import { CLIENT_STATIC_FILES_RUNTIME_AMP, CLIENT_STATIC_FILES_RUNTIME_MAIN, CLIENT_STATIC_FILES_RUNTIME_MAIN_APP, + CLIENT_STATIC_FILES_RUNTIME_POLYFILLS, CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, + CompilerNameValues, + COMPILER_NAMES, EDGE_RUNTIME_WEBPACK, } from '../shared/lib/constants' import { __ApiPreviewProps } from '../server/api-utils' @@ -203,7 +207,7 @@ export function getEdgeServerEntry(opts: { return { import: `next-edge-ssr-loader?${stringify(loaderParams)}!`, - layer: opts.isServerComponent ? 'sc_server' : undefined, + layer: opts.isServerComponent ? WEBPACK_LAYERS.server : undefined, } } @@ -215,7 +219,7 @@ export function getAppEntry(opts: { }) { return { import: `next-app-loader?${stringify(opts)}!`, - layer: 'sc_server', + layer: WEBPACK_LAYERS.server, } } @@ -359,7 +363,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { } } - runDependingOnPageType({ + await runDependingOnPageType({ page, pageRuntime: staticInfo.runtime, onClient: () => { @@ -393,7 +397,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { server[serverBundlePath] = isServerComponent ? { import: mappings[page], - layer: 'sc_server', + layer: WEBPACK_LAYERS.server, } : [mappings[page]] } @@ -435,33 +439,46 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { } } -export function runDependingOnPageType(params: { +export async function runDependingOnPageType(params: { onClient: () => T onEdgeServer: () => T onServer: () => T page: string pageRuntime: ServerRuntime -}) { +}): Promise { if (isMiddlewareFile(params.page)) { - return { edgeServer: params.onEdgeServer() } - } else if (params.page.match(API_ROUTE)) { - return params.pageRuntime === SERVER_RUNTIME.edge - ? { edgeServer: params.onEdgeServer() } - : { server: params.onServer() } - } else if (params.page === '/_document') { - return { server: params.onServer() } - } else if ( + await params.onEdgeServer() + return + } + if (params.page.match(API_ROUTE)) { + if (params.pageRuntime === SERVER_RUNTIME.edge) { + await params.onEdgeServer() + return + } + + await params.onServer() + return + } + if (params.page === '/_document') { + await params.onServer() + return + } + if ( params.page === '/_app' || params.page === '/_error' || params.page === '/404' || params.page === '/500' ) { - return { client: params.onClient(), server: params.onServer() } - } else { - return params.pageRuntime === SERVER_RUNTIME.edge - ? { client: params.onClient(), edgeServer: params.onEdgeServer() } - : { client: params.onClient(), server: params.onServer() } + await Promise.all([params.onClient(), params.onServer()]) + return } + if (params.pageRuntime === SERVER_RUNTIME.edge) { + await Promise.all([params.onClient(), params.onEdgeServer()]) + return + } + + await Promise.all([params.onClient(), params.onServer()]) + return } export function finalizeEntrypoint({ @@ -471,7 +488,7 @@ export function finalizeEntrypoint({ isServerComponent, appDir, }: { - compilerType?: 'client' | 'server' | 'edge-server' + compilerType?: CompilerNameValues name: string value: ObjectValue isServerComponent?: boolean @@ -483,18 +500,25 @@ export function finalizeEntrypoint({ : value const isApi = name.startsWith('pages/api/') - if (compilerType === 'server') { + if (compilerType === COMPILER_NAMES.server) { return { publicPath: isApi ? '' : undefined, runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime', - layer: isApi ? 'api' : isServerComponent ? 'sc_server' : undefined, + layer: isApi + ? WEBPACK_LAYERS.api + : isServerComponent + ? WEBPACK_LAYERS.server + : undefined, ...entry, } } - if (compilerType === 'edge-server') { + if (compilerType === COMPILER_NAMES.edgeServer) { return { - layer: isMiddlewareFilename(name) || isApi ? 'middleware' : undefined, + layer: + isMiddlewareFilename(name) || isApi + ? WEBPACK_LAYERS.middleware + : undefined, library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' }, runtime: EDGE_RUNTIME_WEBPACK, asyncChunks: false, @@ -504,7 +528,7 @@ export function finalizeEntrypoint({ if ( // Client special cases - name !== 'polyfills' && + name !== CLIENT_STATIC_FILES_RUNTIME_POLYFILLS && name !== CLIENT_STATIC_FILES_RUNTIME_MAIN && name !== CLIENT_STATIC_FILES_RUNTIME_MAIN_APP && name !== CLIENT_STATIC_FILES_RUNTIME_AMP && diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 2d942a40070..58b61955249 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -55,6 +55,7 @@ import { MIDDLEWARE_MANIFEST, APP_PATHS_MANIFEST, APP_PATH_ROUTES_MANIFEST, + COMPILER_NAMES, APP_BUILD_MANIFEST, } from '../shared/lib/constants' import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils' @@ -749,17 +750,17 @@ export default async function build( Promise.all([ getBaseWebpackConfig(dir, { ...commonWebpackOptions, - compilerType: 'client', + compilerType: COMPILER_NAMES.client, entrypoints: entrypoints.client, }), getBaseWebpackConfig(dir, { ...commonWebpackOptions, - compilerType: 'server', + compilerType: COMPILER_NAMES.server, entrypoints: entrypoints.server, }), getBaseWebpackConfig(dir, { ...commonWebpackOptions, - compilerType: 'edge-server', + compilerType: COMPILER_NAMES.edgeServer, entrypoints: entrypoints.edgeServer, }), ]) diff --git a/packages/next/build/output/index.ts b/packages/next/build/output/index.ts index 44d747e3e59..da24db9a541 100644 --- a/packages/next/build/output/index.ts +++ b/packages/next/build/output/index.ts @@ -5,6 +5,7 @@ import createStore from 'next/dist/compiled/unistore' import formatWebpackMessages from '../../client/dev/error-overlay/format-webpack-messages' import { OutputState, store as consoleStore } from './store' import type { webpack5 } from 'next/dist/compiled/webpack/webpack' +import { CompilerNameValues, COMPILER_NAMES } from '../../shared/lib/constants' export function startedDevelopmentServer(appUrl: string, bindAddr: string) { consoleStore.setState({ appUrl, bindAddr }) @@ -238,7 +239,7 @@ export function watchCompilers( }) function tapCompiler( - key: 'client' | 'server' | 'edgeServer', + key: CompilerNameValues, compiler: webpack5.Compiler, onEvent: (status: WebpackStatus) => void ) { @@ -268,7 +269,7 @@ export function watchCompilers( }) } - tapCompiler('client', client, (status) => { + tapCompiler(COMPILER_NAMES.client, client, (status) => { if ( !status.loading && !buildStore.getState().server.loading && @@ -284,7 +285,7 @@ export function watchCompilers( }) } }) - tapCompiler('server', server, (status) => { + tapCompiler(COMPILER_NAMES.server, server, (status) => { if ( !status.loading && !buildStore.getState().client.loading && @@ -300,7 +301,7 @@ export function watchCompilers( }) } }) - tapCompiler('edgeServer', edgeServer, (status) => { + tapCompiler(COMPILER_NAMES.edgeServer, edgeServer, (status) => { if ( !status.loading && !buildStore.getState().client.loading && diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 964b61f3587..9dbcf267977 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -13,6 +13,7 @@ import { ROOT_DIR_ALIAS, APP_DIR_ALIAS, SERVER_RUNTIME, + WEBPACK_LAYERS, } from '../lib/constants' import { fileExists } from '../lib/file-exists' import { CustomRoutes } from '../lib/load-custom-routes.js' @@ -28,6 +29,8 @@ import { SERVERLESS_DIRECTORY, SERVER_DIRECTORY, MODERN_BROWSERSLIST_TARGET, + COMPILER_NAMES, + CompilerNameValues, } from '../shared/lib/constants' import { execOnce } from '../shared/lib/utils' import { NextConfigComplete } from '../server/config-shared' @@ -334,13 +337,13 @@ export default async function getBaseWebpackConfig( reactProductionProfiling = false, rewrites, runWebpackSpan, - target = 'server', + target = COMPILER_NAMES.server, appDir, middlewareRegex, }: { buildId: string config: NextConfigComplete - compilerType: 'client' | 'server' | 'edge-server' + compilerType: CompilerNameValues dev?: boolean entrypoints: webpack5.EntryObject hasReactRoot: boolean @@ -354,9 +357,9 @@ export default async function getBaseWebpackConfig( middlewareRegex?: string } ): Promise { - const isClient = compilerType === 'client' - const isEdgeServer = compilerType === 'edge-server' - const isNodeServer = compilerType === 'server' + const isClient = compilerType === COMPILER_NAMES.client + const isEdgeServer = compilerType === COMPILER_NAMES.edgeServer + const isNodeServer = compilerType === COMPILER_NAMES.server const { useTypeScript, jsConfig, resolvedBaseUrl } = await loadJsConfig( dir, config @@ -650,9 +653,9 @@ export default async function getBaseWebpackConfig( const reactDomDir = dirname(require.resolve('react-dom/package.json')) const mainFieldsPerCompiler: Record = { - server: ['main', 'module'], - client: ['browser', 'module', 'main'], - 'edge-server': ['browser', 'module', 'main'], + [COMPILER_NAMES.server]: ['main', 'module'], + [COMPILER_NAMES.client]: ['browser', 'module', 'main'], + [COMPILER_NAMES.edgeServer]: ['browser', 'module', 'main'], } const resolveConfig = { @@ -1310,14 +1313,14 @@ export default async function getBaseWebpackConfig( // RSC server compilation loaders { ...serverComponentCodeCondition, - issuerLayer: 'sc_server', + issuerLayer: WEBPACK_LAYERS.server, use: { loader: 'next-flight-server-loader', }, }, { test: clientComponentRegex, - issuerLayer: 'sc_server', + issuerLayer: WEBPACK_LAYERS.server, use: { loader: 'next-flight-client-loader', }, @@ -1325,7 +1328,7 @@ export default async function getBaseWebpackConfig( // _app should be treated as a client component as well as all its dependencies. { test: new RegExp(`_app\\.(${rawPageExtensions.join('|')})$`), - layer: 'sc_client', + layer: WEBPACK_LAYERS.client, }, ] : [] @@ -1336,13 +1339,13 @@ export default async function getBaseWebpackConfig( // same layer. { test: rscSharedRegex, - layer: 'rsc_shared_deps', + layer: WEBPACK_LAYERS.rscShared, }, ] : []), { test: /\.(js|cjs|mjs)$/, - issuerLayer: 'api', + issuerLayer: WEBPACK_LAYERS.api, parser: { // Switch back to normal URL handling url: true, @@ -1352,7 +1355,7 @@ export default async function getBaseWebpackConfig( oneOf: [ { ...codeCondition, - issuerLayer: 'api', + issuerLayer: WEBPACK_LAYERS.api, parser: { // Switch back to normal URL handling url: true, @@ -1361,7 +1364,7 @@ export default async function getBaseWebpackConfig( }, { ...codeCondition, - issuerLayer: 'middleware', + issuerLayer: WEBPACK_LAYERS.middleware, use: getBabelOrSwcLoader(), }, { @@ -1399,7 +1402,7 @@ export default async function getBaseWebpackConfig( { oneOf: [ { - issuerLayer: 'middleware', + issuerLayer: WEBPACK_LAYERS.middleware, resolve: { fallback: { process: require.resolve('./polyfills/process'), @@ -1499,7 +1502,7 @@ export default async function getBaseWebpackConfig( [`process.env.${key}`]: JSON.stringify(config.env[key]), } }, {}), - ...(compilerType !== 'edge-server' + ...(compilerType !== COMPILER_NAMES.edgeServer ? {} : { EdgeRuntime: JSON.stringify( @@ -1785,10 +1788,10 @@ export default async function getBaseWebpackConfig( dependency: 'url', loader: 'next-middleware-asset-loader', type: 'javascript/auto', - layer: 'edge-asset', + layer: WEBPACK_LAYERS.edgeAsset, }) webpack5Config.module?.rules?.unshift({ - issuerLayer: 'edge-asset', + issuerLayer: WEBPACK_LAYERS.edgeAsset, type: 'asset/source', }) } diff --git a/packages/next/build/webpack/config/blocks/base.ts b/packages/next/build/webpack/config/blocks/base.ts index 1cbdc083d8a..a203ba0583b 100644 --- a/packages/next/build/webpack/config/blocks/base.ts +++ b/packages/next/build/webpack/config/blocks/base.ts @@ -1,5 +1,6 @@ import curry from 'next/dist/compiled/lodash.curry' import { webpack } from 'next/dist/compiled/webpack/webpack' +import { COMPILER_NAMES } from '../../../../shared/lib/constants' import { ConfigurationContext } from '../utils' export const base = curry(function base( @@ -9,9 +10,9 @@ export const base = curry(function base( config.mode = ctx.isDevelopment ? 'development' : 'production' config.name = ctx.isServer ? ctx.isEdgeRuntime - ? 'edge-server' - : 'server' - : 'client' + ? COMPILER_NAMES.edgeServer + : COMPILER_NAMES.server + : COMPILER_NAMES.client // @ts-ignore TODO webpack 5 typings config.target = !ctx.targetWeb 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 index baec601211f..e30d5037569 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -1,7 +1,17 @@ -import { SERVER_RUNTIME } from '../../../lib/constants' +export type ClientComponentImports = string[] +export type CssImports = Record + +export type NextFlightClientEntryLoaderOptions = { + modules: ClientComponentImports + /** This is transmitted as a string to `getOptions` */ + server: boolean | 'true' | 'false' +} export default async function transformSource(this: any): Promise { - let { modules, runtime, ssr, server } = this.getOptions() + let { modules, server }: NextFlightClientEntryLoaderOptions = + this.getOptions() + const isServer = server === 'true' + if (!Array.isArray(modules)) { modules = modules ? [modules] : [] } @@ -9,25 +19,17 @@ export default async function transformSource(this: any): Promise { const requests = modules as string[] const code = requests - .filter((request) => (server ? !request.endsWith('.css') : true)) + // Filter out css files on the server + .filter((request) => (isServer ? !request.endsWith('.css') : true)) .map((request) => `import(/* webpackMode: "eager" */ '${request}')`) .join(';\n') + ` - export const __next_rsc_css__ = ${JSON.stringify( - requests.filter((request) => request.endsWith('.css')) - )}; 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 === SERVER_RUNTIME.edge - ? 'export const __N_SSP = true;' - : ssr - ? `export const __N_SSP = true;` - : `export const __N_SSG = true;`) + ` return code } diff --git a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts index 8a17b9b7da3..b4812db7f1c 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -1,21 +1,28 @@ import { stringify } from 'querystring' -import { webpack } from 'next/dist/compiled/webpack/webpack' +import path from 'path' +import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import type { webpack5 } from 'next/dist/compiled/webpack/webpack' -import { - EDGE_RUNTIME_WEBPACK, - NEXT_CLIENT_SSR_ENTRY_SUFFIX, -} 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, + EntryTypes, } from '../../../server/dev/on-demand-entry-handler' -import { getPageStaticInfo } from '../../analysis/get-page-static-info' -import { SERVER_RUNTIME } from '../../../lib/constants' +import type { + CssImports, + ClientComponentImports, + NextFlightClientEntryLoaderOptions, +} from '../loaders/next-flight-client-entry-loader' +import { APP_DIR_ALIAS } from '../../../lib/constants' +import { + COMPILER_NAMES, + FLIGHT_SERVER_CSS_MANIFEST, +} from '../../../shared/lib/constants' +import { FlightCSSManifest } from './flight-manifest-plugin' -type Options = { +interface Options { dev: boolean isEdgeServer: boolean } @@ -23,16 +30,19 @@ type Options = { const PLUGIN_NAME = 'ClientEntryPlugin' export const injectedClientEntries = new Map() + +// TODO-APP: ensure .scss / .sass also works. const regexCSS = /\.css$/ +// TODO-APP: move CSS manifest generation to the flight manifest plugin. +const flightCSSManifest: FlightCSSManifest = {} + export class FlightClientEntryPlugin { - dev: boolean = false + dev: boolean isEdgeServer: boolean constructor(options: Options) { - if (typeof options.dev === 'boolean') { - this.dev = options.dev - } + this.dev = options.dev this.isEdgeServer = options.isEdgeServer } @@ -52,160 +62,308 @@ export class FlightClientEntryPlugin { ) compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, (compilation) => { - return this.createClientEndpoints(compilation) + return this.createClientEndpoints(compiler, compilation) }) } - async createClientEndpoints(compilation: any) { - const context = (this as any).context - const promises: Array> = [] + async createClientEndpoints(compiler: any, compilation: any) { + const promises: Array< + ReturnType + > = [] // For each SC server compilation entry, we need to create its corresponding // client component entry. for (const [name, entry] of compilation.entries.entries()) { + // If the request is not for `app` directory entry skip it. + // if (!entry.request || !entry.request.startsWith('next-app-loader')) { + // 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 mod = compilation.moduleGraph.getResolvedModule(dependency) - if (!mod) return - - // Keep client imports as simple - // native or installed js module: -> raw request, e.g. next/head - // client js or css: -> user request - const rawRequest = mod.rawRequest || '' - const modRequest = - !rawRequest.endsWith('.css') && - !rawRequest.startsWith('.') && - !rawRequest.startsWith('/') - ? rawRequest - : mod.resourceResolveData?.path - - if (!modRequest || visited.has(modRequest)) return - visited.add(modRequest) - - if ( - clientComponentRegex.test(modRequest) || - regexCSS.test(modRequest) - ) { - clientComponentImports.push(modRequest) - } + // Ensure only next-app-loader entries are handled. + if ( + !entryDependency || + !entryDependency.request || + !entryDependency.request.startsWith('next-app-loader?') + ) { + continue + } - compilation.moduleGraph - .getOutgoingConnections(mod) - .forEach((connection: any) => { - filterClientComponents(connection.dependency) - }) - } + // TODO-APP: create client-side entrypoint per layout/page. + // const entryModule: webpack5.NormalModule = + // compilation.moduleGraph.getResolvedModule(entryDependency) - // Traverse the module graph to find all client components. - filterClientComponents(entryDependency) + // for (const connection of compilation.moduleGraph.getOutgoingConnections( + // entryModule + // )) { + // const layoutOrPageDependency = connection.dependency + // // const layoutOrPageRequest = connection.dependency.request - const entryModule = - compilation.moduleGraph.getResolvedModule(entryDependency) - const routeInfo = entryModule.buildInfo.route || { - page: denormalizePagePath(name.replace(/^pages/, '')), - absolutePagePath: entryModule.resource, - } + // const [clientComponentImports, cssImports] = + // this.collectClientComponentsAndCSSForDependency( + // compiler.context, + // compilation, + // layoutOrPageDependency + // ) + + // Object.assign(serverCSSManifest, cssImports) + + // promises.push( + // this.injectClientEntryAndSSRModules( + // compiler, + // compilation, + // name, + // entryDependency, + // clientComponentImports + // ) + // ) + // } + + const [clientComponentImports, cssImports] = + this.collectClientComponentsAndCSSForDependency( + compiler.context, + compilation, + entryDependency + ) + + Object.assign(flightCSSManifest, cssImports) - // Parse gSSP and gSP exports from the page source. - const pageStaticInfo = this.isEdgeServer - ? {} - : await getPageStaticInfo({ - pageFilePath: routeInfo.absolutePagePath, - nextConfig: {}, - isDev: this.dev, - }) - - const loaderOptions = { - modules: clientComponentImports, - runtime: this.isEdgeServer - ? SERVER_RUNTIME.edge - : SERVER_RUNTIME.nodejs, - ssr: pageStaticInfo.ssr, - // Adding name here to make the entry key unique. + promises.push( + this.injectClientEntryAndSSRModules( + compiler, + compilation, name, + entryDependency, + clientComponentImports + ) + ) + } + + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + // Have to be in the optimize stage to run after updating the CSS + // asset hash via extract mini css plugin. + // @ts-ignore TODO: Remove ignore when webpack 5 is stable + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH, + }, + (assets: webpack5.Compilation['assets']) => { + assets[FLIGHT_SERVER_CSS_MANIFEST + '.json'] = new sources.RawSource( + JSON.stringify(flightCSSManifest) + ) as unknown as webpack5.sources.RawSource + } + ) + + const res = await Promise.all(promises) + + // Invalidate in development to trigger recompilation + const invalidator = getInvalidator() + // Check if any of the entry injections need an invalidation + if (invalidator && res.includes(true)) { + invalidator.invalidate([COMPILER_NAMES.client]) + } + } + + collectClientComponentsAndCSSForDependency( + context: string, + compilation: any, + dependency: any /* Dependency */ + ): [ClientComponentImports, CssImports] { + /** + * Keep track of checked modules to avoid infinite loops with recursive imports. + */ + const visitedBySegment: { [segment: string]: Set } = {} + const clientComponentImports: ClientComponentImports = [] + const serverCSSImports: CssImports = {} + + const filterClientComponents = ( + dependencyToFilter: any, + segmentPath: string + ): void => { + const mod: webpack5.NormalModule = + compilation.moduleGraph.getResolvedModule(dependencyToFilter) + if (!mod) return + + // Keep client imports as simple + // native or installed js module: -> raw request, e.g. next/head + // client js or css: -> user request + const rawRequest = mod.rawRequest + + // Request could be undefined or '' + if (!rawRequest) return + + const modRequest: string | undefined = + !rawRequest.endsWith('.css') && + !rawRequest.startsWith('.') && + !rawRequest.startsWith('/') && + !rawRequest.startsWith(APP_DIR_ALIAS) + ? rawRequest + : mod.resourceResolveData?.path + + // Ensure module is not walked again if it's already been visited + if (!visitedBySegment[segmentPath]) { + visitedBySegment[segmentPath] = new Set() + } + if (!modRequest || visitedBySegment[segmentPath].has(modRequest)) return + visitedBySegment[segmentPath].add(modRequest) + + const isLayoutOrPage = + /\/(layout|page)(\.server|\.client)?\.(js|ts)x?$/.test(modRequest) + const isCSS = regexCSS.test(modRequest) + const isClientComponent = clientComponentRegex.test(modRequest) + + if (isCSS) { + serverCSSImports[segmentPath] = serverCSSImports[segmentPath] || [] + serverCSSImports[segmentPath].push(modRequest) + } + + // Check if request is for css file. + if (isClientComponent || isCSS) { + clientComponentImports.push(modRequest) + return + } + + if (isLayoutOrPage) { + segmentPath = path + .relative(path.join(context, 'app'), path.dirname(modRequest)) + .replace(/\\/g, '/') + + if (segmentPath !== '') { + segmentPath = '/' + segmentPath } - const clientLoader = `next-flight-client-entry-loader?${stringify( - loaderOptions - )}!` - const clientSSRLoader = `next-flight-client-entry-loader?${stringify({ - ...loaderOptions, - server: true, - })}!` - - const bundlePath = 'app' + 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 - ) + + // If it's a page, add an extra '/' to the segments + if (/\/(page)(\.server|\.client)?\.(js|ts)x?$/.test(modRequest)) { + segmentPath += '/' } + } - // Inject the entry to the server compiler (__sc_client__). - const clientComponentEntryDep = ( - webpack as any - ).EntryPlugin.createDependency(clientSSRLoader, { - name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, + compilation.moduleGraph + .getOutgoingConnections(mod) + .forEach((connection: any) => { + filterClientComponents(connection.dependency, segmentPath) }) - promises.push( - new Promise((res, rej) => { - compilation.addEntry( - context, - clientComponentEntryDep, - this.isEdgeServer - ? { - name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, - library: { - name: ['self._CLIENT_ENTRY'], - type: 'assign', - }, - runtime: EDGE_RUNTIME_WEBPACK, - asyncChunks: false, - } - : { - name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, - runtime: 'webpack-runtime', - }, - (err: any) => { - if (err) { - rej(err) - } else { - res() - } - } - ) - }) - ) + } + + // Traverse the module graph to find all client components. + filterClientComponents(dependency, '') + + return [clientComponentImports, serverCSSImports] + } + + async injectClientEntryAndSSRModules( + compiler: any, + compilation: any, + entryName: string, + entryDependency: any, + clientComponentImports: ClientComponentImports + ): Promise { + let shouldInvalidate = false + + const entryModule = + compilation.moduleGraph.getResolvedModule(entryDependency) + const routeInfo = entryModule.buildInfo.route || { + page: denormalizePagePath(entryName.replace(/^pages/, '')), + absolutePagePath: entryModule.resource, + } + + const loaderOptions: NextFlightClientEntryLoaderOptions = { + modules: clientComponentImports, + server: false, + } + const clientLoader = `next-flight-client-entry-loader?${stringify( + loaderOptions + )}!` + const clientSSRLoader = `next-flight-client-entry-loader?${stringify({ + ...loaderOptions, + server: true, + })}!` + + const bundlePath = 'app' + normalizePagePath(routeInfo.page) + + // Add for the client compilation + // Inject the entry to the client compiler. + if (this.dev) { + const pageKey = COMPILER_NAMES.client + routeInfo.page + if (!entries[pageKey]) { + entries[pageKey] = { + type: EntryTypes.CHILD_ENTRY, + parentEntries: new Set([entryName]), + bundlePath, + // absolutePagePath: routeInfo.absolutePagePath, + request: clientLoader, + dispose: false, + lastActiveTime: Date.now(), + } + shouldInvalidate = true + } else { + const entryData = entries[pageKey] + // New version of the client loader + if (entryData.request !== clientLoader) { + entryData.request = clientLoader + shouldInvalidate = true + } + if (entryData.type === EntryTypes.CHILD_ENTRY) { + entryData.parentEntries.add(entryName) + } } + } else { + injectedClientEntries.set(bundlePath, clientLoader) } - await Promise.all(promises) + // Inject the entry to the server compiler (__sc_client__). + const clientComponentEntryDep = ( + webpack as any + ).EntryPlugin.createDependency(clientSSRLoader, { + name: bundlePath, + }) + + // Add the dependency to the server compiler. + await this.addEntry( + compilation, + // Reuse compilation context. + compiler.context, + clientComponentEntryDep, + { + // By using the same entry name + name: entryName, + // Layer should be undefined for the SSR modules + // This ensures the client components are + layer: undefined, + } + ) + + return shouldInvalidate + } + + addEntry( + compilation: any, + context: string, + entry: any /* Dependency */, + options: { + name: string + layer: string | undefined + } /* EntryOptions */ + ): Promise /* Promise */ { + return new Promise((resolve, reject) => { + compilation.entries.get(options.name).includeDependencies.push(entry) + compilation.hooks.addEntry.call(entry, options) + compilation.addModuleTree( + { + context, + dependency: entry, + contextInfo: { issuerLayer: options.layer }, + }, + (err: Error | undefined, module: any) => { + if (err) { + compilation.hooks.failedEntry.call(entry, options, err) + return reject(err) + } + compilation.hooks.succeedEntry.call(entry, options, module) + return resolve(module) + } + ) + }) } } diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index b17e394a1f3..eff81722042 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -24,9 +24,13 @@ interface Options { pageExtensions: string[] } -type ModuleId = string | number +/** + * Webpack module id + */ +// TODO-APP ensure `null` is included as it is used. +type ModuleId = string | number /*| null*/ -type ManifestChunks = Array<`${string}:${string}` | string> +export type ManifestChunks = Array<`${string}:${string}` | string> interface ManifestNode { [moduleExport: string]: { @@ -45,7 +49,7 @@ interface ManifestNode { } } -type FlightManifest = { +export type FlightManifest = { __ssr_module_mapping__: { [moduleId: string]: ManifestNode } @@ -53,6 +57,10 @@ type FlightManifest = { [modulePath: string]: ManifestNode } +export type FlightCSSManifest = { + [modulePath: string]: string[] +} + const PLUGIN_NAME = 'FlightManifestPlugin' export class FlightManifestPlugin { @@ -139,7 +147,6 @@ export class FlightManifestPlugin { const moduleExports = manifest[resource] || {} const moduleIdMapping = manifest.__ssr_module_mapping__ - moduleIdMapping[id] = moduleIdMapping[id] || {} // Note that this isn't that reliable as webpack is still possible to assign // additional queries to make sure there's no conflict even using the `named` @@ -161,6 +168,7 @@ export class FlightManifestPlugin { chunks, }, } + moduleIdMapping[id] = moduleIdMapping[id] || {} moduleIdMapping[id]['default'] = { id: ssrNamedModuleId, name: 'default', @@ -269,6 +277,8 @@ export class FlightManifestPlugin { chunks: requiredChunks.concat([...cssChunks]), } } + + moduleIdMapping[id] = moduleIdMapping[id] || {} if (!moduleIdMapping[id][name]) { moduleIdMapping[id][name] = { ...moduleExports[name], diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 76bca9ee85e..7ebf2bd1bc9 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -626,6 +626,7 @@ function getEntryFiles(entryFiles: string[], meta: EntryMetadata) { .map( (file) => 'server/' + + // TODO-APP: seems this should be removed. file.replace('.js', NEXT_CLIENT_SSR_ENTRY_SUFFIX + '.js') ) ) diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx index a5f2f9e5862..2899be27bb2 100644 --- a/packages/next/client/components/app-router.client.tsx +++ b/packages/next/client/components/app-router.client.tsx @@ -89,13 +89,11 @@ let initialParallelRoutes: CacheNode['parallelRoutes'] = export default function AppRouter({ initialTree, initialCanonicalUrl, - initialStylesheets, children, hotReloader, }: { initialTree: FlightRouterState initialCanonicalUrl: string - initialStylesheets: string[] children: React.ReactNode hotReloader?: React.ReactNode }) { @@ -293,6 +291,7 @@ export default function AppRouter({ window.removeEventListener('popstate', onPopState) } }, [onPopState]) + return ( @@ -311,7 +310,6 @@ export default function AppRouter({ // Root node always has `url` // Provided in AppTreeContext to ensure it can be overwritten in layout-router url: canonicalUrl, - stylesheets: initialStylesheets, }} > diff --git a/packages/next/client/components/layout-router.client.tsx b/packages/next/client/components/layout-router.client.tsx index 1b0b93ad123..bffb974c706 100644 --- a/packages/next/client/components/layout-router.client.tsx +++ b/packages/next/client/components/layout-router.client.tsx @@ -360,11 +360,6 @@ export default function OuterLayoutRouter({ return ( <> - {/* {stylesheets - ? stylesheets.map((href) => ( - - )) - : null} */} {preservedSegments.map((preservedSegment) => { return ( // Loading boundary is render for each segment to ensure they have their own loading state. diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index a7f0d8018b0..ae0fc0cdd87 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -75,3 +75,12 @@ export const SERVER_RUNTIME: Record = { edge: 'experimental-edge', nodejs: 'nodejs', } + +export const WEBPACK_LAYERS = { + server: 'sc_server', + client: 'sc_client', + api: 'api', + rscShared: 'rsc_shared_deps', + middleware: 'middleware', + edgeAsset: 'edge-asset', +} diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index a659a921895..b3906ed2b01 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -22,6 +22,10 @@ import { htmlEscapeJsonString } from './htmlescape' import { shouldUseReactRoot, stripInternalQueries } from './utils' import { NextApiRequestCookies } from './api-utils' import { matchSegment } from '../client/components/match-segments' +import { + FlightCSSManifest, + FlightManifest, +} from '../build/webpack/plugins/flight-manifest-plugin' import { FlushEffectsContext } from '../client/components/hooks-client' // this needs to be required lazily so that `next-server` can set @@ -34,6 +38,7 @@ export type RenderOptsPartial = { err?: Error | null dev?: boolean serverComponentManifest?: any + serverCSSManifest?: any supportsDynamicHTML?: boolean runtime?: ServerRuntime serverComponents?: boolean @@ -369,26 +374,32 @@ function getSegmentParam(segment: string): { } /** - * Get inline tags based on __next_rsc_css__ manifest. Only used when rendering to HTML. + * Get inline tags based on server CSS manifest. Only used when rendering to HTML. */ function getCssInlinedLinkTags( - ComponentMod: any, - serverComponentManifest: any + serverComponentManifest: FlightManifest, + serverCSSManifest: FlightCSSManifest ) { - const importedServerCSSFiles: string[] = - ComponentMod.__client__?.__next_rsc_css__ || [] - - return Array.from( - new Set( - importedServerCSSFiles - .map((css) => - css.endsWith('.css') - ? serverComponentManifest[css].default.chunks - : [] - ) - .flat() - ) - ) + const chunks: { [file: string]: string[] } = {} + + // APP-TODO: Remove this once we have CSS injections at each level. + const allChunks = new Set() + + for (const layoutOrPage in serverCSSManifest) { + const uniqueChunks = new Set() + for (const css of serverCSSManifest[layoutOrPage]) { + for (const chunk of serverComponentManifest[css].default.chunks) { + if (!uniqueChunks.has(chunk)) { + uniqueChunks.add(chunk) + chunks[layoutOrPage] = chunks[layoutOrPage] || [] + chunks[layoutOrPage].push(chunk) + } + allChunks.add(chunk) + } + } + } + + return [chunks, [...allChunks]] as [{ [file: string]: string[] }, string[]] } export async function renderToHTMLOrFlight( @@ -412,6 +423,7 @@ export async function renderToHTMLOrFlight( const { buildManifest, serverComponentManifest, + serverCSSManifest, supportsDynamicHTML, ComponentMod, } = renderOpts @@ -583,12 +595,16 @@ export async function renderToHTMLOrFlight( parentParams, firstItem, rootLayoutIncluded, - }: { + serverStylesheets, + }: // parentSegmentPath, + { createSegmentPath: CreateSegmentPath loaderTree: LoaderTree parentParams: { [key: string]: any } rootLayoutIncluded?: boolean firstItem?: boolean + serverStylesheets: { [file: string]: string[] } + // parentSegmentPath: string }): Promise<{ Component: React.ComponentType }> => { const Loading = loading ? await interopDefault(loading()) : undefined const isLayout = typeof layout !== 'undefined' @@ -608,6 +624,10 @@ export async function renderToHTMLOrFlight( const rootLayoutIncludedAtThisLevelOrAbove = rootLayoutIncluded || rootLayoutAtThisLevel + // const cssSegmentPath = + // !parentSegmentPath && !segment ? '' : parentSegmentPath + '/' + segment + // const stylesheets = serverStylesheets[cssSegmentPath] + /** * Check if the current layout/page is a client component */ @@ -668,6 +688,8 @@ export async function renderToHTMLOrFlight( loaderTree: parallelRoutes[parallelRouteKey], parentParams: currentParams, rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, + serverStylesheets, + // parentSegmentPath: cssSegmentPath, }) const childSegment = parallelRoutes[parallelRouteKey][0] @@ -807,16 +829,23 @@ export async function renderToHTMLOrFlight( } return ( - + <> + {/* {stylesheets + ? stylesheets.map((href) => ( + + )) + : null} */} + + ) }, } @@ -881,6 +910,8 @@ export async function renderToHTMLOrFlight( loaderTree: loaderTreeToFilter, parentParams: currentParams, firstItem: true, + serverStylesheets: serverCSSManifest, + // parentSegmentPath: '', } ) ).Component @@ -928,12 +959,20 @@ export async function renderToHTMLOrFlight( // Below this line is handling for rendering to HTML. + // Get all the server imported styles. + const [mappedServerCSSManifest, initialStylesheets] = getCssInlinedLinkTags( + serverComponentManifest, + serverCSSManifest + ) + // Create full component tree from root to leaf. const { Component: ComponentTree } = await createComponentTree({ createSegmentPath: (child) => child, loaderTree: loaderTree, parentParams: {}, firstItem: true, + serverStylesheets: mappedServerCSSManifest, + // parentSegmentPath: '', }) // AppRouter is provided by next-app-loader @@ -947,10 +986,6 @@ export async function renderToHTMLOrFlight( // TODO-APP: validate req.url as it gets passed to render. const initialCanonicalUrl = req.url! - const initialStylesheets: string[] = getCssInlinedLinkTags( - ComponentMod, - serverComponentManifest - ) /** * A new React Component that renders the provided React Component @@ -969,7 +1004,6 @@ export async function renderToHTMLOrFlight( } initialCanonicalUrl={initialCanonicalUrl} initialTree={initialTree} - initialStylesheets={initialStylesheets} > diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 8fdce36bbf9..ee2003f6791 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -30,7 +30,6 @@ import { format as formatUrl, parse as parseUrl } from 'url' import { getRedirectStatus } from '../lib/redirect-status' import { NEXT_BUILTIN_DOCUMENT, - NEXT_CLIENT_SSR_ENTRY_SUFFIX, SERVERLESS_DIRECTORY, SERVER_DIRECTORY, STATIC_STATUS_PAGES, @@ -211,6 +210,7 @@ export default abstract class Server { protected appPathRoutes?: Record protected customRoutes: CustomRoutes protected serverComponentManifest?: any + protected serverCSSManifest?: any public readonly hostname?: string public readonly port?: number @@ -245,6 +245,7 @@ export default abstract class Server { protected abstract getRoutesManifest(): CustomRoutes protected abstract getPrerenderManifest(): PrerenderManifest protected abstract getServerComponentManifest(): any + protected abstract getServerCSSManifest(): any protected abstract attachRequestMeta( req: BaseNextRequest, parsedUrl: NextUrlWithParsedQuery @@ -331,6 +332,9 @@ export default abstract class Server { this.serverComponentManifest = serverComponents ? this.getServerComponentManifest() : undefined + this.serverCSSManifest = serverComponents + ? this.getServerCSSManifest() + : undefined this.renderOpts = { poweredByHeader: this.nextConfig.poweredByHeader, @@ -1046,9 +1050,6 @@ export default abstract class Server { const appPathRoutes: Record = {} Object.keys(this.appPathsManifest || {}).forEach((entry) => { - if (entry.endsWith(NEXT_CLIENT_SSR_ENTRY_SUFFIX)) { - return - } appPathRoutes[normalizeAppPath(entry) || '/'] = entry }) return appPathRoutes diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 73d879c3022..6fd55bb5a49 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -21,16 +21,14 @@ import * as Log from '../../build/output/log' import getBaseWebpackConfig from '../../build/webpack-config' import { APP_DIR_ALIAS } from '../../lib/constants' import { recursiveDelete } from '../../lib/recursive-delete' -import { - BLOCKED_PAGES, - NEXT_CLIENT_SSR_ENTRY_SUFFIX, -} from '../../shared/lib/constants' +import { BLOCKED_PAGES, COMPILER_NAMES } from '../../shared/lib/constants' import { __ApiPreviewProps } from '../api-utils' import { getPathMatch } from '../../shared/lib/router/utils/path-match' import { findPageFile } from '../lib/find-page-file' import { BUILDING, entries, + EntryTypes, getInvalidator, onDemandEntryHandler, } from './on-demand-entry-handler' @@ -50,7 +48,6 @@ import ws from 'next/dist/compiled/ws' import { promises as fs } from 'fs' import { getPageStaticInfo } from '../../build/analysis/get-page-static-info' import { serverComponentRegex } from '../../build/webpack/loaders/utils' -import { stringify } from 'querystring' const wsServer = new ws.Server({ noServer: true }) @@ -457,17 +454,17 @@ export default class HotReloader { Promise.all([ getBaseWebpackConfig(this.dir, { ...commonWebpackOptions, - compilerType: 'client', + compilerType: COMPILER_NAMES.client, entrypoints: entrypoints.client, }), getBaseWebpackConfig(this.dir, { ...commonWebpackOptions, - compilerType: 'server', + compilerType: COMPILER_NAMES.server, entrypoints: entrypoints.server, }), getBaseWebpackConfig(this.dir, { ...commonWebpackOptions, - compilerType: 'edge-server', + compilerType: COMPILER_NAMES.edgeServer, entrypoints: entrypoints.edgeServer, }), ]) @@ -481,7 +478,7 @@ export default class HotReloader { const fallbackConfig = await getBaseWebpackConfig(this.dir, { runWebpackSpan: this.hotReloaderSpan, dev: true, - compilerType: 'client', + compilerType: COMPILER_NAMES.client, config: this.config, buildId: this.buildId, pagesDir: this.pagesDir, @@ -550,51 +547,60 @@ export default class HotReloader { config.entry = async (...args) => { // @ts-ignore entry is always a function const entrypoints = await defaultEntry(...args) - const isClientCompilation = config.name === 'client' - const isNodeServerCompilation = config.name === 'server' - const isEdgeServerCompilation = config.name === 'edge-server' + const isClientCompilation = config.name === COMPILER_NAMES.client + const isNodeServerCompilation = config.name === COMPILER_NAMES.server + const isEdgeServerCompilation = + config.name === COMPILER_NAMES.edgeServer await Promise.all( - Object.keys(entries).map(async (pageKey) => { - const { bundlePath, absolutePagePath, dispose } = entries[pageKey] - - // @FIXME - const { clientLoader } = entries[pageKey] as any + Object.keys(entries).map(async (entryKey) => { + const entryData = entries[entryKey] + const { bundlePath, dispose } = entryData - const result = /^(client|server|edge-server)(.*)/g.exec(pageKey) + const result = /^(client|server|edge-server)(.*)/g.exec(entryKey) const [, key, page] = result! // this match should always happen - if (key === 'client' && !isClientCompilation) return - if (key === 'server' && !isNodeServerCompilation) return - if (key === 'edge-server' && !isEdgeServerCompilation) return + if (key === COMPILER_NAMES.client && !isClientCompilation) return + if (key === COMPILER_NAMES.server && !isNodeServerCompilation) + return + if (key === COMPILER_NAMES.edgeServer && !isEdgeServerCompilation) + return + + const isEntry = entryData.type === EntryTypes.ENTRY + const isChildEntry = entryData.type === EntryTypes.CHILD_ENTRY // Check if the page was removed or disposed and remove it - const pageExists = !dispose && (await fileExists(absolutePagePath)) - if (!pageExists) { - delete entries[pageKey] - return + if (isEntry) { + const pageExists = + !dispose && (await fileExists(entryData.absolutePagePath)) + if (!pageExists) { + delete entries[entryKey] + return + } } - const isServerComponent = - serverComponentRegex.test(absolutePagePath) - const isInsideAppDir = - this.appDir && absolutePagePath.startsWith(this.appDir) + const isServerComponent = isEntry + ? serverComponentRegex.test(entryData.absolutePagePath) + : false - const staticInfo = await getPageStaticInfo({ - pageFilePath: absolutePagePath, - nextConfig: this.config, - }) + const staticInfo = isEntry + ? await getPageStaticInfo({ + pageFilePath: entryData.absolutePagePath, + nextConfig: this.config, + }) + : {} - runDependingOnPageType({ + await runDependingOnPageType({ page, pageRuntime: staticInfo.runtime, onEdgeServer: () => { - if (!isEdgeServerCompilation) return - entries[pageKey].status = BUILDING + // TODO-APP: verify if child entry should support. + if (!isEdgeServerCompilation || !isEntry) return + entries[entryKey].status = BUILDING entrypoints[bundlePath] = finalizeEntrypoint({ - compilerType: 'edge-server', + compilerType: COMPILER_NAMES.edgeServer, name: bundlePath, value: getEdgeServerEntry({ - absolutePagePath, + absolutePagePath: entryData.absolutePagePath, buildId: this.buildId, bundlePath, config: this.config, @@ -608,28 +614,21 @@ export default class HotReloader { }, onClient: () => { if (!isClientCompilation) return - if (isServerComponent || isInsideAppDir) { - entries[pageKey].status = BUILDING + if (isChildEntry) { + entries[entryKey].status = BUILDING entrypoints[bundlePath] = finalizeEntrypoint({ name: bundlePath, - compilerType: 'client', - value: - `next-client-pages-loader?${stringify({ - isServerComponent, - page: denormalizePagePath( - bundlePath.replace(/^pages/, '') - ), - absolutePagePath: clientLoader, - })}!` + clientLoader, + compilerType: COMPILER_NAMES.client, + value: entryData.request, appDir: this.config.experimental.appDir, }) } else { - entries[pageKey].status = BUILDING + entries[entryKey].status = BUILDING entrypoints[bundlePath] = finalizeEntrypoint({ name: bundlePath, - compilerType: 'client', + compilerType: COMPILER_NAMES.client, value: getClientEntry({ - absolutePagePath, + absolutePagePath: entryData.absolutePagePath, page, }), appDir: this.config.experimental.appDir, @@ -637,11 +636,18 @@ export default class HotReloader { } }, onServer: () => { - if (!isNodeServerCompilation) return - entries[pageKey].status = BUILDING - let request = relative(config.context!, absolutePagePath) - if (!isAbsolute(request) && !request.startsWith('../')) { - request = `./${request}` + // TODO-APP: verify if child entry should support. + if (!isNodeServerCompilation || !isEntry) return + entries[entryKey].status = BUILDING + let relativeRequest = relative( + config.context!, + entryData.absolutePagePath + ) + if ( + !isAbsolute(relativeRequest) && + !relativeRequest.startsWith('../') + ) { + relativeRequest = `./${relativeRequest}` } entrypoints[bundlePath] = finalizeEntrypoint({ @@ -654,12 +660,12 @@ export default class HotReloader { name: bundlePath, pagePath: join( APP_DIR_ALIAS, - relative(this.appDir!, absolutePagePath) + relative(this.appDir!, entryData.absolutePagePath) ), appDir: this.appDir!, pageExtensions: this.config.pageExtensions, }) - : request, + : relativeRequest, appDir: this.config.experimental.appDir, }) }, @@ -699,8 +705,7 @@ export default class HotReloader { stats.entrypoints.forEach((entry, key) => { if ( key.startsWith('pages/') || - (key.startsWith('app/') && - !key.endsWith(NEXT_CLIENT_SSR_ENTRY_SUFFIX)) || + key.startsWith('app/') || isMiddlewareFilename(key) ) { // TODO this doesn't handle on demand loaded chunks @@ -991,7 +996,10 @@ export default class HotReloader { ) } - public async ensurePage(page: string, clientOnly: boolean = false) { + public async ensurePage( + page: string, + clientOnly: boolean = false + ): Promise { // Make sure we don't re-build or dispose prebuilt pages if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) { return diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 59fc5d88f50..bb694fde62a 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -30,6 +30,7 @@ import { CLIENT_STATIC_FILES_PATH, DEV_CLIENT_PAGES_MANIFEST, DEV_MIDDLEWARE_MANIFEST, + COMPILER_NAMES, } from '../../shared/lib/constants' import Server, { WrappedBuildError } from '../next-server' import { getRouteMatcher } from '../../shared/lib/router/utils/route-matcher' @@ -376,7 +377,7 @@ export default class DevServer extends Server { continue } - runDependingOnPageType({ + await runDependingOnPageType({ page: pageName, pageRuntime: staticInfo.runtime, onClient: () => {}, @@ -836,7 +837,7 @@ export default class DevServer extends Server { const src = getErrorSource(err as Error) const compilation = ( - src === 'edge-server' + src === COMPILER_NAMES.edgeServer ? this.hotReloader?.edgeServerStats?.compilation : this.hotReloader?.serverStats?.compilation )! @@ -865,7 +866,7 @@ export default class DevServer extends Server { Log[type === 'warning' ? 'warn' : 'error']( `${file} (${lineNumber}:${column}) @ ${methodName}` ) - if (src === 'edge-server') { + if (src === COMPILER_NAMES.edgeServer) { err = err.message } if (type === 'warning') { @@ -939,6 +940,10 @@ export default class DevServer extends Server { return undefined } + protected getServerCSSManifest() { + return undefined + } + protected async hasMiddleware(): Promise { return this.hasPage(this.actualMiddlewareFile!) } @@ -1123,7 +1128,7 @@ export default class DevServer extends Server { } } - protected async ensureApiPage(pathname: string) { + protected async ensureApiPage(pathname: string): Promise { return this.hotReloader!.ensurePage(pathname) } @@ -1148,6 +1153,7 @@ export default class DevServer extends Server { // manifest. if (serverComponents) { this.serverComponentManifest = super.getServerComponentManifest() + this.serverCSSManifest = super.getServerCSSManifest() } return super.findPageComponents(pathname, query, params, isAppDir) diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 76f126abf6c..950f6b28f7f 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -16,6 +16,18 @@ import { getPageStaticInfo } from '../../build/analysis/get-page-static-info' import { isMiddlewareFile, isMiddlewareFilename } from '../../build/utils' import { PageNotFoundError } from '../../shared/lib/utils' import { DynamicParamTypesShort, FlightRouterState } from '../app-render' +import { + CompilerNameValues, + COMPILER_INDEXES, + COMPILER_NAMES, +} from '../../shared/lib/constants' + +/** + * Returns object keys with type inferred from the object key + */ +const keys = Object.keys as (o: T) => Extract[] + +const COMPILER_KEYS = keys(COMPILER_INDEXES) function treePathToEntrypoint( segmentPath: string[], @@ -23,7 +35,7 @@ function treePathToEntrypoint( ): string { const [parallelRouteKey, segment] = segmentPath - // TODO: modify this path to cover parallelRouteKey convention + // TODO-APP: modify this path to cover parallelRouteKey convention const path = (parentPath ? parentPath + '/' : '') + (parallelRouteKey !== 'children' ? parallelRouteKey + '/' : '') + @@ -89,106 +101,124 @@ export const ADDED = Symbol('added') export const BUILDING = Symbol('building') export const BUILT = Symbol('built') +interface EntryType { + /** + * Tells if a page is scheduled to be disposed. + */ + dispose?: boolean + /** + * Timestamp with the last time the page was active. + */ + lastActiveTime?: number + /** + * Page build status. + */ + status?: typeof ADDED | typeof BUILDING | typeof BUILT + + /** + * Path to the page file relative to the dist folder with no extension. + * For example: `pages/about/index` + */ + bundlePath: string + + /** + * Webpack request to create a dependency for. + */ + request: string +} + +// Shadowing check in ESLint does not account for enum +// eslint-disable-next-line no-shadow +export const enum EntryTypes { + ENTRY, + CHILD_ENTRY, +} +interface Entry extends EntryType { + type: EntryTypes.ENTRY + /** + * The absolute page to the page file. Used for detecting if the file was removed. For example: + * `/Users/Rick/project/pages/about/index.js` + */ + absolutePagePath: string +} + +interface ChildEntry extends EntryType { + type: EntryTypes.CHILD_ENTRY + /** + * Which parent entries use this childEntry. + */ + parentEntries: Set +} + export const entries: { /** * The key composed of the compiler name and the page. For example: * `edge-server/about` */ - [page: string]: { - /** - * The absolute page to the page file. For example: - * `/Users/Rick/project/pages/about/index.js` - */ - absolutePagePath: string - /** - * Path to the page file relative to the dist folder with no extension. - * 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. - */ - dispose?: boolean - /** - * Timestamp with the last time the page was active. - */ - lastActiveTime?: number - /** - * Page build status. - */ - status?: typeof ADDED | typeof BUILDING | typeof BUILT - } + [entryName: string]: Entry | ChildEntry } = {} -// Make sure only one invalidation happens at a timeāˆ« +let invalidator: Invalidator +export const getInvalidator = () => invalidator + +const doneCallbacks: EventEmitter | null = new EventEmitter() +const lastClientAccessPages = [''] +const lastServerAccessPagesForAppDir = [''] + +type BuildingTracker = Set +type RebuildTracker = Set + +// Make sure only one invalidation happens at a time // Otherwise, webpack hash gets changed and it'll force the client to reload. class Invalidator { private multiCompiler: webpack.MultiCompiler - private building: boolean - public rebuildAgain: boolean + + private building: BuildingTracker = new Set() + private rebuildAgain: RebuildTracker = new Set() constructor(multiCompiler: webpack.MultiCompiler) { this.multiCompiler = multiCompiler - // contains an array of types of compilers currently building - this.building = false - this.rebuildAgain = false } - invalidate(keys: string[] = []) { - // If there's a current build is processing, we won't abort it by invalidating. - // (If aborted, it'll cause a client side hard reload) - // But let it to invalidate just after the completion. - // So, it can re-build the queued pages at once. - if (this.building) { - this.rebuildAgain = true - return - } - - this.building = true + public shouldRebuildAll() { + return this.rebuildAgain.size > 0 + } - if (!keys || keys.length === 0) { - this.multiCompiler.compilers[0].watching?.invalidate() - this.multiCompiler.compilers[1].watching?.invalidate() - this.multiCompiler.compilers[2].watching?.invalidate() - return - } + invalidate(compilerKeys: typeof COMPILER_KEYS = COMPILER_KEYS): void { + for (const key of compilerKeys) { + // If there's a current build is processing, we won't abort it by invalidating. + // (If aborted, it'll cause a client side hard reload) + // But let it to invalidate just after the completion. + // So, it can re-build the queued pages at once. - for (const key of keys) { - if (key === 'client') { - this.multiCompiler.compilers[0].watching?.invalidate() - } else if (key === 'server') { - this.multiCompiler.compilers[1].watching?.invalidate() - } else if (key === 'edgeServer') { - this.multiCompiler.compilers[2].watching?.invalidate() + if (this.building.has(key)) { + this.rebuildAgain.add(key) + continue } + + this.multiCompiler.compilers[COMPILER_INDEXES[key]].watching?.invalidate() + this.building.add(key) } } - startBuilding() { - this.building = true + public startBuilding(compilerKey: keyof typeof COMPILER_INDEXES) { + this.building.add(compilerKey) } - doneBuilding() { - this.building = false + public doneBuilding() { + const rebuild: typeof COMPILER_KEYS = [] + for (const key of COMPILER_KEYS) { + this.building.delete(key) - if (this.rebuildAgain) { - this.rebuildAgain = false - this.invalidate() + if (this.rebuildAgain.has(key)) { + rebuild.push(key) + this.rebuildAgain.delete(key) + } } + this.invalidate(rebuild) } } -let invalidator: Invalidator -export const getInvalidator = () => invalidator - -const doneCallbacks: EventEmitter | null = new EventEmitter() -const lastClientAccessPages = [''] -const lastServerAccessPagesForAppDir = [''] - export function onDemandEntryHandler({ maxInactiveAge, multiCompiler, @@ -208,15 +238,16 @@ export function onDemandEntryHandler({ }) { invalidator = new Invalidator(multiCompiler) - const startBuilding = (_compilation: webpack.Compilation) => { - invalidator.startBuilding() + const startBuilding = (compilation: webpack.Compilation) => { + const compilationName = compilation.name as any as CompilerNameValues + invalidator.startBuilding(compilationName) } for (const compiler of multiCompiler.compilers) { compiler.hooks.make.tap('NextJsOnDemandEntries', startBuilding) } function getPagePathsFromEntrypoints( - type: 'client' | 'server' | 'edge-server', + type: CompilerNameValues, entrypoints: Map, root?: boolean ) { @@ -237,25 +268,25 @@ export function onDemandEntryHandler({ } multiCompiler.hooks.done.tap('NextJsOnDemandEntries', (multiStats) => { - if (invalidator.rebuildAgain) { + if (invalidator.shouldRebuildAll()) { return invalidator.doneBuilding() } const [clientStats, serverStats, edgeServerStats] = multiStats.stats const root = !!appDir const pagePaths = [ ...getPagePathsFromEntrypoints( - 'client', + COMPILER_NAMES.client, clientStats.compilation.entrypoints, root ), ...getPagePathsFromEntrypoints( - 'server', + COMPILER_NAMES.server, serverStats.compilation.entrypoints, root ), ...(edgeServerStats ? getPagePathsFromEntrypoints( - 'edge-server', + COMPILER_NAMES.edgeServer, edgeServerStats.compilation.entrypoints, root ) @@ -352,7 +383,7 @@ export function onDemandEntryHandler({ } return { - async ensurePage(page: string, clientOnly: boolean) { + async ensurePage(page: string, clientOnly: boolean): Promise { const pagePathData = await findPagePathData( rootDir, pagesDir, @@ -362,44 +393,65 @@ export function onDemandEntryHandler({ ) let entryAdded = false + const added = new Map>() - const addPageEntry = (type: 'client' | 'server' | 'edge-server') => { - return new Promise((resolve, reject) => { - const isServerComponent = serverComponentRegex.test( - pagePathData.absolutePagePath - ) - const isInsideAppDir = - appDir && pagePathData.absolutePagePath.startsWith(appDir) + const isServerComponent = serverComponentRegex.test( + pagePathData.absolutePagePath + ) + const isInsideAppDir = + appDir && pagePathData.absolutePagePath.startsWith(appDir) + + const addPageEntry = ( + compilerType: CompilerNameValues + ): Promise => { + let resolve: (value: void | PromiseLike) => void + let reject: (reason?: any) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) - const pageKey = `${type}${pagePathData.page}` + const pageKey = `${compilerType}${pagePathData.page}` - if (entries[pageKey]) { - entries[pageKey].dispose = false - entries[pageKey].lastActiveTime = Date.now() - if (entries[pageKey].status === BUILT) { - resolve() - return - } + if (entries[pageKey]) { + added.set(compilerType, promise) + + entries[pageKey].dispose = false + entries[pageKey].lastActiveTime = Date.now() + if (entries[pageKey].status === BUILT) { + resolve!() + return promise + } + } else { + if ( + compilerType === COMPILER_NAMES.client && + (isServerComponent || isInsideAppDir) + ) { + // Skip adding the client entry here. } else { - if (type === 'client' && (isServerComponent || isInsideAppDir)) { - // Skip adding the client entry here. - } else { - entryAdded = true - entries[pageKey] = { - absolutePagePath: pagePathData.absolutePagePath, - bundlePath: pagePathData.bundlePath, - dispose: false, - lastActiveTime: Date.now(), - status: ADDED, - } + added.set(compilerType, promise) + + entryAdded = true + entries[pageKey] = { + type: EntryTypes.ENTRY, + absolutePagePath: pagePathData.absolutePagePath, + request: pagePathData.absolutePagePath, + bundlePath: pagePathData.bundlePath, + dispose: false, + lastActiveTime: Date.now(), + status: ADDED, } } + } - doneCallbacks!.once(pageKey, (err: Error) => { - if (err) return reject(err) - resolve() - }) + doneCallbacks!.once(pageKey, (err: Error) => { + if (err) { + return reject(err) + } + resolve() }) + + return promise } const staticInfo = await getPageStaticInfo({ @@ -407,25 +459,30 @@ export function onDemandEntryHandler({ nextConfig, }) - const result = runDependingOnPageType({ + await runDependingOnPageType({ page: pagePathData.page, pageRuntime: staticInfo.runtime, - onClient: () => addPageEntry('client'), - onServer: () => addPageEntry('server'), - onEdgeServer: () => addPageEntry('edge-server'), + onClient: () => { + addPageEntry(COMPILER_NAMES.client) + }, + onServer: () => { + addPageEntry(COMPILER_NAMES.server) + }, + onEdgeServer: () => { + addPageEntry(COMPILER_NAMES.edgeServer) + }, }) - const promises = Object.values(result) if (entryAdded) { reportTrigger( - !clientOnly && promises.length > 1 + !clientOnly && added.size > 1 ? `${pagePathData.page} (client and server)` : pagePathData.page ) - invalidator.invalidate(Object.keys(result)) + invalidator.invalidate([...added.keys()]) } - return Promise.all(promises) + await Promise.all(added.values()) }, onHMR(client: ws) { @@ -453,17 +510,18 @@ export function onDemandEntryHandler({ } function disposeInactiveEntries(maxInactiveAge: number) { - Object.keys(entries).forEach((page) => { - const { lastActiveTime, status, dispose, bundlePath } = entries[page] + Object.keys(entries).forEach((entryKey) => { + const entryData = entries[entryKey] + const { lastActiveTime, status, dispose } = entryData - const isClientComponentsEntry = - bundlePath.startsWith('app/') && page.startsWith('client/') - - // Disposing client component entry is handled when disposing server component entry - if (isClientComponentsEntry) return + // TODO-APP: implement disposing of CHILD_ENTRY + if (entryData.type === EntryTypes.CHILD_ENTRY) { + return + } - // Skip pages already scheduled for disposing - if (dispose) return + if (dispose) + // Skip pages already scheduled for disposing + return // This means this entry is currently building or just added // We don't need to dispose those entries. @@ -473,20 +531,13 @@ function disposeInactiveEntries(maxInactiveAge: number) { // Sometimes, it's possible our XHR ping to wait before completing other requests. // In that case, we should not dispose the current viewing page if ( - lastClientAccessPages.includes(page) || - lastServerAccessPagesForAppDir.includes(page) + lastClientAccessPages.includes(entryKey) || + lastServerAccessPagesForAppDir.includes(entryKey) ) return if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) { - const isServerComponentsEntry = - bundlePath.startsWith('app/') && page.startsWith('server/') - - // Dispose client component entrypoint when server component entrypoint is disposed. - if (isServerComponentsEntry) { - entries[page.replace('server/', 'client/')].dispose = true - } - entries[page].dispose = true + entries[entryKey].dispose = true } }) } diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 68ed0c65499..e54139ce051 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -13,13 +13,11 @@ import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, FLIGHT_MANIFEST, - NEXT_CLIENT_SSR_ENTRY_SUFFIX, } from '../shared/lib/constants' import { join } from 'path' import { requirePage, getPagePath } from './require' import { BuildManifest } from './get-page-files' import { interopDefault } from '../lib/interop-default' -import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' export type ManifestItem = { id: number | string @@ -122,22 +120,6 @@ export async function loadComponents( : null, ]) - if (hasServerComponents) { - try { - // Make sure to also load the client entry in cache. - const __client__ = await requirePage( - normalizePagePath(pathname) + NEXT_CLIENT_SSR_ENTRY_SUFFIX, - distDir, - serverless, - appDirEnabled - ) - ComponentMod.__client__ = __client__ - } catch (_) { - // This page might not be a server component page, so there is no - // client entry to load. - } - } - const Component = interopDefault(ComponentMod) const Document = interopDefault(DocumentMod) const App = interopDefault(AppMod) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 69bee1ffbfd..2148b9d96f3 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -38,6 +38,7 @@ import { FLIGHT_MANIFEST, CLIENT_PUBLIC_FILES_PATH, APP_PATHS_MANIFEST, + FLIGHT_SERVER_CSS_MANIFEST, } from '../shared/lib/constants' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { format as formatUrl, UrlWithParsedQuery } from 'url' @@ -642,6 +643,7 @@ export default class NextNodeServer extends BaseServer { // object here but only updating its `serverComponentManifest` field. // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 renderOpts.serverComponentManifest = this.serverComponentManifest + renderOpts.serverCSSManifest = this.serverCSSManifest if ( this.nextConfig.experimental.appDir && @@ -787,6 +789,15 @@ export default class NextNodeServer extends BaseServer { return require(join(this.distDir, 'server', FLIGHT_MANIFEST + '.json')) } + protected getServerCSSManifest() { + if (!this.nextConfig.experimental.serverComponents) return undefined + return require(join( + this.distDir, + 'server', + FLIGHT_SERVER_CSS_MANIFEST + '.json' + )) + } + protected getCacheFilesystem(): CacheFs { return { readFile: (f) => fs.promises.readFile(f, 'utf8'), diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index f91f62d8935..f4de05677fa 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -136,6 +136,18 @@ export function createFlushEffectStream( }) } +export function renderToInitialStream({ + ReactDOMServer, + element, + streamOptions, +}: { + ReactDOMServer: any + element: React.ReactElement + streamOptions?: any +}): Promise { + return ReactDOMServer.renderToReadableStream(element, streamOptions) +} + export function createHeadInjectionTransformStream( inject: () => string ): TransformStream { @@ -156,18 +168,6 @@ export function createHeadInjectionTransformStream( }) } -export function renderToInitialStream({ - ReactDOMServer, - element, - streamOptions, -}: { - ReactDOMServer: any - element: React.ReactElement - streamOptions?: any -}): Promise { - return ReactDOMServer.renderToReadableStream(element, streamOptions) -} - export async function continueFromInitialStream( renderStream: ReactReadableStream, { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 093db7c9fa8..779120642f1 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -40,6 +40,7 @@ import { UNSTABLE_REVALIDATE_RENAME_ERROR, } from '../lib/constants' import { + COMPILER_NAMES, NEXT_BUILTIN_DOCUMENT, SERVER_PROPS_ID, STATIC_PROPS_ID, @@ -229,6 +230,7 @@ export type RenderOptsPartial = { resolvedUrl?: string resolvedAsPath?: string serverComponentManifest?: any + serverCSSManifest?: any distDir?: string locale?: string locales?: string[] @@ -1491,7 +1493,8 @@ export async function renderToHTML( } function errorToJSON(err: Error) { - let source: 'server' | 'edge-server' = 'server' + let source: typeof COMPILER_NAMES.server | typeof COMPILER_NAMES.edgeServer = + 'server' if (process.env.NEXT_RUNTIME !== 'edge') { source = @@ -1511,7 +1514,10 @@ function errorToJSON(err: Error) { function serializeError( dev: boolean | undefined, err: Error -): Error & { statusCode?: number; source?: 'edge-server' | 'server' } { +): Error & { + statusCode?: number + source?: typeof COMPILER_NAMES.server | typeof COMPILER_NAMES.edgeServer +} { if (dev) { return errorToJSON(err) } diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 9b067fb5e1f..4970c4a9a98 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -127,6 +127,10 @@ export default class NextWebServer extends BaseServer { // @TODO: Need to return `extendRenderOpts.serverComponentManifest` here. return undefined } + protected getServerCSSManifest() { + // TODO-APP: Support web server. + return undefined + } protected async renderHTML( req: WebNextRequest, _res: WebNextResponse, diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index b641c2e3a61..6705ab73e29 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -3,7 +3,10 @@ import { decorateServerError, getServerError, } from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware' -import { EDGE_UNSUPPORTED_NODE_APIS } from '../../../shared/lib/constants' +import { + COMPILER_NAMES, + EDGE_UNSUPPORTED_NODE_APIS, +} from '../../../shared/lib/constants' import { EdgeRuntime } from 'next/dist/compiled/edge-runtime' import { readFileSync, promises as fs } from 'fs' import { validateURL } from '../utils' @@ -126,7 +129,7 @@ async function createModuleContext(options: ModuleContextOptions) { new Error( `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` ), - 'edge-server' + COMPILER_NAMES.edgeServer ) warning.name = 'DynamicCodeEvaluationWarning' Error.captureStackTrace(warning, __next_eval__) @@ -143,7 +146,7 @@ async function createModuleContext(options: ModuleContextOptions) { const warning = getServerError( new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime. Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`), - 'edge-server' + COMPILER_NAMES.edgeServer ) warning.name = 'DynamicWasmCodeGenerationWarning' Error.captureStackTrace(warning, __next_webassembly_compile__) @@ -170,7 +173,7 @@ Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation const warning = getServerError( new Error(`Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Edge Runtime. Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`), - 'edge-server' + COMPILER_NAMES.edgeServer ) warning.name = 'DynamicWasmCodeGenerationWarning' Error.captureStackTrace(warning, __next_webassembly_instantiate__) @@ -334,7 +337,7 @@ function throwUnsupportedAPIError(name: string) { const error = new Error(`A Node.js API is used (${name}) which is not supported in the Edge Runtime. Learn more: https://nextjs.org/docs/api-reference/edge-runtime`) - decorateServerError(error, 'edge-server') + decorateServerError(error, COMPILER_NAMES.edgeServer) throw error } @@ -342,7 +345,7 @@ function getDecorateUnhandledError(runtime: EdgeRuntime) { const EdgeRuntimeError = runtime.evaluate(`Error`) return (error: any) => { if (error instanceof EdgeRuntimeError) { - decorateServerError(error, 'edge-server') + decorateServerError(error, COMPILER_NAMES.edgeServer) } } } diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 7110358304c..4f1008d119d 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -75,10 +75,12 @@ function withTaggedErrors(fn: RunnerFn): RunnerFn { .then((result) => ({ ...result, waitUntil: result?.waitUntil?.catch((error) => { + // TODO: used COMPILER_NAMES.edgeServer instead. Verify that it does not increase the runtime size. throw getServerError(error, 'edge-server') }), })) .catch((error) => { + // TODO: used COMPILER_NAMES.edgeServer instead throw getServerError(error, 'edge-server') }) } diff --git a/packages/next/shared/lib/app-router-context.ts b/packages/next/shared/lib/app-router-context.ts index 9483ebb6362..06c78f84e79 100644 --- a/packages/next/shared/lib/app-router-context.ts +++ b/packages/next/shared/lib/app-router-context.ts @@ -62,7 +62,6 @@ export const LayoutRouterContext = React.createContext<{ childNodes: CacheNode['parallelRoutes'] tree: FlightRouterState url: string - stylesheets?: string[] }>(null as any) export const GlobalLayoutRouterContext = React.createContext<{ tree: FlightRouterState diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index 9e97927eeed..c314f8517b5 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -1,3 +1,21 @@ +type ValueOf = Required[keyof T] + +export const COMPILER_NAMES = { + client: 'client', + server: 'server', + edgeServer: 'edge-server', +} as const + +export type CompilerNameValues = ValueOf + +export const COMPILER_INDEXES: { + [compilerKey in CompilerNameValues]: number +} = { + [COMPILER_NAMES.client]: 0, + [COMPILER_NAMES.server]: 1, + [COMPILER_NAMES.edgeServer]: 2, +} as const + export const PHASE_EXPORT = 'phase-export' export const PHASE_PRODUCTION_BUILD = 'phase-production-build' export const PHASE_PRODUCTION_SERVER = 'phase-production-server' @@ -40,6 +58,8 @@ export const NEXT_CLIENT_SSR_ENTRY_SUFFIX = '.__sc_client__' // server/flight-manifest.js export const FLIGHT_MANIFEST = 'flight-manifest' +// server/flight-server-css-manifest.json +export const FLIGHT_SERVER_CSS_MANIFEST = 'flight-server-css-manifest' // server/middleware-build-manifest.js export const MIDDLEWARE_BUILD_MANIFEST = 'middleware-build-manifest' // server/middleware-react-loadable-manifest.js @@ -56,7 +76,10 @@ export const CLIENT_STATIC_FILES_RUNTIME_AMP = `amp` // static/runtime/webpack.js export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `webpack` // static/runtime/polyfills.js -export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL = Symbol(`polyfills`) +export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS = 'polyfills' +export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL = Symbol( + CLIENT_STATIC_FILES_RUNTIME_POLYFILLS +) export const EDGE_RUNTIME_WEBPACK = 'edge-runtime-webpack' export const TEMPORARY_REDIRECT_STATUS = 307 export const PERMANENT_REDIRECT_STATUS = 308 diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index b279a0df4be..6ec6a7dcc0d 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -6,6 +6,7 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { NextRouter } from './router/router' import type { ParsedUrlQuery } from 'querystring' import type { PreviewData } from 'next/types' +import { COMPILER_NAMES } from './constants' export type NextComponentType< C extends BaseContext = NextPageContext, @@ -92,7 +93,10 @@ export type NEXT_DATA = { autoExport?: boolean isFallback?: boolean dynamicIds?: (string | number)[] - err?: Error & { statusCode?: number; source?: 'server' | 'edge-server' } + err?: Error & { + statusCode?: number + source?: typeof COMPILER_NAMES.server | typeof COMPILER_NAMES.edgeServer + } gsp?: boolean gssp?: boolean customServer?: boolean diff --git a/test/e2e/app-dir/app/app/css/css-page/layout.server.js b/test/e2e/app-dir/app/app/css/css-page/layout.server.js new file mode 100644 index 00000000000..81c0ed0f476 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/layout.server.js @@ -0,0 +1,5 @@ +import './style2.css' + +export default function ServerLayout({ children }) { + return <>{children} +} diff --git a/test/e2e/app-dir/app/app/css/css-page/page.server.js b/test/e2e/app-dir/app/app/css/css-page/page.server.js new file mode 100644 index 00000000000..bfef13a4b5c --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/page.server.js @@ -0,0 +1,5 @@ +import './style.css' + +export default function Page() { + return

Page

+} diff --git a/test/e2e/app-dir/app/app/css/css-page/style.css b/test/e2e/app-dir/app/app/css/css-page/style.css new file mode 100644 index 00000000000..61083abf180 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/style.css @@ -0,0 +1,3 @@ +h1 { + color: blueviolet; +} diff --git a/test/e2e/app-dir/app/app/css/css-page/style2.css b/test/e2e/app-dir/app/app/css/css-page/style2.css new file mode 100644 index 00000000000..cfddd0e0398 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/style2.css @@ -0,0 +1,3 @@ +body { + background: #ccc; +} diff --git a/test/e2e/app-dir/app/app/css/foo.js b/test/e2e/app-dir/app/app/css/foo.js new file mode 100644 index 00000000000..d31bab07a9d --- /dev/null +++ b/test/e2e/app-dir/app/app/css/foo.js @@ -0,0 +1 @@ +import './style.module.css' diff --git a/test/e2e/app-dir/app/app/css/layout.server.js b/test/e2e/app-dir/app/app/css/layout.server.js index 2107d1daf36..da0f5ee6228 100644 --- a/test/e2e/app-dir/app/app/css/layout.server.js +++ b/test/e2e/app-dir/app/app/css/layout.server.js @@ -1,4 +1,5 @@ import './style.css' +import './css-page/style.css' import styles from './style.module.css' export default function ServerLayout({ children }) { diff --git a/test/e2e/app-dir/app/app/layout.server.js b/test/e2e/app-dir/app/app/layout.server.js index 738b8e97797..35f7c0c55ea 100644 --- a/test/e2e/app-dir/app/app/layout.server.js +++ b/test/e2e/app-dir/app/app/layout.server.js @@ -1,4 +1,5 @@ import '../styles/global.css' +import './style.css' export async function getServerSideProps() { return { diff --git a/test/e2e/app-dir/app/app/style.css b/test/e2e/app-dir/app/app/style.css new file mode 100644 index 00000000000..e69de29bb2d