Skip to content

Commit

Permalink
Merge branch 'canary' into wyattjoh/NEXT-986
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Jul 14, 2023
2 parents 989dd95 + a28ae63 commit ef8255d
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 86 deletions.
1 change: 1 addition & 0 deletions packages/next/src/client/on-recoverable-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export default function onRecoverableError(err: any) {

// Skip certain custom errors which are not expected to be reported on client
if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) return

defaultOnRecoverableError(err)
}
16 changes: 15 additions & 1 deletion packages/next/src/lib/metadata/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
import { IconsMetadata } from './generate/icons'
import { accumulateMetadata, resolveMetadata } from './resolve-metadata'
import { MetaFilter } from './generate/meta'
import { ResolvedMetadata } from './types/metadata-interface'
import { createDefaultMetadata } from './default-metadata'

// Generate the actual React elements from the resolved metadata.
export async function MetadataTree({
Expand All @@ -26,24 +28,36 @@ export async function MetadataTree({
searchParams,
getDynamicParamFromSegment,
appUsingSizeAdjust,
errorType,
}: {
tree: LoaderTree
pathname: string
searchParams: { [key: string]: any }
getDynamicParamFromSegment: GetDynamicParamFromSegment
appUsingSizeAdjust: boolean
errorType?: 'not-found' | 'redirect'
}) {
const metadataContext = {
pathname,
}

const resolvedMetadata = await resolveMetadata({
tree,
parentParams: {},
metadataItems: [],
searchParams,
getDynamicParamFromSegment,
errorConvention: errorType === 'redirect' ? undefined : errorType,
})
const metadata = await accumulateMetadata(resolvedMetadata, metadataContext)
let metadata: ResolvedMetadata | undefined = undefined

const defaultMetadata = createDefaultMetadata()
// Skip for redirect case as for the temporary redirect case we don't need the metadata on client
if (errorType === 'redirect') {
metadata = defaultMetadata
} else {
metadata = await accumulateMetadata(resolvedMetadata, metadataContext)
}

const elements = MetaFilter([
BasicMetadata({ metadata }),
Expand Down
51 changes: 33 additions & 18 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { resolveTitle } from './resolvers/resolve-title'
import { resolveAsArrayOrUndefined } from './generate/utils'
import { isClientReference } from '../client-reference'
import {
getErrorOrLayoutModule,
getLayoutOrPageModule,
LoaderTree,
} from '../../server/lib/app-dir-module'
Expand Down Expand Up @@ -248,28 +249,28 @@ function merge({
async function getDefinedMetadata(
mod: any,
props: any,
route: string
tracingProps: { route: string }
): Promise<Metadata | MetadataResolver | null> {
// Layer is a client component, we just skip it. It can't have metadata exported.
// Return early to avoid accessing properties error for client references.
if (isClientReference(mod)) {
return null
}
return (
(mod.generateMetadata
? (parent: ResolvingMetadata) =>
getTracer().trace(
ResolveMetadataSpan.generateMetadata,
{
spanName: `generateMetadata ${route}`,
attributes: {
'next.page': route,
},
},
() => mod.generateMetadata(props, parent)
)
: mod.metadata) || null
)
if (typeof mod.generateMetadata === 'function') {
const { route } = tracingProps
return (parent: ResolvingMetadata) =>
getTracer().trace(
ResolveMetadataSpan.generateMetadata,
{
spanName: `generateMetadata ${route}`,
attributes: {
'next.page': route,
},
},
() => mod.generateMetadata(props, parent)
)
}
return mod.metadata || null
}

async function collectStaticImagesFiles(
Expand Down Expand Up @@ -317,21 +318,30 @@ export async function collectMetadata({
metadataItems: array,
props,
route,
errorConvention,
}: {
tree: LoaderTree
metadataItems: MetadataItems
props: any
route: string
errorConvention?: 'not-found'
}) {
const [mod, modType] = await getLayoutOrPageModule(tree)
let mod
let modType
if (errorConvention) {
mod = await getErrorOrLayoutModule(tree, errorConvention)
modType = errorConvention
} else {
;[mod, modType] = await getLayoutOrPageModule(tree)
}

if (modType) {
route += `/${modType}`
}

const staticFilesMetadata = await resolveStaticMetadata(tree[2], props)
const metadataExport = mod
? await getDefinedMetadata(mod, props, route)
? await getDefinedMetadata(mod, props, { route })
: null

array.push([metadataExport, staticFilesMetadata])
Expand All @@ -344,6 +354,7 @@ export async function resolveMetadata({
treePrefix = [],
getDynamicParamFromSegment,
searchParams,
errorConvention,
}: {
tree: LoaderTree
parentParams: { [key: string]: any }
Expand All @@ -352,10 +363,12 @@ export async function resolveMetadata({
treePrefix?: string[]
getDynamicParamFromSegment: GetDynamicParamFromSegment
searchParams: { [key: string]: any }
errorConvention: 'not-found' | undefined
}): Promise<MetadataItems> {
const [segment, parallelRoutes, { page }] = tree
const currentTreePrefix = [...treePrefix, segment]
const isPage = typeof page !== 'undefined'

// Handle dynamic segment params.
const segmentParam = getDynamicParamFromSegment(segment)
/**
Expand All @@ -379,6 +392,7 @@ export async function resolveMetadata({
await collectMetadata({
tree,
metadataItems,
errorConvention,
props: layerProps,
route: currentTreePrefix
// __PAGE__ shouldn't be shown in a route
Expand All @@ -395,6 +409,7 @@ export async function resolveMetadata({
treePrefix: currentTreePrefix,
searchParams,
getDynamicParamFromSegment,
errorConvention,
})
}

Expand Down
46 changes: 29 additions & 17 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ import { ModuleReference } from '../../build/webpack/loaders/metadata/types'

export const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'

const emptyLoaderTree: LoaderTree = ['', {}, {}]

export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
segment: string
Expand Down Expand Up @@ -1361,12 +1359,13 @@ export async function renderToHTMLOrFlight(
query
)

const createMetadata = (tree: LoaderTree) => (
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}
Expand All @@ -1388,12 +1387,12 @@ export async function renderToHTMLOrFlight(
assetPrefix={assetPrefix}
initialCanonicalUrl={pathname}
initialTree={initialTree}
initialHead={<>{createMetadata(loaderTree)}</>}
initialHead={<>{createMetadata(loaderTree, undefined)}</>}
globalErrorComponent={GlobalError}
notFound={
NotFound ? (
<ErrorHtml>
{createMetadata(loaderTree)}
{createMetadata(loaderTree, 'not-found')}
{notFoundStyles}
<NotFound />
</ErrorHtml>
Expand Down Expand Up @@ -1594,7 +1593,9 @@ export async function renderToHTMLOrFlight(
if (isNotFoundError(err)) {
res.statusCode = 404
}
let hasRedirectError = false
if (isRedirectError(err)) {
hasRedirectError = true
res.statusCode = 307
if (err.mutableCookies) {
const headers = new Headers()
Expand All @@ -1609,7 +1610,7 @@ export async function renderToHTMLOrFlight(
}

const use404Error = res.statusCode === 404
const useDefaultError = res.statusCode < 400 || res.statusCode === 307
const useDefaultError = res.statusCode < 400 || hasRedirectError

const { layout } = loaderTree[2]
const injectedCSS = new Set<string>()
Expand All @@ -1624,19 +1625,28 @@ export async function renderToHTMLOrFlight(
? interopDefault(await rootLayoutModule())
: null

const metadata = (
// @ts-expect-error allow to use async server component
<MetadataTree
key={requestId}
tree={loaderTree}
pathname={pathname}
errorType={
use404Error
? 'not-found'
: hasRedirectError
? 'redirect'
: undefined
}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
)
const serverErrorElement = (
<ErrorHtml
head={
// @ts-expect-error allow to use async server component
<MetadataTree
key={requestId}
tree={emptyLoaderTree}
pathname={pathname}
searchParams={providedSearchParams}
getDynamicParamFromSegment={getDynamicParamFromSegment}
appUsingSizeAdjust={appUsingSizeAdjust}
/>
}
// For default error we render metadata directly into the head
head={useDefaultError ? metadata : null}
>
{useDefaultError
? null
Expand All @@ -1645,6 +1655,8 @@ export async function renderToHTMLOrFlight(
async () => {
return (
<>
{/* For server components error metadata needs to be inside inline flight data, so they can be hydrated */}
{metadata}
{use404Error ? (
<RootLayout params={{}}>
{notFoundStyles}
Expand Down
14 changes: 14 additions & 0 deletions packages/next/src/server/lib/app-dir-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ export async function getLayoutOrPageModule(loaderTree: LoaderTree) {

return [value, modType] as const
}

// First check not-found, if it doesn't exist then pick layout
export async function getErrorOrLayoutModule(
loaderTree: LoaderTree,
errorType: 'error' | 'not-found'
) {
const { [errorType]: error, layout } = loaderTree[2]
if (typeof error !== 'undefined') {
return await error[0]()
} else if (typeof layout !== 'undefined') {
return await layout[0]()
}
return undefined
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/metadata/app/async/not-found/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function notFound() {
return <h2>local found boundary</h2>
}

export const metadata = {
title: 'Local not found',
description: 'Local not found description',
}
1 change: 1 addition & 0 deletions test/e2e/app-dir/metadata/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export default function Layout({ children }) {
export const metadata = {
title: 'this is the layout title',
description: 'this is the layout description',
keywords: ['nextjs', 'react'],
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/metadata/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export default function notFound() {
return <h2>root not found page</h2>
}

export const metadata = {
title: 'Root not found',
description: 'Root not found description',
}

0 comments on commit ef8255d

Please sign in to comment.