Remove the erroring on force-dynamic in static generation for app route (#63526)
### What * Remove the erroring of force-dynamic is not able to use during static generation * Export the route segments properly in sitemap conventions ### Why We discovered this error is showing up when users are using force-dynamic with generating multi sitemaps. When you have a dynamic `route /[id]/route.js` , and you have generateSitemaps defined which is actually using `generateSitemaps` under the hood , then you set the route to dynamic with `export dynamic = 'force-dynamic'`. We should keep the route still as dynamic. `generateStaticParams` is only for generating the paths, which is static in build time. And the `force-dynamic` is going to be applied to each generated path. Closes NEXT-2881
This commit is contained in:
parent
4c467a2638
commit
053db6af8e
9 changed files with 149 additions and 46 deletions
|
@ -15,6 +15,7 @@ import { imageExtMimeTypeMap } from '../../../lib/mime-type'
|
|||
import { WEBPACK_RESOURCE_QUERIES } from '../../../lib/constants'
|
||||
import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep'
|
||||
import type { PageExtensions } from '../../page-extensions-type'
|
||||
import { getLoaderModuleNamedExports } from './utils'
|
||||
|
||||
interface Options {
|
||||
segment: string
|
||||
|
@ -23,40 +24,6 @@ interface Options {
|
|||
basePath: string
|
||||
}
|
||||
|
||||
export async function getNamedExports(
|
||||
resourcePath: string,
|
||||
context: webpack.LoaderContext<any>
|
||||
): Promise<string[]> {
|
||||
const mod = await new Promise<webpack.NormalModule>((res, rej) => {
|
||||
context.loadModule(
|
||||
resourcePath,
|
||||
(err: null | Error, _source: any, _sourceMap: any, module: any) => {
|
||||
if (err) {
|
||||
return rej(err)
|
||||
}
|
||||
res(module)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const exportNames =
|
||||
mod.dependencies
|
||||
?.filter((dep) => {
|
||||
return (
|
||||
[
|
||||
'HarmonyExportImportedSpecifierDependency',
|
||||
'HarmonyExportSpecifierDependency',
|
||||
].includes(dep.constructor.name) &&
|
||||
'name' in dep &&
|
||||
dep.name !== 'default'
|
||||
)
|
||||
})
|
||||
.map((dep: any) => {
|
||||
return dep.name
|
||||
}) || []
|
||||
return exportNames
|
||||
}
|
||||
|
||||
// [NOTE] For turbopack
|
||||
// refer loader_tree's write_static|dynamic_metadata for corresponding features
|
||||
async function nextMetadataImageLoader(
|
||||
|
@ -95,7 +62,7 @@ async function nextMetadataImageLoader(
|
|||
|
||||
if (isDynamicResource) {
|
||||
const exportedFieldsExcludingDefault = (
|
||||
await getNamedExports(resourcePath, this)
|
||||
await getLoaderModuleNamedExports(resourcePath, this)
|
||||
).filter((name) => name !== 'default')
|
||||
|
||||
// re-export and spread as `exportedImageData` to avoid non-exported error
|
||||
|
|
|
@ -2,7 +2,7 @@ import type webpack from 'webpack'
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { imageExtMimeTypeMap } from '../../../lib/mime-type'
|
||||
import { getNamedExports } from './next-metadata-image-loader'
|
||||
import { getLoaderModuleNamedExports } from './utils'
|
||||
|
||||
function errorOnBadHandler(resourcePath: string) {
|
||||
return `
|
||||
|
@ -154,7 +154,15 @@ async function getDynamicSiteMapRouteCode(
|
|||
) {
|
||||
let staticGenerationCode = ''
|
||||
|
||||
const exportNames = await getNamedExports(resourcePath, loaderContext)
|
||||
const exportNames = await getLoaderModuleNamedExports(
|
||||
resourcePath,
|
||||
loaderContext
|
||||
)
|
||||
// Re-export configs but avoid conflicted exports
|
||||
const reExportNames = exportNames.filter(
|
||||
(name) => name !== 'default' && name !== 'generateSitemaps'
|
||||
)
|
||||
|
||||
const hasGenerateSiteMaps = exportNames.includes('generateSitemaps')
|
||||
if (
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
|
@ -189,8 +197,13 @@ const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)}
|
|||
${errorOnBadHandler(resourcePath)}
|
||||
|
||||
${'' /* re-export the userland route configs */}
|
||||
export * from ${JSON.stringify(resourcePath)}
|
||||
|
||||
${
|
||||
reExportNames.length > 0
|
||||
? `export { ${reExportNames.join(', ')} } from ${JSON.stringify(
|
||||
resourcePath
|
||||
)}\n`
|
||||
: ''
|
||||
}
|
||||
|
||||
export async function GET(_, ctx) {
|
||||
const { __metadata_id__, ...params } = ctx.params || {}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type webpack from 'webpack'
|
||||
import { createHash } from 'crypto'
|
||||
import { RSC_MODULE_TYPES } from '../../../shared/lib/constants'
|
||||
|
||||
|
@ -58,3 +59,37 @@ export function encodeToBase64<T extends {}>(obj: T): string {
|
|||
export function decodeFromBase64<T extends {}>(str: string): T {
|
||||
return JSON.parse(Buffer.from(str, 'base64').toString('utf8'))
|
||||
}
|
||||
|
||||
export async function getLoaderModuleNamedExports(
|
||||
resourcePath: string,
|
||||
context: webpack.LoaderContext<any>
|
||||
): Promise<string[]> {
|
||||
const mod = await new Promise<webpack.NormalModule>((res, rej) => {
|
||||
context.loadModule(
|
||||
resourcePath,
|
||||
(err: null | Error, _source: any, _sourceMap: any, module: any) => {
|
||||
if (err) {
|
||||
return rej(err)
|
||||
}
|
||||
res(module)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const exportNames =
|
||||
mod.dependencies
|
||||
?.filter((dep) => {
|
||||
return (
|
||||
[
|
||||
'HarmonyExportImportedSpecifierDependency',
|
||||
'HarmonyExportSpecifierDependency',
|
||||
].includes(dep.constructor.name) &&
|
||||
'name' in dep &&
|
||||
dep.name !== 'default'
|
||||
)
|
||||
})
|
||||
.map((dep: any) => {
|
||||
return dep.name
|
||||
}) || []
|
||||
return exportNames
|
||||
}
|
||||
|
|
|
@ -304,19 +304,17 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
}
|
||||
|
||||
// We assume we can pass the original request through however we may end up
|
||||
// proxying it in certain circumstances based on execution type and configuraiton
|
||||
// proxying it in certain circumstances based on execution type and configuration
|
||||
let request = rawRequest
|
||||
|
||||
// Update the static generation store based on the dynamic property.
|
||||
if (isStaticGeneration) {
|
||||
switch (this.dynamic) {
|
||||
case 'force-dynamic':
|
||||
// We should never be in this case but since it can happen based on the way our build/execution is structured
|
||||
// We defend against it for the time being
|
||||
throw new Error(
|
||||
'Invariant: `force-dynamic` during static generation not expected for app routes. This is a bug in Next.js'
|
||||
)
|
||||
case 'force-dynamic': {
|
||||
// Routes of generated paths should be dynamic
|
||||
staticGenerationStore.forceDynamic = true
|
||||
break
|
||||
}
|
||||
case 'force-static':
|
||||
// The dynamic property is set to force-static, so we should
|
||||
// force the page to be static.
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export function GET() {
|
||||
return new Response('force-dynamic')
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [{ id: '0' }, { id: '1' }]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export default function Page() {
|
||||
return 'page'
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [{ id: '0' }, { id: '1' }]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
22
test/e2e/app-dir/dynamic-in-generate-params/app/sitemap.js
Normal file
22
test/e2e/app-dir/dynamic-in-generate-params/app/sitemap.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
export default function sitemap() {
|
||||
return [
|
||||
{
|
||||
url: 'https://acme.com',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'secondly',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: 'https://acme.com/about',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'secondly',
|
||||
priority: 0.8,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function generateSitemaps() {
|
||||
return [{ id: 0 }, { id: 1 }]
|
||||
}
|
43
test/e2e/app-dir/dynamic-in-generate-params/index.test.ts
Normal file
43
test/e2e/app-dir/dynamic-in-generate-params/index.test.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { type NextInstance, createNextDescribe } from 'e2e-utils'
|
||||
|
||||
async function getLastModifiedTime(next: NextInstance, pathname: string) {
|
||||
const content = await (await next.fetch(pathname)).text()
|
||||
return content.match(/<lastmod>([^<]+)<\/lastmod>/)[1]
|
||||
}
|
||||
|
||||
createNextDescribe(
|
||||
'app-dir - dynamic in generate params',
|
||||
{
|
||||
files: __dirname,
|
||||
},
|
||||
({ next, isNextDev }) => {
|
||||
it('should render sitemap with generateSitemaps in force-dynamic config dynamically', async () => {
|
||||
const firstTime = await getLastModifiedTime(
|
||||
next,
|
||||
isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'
|
||||
)
|
||||
const secondTime = await getLastModifiedTime(
|
||||
next,
|
||||
isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'
|
||||
)
|
||||
|
||||
expect(firstTime).not.toEqual(secondTime)
|
||||
})
|
||||
|
||||
it('should be able to call while generating multiple dynamic sitemaps', async () => {
|
||||
expect(
|
||||
(await next.fetch(isNextDev ? 'sitemap.xml/0' : '/sitemap/0.xml'))
|
||||
.status
|
||||
).toBe(200)
|
||||
expect(
|
||||
(await next.fetch(isNextDev ? 'sitemap.xml/1' : '/sitemap/1.xml'))
|
||||
.status
|
||||
).toBe(200)
|
||||
})
|
||||
|
||||
it('should be able to call fetch while generating multiple dynamic pages', async () => {
|
||||
expect((await next.fetch('/dynamic/0')).status).toBe(200)
|
||||
expect((await next.fetch('/dynamic/1')).status).toBe(200)
|
||||
})
|
||||
}
|
||||
)
|
Loading…
Reference in a new issue