Skip to content

Commit

Permalink
Fix not found hangs the build with overridden node env (#53106)
Browse files Browse the repository at this point in the history
### Why

In #52985 the not found solution introduces `NODE_ENV` to determine if it his the not found boundary and should render the not found, as in the next build mode, we have `/_not-found` as a special route which has a empty parallel route, but in next dev mode so far it his the `parallel-default-route`. This could dependend on the `NODE_ENV` passing to next server but not necessarily.

### What

Fixes #53082
Fixes #53083 

### How

When server actions `not-found` hits, now we create a new loader tree based on the previous one, including `layout` and other components but not the children parallel routes

For production case, to make the rendering independent from the `NODE_ENV`, we're using original pathname to check if it's `/_not-found` to determine if it's production build 404 page

To support replace the loader tree of action, did a little refactor that passing down the loader tree from top level to `bodyResult`. Then we can change the loader tree itself before rendering, in short, we tweak it from original tree to one for not-found case, so server actions could render it properly
  • Loading branch information
huozhi authored Jul 24, 2023
1 parent 9da305f commit d8f4fa8
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 165 deletions.
170 changes: 91 additions & 79 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ function ErrorHtml({
)
}

function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree {
// Align the segment with parallel-route-default in next-app-loader
return ['__DEFAULT__', {}, loaderTree[2]]
}

// Find the closest matched component in the loader tree for a given component type
function findMatchedComponent(
loaderTree: LoaderTree,
Expand Down Expand Up @@ -614,7 +619,7 @@ export async function renderToHTMLOrFlight(
firstItem?: boolean
injectedCSS: Set<string>
injectedFontPreloadTags: Set<string>
asNotFound?: boolean | 'force'
asNotFound?: boolean
}): Promise<{
Component: React.ComponentType
styles: React.ReactNode
Expand Down Expand Up @@ -944,18 +949,15 @@ export async function renderToHTMLOrFlight(

// If it's a not found route, and we don't have any matched parallel
// routes, we try to render the not found component if it exists.
let isLeaf =
process.env.NODE_ENV === 'production'
? !segment && !rootLayoutIncluded
: !parallelRouteMap.length && segment === '__DEFAULT__' // hit parallel-route-default

let notFoundComponent = {}
if (
NotFound &&
// For action not-found we force render the NotFound and stop checking the parallel routes.
(asNotFound === 'force' ||
// For normal case where we should look up for not-found, keep checking the parallel routes.
(asNotFound && isLeaf))
asNotFound &&
// In development, it could hit the parallel-route-default not found, so we only need to check the segment.
// Or if there's no parallel routes means it reaches the end.
((segment === '__DEFAULT__' && !parallelRouteMap.length) ||
// For production build the original pathname is /_not-found, always render not-found component.
renderOpts.originalPathname === '/_not-found')
) {
notFoundComponent = {
children: (
Expand Down Expand Up @@ -1382,69 +1384,70 @@ export async function renderToHTMLOrFlight(
* A new React Component that renders the provided React Component
* using Flight which can then be rendered to HTML.
*/
const ServerComponentsRenderer = createServerComponentRenderer<{
asNotFound: boolean | 'force'
}>(
async (props) => {
// Create full component tree from root to leaf.
const injectedCSS = new Set<string>()
const injectedFontPreloadTags = new Set<string>()

const { Component: ComponentTree, styles } = await createComponentTree({
createSegmentPath: (child) => child,
loaderTree,
parentParams: {},
firstItem: true,
injectedCSS,
injectedFontPreloadTags,
rootLayoutIncluded: false,
asNotFound: props.asNotFound,
})
const createServerComponentsRenderer = (loaderTreeToRender: LoaderTree) =>
createServerComponentRenderer<{
asNotFound: boolean
}>(
async (props) => {
// Create full component tree from root to leaf.
const injectedCSS = new Set<string>()
const injectedFontPreloadTags = new Set<string>()

const createMetadata = (tree: LoaderTree, errorType?: 'not-found') => (
// Adding key={requestId} to make metadata remount for each render
// @ts-expect-error allow to use async server component
<MetadataTree
key={requestId}
tree={tree}
errorType={errorType}
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
)
const { Component: ComponentTree, styles } =
await createComponentTree({
createSegmentPath: (child) => child,
loaderTree: loaderTreeToRender,
parentParams: {},
firstItem: true,
injectedCSS,
injectedFontPreloadTags,
rootLayoutIncluded: false,
asNotFound: props.asNotFound,
})

const initialTree = createFlightRouterStateFromLoaderTree(
loaderTree,
getDynamicParamFromSegment,
query
)
const createMetadata = (errorType?: 'not-found') => (
// Adding key={requestId} to make metadata remount for each render
// @ts-expect-error allow to use async server component
<MetadataTree
key={requestId}
tree={loaderTreeToRender}
errorType={errorType}
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
)

return (
<>
{styles}
<AppRouter
buildId={renderOpts.buildId}
assetPrefix={assetPrefix}
initialCanonicalUrl={pathname}
initialTree={initialTree}
initialHead={createMetadata(
loaderTree,
props.asNotFound ? 'not-found' : undefined
)}
globalErrorComponent={GlobalError}
>
<ComponentTree />
</AppRouter>
</>
)
},
ComponentMod,
serverComponentsRenderOpts,
serverComponentsErrorHandler,
nonce
)
const initialTree = createFlightRouterStateFromLoaderTree(
loaderTreeToRender,
getDynamicParamFromSegment,
query
)

return (
<>
{styles}
<AppRouter
buildId={renderOpts.buildId}
assetPrefix={assetPrefix}
initialCanonicalUrl={pathname}
initialTree={initialTree}
initialHead={createMetadata(
props.asNotFound ? 'not-found' : undefined
)}
globalErrorComponent={GlobalError}
>
<ComponentTree />
</AppRouter>
</>
)
},
ComponentMod,
serverComponentsRenderOpts,
serverComponentsErrorHandler,
nonce
)

const { HeadManagerContext } =
require('../../shared/lib/head-manager-context') as typeof import('../../shared/lib/head-manager-context')
Expand All @@ -1465,15 +1468,16 @@ export async function renderToHTMLOrFlight(
},
async ({
asNotFound,
tree,
}: {
/**
* This option is used to indicate that the page should be rendered as
* if it was not found. When it's enabled, instead of rendering the
* page component, it renders the not-found segment.
*
* If it's 'force', we don't traverse the tree and directly render the NotFound.
*/
asNotFound: boolean | 'force'
asNotFound: boolean
tree: LoaderTree
}) => {
const polyfills = buildManifest.polyfillFiles
.filter(
Expand All @@ -1487,6 +1491,7 @@ export async function renderToHTMLOrFlight(
integrity: subresourceIntegrityManifest?.[polyfill],
}))

const ServerComponentsRenderer = createServerComponentsRenderer(tree)
const content = (
<HeadManagerContext.Provider
value={{
Expand Down Expand Up @@ -1643,12 +1648,12 @@ export async function renderToHTMLOrFlight(
const injectedCSS = new Set<string>()
const injectedFontPreloadTags = new Set<string>()
const [RootLayout, rootStyles] = await getRootLayout(
loaderTree,
tree,
injectedCSS,
injectedFontPreloadTags
)
const [NotFound, notFoundStyles] = await getNotFound(
loaderTree,
tree,
injectedCSS,
pathname
)
Expand Down Expand Up @@ -1690,7 +1695,7 @@ export async function renderToHTMLOrFlight(
{/* @ts-expect-error allow to use async server component */}
<MetadataTree
key={requestId}
tree={loaderTree}
tree={tree}
pathname={pathname}
errorType={errorType}
searchParams={providedSearchParams}
Expand All @@ -1702,8 +1707,8 @@ export async function renderToHTMLOrFlight(
)

const notFoundLoaderTree: LoaderTree = is404
? ['__DEFAULT__', {}, loaderTree[2]]
: loaderTree
? createNotFoundLoaderTree(tree)
: tree

const initialTree = createFlightRouterStateFromLoaderTree(
notFoundLoaderTree,
Expand Down Expand Up @@ -1777,7 +1782,7 @@ export async function renderToHTMLOrFlight(
})
} catch (finalErr: any) {
if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV === 'development' &&
isNotFoundError(finalErr)
) {
const bailOnNotFound: typeof import('../../client/components/dev-root-not-found-boundary').bailOnNotFound =
Expand All @@ -1804,14 +1809,21 @@ export async function renderToHTMLOrFlight(
})

if (actionRequestResult === 'not-found') {
return new RenderResult(await bodyResult({ asNotFound: 'force' }))
const notFoundLoaderTree = createNotFoundLoaderTree(loaderTree)
return new RenderResult(
await bodyResult({
asNotFound: true,
tree: notFoundLoaderTree,
})
)
} else if (actionRequestResult) {
return actionRequestResult
}

const renderResult = new RenderResult(
await bodyResult({
asNotFound: pagePath === '/404',
tree: loaderTree,
})
)

Expand Down
Loading

0 comments on commit d8f4fa8

Please sign in to comment.