From 57b6eff904a53784d08d284382945b93b082df02 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 16 Aug 2022 18:00:23 +0100 Subject: [PATCH] Add separate entry per layout/page. (#39611) Builds on top of #39162 which adds support for creating any kind of bundle path without breaking the compilation. Ensures every layout gets a separate client-side bundle if it has client components being used. Bug Related issues linked using fixes #number Integration tests added Errors have helpful link attached, see contributing.md Feature Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. Related issues linked using fixes #number Integration tests added Documentation added Telemetry added. In case of a feature if it's used or not. Errors have helpful link attached, see contributing.md Documentation / Examples Make sure the linting passes by running pnpm lint The examples guidelines are followed from our contributing doc Co-authored-by: Jiachi Liu Co-authored-by: Shu Ding --- .../build/webpack/loaders/next-app-loader.ts | 4 +- .../plugins/flight-client-entry-plugin.ts | 183 +++++++++--------- packages/next/server/app-render.tsx | 62 ++++-- 3 files changed, 139 insertions(+), 110 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index 2f1e9ed84e75..633eedbdc95a 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -24,8 +24,9 @@ async function createTreeCodeFromPath({ // First item in the list is the page which can't have layouts by itself if (i === segments.length - 1) { + const resolvedPagePath = await resolve(pagePath) // Use '' for segment as it's the page. There can't be a segment called '' so this is the safest way to add it. - tree = `['', {}, {page: () => require('${pagePath}')}]` + tree = `['', {}, {filePath: '${resolvedPagePath}', page: () => require('${resolvedPagePath}')}]` continue } @@ -46,6 +47,7 @@ async function createTreeCodeFromPath({ children ? `children: ${children},` : '' } }, { + filePath: '${resolvedLayoutPath}', ${ resolvedLayoutPath ? `layout: () => require('${resolvedLayoutPath}'),` 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 3a396aabf87e..cd9a10fb2ab6 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -2,8 +2,6 @@ import { stringify } from 'querystring' import path from 'path' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' 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, @@ -89,53 +87,68 @@ export class FlightClientEntryPlugin { continue } - // TODO-APP: create client-side entrypoint per layout/page. - // const entryModule: webpack.NormalModule = - // compilation.moduleGraph.getResolvedModule(entryDependency) - - // for (const connection of compilation.moduleGraph.getOutgoingConnections( - // entryModule - // )) { - // const layoutOrPageDependency = connection.dependency - // // const layoutOrPageRequest = connection.dependency.request - - // const [clientComponentImports, cssImports] = - // this.collectClientComponentsAndCSSForDependency( - // compiler.context, - // compilation, - // layoutOrPageDependency - // ) - - // Object.assign(serverCSSManifest, cssImports) - - // promises.push( - // this.injectClientEntryAndSSRModules( - // compiler, - // compilation, - // name, - // entryDependency, - // clientComponentImports - // ) - // ) - // } + const entryModule: webpack.NormalModule = + compilation.moduleGraph.getResolvedModule(entryDependency) - const [clientComponentImports, cssImports] = - this.collectClientComponentsAndCSSForDependency( - compiler.context, - compilation, - entryDependency + const internalClientComponentEntryImports = new Set< + ClientComponentImports[0] + >() + + for (const connection of compilation.moduleGraph.getOutgoingConnections( + entryModule + )) { + const layoutOrPageDependency = connection.dependency + const layoutOrPageRequest = connection.dependency.request + + const [clientComponentImports, cssImports] = + this.collectClientComponentsAndCSSForDependency({ + layoutOrPageRequest, + compilation, + dependency: layoutOrPageDependency, + }) + + Object.assign(flightCSSManifest, cssImports) + + const isAbsoluteRequest = layoutOrPageRequest[0] === '/' + + // Next.js internals are put into a separate entry. + if (!isAbsoluteRequest) { + clientComponentImports.forEach((value) => + internalClientComponentEntryImports.add(value) + ) + continue + } + + const relativeRequest = isAbsoluteRequest + ? path.relative(compilation.options.context, layoutOrPageRequest) + : layoutOrPageRequest + + // Replace file suffix as `.js` will be added. + const bundlePath = relativeRequest.replace( + /(\.server|\.client)?\.(js|ts)x?$/, + '' ) - Object.assign(flightCSSManifest, cssImports) + promises.push( + this.injectClientEntryAndSSRModules({ + compiler, + compilation, + entryName: name, + clientComponentImports, + bundlePath, + }) + ) + } + // Create internal app promises.push( - this.injectClientEntryAndSSRModules( + this.injectClientEntryAndSSRModules({ compiler, compilation, - name, - entryDependency, - clientComponentImports - ) + entryName: name, + clientComponentImports: [...internalClientComponentEntryImports], + bundlePath: 'app-internals', + }) ) } @@ -164,11 +177,15 @@ export class FlightClientEntryPlugin { } } - collectClientComponentsAndCSSForDependency( - context: string, - compilation: any, + collectClientComponentsAndCSSForDependency({ + layoutOrPageRequest, + compilation, + dependency, + }: { + layoutOrPageRequest: string + compilation: any dependency: any /* Dependency */ - ): [ClientComponentImports, CssImports] { + }): [ClientComponentImports, CssImports] { /** * Keep track of checked modules to avoid infinite loops with recursive imports. */ @@ -176,10 +193,7 @@ export class FlightClientEntryPlugin { const clientComponentImports: ClientComponentImports = [] const serverCSSImports: CssImports = {} - const filterClientComponents = ( - dependencyToFilter: any, - segmentPath: string - ): void => { + const filterClientComponents = (dependencyToFilter: any): void => { const mod: webpack.NormalModule = compilation.moduleGraph.getResolvedModule(dependencyToFilter) if (!mod) return @@ -201,20 +215,24 @@ export class FlightClientEntryPlugin { : mod.resourceResolveData?.path // Ensure module is not walked again if it's already been visited - if (!visitedBySegment[segmentPath]) { - visitedBySegment[segmentPath] = new Set() + if (!visitedBySegment[layoutOrPageRequest]) { + visitedBySegment[layoutOrPageRequest] = new Set() + } + if ( + !modRequest || + visitedBySegment[layoutOrPageRequest].has(modRequest) + ) { + return } - if (!modRequest || visitedBySegment[segmentPath].has(modRequest)) return - visitedBySegment[segmentPath].add(modRequest) + visitedBySegment[layoutOrPageRequest].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) + serverCSSImports[layoutOrPageRequest] = + serverCSSImports[layoutOrPageRequest] || [] + serverCSSImports[layoutOrPageRequest].push(modRequest) } // Check if request is for css file. @@ -223,50 +241,34 @@ export class FlightClientEntryPlugin { return } - if (isLayoutOrPage) { - segmentPath = path - .relative(path.join(context, 'app'), path.dirname(modRequest)) - .replace(/\\/g, '/') - - if (segmentPath !== '') { - segmentPath = '/' + segmentPath - } - - // If it's a page, add an extra '/' to the segments - if (/\/(page)(\.server|\.client)?\.(js|ts)x?$/.test(modRequest)) { - segmentPath += '/' - } - } - compilation.moduleGraph .getOutgoingConnections(mod) .forEach((connection: any) => { - filterClientComponents(connection.dependency, segmentPath) + filterClientComponents(connection.dependency) }) } // Traverse the module graph to find all client components. - filterClientComponents(dependency, '') + filterClientComponents(dependency) return [clientComponentImports, serverCSSImports] } - async injectClientEntryAndSSRModules( - compiler: any, - compilation: any, - entryName: string, - entryDependency: any, + async injectClientEntryAndSSRModules({ + compiler, + compilation, + entryName, + clientComponentImports, + bundlePath, + }: { + compiler: any + compilation: any + entryName: string clientComponentImports: ClientComponentImports - ): Promise { + bundlePath: string + }): 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, @@ -279,18 +281,15 @@ export class FlightClientEntryPlugin { 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 + const pageKey = COMPILER_NAMES.client + bundlePath if (!entries[pageKey]) { entries[pageKey] = { type: EntryTypes.CHILD_ENTRY, parentEntries: new Set([entryName]), bundlePath, - // absolutePagePath: routeInfo.absolutePagePath, request: clientLoader, dispose: false, lastActiveTime: Date.now(), diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index af038ae9d27e..6ae16e35cc0b 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -37,8 +37,8 @@ const ReactDOMServer = shouldUseReactRoot export type RenderOptsPartial = { err?: Error | null dev?: boolean - serverComponentManifest?: any - serverCSSManifest?: any + serverComponentManifest?: FlightManifest + serverCSSManifest?: FlightCSSManifest supportsDynamicHTML?: boolean runtime?: ServerRuntime serverComponents?: boolean @@ -66,6 +66,7 @@ const enum RecordStatus { type Record = { status: RecordStatus + // Could hold the existing promise or the resolved Promise value: any } @@ -284,6 +285,7 @@ type LoaderTree = [ segment: string, parallelRoutes: { [parallelRouterKey: string]: LoaderTree }, components: { + filePath: string layout?: () => any loading?: () => any page?: () => any @@ -376,10 +378,35 @@ function getSegmentParam(segment: string): { /** * Get inline tags based on server CSS manifest. Only used when rendering to HTML. */ -function getCssInlinedLinkTags( +// function getCssInlinedLinkTags( +// serverComponentManifest: FlightManifest, +// serverCSSManifest: FlightCSSManifest, +// filePath: string +// ): string[] { +// const layoutOrPageCss = serverCSSManifest[filePath] + +// if (!layoutOrPageCss) { +// return [] +// } + +// const chunks = new Set() + +// for (const css of layoutOrPageCss) { +// for (const chunk of serverComponentManifest[css].default.chunks) { +// chunks.add(chunk) +// } +// } + +// return [...chunks] +// } + +/** + * Get inline tags based on server CSS manifest. Only used when rendering to HTML. + */ +function getAllCssInlinedLinkTags( serverComponentManifest: FlightManifest, serverCSSManifest: FlightCSSManifest -) { +): string[] { const chunks: { [file: string]: string[] } = {} // APP-TODO: Remove this once we have CSS injections at each level. @@ -399,7 +426,7 @@ function getCssInlinedLinkTags( } } - return [chunks, [...allChunks]] as [{ [file: string]: string[] }, string[]] + return [...allChunks] } export async function renderToHTMLOrFlight( @@ -591,11 +618,14 @@ export async function renderToHTMLOrFlight( */ const createComponentTree = async ({ createSegmentPath, - loaderTree: [segment, parallelRoutes, { layout, loading, page }], + loaderTree: [ + segment, + parallelRoutes, + { /* filePath, */ layout, loading, page }, + ], parentParams, firstItem, rootLayoutIncluded, - serverStylesheets, }: // parentSegmentPath, { createSegmentPath: CreateSegmentPath @@ -603,9 +633,14 @@ export async function renderToHTMLOrFlight( parentParams: { [key: string]: any } rootLayoutIncluded?: boolean firstItem?: boolean - serverStylesheets: { [file: string]: string[] } // parentSegmentPath: string }): Promise<{ Component: React.ComponentType }> => { + // TODO-APP: enable stylesheet per layout/page + // const stylesheets = getCssInlinedLinkTags( + // serverComponentManifest, + // serverCSSManifest!, + // filePath + // ) const Loading = loading ? await interopDefault(loading()) : undefined const isLayout = typeof layout !== 'undefined' const isPage = typeof page !== 'undefined' @@ -624,10 +659,6 @@ 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 */ @@ -688,7 +719,6 @@ export async function renderToHTMLOrFlight( loaderTree: parallelRoutes[parallelRouteKey], parentParams: currentParams, rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, - serverStylesheets, // parentSegmentPath: cssSegmentPath, }) @@ -910,7 +940,6 @@ export async function renderToHTMLOrFlight( loaderTree: loaderTreeToFilter, parentParams: currentParams, firstItem: true, - serverStylesheets: serverCSSManifest, // parentSegmentPath: '', } ) @@ -960,9 +989,9 @@ export async function renderToHTMLOrFlight( // Below this line is handling for rendering to HTML. // Get all the server imported styles. - const [mappedServerCSSManifest, initialStylesheets] = getCssInlinedLinkTags( + const initialStylesheets = getAllCssInlinedLinkTags( serverComponentManifest, - serverCSSManifest + serverCSSManifest || {} ) // Create full component tree from root to leaf. @@ -971,7 +1000,6 @@ export async function renderToHTMLOrFlight( loaderTree: loaderTree, parentParams: {}, firstItem: true, - serverStylesheets: mappedServerCSSManifest, // parentSegmentPath: '', })