Skip to content

Commit f816f27

Browse files
committed
feat: generate type guard file
1 parent ec67a97 commit f816f27

File tree

15 files changed

+629
-55
lines changed

15 files changed

+629
-55
lines changed

packages/next/src/build/entries.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,25 @@ import type { MappedPages } from './build-context'
7676
import { PAGE_TYPES } from '../lib/page-types'
7777
import { isAppPageRoute } from '../lib/is-app-page-route'
7878
import { recursiveReadDir } from '../lib/recursive-readdir'
79-
import { createValidFileMatcher } from '../server/lib/find-page-file'
79+
import type { createValidFileMatcher } from '../server/lib/find-page-file'
8080
import { isReservedPage } from './utils'
8181
import { isParallelRouteSegment } from '../shared/lib/segment'
8282
import { ensureLeadingSlash } from '../shared/lib/page-path/ensure-leading-slash'
8383

8484
/**
8585
* Collect app pages and layouts from the app directory
8686
* @param appDir - The app directory path
87-
* @param pageExtensions - The configured page extensions
87+
* @param validFileMatcher - File matcher object
8888
* @param options - Optional configuration
8989
* @returns Object containing appPaths and layoutPaths arrays
9090
*/
9191
export async function collectAppFiles(
9292
appDir: string,
93-
pageExtensions: PageExtensions
93+
validFileMatcher: ReturnType<typeof createValidFileMatcher>
9494
): Promise<{
9595
appPaths: string[]
9696
layoutPaths: string[]
9797
}> {
98-
const validFileMatcher = createValidFileMatcher(pageExtensions, appDir)
99-
10098
// Collect both app pages and layouts in a single directory traversal
10199
const allAppFiles = await recursiveReadDir(appDir, {
102100
pathnameFilter: (absolutePath) =>
@@ -127,14 +125,10 @@ export async function collectAppFiles(
127125
*/
128126
export async function collectPagesFiles(
129127
pagesDir: string,
130-
pageExtensions: PageExtensions
128+
validFileMatcher: ReturnType<typeof createValidFileMatcher>
131129
): Promise<string[]> {
132130
return recursiveReadDir(pagesDir, {
133-
pathnameFilter: (absolutePath) => {
134-
const relativePath = absolutePath.replace(pagesDir + '/', '')
135-
return pageExtensions.some((ext) => relativePath.endsWith(`.${ext}`))
136-
},
137-
ignorePartFilter: (part) => part.startsWith('_'),
131+
pathnameFilter: validFileMatcher.isPageFile,
138132
})
139133
}
140134

@@ -251,22 +245,34 @@ export function extractSlotsFromAppRoutes(mappedAppPages: {
251245
*/
252246
export function processAppRoutes(
253247
mappedAppPages: { [page: string]: string },
248+
validFileMatcher: ReturnType<typeof createValidFileMatcher>,
254249
baseDir: string
255-
): RouteInfo[] {
250+
): {
251+
appRoutes: RouteInfo[]
252+
appRouteHandlers: RouteInfo[]
253+
} {
256254
const appRoutes: RouteInfo[] = []
255+
const appRouteHandlers: RouteInfo[] = []
257256

258257
for (const [route, filePath] of Object.entries(mappedAppPages)) {
259258
if (route === '/_not-found/page') continue
260259

261260
const relativeFilePath = createRelativeFilePath(baseDir, filePath, 'app')
262261

263-
appRoutes.push({
264-
route: normalizeAppPath(normalizePathSep(route)),
265-
filePath: relativeFilePath,
266-
})
262+
if (validFileMatcher.isAppRouterRoute(filePath)) {
263+
appRouteHandlers.push({
264+
route: normalizeAppPath(normalizePathSep(route)),
265+
filePath: relativeFilePath,
266+
})
267+
} else {
268+
appRoutes.push({
269+
route: normalizeAppPath(normalizePathSep(route)),
270+
filePath: relativeFilePath,
271+
})
272+
}
267273
}
268274

269-
return appRoutes
275+
return { appRoutes, appRouteHandlers }
270276
}
271277

272278
/**

packages/next/src/build/index.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import {
117117
extractSlotsFromAppRoutes,
118118
type RouteInfo,
119119
type SlotInfo,
120+
collectPagesFiles,
120121
} from './entries'
121122
import { PAGE_TYPES } from '../lib/page-types'
122123
import { generateBuildId } from './generate-build-id'
@@ -145,7 +146,6 @@ import isError from '../lib/is-error'
145146
import type { NextError } from '../lib/is-error'
146147
import { isEdgeRuntime } from '../lib/is-edge-runtime'
147148
import { recursiveCopy } from '../lib/recursive-copy'
148-
import { recursiveReadDir } from '../lib/recursive-readdir'
149149
import { lockfilePatchPromise, teardownTraceSubscriber } from './swc'
150150
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
151151
import { getFilesInDir } from '../lib/get-files-in-dir'
@@ -218,9 +218,11 @@ import {
218218
sortPages,
219219
sortSortableRouteObjects,
220220
} from '../shared/lib/router/utils/sortable-routes'
221+
import { mkdir } from 'fs/promises'
221222
import {
222223
createRouteTypesManifest,
223224
writeRouteTypesManifest,
225+
writeValidatorFile,
224226
} from '../server/lib/router-utils/route-types-utils'
225227

226228
type Fallback = null | boolean | string
@@ -1153,11 +1155,9 @@ export default async function build(
11531155
let pagesPaths = Boolean(process.env.NEXT_PRIVATE_PAGE_PATHS)
11541156
? providedPagePaths
11551157
: !appDirOnly && pagesDir
1156-
? await nextBuildSpan.traceChild('collect-pages').traceAsyncFn(() =>
1157-
recursiveReadDir(pagesDir, {
1158-
pathnameFilter: validFileMatcher.isPageFile,
1159-
})
1160-
)
1158+
? await nextBuildSpan
1159+
.traceChild('collect-pages')
1160+
.traceAsyncFn(() => collectPagesFiles(pagesDir, validFileMatcher))
11611161
: []
11621162

11631163
const middlewareDetectionRegExp = new RegExp(
@@ -1223,13 +1223,14 @@ export default async function build(
12231223
let layoutPaths: string[]
12241224

12251225
if (Boolean(process.env.NEXT_PRIVATE_APP_PATHS)) {
1226+
// used for testing?
12261227
appPaths = providedAppPaths
12271228
layoutPaths = []
12281229
} else {
12291230
// Collect both app pages and layouts in a single directory traversal
12301231
const result = await nextBuildSpan
12311232
.traceChild('collect-app-files')
1232-
.traceAsyncFn(() => collectAppFiles(appDir, config.pageExtensions))
1233+
.traceAsyncFn(() => collectAppFiles(appDir, validFileMatcher))
12331234

12341235
appPaths = result.appPaths
12351236
layoutPaths = result.layoutPaths
@@ -1313,25 +1314,29 @@ export default async function build(
13131314
.traceChild('generate-route-types')
13141315
.traceAsyncFn(async () => {
13151316
const routeTypesFilePath = path.join(distDir, 'types', 'routes.d.ts')
1316-
await fs.mkdir(path.dirname(routeTypesFilePath), { recursive: true })
1317+
const validatorFilePath = path.join(distDir, 'types', 'validator.ts')
1318+
await mkdir(path.dirname(routeTypesFilePath), { recursive: true })
13171319

1318-
let pageRoutes: RouteInfo[] = []
13191320
let appRoutes: RouteInfo[] = []
1321+
let appRouteHandlers: RouteInfo[] = []
13201322
let layoutRoutes: RouteInfo[] = []
13211323
let slots: SlotInfo[] = []
13221324

1323-
// Build pages routes
1324-
const processedPages = processPageRoutes(mappedPages, dir)
1325-
// We combine both page routes and API routes
1326-
pageRoutes = [
1327-
...processedPages.pageRoutes,
1328-
...processedPages.pageApiRoutes,
1329-
]
1325+
const { pageRoutes, pageApiRoutes } = processPageRoutes(
1326+
mappedPages,
1327+
dir
1328+
)
13301329

13311330
// Build app routes
13321331
if (appDir && mappedAppPages) {
13331332
slots = extractSlotsFromAppRoutes(mappedAppPages)
1334-
appRoutes = processAppRoutes(mappedAppPages, dir)
1333+
const result = processAppRoutes(
1334+
mappedAppPages,
1335+
validFileMatcher,
1336+
dir
1337+
)
1338+
appRoutes = result.appRoutes
1339+
appRouteHandlers = result.appRouteHandlers
13351340
}
13361341

13371342
// Build app layouts
@@ -1343,6 +1348,8 @@ export default async function build(
13431348
dir,
13441349
pageRoutes,
13451350
appRoutes,
1351+
appRouteHandlers,
1352+
pageApiRoutes,
13461353
layoutRoutes,
13471354
slots,
13481355
redirects: config.redirects,
@@ -1354,6 +1361,7 @@ export default async function build(
13541361
routeTypesFilePath,
13551362
config
13561363
)
1364+
await writeValidatorFile(routeTypesManifest, validatorFilePath)
13571365
})
13581366

13591367
// Turbopack already handles conflicting app and page routes.

packages/next/src/cli/next-typegen.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
createRouteTypesManifest,
2828
writeRouteTypesManifest,
2929
} from '../server/lib/router-utils/route-types-utils'
30+
import { createValidFileMatcher } from '../server/lib/find-page-file'
3031

3132
export type NextTypegenOptions = {
3233
dir?: string
@@ -65,10 +66,11 @@ const nextTypegen = async (
6566

6667
let pageRoutes: RouteInfo[] = []
6768
let appRoutes: RouteInfo[] = []
69+
let appRouteHandlers: RouteInfo[] = []
6870
let layoutRoutes: RouteInfo[] = []
6971
let slots: SlotInfo[] = []
7072

71-
let _pageApiRoutes: RouteInfo[] = []
73+
let pageApiRoutes: RouteInfo[] = []
7274

7375
let mappedPages: { [page: string]: string } = {}
7476
let mappedAppPages: { [page: string]: string } = {}
@@ -85,35 +87,39 @@ const nextTypegen = async (
8587
appDir,
8688
})
8789

90+
const validFileMatcher = createValidFileMatcher(
91+
nextConfig.pageExtensions,
92+
appDir
93+
)
94+
8895
// Build pages routes
8996
if (pagesDir) {
90-
const pagePaths = await collectPagesFiles(
91-
pagesDir,
92-
nextConfig.pageExtensions
93-
)
97+
const pagePaths = await collectPagesFiles(pagesDir, validFileMatcher)
9498

9599
mappedPages = await createMapping(pagePaths, PAGE_TYPES.PAGES)
96100

97101
// Process pages routes
98102
const processedPages = processPageRoutes(mappedPages, baseDir)
99103
pageRoutes = processedPages.pageRoutes
100-
_pageApiRoutes = processedPages.pageApiRoutes
104+
pageApiRoutes = processedPages.pageApiRoutes
101105
}
102106

103107
// Build app routes
104108
if (appDir) {
105109
// Collect both app pages and layouts in a single directory traversal
106110
const { appPaths, layoutPaths } = await collectAppFiles(
107111
appDir,
108-
nextConfig.pageExtensions
112+
validFileMatcher
109113
)
110114

111115
mappedAppPages = await createMapping(appPaths, PAGE_TYPES.APP)
112116
mappedAppLayouts = await createMapping(layoutPaths, PAGE_TYPES.APP)
113117

114118
// Process app routes and extract slots
115119
slots = extractSlotsFromAppRoutes(mappedAppPages)
116-
appRoutes = processAppRoutes(mappedAppPages, baseDir)
120+
const result = processAppRoutes(mappedAppPages, validFileMatcher, baseDir)
121+
appRoutes = result.appRoutes
122+
appRouteHandlers = result.appRouteHandlers
117123

118124
// Process layout routes
119125
layoutRoutes = processLayoutRoutes(mappedAppLayouts, baseDir)
@@ -123,6 +129,8 @@ const nextTypegen = async (
123129
dir: baseDir,
124130
pageRoutes,
125131
appRoutes,
132+
appRouteHandlers,
133+
pageApiRoutes,
126134
layoutRoutes,
127135
slots,
128136
redirects: nextConfig.redirects,

packages/next/src/lib/typescript/writeAppTypeDeclarations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ export async function writeAppTypeDeclarations({
6161
distDir.replaceAll(path.win32.sep, path.posix.sep),
6262
'types/routes.d.ts'
6363
)
64+
const validatorPath = routeTypesPath.replace(/routes\.d\.ts$/, 'validator.ts')
6465

6566
directives.push(`/// <reference path="./${routeTypesPath}" />`)
67+
directives.push(`/// <reference path="./${validatorPath}" />`)
6668

6769
// Push the notice in.
6870
directives.push(

packages/next/src/server/lib/find-page-file.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export function createValidFileMatcher(
9090
pageExtensions
9191
)}$`
9292
)
93+
94+
const leafOnlyRouteFileRegex = new RegExp(
95+
`(^route|[\\\\/]route)\\.${getExtensionRegexString(pageExtensions)}$`
96+
)
9397
const leafOnlyLayoutFileRegex = new RegExp(
9498
`(^(layout)|[\\\\/](layout))\\.${getExtensionRegexString(pageExtensions)}$`
9599
)
@@ -126,6 +130,11 @@ export function createValidFileMatcher(
126130
return leafOnlyPageFileRegex.test(filePath) || isMetadataFile(filePath)
127131
}
128132

133+
// Determine if the file is leaf node route file under app directory
134+
function isAppRouterRoute(filePath: string) {
135+
return leafOnlyRouteFileRegex.test(filePath)
136+
}
137+
129138
function isAppLayoutPage(filePath: string) {
130139
return leafOnlyLayoutFileRegex.test(filePath)
131140
}
@@ -148,6 +157,7 @@ export function createValidFileMatcher(
148157
return {
149158
isPageFile,
150159
isAppRouterPage,
160+
isAppRouterRoute,
151161
isAppLayoutPage,
152162
isMetadataFile,
153163
isRootNotFound,

0 commit comments

Comments
 (0)