Skip to content

Commit c261a85

Browse files
committed
refactor: added more strict app segment config validation
1 parent 6a4a0c0 commit c261a85

File tree

2 files changed

+174
-142
lines changed

2 files changed

+174
-142
lines changed

packages/next/src/build/analysis/get-page-static-info.ts

Lines changed: 165 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { isEdgeRuntime } from '../../lib/is-edge-runtime'
1919
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'
2020
import type { RSCMeta } from '../webpack/loaders/get-module-build-info'
2121
import { PAGE_TYPES } from '../../lib/page-types'
22+
import { AppSegmentConfigSchemaKeys } from '../app-segment-config'
2223

2324
// TODO: migrate preferredRegion here
2425
// Don't forget to update the next-types-plugin file as well
@@ -503,180 +504,202 @@ export async function getPageStaticInfo(params: {
503504
}): Promise<PageStaticInfo> {
504505
const { isDev, pageFilePath, nextConfig, page, pageType } = params
505506

506-
const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || ''
507+
const fileContent = await tryToReadFile(pageFilePath, !isDev)
508+
509+
// If there's no file content or the file doesn't contain any of the keywords
510+
// that we expect to find, then we should just bail the detection now.
507511
if (
508-
/(?<!(_jsx|jsx-))runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export const|generateImageMetadata|generateSitemaps/.test(
512+
!fileContent ||
513+
!/(?<!(_jsx|jsx-))runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export const|generateImageMetadata|generateSitemaps/.test(
509514
fileContent
510515
)
511516
) {
512-
const swcAST = await parseModule(pageFilePath, fileContent)
513-
const {
514-
ssg,
515-
ssr,
516-
runtime,
517-
preferredRegion,
518-
generateStaticParams,
519-
generateImageMetadata,
520-
generateSitemaps,
521-
extraProperties,
522-
directives,
523-
} = checkExports(swcAST, pageFilePath)
524-
const rscInfo = getRSCModuleInformation(fileContent, true)
525-
const rsc = rscInfo.type
526-
527-
// default / failsafe value for config
528-
let config: any // TODO: type this as unknown
529-
try {
530-
config = extractExportedConstValue(swcAST, 'config')
531-
} catch (e) {
532-
if (e instanceof UnsupportedValueError) {
533-
warnAboutUnsupportedValue(pageFilePath, page, e)
534-
}
535-
// `export config` doesn't exist, or other unknown error thrown by swc, silence them
517+
return {
518+
ssr: false,
519+
ssg: false,
520+
rsc: RSC_MODULE_TYPES.server,
521+
generateStaticParams: false,
522+
generateImageMetadata: false,
523+
generateSitemaps: false,
524+
amp: false,
525+
runtime: undefined,
536526
}
527+
}
537528

538-
const extraConfig: Record<string, any> = {}
539-
540-
if (extraProperties && pageType === PAGE_TYPES.APP) {
541-
for (const prop of extraProperties) {
542-
if (!AUTHORIZED_EXTRA_ROUTER_PROPS.includes(prop)) continue
543-
try {
544-
extraConfig[prop] = extractExportedConstValue(swcAST, prop)
545-
} catch (e) {
546-
if (e instanceof UnsupportedValueError) {
547-
warnAboutUnsupportedValue(pageFilePath, page, e)
548-
}
549-
}
550-
}
551-
} else if (pageType === PAGE_TYPES.PAGES) {
552-
for (const key in config) {
553-
if (!AUTHORIZED_EXTRA_ROUTER_PROPS.includes(key)) continue
554-
extraConfig[key] = config[key]
555-
}
529+
const swcAST = await parseModule(pageFilePath, fileContent)
530+
531+
const {
532+
ssg,
533+
ssr,
534+
runtime,
535+
preferredRegion,
536+
generateStaticParams,
537+
generateImageMetadata,
538+
generateSitemaps,
539+
extraProperties,
540+
directives,
541+
} = checkExports(swcAST, pageFilePath)
542+
543+
const rscInfo = getRSCModuleInformation(fileContent, true)
544+
const rsc = rscInfo.type
545+
546+
// default / failsafe value for config
547+
let config: any // TODO: type this as unknown
548+
try {
549+
config = extractExportedConstValue(swcAST, 'config')
550+
} catch (e) {
551+
if (e instanceof UnsupportedValueError) {
552+
warnAboutUnsupportedValue(pageFilePath, page, e)
556553
}
554+
// `export config` doesn't exist, or other unknown error thrown by swc, silence them
555+
}
557556

558-
if (pageType === PAGE_TYPES.APP) {
559-
if (config) {
560-
let message = `Page config in ${pageFilePath} is deprecated. Replace \`export const config=…\` with the following:`
557+
const extraConfig: Record<string, any> = {}
561558

562-
if (config.runtime) {
563-
message += `\n - \`export const runtime = ${JSON.stringify(
564-
config.runtime
565-
)}\``
559+
if (extraProperties && pageType === PAGE_TYPES.APP) {
560+
for (const prop of extraProperties) {
561+
if (!AUTHORIZED_EXTRA_ROUTER_PROPS.includes(prop)) continue
562+
try {
563+
extraConfig[prop] = extractExportedConstValue(swcAST, prop)
564+
} catch (e) {
565+
if (e instanceof UnsupportedValueError) {
566+
warnAboutUnsupportedValue(pageFilePath, page, e)
566567
}
568+
}
569+
}
570+
} else if (pageType === PAGE_TYPES.PAGES) {
571+
for (const key in config) {
572+
if (!AUTHORIZED_EXTRA_ROUTER_PROPS.includes(key)) continue
573+
extraConfig[key] = config[key]
574+
}
575+
}
567576

568-
if (config.regions) {
569-
message += `\n - \`export const preferredRegion = ${JSON.stringify(
570-
config.regions
571-
)}\``
572-
}
577+
if (pageType === PAGE_TYPES.APP) {
578+
if (config) {
579+
let message = `Page config in ${pageFilePath} is deprecated. Replace \`export const config=…\` with the following:`
573580

574-
message += `\nVisit https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config for more information.`
581+
if (config.runtime) {
582+
message += `\n - \`export const runtime = ${JSON.stringify(
583+
config.runtime
584+
)}\``
585+
}
575586

576-
if (isDev) {
577-
Log.warnOnce(message)
578-
} else {
579-
throw new Error(message)
580-
}
581-
config = {}
587+
if (config.regions) {
588+
message += `\n - \`export const preferredRegion = ${JSON.stringify(
589+
config.regions
590+
)}\``
582591
}
583-
}
584-
if (!config) config = {}
585-
586-
// We use `export const config = { runtime: '...' }` to specify the page runtime for pages/.
587-
// In the new app directory, we prefer to use `export const runtime = '...'`
588-
// and deprecate the old way. To prevent breaking changes for `pages`, we use the exported config
589-
// as the fallback value.
590-
let resolvedRuntime
591-
if (pageType === PAGE_TYPES.APP) {
592-
resolvedRuntime = runtime
593-
} else {
594-
resolvedRuntime = runtime || config.runtime
595-
}
596592

597-
if (
598-
typeof resolvedRuntime !== 'undefined' &&
599-
resolvedRuntime !== SERVER_RUNTIME.nodejs &&
600-
!isEdgeRuntime(resolvedRuntime)
601-
) {
602-
const options = Object.values(SERVER_RUNTIME).join(', ')
603-
const message =
604-
typeof resolvedRuntime !== 'string'
605-
? `The \`runtime\` config must be a string. Please leave it empty or choose one of: ${options}`
606-
: `Provided runtime "${resolvedRuntime}" is not supported. Please leave it empty or choose one of: ${options}`
593+
message += `\nVisit https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config for more information.`
594+
607595
if (isDev) {
608-
Log.error(message)
596+
Log.warnOnce(message)
609597
} else {
610598
throw new Error(message)
611599
}
600+
config = {}
601+
}
602+
}
603+
if (!config) config = {}
604+
605+
// We use `export const config = { runtime: '...' }` to specify the page runtime for pages/.
606+
// In the new app directory, we prefer to use `export const runtime = '...'`
607+
// and deprecate the old way. To prevent breaking changes for `pages`, we use the exported config
608+
// as the fallback value.
609+
let resolvedRuntime
610+
if (pageType === PAGE_TYPES.APP) {
611+
resolvedRuntime = runtime
612+
} else {
613+
resolvedRuntime = runtime || config.runtime
614+
}
615+
616+
if (
617+
typeof resolvedRuntime !== 'undefined' &&
618+
resolvedRuntime !== SERVER_RUNTIME.nodejs &&
619+
!isEdgeRuntime(resolvedRuntime)
620+
) {
621+
const options = Object.values(SERVER_RUNTIME).join(', ')
622+
const message =
623+
typeof resolvedRuntime !== 'string'
624+
? `The \`runtime\` config must be a string. Please leave it empty or choose one of: ${options}`
625+
: `Provided runtime "${resolvedRuntime}" is not supported. Please leave it empty or choose one of: ${options}`
626+
if (isDev) {
627+
Log.error(message)
628+
} else {
629+
throw new Error(message)
612630
}
631+
}
613632

614-
const requiresServerRuntime = ssr || ssg || pageType === PAGE_TYPES.APP
633+
const requiresServerRuntime = ssr || ssg || pageType === PAGE_TYPES.APP
615634

616-
const isAnAPIRoute = isAPIRoute(page?.replace(/^(?:\/src)?\/pages\//, '/'))
635+
const isAnAPIRoute =
636+
pageType === PAGE_TYPES.PAGES &&
637+
isAPIRoute(page?.replace(/^(?:\/src)?\/pages\//, '/'))
617638

618-
resolvedRuntime =
619-
isEdgeRuntime(resolvedRuntime) || requiresServerRuntime
620-
? resolvedRuntime
621-
: undefined
639+
resolvedRuntime =
640+
isEdgeRuntime(resolvedRuntime) || requiresServerRuntime
641+
? resolvedRuntime
642+
: undefined
622643

623-
if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) {
624-
warnAboutExperimentalEdge(isAnAPIRoute ? page! : null)
625-
}
644+
if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) {
645+
warnAboutExperimentalEdge(isAnAPIRoute ? page! : null)
646+
}
626647

627-
if (
628-
resolvedRuntime === SERVER_RUNTIME.edge &&
629-
pageType === PAGE_TYPES.PAGES &&
630-
page &&
631-
!isAnAPIRoute
632-
) {
633-
const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.`
634-
if (isDev) {
635-
Log.error(message)
636-
} else {
637-
throw new Error(message)
638-
}
648+
if (
649+
resolvedRuntime === SERVER_RUNTIME.edge &&
650+
pageType === PAGE_TYPES.PAGES &&
651+
page &&
652+
!isAnAPIRoute
653+
) {
654+
const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.`
655+
if (isDev) {
656+
Log.error(message)
657+
} else {
658+
throw new Error(message)
639659
}
660+
}
640661

641-
const middlewareConfig = getMiddlewareConfig(
642-
page ?? 'middleware/edge API route',
643-
config,
644-
nextConfig
645-
)
662+
const middlewareConfig = getMiddlewareConfig(
663+
page ?? 'middleware/edge API route',
664+
config,
665+
nextConfig
666+
)
646667

647-
if (
648-
pageType === PAGE_TYPES.APP &&
649-
directives?.has('client') &&
650-
generateStaticParams
651-
) {
652-
throw new Error(
653-
`Page "${page}" cannot use both "use client" and export function "generateStaticParams()".`
654-
)
655-
}
668+
const isClientComponent = directives ? directives.has('client') : false
656669

657-
return {
658-
ssr,
659-
ssg,
660-
rsc,
661-
generateStaticParams,
662-
generateImageMetadata,
663-
generateSitemaps,
664-
amp: config.amp || false,
665-
...(middlewareConfig && { middleware: middlewareConfig }),
666-
...(resolvedRuntime && { runtime: resolvedRuntime }),
667-
preferredRegion,
668-
extraConfig,
670+
if (pageType === PAGE_TYPES.APP) {
671+
if (isClientComponent) {
672+
if (generateStaticParams) {
673+
throw new Error(
674+
`Page "${page}" cannot use both "use client" and export function "generateStaticParams()".`
675+
)
676+
}
677+
678+
// Discover if any app configurations are provided on the component. If
679+
// any are, we need to error because the configuration is not used.
680+
if (extraProperties) {
681+
for (const key of AppSegmentConfigSchemaKeys) {
682+
if (!extraProperties.has(key)) continue
683+
684+
throw new Error(
685+
`Page "${page}" cannot use both "use client" and export "${key}"`
686+
)
687+
}
688+
}
669689
}
670690
}
671691

672692
return {
673-
ssr: false,
674-
ssg: false,
675-
rsc: RSC_MODULE_TYPES.server,
676-
generateStaticParams: false,
677-
generateImageMetadata: false,
678-
generateSitemaps: false,
679-
amp: false,
680-
runtime: undefined,
693+
ssr,
694+
ssg,
695+
rsc,
696+
generateStaticParams,
697+
generateImageMetadata,
698+
generateSitemaps,
699+
amp: config.amp || false,
700+
...(middlewareConfig && { middleware: middlewareConfig }),
701+
...(resolvedRuntime && { runtime: resolvedRuntime }),
702+
preferredRegion,
703+
extraConfig,
681704
}
682705
}

packages/next/src/build/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import {
102102
type AppSegmentConfig,
103103
} from './app-segment-config'
104104
import { DEFAULT_SEGMENT_KEY, PAGE_SEGMENT_KEY } from '../shared/lib/segment'
105+
import { normalizeZodErrors } from '../shared/lib/zod'
105106
import type { AppDirModules } from './webpack/loaders/next-app-loader'
106107

107108
export type ROUTER_TYPE = 'pages' | 'app'
@@ -1327,6 +1328,14 @@ export async function collectGenerateParams(tree: LoaderTree) {
13271328
`${getModuleName(type)} "${segmentAppFilePath}" must export "generateStaticParams" because it exports "dynamicParams: false"`
13281329
)
13291330
}
1331+
} else {
1332+
const messages = [
1333+
`Invalid segment configuration options detected for ${getModuleName(type)} "${segmentAppFilePath}": `,
1334+
]
1335+
for (const { message } of normalizeZodErrors(config.error)) {
1336+
messages.push(` ${message}`)
1337+
}
1338+
throw new Error(messages.join('\n'))
13301339
}
13311340
}
13321341

0 commit comments

Comments
 (0)