From 482fe25cbbf6e0a0c72003c61e36087673dd18b6 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 10 May 2022 11:57:14 -0500 Subject: [PATCH] Update root component handling (#36781) --- .../build/webpack/loaders/next-view-loader.ts | 28 +++++-- packages/next/server/view-render.tsx | 80 ++++++++++--------- .../dashboard/another/page.server.js | 7 ++ .../app/views/(newroot)/layout.server.js | 19 +++++ .../deployments/breakdown/page.server.js | 7 ++ .../views/dashboard/(custom)/layout.server.js | 8 ++ test/e2e/views-dir/app/views/layout.server.js | 12 ++- test/e2e/views-dir/index.test.ts | 55 +++++++++++-- 8 files changed, 166 insertions(+), 50 deletions(-) create mode 100644 test/e2e/views-dir/app/views/(newroot)/dashboard/another/page.server.js create mode 100644 test/e2e/views-dir/app/views/(newroot)/layout.server.js create mode 100644 test/e2e/views-dir/app/views/dashboard/(custom)/deployments/breakdown/page.server.js create mode 100644 test/e2e/views-dir/app/views/dashboard/(custom)/layout.server.js diff --git a/packages/next/build/webpack/loaders/next-view-loader.ts b/packages/next/build/webpack/loaders/next-view-loader.ts index 1f4ca331584f..4374b9ff3673 100644 --- a/packages/next/build/webpack/loaders/next-view-loader.ts +++ b/packages/next/build/webpack/loaders/next-view-loader.ts @@ -22,18 +22,34 @@ async function resolveLayoutPathsByPage({ }) { const layoutPaths = new Map() const parts = pagePath.split('/') + const isNewRootLayout = + parts[1]?.length > 2 && parts[1]?.startsWith('(') && parts[1]?.endsWith(')') - for (let i = 1; i < parts.length; i++) { + for (let i = parts.length; i >= 0; i--) { const pathWithoutSlashLayout = parts.slice(0, i).join('/') - const layoutPath = `${pathWithoutSlashLayout}/layout` - - const resolvedLayoutPath = await resolve(layoutPath) + if (!pathWithoutSlashLayout) { + continue + } + const layoutPath = `${pathWithoutSlashLayout}/layout` + let resolvedLayoutPath = await resolve(layoutPath) let urlPath = pathToUrlPath(pathWithoutSlashLayout) + // if we are in a new root views/(root) and a custom root layout was + // not provided or a root layout views/layout is not present, we use + // a default root layout to provide the html/body tags + const isCustomRootLayout = isNewRootLayout && i === 2 + + if ((isCustomRootLayout || i === 1) && !resolvedLayoutPath) { + resolvedLayoutPath = await resolve('next/dist/lib/views-layout') + } layoutPaths.set(urlPath, resolvedLayoutPath) - } + // if we're in a new root layout don't add the top-level view/layout + if (isCustomRootLayout) { + break + } + } return layoutPaths } @@ -84,7 +100,7 @@ const nextViewLoader: webpack.LoaderDefinitionFunction<{ // Add page itself to the list of components componentsCode.push( `'${pathToUrlPath(pagePath).replace( - new RegExp(`/page\\.+(${extensions.join('|')})$`), + new RegExp(`/page+(${extensions.join('|')})$`), '' // use require so that we can bust the require cache )}': () => require('${pagePath}')` diff --git a/packages/next/server/view-render.tsx b/packages/next/server/view-render.tsx index f304f2472ce6..8952c5f493aa 100644 --- a/packages/next/server/view-render.tsx +++ b/packages/next/server/view-render.tsx @@ -19,7 +19,6 @@ import { import { FlushEffectsContext } from '../shared/lib/flush-effects' import { isDynamicRoute } from '../shared/lib/router/utils' import { tryGetPreviewData } from './api-utils/node' -import DefaultRootLayout from '../lib/views-layout' const ReactDOMServer = process.env.__NEXT_REACT_ROOT ? require('react-dom/server.browser') @@ -218,10 +217,13 @@ export async function renderToHTML( const isFlight = query.__flight__ !== undefined const flightRouterPath = isFlight ? query.__flight_router_path__ : undefined + delete query.__flight__ + delete query.__flight_router_path__ const hasConcurrentFeatures = !!runtime const pageIsDynamic = isDynamicRoute(pathname) - const components = Object.keys(ComponentMod.components) + const componentPaths = Object.keys(ComponentMod.components) + const components = componentPaths .filter((path) => { // Rendering part of the page is only allowed for flight data if (flightRouterPath) { @@ -238,6 +240,8 @@ export async function renderToHTML( return mod }) + const isSubtreeRender = components.length < componentPaths.length + // 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 // invoke, where we'd have to consider server & serverless. @@ -247,22 +251,12 @@ export async function renderToHTML( (renderOpts as any).previewProps ) const isPreview = previewData !== false - - let WrappedComponent: any - let RootLayout: any - const dataCache = new Map() + let WrappedComponent: any for (let i = components.length - 1; i >= 0; i--) { const dataCacheKey = i.toString() const layout = components[i] - - if (i === 0) { - // top-most layout is the root layout that renders - // the html/body tags - RootLayout = layout.Component - continue - } let fetcher: any // TODO: pass a shared cache from previous getStaticProps/ @@ -313,8 +307,7 @@ export async function renderToHTML( // eslint-disable-next-line no-loop-func const lastComponent = WrappedComponent - WrappedComponent = () => { - let props: any + WrappedComponent = (props: any) => { if (fetcher) { // The data fetching was kicked off before rendering (see above) // if the data was not resolved yet the layout rendering will be suspended @@ -325,7 +318,25 @@ export async function renderToHTML( ) // Result of calling getStaticProps or getServerSideProps. If promise is not resolve yet it will suspend. const recordValue = readRecordValue(record) - props = recordValue.props + + if (props) { + props = Object.assign({}, props, recordValue.props) + } else { + props = recordValue.props + } + } + + // if this is the root layout pass children as bodyChildren prop + if (!isSubtreeRender && i === 0) { + return React.createElement(layout.Component, { + ...props, + headChildren: props.headChildren, + bodyChildren: React.createElement( + lastComponent || React.Fragment, + {}, + null + ), + }) } return React.createElement( @@ -345,14 +356,11 @@ export async function renderToHTML( // } } - // Fall back to default root layout that renders / / - if (!RootLayout) { - RootLayout = DefaultRootLayout - } - - const headChildren = buildManifest.rootMainFiles.map((src) => ( -