diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 1e0eacd25890..faf8f7224a6e 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -285,6 +285,15 @@ export function getEdgeServerEntry(opts: { return `next-middleware-ssr-loader?${stringify(loaderParams)}!` } +export function getViewsEntry(opts: { pagePath: string; viewsDir: string }) { + const loaderParams = { + pagePath: opts.pagePath, + viewsDir: opts.viewsDir, + } + + return `next-view-loader?${stringify(loaderParams)}!` +} + export function getServerlessEntry(opts: { absolutePagePath: string buildId: string @@ -357,6 +366,11 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { const getEntryHandler = (mappings: Record, isViews: boolean) => async (page: string) => { + // TODO: @timneutkens do not pass layouts to entry here + if (isViews && page.endsWith('/layout')) { + return + } + const bundleFile = normalizePagePath(page) const clientBundlePath = posix.join('pages', bundleFile) const serverBundlePath = posix.join( @@ -388,7 +402,12 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }) }, onServer: () => { - if (isTargetLikeServerless(target)) { + if (isViews && viewsDir) { + server[serverBundlePath] = getViewsEntry({ + pagePath: mappings[page], + viewsDir, + }) + } else if (isTargetLikeServerless(target)) { if (page !== '/_app' && page !== '/_document') { server[serverBundlePath] = getServerlessEntry({ ...params, diff --git a/packages/next/build/webpack/loaders/next-view-loader.ts b/packages/next/build/webpack/loaders/next-view-loader.ts index af12ebee0070..6085337c49b9 100644 --- a/packages/next/build/webpack/loaders/next-view-loader.ts +++ b/packages/next/build/webpack/loaders/next-view-loader.ts @@ -1,17 +1,105 @@ +import path from 'path' import type webpack from 'webpack5' +import { NODE_RESOLVE_OPTIONS } from '../../webpack-config' + +function pathToUrlPath(pathname: string) { + let urlPath = pathname.replace(/^private-next-views-dir/, '') + + // For `views/layout.js` + if (urlPath === '') { + urlPath = '/' + } + + return urlPath +} + +async function resolveLayoutPathsByPage({ + pagePath, + resolve, +}: { + pagePath: string + resolve: (pathname: string) => Promise +}) { + const layoutPaths = new Map() + const parts = pagePath.split('/') + + for (let i = 1; i < parts.length; i++) { + const pathWithoutSlashLayout = parts.slice(0, i).join('/') + const layoutPath = `${pathWithoutSlashLayout}/layout` + + const resolvedLayoutPath = await resolve(layoutPath) + + let urlPath = pathToUrlPath(pathWithoutSlashLayout) + + layoutPaths.set(urlPath, resolvedLayoutPath) + } + + return layoutPaths +} + +const extensions = [ + ...NODE_RESOLVE_OPTIONS.extensions, + ...NODE_RESOLVE_OPTIONS.extensions.map((ext) => `.server${ext}`), + ...NODE_RESOLVE_OPTIONS.extensions.map((ext) => `.client${ext}`), +] +const resolveOptions: any = { + ...NODE_RESOLVE_OPTIONS, + extensions, +} const nextViewLoader: webpack.LoaderDefinitionFunction<{ - components: string[] -}> = function nextViewLoader() { + pagePath: string + viewsDir: string +}> = async function nextViewLoader() { const loaderOptions = this.getOptions() || {} + const resolve = this.getResolve(resolveOptions) + const viewsDir = loaderOptions.viewsDir - return ` - export const components = { - ${loaderOptions.components - .map((component) => `'${component}': () => import('${component}')`) - .join(',\n')} + const layoutPaths = await resolveLayoutPathsByPage({ + pagePath: loaderOptions.pagePath, + resolve: async (pathname) => { + try { + return await resolve(this.rootContext, pathname) + } catch (err: any) { + if (err.message.includes("Can't resolve")) { + return undefined + } + throw err + } + }, + }) + + const componentsCode = [] + for (const [layoutPath, resolvedLayoutPath] of layoutPaths) { + if (resolvedLayoutPath) { + this.addDependency(resolvedLayoutPath) + // use require so that we can bust the require cache + const codeLine = `'${layoutPath}': () => require('${resolvedLayoutPath}')` + componentsCode.push(codeLine) + } else { + for (const ext of extensions) { + this.addMissingDependency( + path.join(viewsDir, layoutPath, `layout${ext}`) + ) + } } + } + + // Add page itself to the list of components + componentsCode.push( + `'${pathToUrlPath(loaderOptions.pagePath).replace( + new RegExp(`/page\\.+(${extensions.join('|')})$`), + '' + // use require so that we can bust the require cache + )}': () => require('${loaderOptions.pagePath}')` + ) + + const result = ` + export const components = { + ${componentsCode.join(',\n')} + }; ` + return result } export default nextViewLoader diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index d80fd6fde386..37070426f429 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1748,24 +1748,6 @@ export default abstract class Server { return null } - const gatherViewLayouts = async ( - viewPath: string, - result: FindComponentsResult - ): Promise => { - const layoutPaths = this.getViewPathLayouts(viewPath) - result.components.viewLayouts = await Promise.all( - layoutPaths.map(async (path) => { - const layoutRes = await this.findPageComponents(path) - return { - isRootLayout: path === '/layout', - Component: layoutRes?.components.Component!, - getStaticProps: layoutRes?.components.getStaticProps, - getServerSideProps: layoutRes?.components.getServerSideProps, - } - }) - ) - } - try { // Ensure a request to the URL /accounts/[id] will be treated as a dynamic // route correctly and not loaded immediately without parsing params. @@ -1778,9 +1760,6 @@ export default abstract class Server { const result = await this.findPageComponents(page, query) if (result) { try { - if (result.components.isViewPath) { - await gatherViewLayouts(page, result) - } return await this.renderToResponseWithComponents(ctx, result) } catch (err) { const isNoFallbackError = err instanceof NoFallbackError @@ -1812,9 +1791,6 @@ export default abstract class Server { ) if (dynamicRouteResult) { try { - if (dynamicRouteResult.components.isViewPath) { - await gatherViewLayouts(page, dynamicRouteResult) - } return await this.renderToResponseWithComponents( { ...ctx, diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index b0035377844b..2fa7844e8f09 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -11,11 +11,16 @@ import { finalizeEntrypoint, getClientEntry, getEdgeServerEntry, + getViewsEntry, runDependingOnPageType, } from '../../build/entries' import { watchCompilers } from '../../build/output' import getBaseWebpackConfig from '../../build/webpack-config' -import { API_ROUTE, MIDDLEWARE_ROUTE } from '../../lib/constants' +import { + API_ROUTE, + MIDDLEWARE_ROUTE, + VIEWS_DIR_ALIAS, +} from '../../lib/constants' import { recursiveDelete } from '../../lib/recursive-delete' import { BLOCKED_PAGES } from '../../shared/lib/constants' import { __ApiPreviewProps } from '../api-utils' @@ -592,7 +597,16 @@ export default class HotReloader { entrypoints[bundlePath] = finalizeEntrypoint({ compilerType: 'server', name: bundlePath, - value: request, + value: + this.viewsDir && bundlePath.startsWith('views/') + ? getViewsEntry({ + pagePath: join( + VIEWS_DIR_ALIAS, + relative(this.viewsDir!, absolutePagePath) + ), + viewsDir: this.viewsDir!, + }) + : request, }) } }, diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 7f44ccac93f1..1bb854461630 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -41,12 +41,6 @@ export type LoadComponentsReturnType = { AppMod: any AppServerMod: any isViewPath?: boolean - viewLayouts?: Array<{ - isRootLayout?: boolean - Component: NextComponentType - getStaticProps?: GetStaticProps - getServerSideProps?: GetServerSideProps - }> } export async function loadDefaultErrorComponents(distDir: string) { diff --git a/packages/next/server/view-render.tsx b/packages/next/server/view-render.tsx index d8bedf081b0f..827e1a2a92e0 100644 --- a/packages/next/server/view-render.tsx +++ b/packages/next/server/view-render.tsx @@ -216,13 +216,13 @@ export async function renderToHTML( const hasConcurrentFeatures = !!runtime const pageIsDynamic = isDynamicRoute(pathname) - const layouts = renderOpts.viewLayouts || [] - - layouts.push({ - Component: renderOpts.Component, - getStaticProps: renderOpts.getStaticProps, - getServerSideProps: renderOpts.getServerSideProps, - }) + const components = Object.keys(ComponentMod.components) + .sort() + .map((key) => { + const mod = ComponentMod.components[key]() + mod.Component = mod.default || mod + return mod + }) // Reads of this are cached on the `req` object, so this should resolve // instantly. There's no need to pass this data down from a previous @@ -239,11 +239,13 @@ export async function renderToHTML( const dataCache = new Map() - for (let i = layouts.length - 1; i >= 0; i--) { + for (let i = components.length - 1; i >= 0; i--) { const dataCacheKey = i.toString() - const layout = layouts[i] + const layout = components[i] - if (layout.isRootLayout) { + if (i === 0) { + // top-most layout is the root layout that renders + // the html/body tags RootLayout = layout.Component continue }