Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor freezing metadata while resolving and fix title merging #45965

Merged
merged 9 commits into from
Feb 17, 2023
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ async function createAppRouteCode({

return `
import 'next/dist/server/node-polyfill-headers'

export * as handlers from ${JSON.stringify(resolvedPagePath)}
export const resolvedPagePath = ${JSON.stringify(resolvedPagePath)}

102 changes: 66 additions & 36 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
@@ -31,10 +31,11 @@ type StaticMetadata = Awaited<ReturnType<typeof resolveStaticMetadata>>
type MetadataResolver = (
_parent: ResolvingMetadata
) => Metadata | Promise<Metadata>
export type MetadataItems = [
Metadata | MetadataResolver | null,
StaticMetadata
][]
export type MetadataItems = {
metadataExport: Metadata | MetadataResolver | null
staticFilesMetadata: StaticMetadata
isLeafLayout: boolean
}[]

function mergeStaticMetadata(
metadata: ResolvedMetadata,
@@ -236,52 +237,81 @@ async function resolveStaticMetadata(components: ComponentsType) {
export async function collectMetadata(
loaderTree: LoaderTree,
props: any,
array: MetadataItems
array: MetadataItems,
isLeafLayout: boolean
) {
const mod = await getLayoutOrPageModule(loaderTree)
const staticFilesMetadata = await resolveStaticMetadata(loaderTree[2])
const metadataExport = mod ? await getDefinedMetadata(mod, props) : null

array.push([metadataExport, staticFilesMetadata])
array.push({ metadataExport, staticFilesMetadata, isLeafLayout })
}

export async function accumulateMetadata(
huozhi marked this conversation as resolved.
Show resolved Hide resolved
metadataItems: MetadataItems
): Promise<ResolvedMetadata> {
const deepClone: <T>(v: T) => T =
process.env.NODE_ENV === 'development'
? require('next/dist/compiled/@edge-runtime/primitives/structured-clone')
.structuredClone
: (v) => v
const resolvedMetadata = createDefaultMetadata()

let parentPromise = Promise.resolve(resolvedMetadata)
const resolvers: ((value: ResolvedMetadata) => void)[] = []
const generateMetadataResults: (Metadata | Promise<Metadata>)[] = []

for (const item of metadataItems) {
const [metadataExport, staticFilesMetadata] = item
const currentMetadata =
typeof metadataExport === 'function'
? metadataExport(parentPromise)
: metadataExport
const layerMetadataPromise =
currentMetadata instanceof Promise
? currentMetadata
: Promise.resolve(currentMetadata)
// call each `generateMetadata function concurrently and stash their resolver
for (let i = 0; i < metadataItems.length; i++) {
const { metadataExport } = metadataItems[i]
if (typeof metadataExport === 'function') {
generateMetadataResults.push(
metadataExport(
new Promise((resolve) => {
resolvers.push(resolve)
})
)
)
}
}
huozhi marked this conversation as resolved.
Show resolved Hide resolved

parentPromise = parentPromise.then((resolved) => {
return layerMetadataPromise.then((metadata) => {
resolved = deepClone(resolved)
merge(resolved, metadata, staticFilesMetadata, {
title: resolved.title?.template || null,
openGraph: resolved.openGraph?.title?.template || null,
twitter: resolved.twitter?.title?.template || null,
})
return process.env.NODE_ENV === 'development'
? Object.freeze(resolved)
: resolved
})
// Loop over all metadata items again, merging synchronously any static object exports,
// awaiting any static promise exports, and resolving parent metadata and awaiting any generated
// metadata
let resolvingIndex = 0
for (let i = 0; i < metadataItems.length; i++) {
const { metadataExport, staticFilesMetadata, isLeafLayout } =
metadataItems[i]
let metadata: Metadata | null = null
if (typeof metadataExport === 'function') {
const resolveParent = resolvers[resolvingIndex]
const generatedMetadata = generateMetadataResults[resolvingIndex]
resolvingIndex++
// In dev we clone and freeze to prevent relying on mutating resolvedMetadata directly.
// In prod we just pass resolvedMetadata through without any copying.
const currentResolvedMetadata: ResolvedMetadata =
process.env.NODE_ENV === 'development'
? Object.freeze(
require('next/dist/compiled/@edge-runtime/primitives/structured-clone').structuredClone(
resolvedMetadata
)
)
: resolvedMetadata

// This resolve should unblock the generateMetadata function if it awaited the parent
// argument. If it didn't await the parent argument it might already have a value since it was
// called concurrently. Regardless we await the return value before continuing on to the next
// layer
resolveParent(currentResolvedMetadata)
metadata = await generatedMetadata
huozhi marked this conversation as resolved.
Show resolved Hide resolved
} else {
metadata = metadataExport
}
huozhi marked this conversation as resolved.
Show resolved Hide resolved

// If the layout is the same layer with page, skip the
if (isLeafLayout && metadata) {
delete metadata.title
}
huozhi marked this conversation as resolved.
Show resolved Hide resolved

merge(resolvedMetadata, metadata, staticFilesMetadata, {
title: resolvedMetadata.title?.template || null,
openGraph: resolvedMetadata.openGraph?.title?.template || null,
twitter: resolvedMetadata.twitter?.title?.template || null,
})
}

return await parentPromise
return resolvedMetadata
}
5 changes: 4 additions & 1 deletion packages/next/src/server/app-render.tsx
Original file line number Diff line number Diff line change
@@ -1138,7 +1138,10 @@ export async function renderToHTMLOrFlight(
...(isPage && searchParamsProps),
}

await collectMetadata(tree, layerProps, metadataItems)
// If the layout is located next to the page as leaf layer
const isLeafLayout =
typeof parallelRoutes.children?.[2].page !== 'undefined'
huozhi marked this conversation as resolved.
Show resolved Hide resolved
await collectMetadata(tree, layerProps, metadataItems, isLeafLayout)

for (const key in parallelRoutes) {
const childTree = parallelRoutes[key]
8 changes: 4 additions & 4 deletions test/e2e/app-dir/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -81,14 +81,14 @@ createNextDescribe(

it('should support title template', async () => {
const browser = await next.browser('/title-template')
expect(await browser.eval(`document.title`)).toBe('Page | Layout')
// Use the parent layout (root layout) instead of app/title-template/layout.tsx
expect(await browser.eval(`document.title`)).toBe('Page')
})

it('should support stashed title in one layer of page and layout', async () => {
const browser = await next.browser('/title-template/extra')
expect(await browser.eval(`document.title`)).toBe(
'Extra Page | Extra Layout'
)
// Use the parent layout (app/title-template/layout.tsx) instead of app/title-template/extra/layout.tsx
expect(await browser.eval(`document.title`)).toBe('Extra Page | Layout')
})

it('should support stashed title in two layers of page and layout', async () => {