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

Optimize non-dynamic metadata routes to static in production build #49109

Merged
merged 7 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 57 additions & 12 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,23 @@ function checkExports(swcAST: any): {
ssg: boolean
runtime?: string
preferredRegion?: string | string[]
generateImageMetadata?: boolean
generateSitemaps?: boolean
} {
const exportsSet = new Set<string>([
'getStaticProps',
'getServerSideProps',
'generateImageMetadata',
'generateSitemaps',
])
if (Array.isArray(swcAST?.body)) {
try {
let runtime: string | undefined
let preferredRegion: string | string[] | undefined
let ssr: boolean = false
let ssg: boolean = false
let generateImageMetadata: boolean = false
let generateSitemaps: boolean = false

for (const node of swcAST.body) {
if (
Expand Down Expand Up @@ -122,22 +132,25 @@ function checkExports(swcAST: any): {
if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'FunctionDeclaration' &&
['getStaticProps', 'getServerSideProps'].includes(
node.declaration.identifier?.value
)
exportsSet.has(node.declaration.identifier?.value)
) {
ssg = node.declaration.identifier.value === 'getStaticProps'
ssr = node.declaration.identifier.value === 'getServerSideProps'
const id = node.declaration.identifier.value
ssg = id === 'getStaticProps'
ssr = id === 'getServerSideProps'
generateImageMetadata = id === 'generateImageMetadata'
generateSitemaps = id === 'generateSitemaps'
}

if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'VariableDeclaration'
) {
const id = node.declaration?.declarations[0]?.id.value
if (['getStaticProps', 'getServerSideProps'].includes(id)) {
if (exportsSet.has(id)) {
ssg = id === 'getStaticProps'
ssr = id === 'getServerSideProps'
generateImageMetadata = id === 'generateImageMetadata'
generateSitemaps = id === 'generateSitemaps'
}
}

Expand All @@ -149,18 +162,36 @@ function checkExports(swcAST: any): {
specifier.orig?.value
)

ssg = values.some((value: any) => ['getStaticProps'].includes(value))
ssr = values.some((value: any) =>
['getServerSideProps'].includes(value)
)
for (const value of values) {
if (!ssg && value === 'getStaticProps') ssg = true
if (!ssr && value === 'getServerSideProps') ssr = true
if (!generateImageMetadata && value === 'generateImageMetadata')
generateImageMetadata = true
if (!generateSitemaps && value === 'generateSitemaps')
generateSitemaps = true
}
}
}

return { ssr, ssg, runtime, preferredRegion }
return {
ssr,
ssg,
runtime,
preferredRegion,
generateImageMetadata,
generateSitemaps,
}
} catch (err) {}
}

return { ssg: false, ssr: false }
return {
ssg: false,
ssr: false,
runtime: undefined,
preferredRegion: undefined,
generateImageMetadata: false,
generateSitemaps: false,
}
}

