From 02ff2b371d189a211b8691e70eec3a0451f0adeb Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 5 May 2022 20:19:45 +0200 Subject: [PATCH 1/3] Add route loader --- packages/next/build/entries.ts | 19 +++- .../build/webpack/loaders/next-view-loader.ts | 99 +++++++++++++++++-- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 1e0eacd25890..4b1dbd508384 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -285,6 +285,14 @@ export function getEdgeServerEntry(opts: { return `next-middleware-ssr-loader?${stringify(loaderParams)}!` } +export function getViewsEntry(opts: { pagePath: string }) { + const loaderParams = { + pagePath: opts.pagePath, + } + + return `next-view-loader?${stringify(loaderParams)}!` +} + export function getServerlessEntry(opts: { absolutePagePath: string buildId: string @@ -357,6 +365,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 +401,11 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }) }, onServer: () => { - if (isTargetLikeServerless(target)) { + if (isViews) { + server[serverBundlePath] = getViewsEntry({ + pagePath: mappings[page], + }) + } 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..d3e073246fb8 100644 --- a/packages/next/build/webpack/loaders/next-view-loader.ts +++ b/packages/next/build/webpack/loaders/next-view-loader.ts @@ -1,17 +1,102 @@ import type webpack from 'webpack5' +import { NODE_RESOLVE_OPTIONS } from '../../webpack-config' + +function pathToUrlPath(path: string) { + let urlPath = path.replace(/^private-next-views-dir/, '') + + // For `views/layout.js` + if (urlPath === '') { + urlPath = '/' + } + + return urlPath +} + +async function resolveLayoutPathsByPage({ + pagePath, + resolve, +}: { + pagePath: string + resolve: (path: 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 nextViewLoader: webpack.LoaderDefinitionFunction<{ - components: string[] -}> = function nextViewLoader() { + pagePath: string +}> = async function nextViewLoader() { const loaderOptions = this.getOptions() || {} - return ` - export const components = { - ${loaderOptions.components - .map((component) => `'${component}': () => import('${component}')`) - .join(',\n')} + // @ts-ignore This is valid currently + const resolve = this.getResolve({ + ...NODE_RESOLVE_OPTIONS, + extensions: [ + ...NODE_RESOLVE_OPTIONS.extensions, + '.server.js', + '.client.js', + '.client.ts', + '.server.ts', + '.client.tsx', + '.server.tsx', + ], + }) + const layoutPaths = await resolveLayoutPathsByPage({ + pagePath: loaderOptions.pagePath, + resolve: async (path) => { + try { + return await resolve(this.rootContext, path) + } 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) + const codeLine = `'${layoutPath}': () => import('${resolvedLayoutPath}')` + componentsCode.push(codeLine) + } else { + // TODO: create all possible paths + // this.addMissingDependency(layoutPath) } + } + + // Add page itself to the list of components + componentsCode.push( + `'${pathToUrlPath(loaderOptions.pagePath).replace( + /\/page\.(server|client)\.(js|ts|tsx)$/, + '' + )}': () => import('${loaderOptions.pagePath}')` + ) + + const result = ` + export const components = { + ${componentsCode.join(',\n')} + }; ` + + console.log(result) + + return result } export default nextViewLoader From 31a359f6512cb83693b0a529a408584dfd356f42 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 5 May 2022 14:23:13 -0500 Subject: [PATCH 2/3] Update to leverage new view-loader --- packages/next/build/entries.ts | 6 ++- .../build/webpack/loaders/next-view-loader.ts | 49 ++++++++++--------- packages/next/server/base-server.ts | 24 --------- packages/next/server/dev/hot-reloader.ts | 18 ++++++- packages/next/server/load-components.ts | 6 --- packages/next/server/view-render.tsx | 22 +++++---- 6 files changed, 58 insertions(+), 67 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 4b1dbd508384..faf8f7224a6e 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -285,9 +285,10 @@ export function getEdgeServerEntry(opts: { return `next-middleware-ssr-loader?${stringify(loaderParams)}!` } -export function getViewsEntry(opts: { pagePath: string }) { +export function getViewsEntry(opts: { pagePath: string; viewsDir: string }) { const loaderParams = { pagePath: opts.pagePath, + viewsDir: opts.viewsDir, } return `next-view-loader?${stringify(loaderParams)}!` @@ -401,9 +402,10 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }) }, onServer: () => { - if (isViews) { + if (isViews && viewsDir) { server[serverBundlePath] = getViewsEntry({ pagePath: mappings[page], + viewsDir, }) } else if (isTargetLikeServerless(target)) { if (page !== '/_app' && page !== '/_document') { diff --git a/packages/next/build/webpack/loaders/next-view-loader.ts b/packages/next/build/webpack/loaders/next-view-loader.ts index d3e073246fb8..b0819ab8ec54 100644 --- a/packages/next/build/webpack/loaders/next-view-loader.ts +++ b/packages/next/build/webpack/loaders/next-view-loader.ts @@ -1,8 +1,9 @@ +import path from 'path' import type webpack from 'webpack5' import { NODE_RESOLVE_OPTIONS } from '../../webpack-config' -function pathToUrlPath(path: string) { - let urlPath = path.replace(/^private-next-views-dir/, '') +function pathToUrlPath(pathname: string) { + let urlPath = pathname.replace(/^private-next-views-dir/, '') // For `views/layout.js` if (urlPath === '') { @@ -36,24 +37,24 @@ async function resolveLayoutPathsByPage({ 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<{ pagePath: string + viewsDir: string }> = async function nextViewLoader() { const loaderOptions = this.getOptions() || {} + const resolve = this.getResolve(resolveOptions) + const viewsDir = loaderOptions.viewsDir - // @ts-ignore This is valid currently - const resolve = this.getResolve({ - ...NODE_RESOLVE_OPTIONS, - extensions: [ - ...NODE_RESOLVE_OPTIONS.extensions, - '.server.js', - '.client.js', - '.client.ts', - '.server.ts', - '.client.tsx', - '.server.tsx', - ], - }) const layoutPaths = await resolveLayoutPathsByPage({ pagePath: loaderOptions.pagePath, resolve: async (path) => { @@ -72,20 +73,25 @@ const nextViewLoader: webpack.LoaderDefinitionFunction<{ for (const [layoutPath, resolvedLayoutPath] of layoutPaths) { if (resolvedLayoutPath) { this.addDependency(resolvedLayoutPath) - const codeLine = `'${layoutPath}': () => import('${resolvedLayoutPath}')` + // use require so that we can bust the require cache + const codeLine = `'${layoutPath}': () => require('${resolvedLayoutPath}')` componentsCode.push(codeLine) } else { - // TODO: create all possible paths - // this.addMissingDependency(layoutPath) + 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( - /\/page\.(server|client)\.(js|ts|tsx)$/, + new RegExp(`/page\\.+(${extensions.join('|')})$`), '' - )}': () => import('${loaderOptions.pagePath}')` + // use require so that we can bust the require cache + )}': () => require('${loaderOptions.pagePath}')` ) const result = ` @@ -93,9 +99,6 @@ const nextViewLoader: webpack.LoaderDefinitionFunction<{ ${componentsCode.join(',\n')} }; ` - - console.log(result) - return result } 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 } From 7a6bb7412fd8617a2151ec8f11087357f1f998f7 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 5 May 2022 14:52:38 -0500 Subject: [PATCH 3/3] fix lint --- packages/next/build/webpack/loaders/next-view-loader.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-view-loader.ts b/packages/next/build/webpack/loaders/next-view-loader.ts index b0819ab8ec54..6085337c49b9 100644 --- a/packages/next/build/webpack/loaders/next-view-loader.ts +++ b/packages/next/build/webpack/loaders/next-view-loader.ts @@ -18,7 +18,7 @@ async function resolveLayoutPathsByPage({ resolve, }: { pagePath: string - resolve: (path: string) => Promise + resolve: (pathname: string) => Promise }) { const layoutPaths = new Map() const parts = pagePath.split('/') @@ -57,9 +57,9 @@ const nextViewLoader: webpack.LoaderDefinitionFunction<{ const layoutPaths = await resolveLayoutPathsByPage({ pagePath: loaderOptions.pagePath, - resolve: async (path) => { + resolve: async (pathname) => { try { - return await resolve(this.rootContext, path) + return await resolve(this.rootContext, pathname) } catch (err: any) { if (err.message.includes("Can't resolve")) { return undefined