diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index 2f1e9ed84e75b26..633eedbdc95a9a9 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 3a396aabf87e602..cd9a10fb2ab6f80 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 af038ae9d27ed54..6ae16e35cc0b846 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: '', })