async function tryToReadFile(filePath: string, shouldThrow: boolean) {
Expand Down Expand Up @@ -329,6 +360,20 @@ function warnAboutUnsupportedValue(
warnedUnsupportedValueMap.set(pageFilePath, true)
}

// Detect if metadata routes is a dynamic route, which containing
// generateImageMetadata or generateSitemaps as export
export async function isDynamicMetadataRoute(
pageFilePath: string
): Promise<boolean> {
const fileContent = (await tryToReadFile(pageFilePath, true)) || ''
if (!/generateImageMetadata|generateSitemaps/.test(fileContent)) return false

const swcAST = await parseModule(pageFilePath, fileContent)
const exportsInfo = checkExports(swcAST)

return !exportsInfo.generateImageMetadata || !exportsInfo.generateSitemaps
}

/**
* For a given pageFilePath and nextConfig, if the config supports it, this
* function will read the file and return the runtime that should be used.
Expand Down
28 changes: 26 additions & 2 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ import {
eventBuildCompleted,
} from '../telemetry/events'
import { Telemetry } from '../telemetry/storage'
import { getPageStaticInfo } from './analysis/get-page-static-info'
import { createPagesMapping } from './entries'
import {
isDynamicMetadataRoute,
getPageStaticInfo,
} from './analysis/get-page-static-info'
import { createPagesMapping, getPageFilePath } from './entries'
import { generateBuildId } from './generate-build-id'
import { isWriteable } from './is-writeable'
import * as Log from './output/log'
Expand Down Expand Up @@ -462,6 +465,27 @@ export default async function build(
pagesDir: pagesDir,
})
)

// If the metadata route doesn't contain generating dynamic exports,
// we can replace the dynamic catch-all route and use the static route instead.
for (const [pageKey, pagePath] of Object.entries(mappedAppPages)) {
if (pageKey.includes('[[...__metadata_id__]]')) {
const pageFilePath = getPageFilePath({
absolutePagePath: pagePath,
pagesDir,
appDir,
rootDir,
})

const isDynamic = await isDynamicMetadataRoute(pageFilePath)
if (!isDynamic) {
delete mappedAppPages[pageKey]
mappedAppPages[pageKey.replace('[[...__metadata_id__]]/', '')] =
pagePath
}
}
}

NextBuildContext.mappedAppPages = mappedAppPages
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const handler = imageModule.default
const generateImageMetadata = imageModule.generateImageMetadata

export async function GET(_, ctx) {
const { __metadata_id__ = [], ...params } = ctx.params
const { __metadata_id__ = [], ...params } = ctx.params || {}
const targetId = __metadata_id__[0]
let id = undefined
const imageMetadata = generateImageMetadata ? await generateImageMetadata({ params }) : null
Expand All @@ -122,7 +122,7 @@ export async function GET(_, ctx) {
})
}
}
return handler({ params, id })
return handler({ params: ctx.params ? params : undefined, id })
}
`
}
Expand All @@ -141,7 +141,7 @@ const contentType = ${JSON.stringify(getContentType(resourcePath))}
const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)}

export async function GET(_, ctx) {
const { __metadata_id__ = [], ...params } = ctx.params
const { __metadata_id__ = [], ...params } = ctx.params || {}
const targetId = __metadata_id__[0]
let id = undefined
const sitemaps = generateSitemaps ? await generateSitemaps() : null
Expand Down
22 changes: 19 additions & 3 deletions test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,31 @@ createNextDescribe(
const edgeRoute = functionRoutes.find((route) =>
route.startsWith('/(group)/twitter-image-')
)
expect(edgeRoute).toMatch(
/\/\(group\)\/twitter-image-\w{6}\/\[\[\.\.\.__metadata_id__\]\]\/route/
expect(edgeRoute).toMatch(/\/\(group\)\/twitter-image-\w{6}\/route/)
})

it('should optimize routes without multiple generation API as static routes', async () => {
const appPathsManifest = JSON.parse(
await next.readFile('.next/server/app-paths-manifest.json')
)

expect(appPathsManifest).toMatchObject({
// static routes
'/twitter-image/route': 'app/twitter-image/route.js',
'/sitemap.xml/route': 'app/sitemap.xml/route.js',

// dynamic
'/(group)/dynamic/[size]/sitemap.xml/[[...__metadata_id__]]/route':
'app/(group)/dynamic/[size]/sitemap.xml/[[...__metadata_id__]]/route.js',
'/(group)/dynamic/[size]/apple-icon-48jo90/[[...__metadata_id__]]/route':
'app/(group)/dynamic/[size]/apple-icon-48jo90/[[...__metadata_id__]]/route.js',
})
})

it('should include default og font files in file trace', async () => {
const fileTrace = JSON.parse(
await next.readFile(
'.next/server/app/opengraph-image/[[...__metadata_id__]]/route.js.nft.json'
'.next/server/app/metadata-base/unset/opengraph-image2/[[...__metadata_id__]]/route.js.nft.json'
)
)

Expand Down