Skip to content

Commit 89529bb

Browse files
committed
feat: generate type guard file
1 parent 623e88e commit 89529bb

File tree

15 files changed

+634
-56
lines changed

15 files changed

+634
-56
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
@@ -1120,11 +1122,9 @@ export default async function build(
11201122
let pagesPaths = Boolean(process.env.NEXT_PRIVATE_PAGE_PATHS)
11211123
? providedPagePaths
11221124
: !appDirOnly && pagesDir
1123-
? await nextBuildSpan.traceChild('collect-pages').traceAsyncFn(() =>
1124-
recursiveReadDir(pagesDir, {
1125-
pathnameFilter: validFileMatcher.isPageFile,
1126-
})
1127-
)
1125+
? await nextBuildSpan
1126+
.traceChild('collect-pages')
1127+
.traceAsyncFn(() => collectPagesFiles(pagesDir, validFileMatcher))
11281128
: []
11291129

11301130
const middlewareDetectionRegExp = new RegExp(
@@ -1190,13 +1190,14 @@ export default async function build(
11901190
let layoutPaths: string[]
11911191

11921192
if (Boolean(process.env.NEXT_PRIVATE_APP_PATHS)) {
1193+
// used for testing?
11931194
appPaths = providedAppPaths
11941195
layoutPaths = []
11951196
} else {
11961197
// Collect both app pages and layouts in a single directory traversal
11971198
const result = await nextBuildSpan
11981199
.traceChild('collect-app-files')
1199-
.traceAsyncFn(() => collectAppFiles(appDir, config.pageExtensions))
1200+
.traceAsyncFn(() => collectAppFiles(appDir, validFileMatcher))
12001201

12011202
appPaths = result.appPaths
12021203
layoutPaths = result.layoutPaths
@@ -1280,25 +1281,29 @@ export default async function build(
12801281
.traceChild('generate-route-types')
12811282
.traceAsyncFn(async () => {
12821283
const routeTypesFilePath = path.join(distDir, 'types', 'routes.d.ts')
1283-
await fs.mkdir(path.dirname(routeTypesFilePath), { recursive: true })
1284+
const validatorFilePath = path.join(distDir, 'types', 'validator.ts')
1285+
await mkdir(path.dirname(routeTypesFilePath), { recursive: true })
12841286

1285-
let pageRoutes: RouteInfo[] = []
12861287
let appRoutes: RouteInfo[] = []
1288+
let appRouteHandlers: RouteInfo[] = []
12871289
let layoutRoutes: RouteInfo[] = []
12881290
let slots: SlotInfo[] = []
12891291

1290-
// Build pages routes
1291-
const processedPages = processPageRoutes(mappedPages, dir)
1292-
// We combine both page routes and API routes
1293-
pageRoutes = [
1294-
...processedPages.pageRoutes,
1295-
...processedPages.pageApiRoutes,
1296-
]
1292+
const { pageRoutes, pageApiRoutes } = processPageRoutes(
1293+
mappedPages,
1294+
dir
1295+
)
12971296

12981297
// Build app routes
12991298
if (appDir && mappedAppPages) {
13001299
slots = extractSlotsFromAppRoutes(mappedAppPages)
1301-
appRoutes = processAppRoutes(mappedAppPages, dir)
1300+
const result = processAppRoutes(
1301+
mappedAppPages,
1302+
validFileMatcher,
1303+
dir
1304+
)
1305+
appRoutes = result.appRoutes
1306+
appRouteHandlers = result.appRouteHandlers
13021307
}
13031308

13041309
// Build app layouts
@@ -1310,6 +1315,8 @@ export default async function build(
13101315
dir,
13111316
pageRoutes,
13121317
appRoutes,
1318+
appRouteHandlers,
1319+
pageApiRoutes,
13131320
layoutRoutes,
13141321
slots,
13151322
redirects: config.redirects,
@@ -1321,6 +1328,7 @@ export default async function build(
13211328
routeTypesFilePath,
13221329
config
13231330
)
1331+
await writeValidatorFile(routeTypesManifest, validatorFilePath)
13241332
})
13251333

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

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import { PAGE_TYPES } from '../lib/page-types'
2626
import {
2727
createRouteTypesManifest,
2828
writeRouteTypesManifest,
29+
writeValidatorFile,
2930
} from '../server/lib/router-utils/route-types-utils'
31+
import { createValidFileMatcher } from '../server/lib/find-page-file'
3032

3133
export type NextTypegenOptions = {
3234
dir?: string
@@ -61,14 +63,16 @@ const nextTypegen = async (
6163
console.log('Generating route types...')
6264

6365
const routeTypesFilePath = join(distDir, 'types', 'routes.d.ts')
66+
const validatorFilePath = join(distDir, 'types', 'validator.ts')
6467
await mkdir(join(distDir, 'types'), { recursive: true })
6568

6669
let pageRoutes: RouteInfo[] = []
6770
let appRoutes: RouteInfo[] = []
71+
let appRouteHandlers: RouteInfo[] = []
6872
let layoutRoutes: RouteInfo[] = []
6973
let slots: SlotInfo[] = []
7074

71-
let _pageApiRoutes: RouteInfo[] = []
75+
let pageApiRoutes: RouteInfo[] = []
7276

7377
let mappedPages: { [page: string]: string } = {}
7478
let mappedAppPages: { [page: string]: string } = {}
@@ -85,35 +89,39 @@ const nextTypegen = async (
8589
appDir,
8690
})
8791

92+
const validFileMatcher = createValidFileMatcher(
93+
nextConfig.pageExtensions,
94+
appDir
95+
)
96+
8897
// Build pages routes
8998
if (pagesDir) {
90-
const pagePaths = await collectPagesFiles(
91-
pagesDir,
92-
nextConfig.pageExtensions
93-
)
99+
const pagePaths = await collectPagesFiles(pagesDir, validFileMatcher)
94100

95101
mappedPages = await createMapping(pagePaths, PAGE_TYPES.PAGES)
96102

97103
// Process pages routes
98104
const processedPages = processPageRoutes(mappedPages, baseDir)
99105
pageRoutes = processedPages.pageRoutes
100-
_pageApiRoutes = processedPages.pageApiRoutes
106+
pageApiRoutes = processedPages.pageApiRoutes
101107
}
102108

103109
// Build app routes
104110
if (appDir) {
105111
// Collect both app pages and layouts in a single directory traversal
106112
const { appPaths, layoutPaths } = await collectAppFiles(
107113
appDir,
108-
nextConfig.pageExtensions
114+
validFileMatcher
109115
)
110116

111117
mappedAppPages = await createMapping(appPaths, PAGE_TYPES.APP)
112118
mappedAppLayouts = await createMapping(layoutPaths, PAGE_TYPES.APP)
113119

114120
// Process app routes and extract slots
115121
slots = extractSlotsFromAppRoutes(mappedAppPages)
116-
appRoutes = processAppRoutes(mappedAppPages, baseDir)
122+
const result = processAppRoutes(mappedAppPages, validFileMatcher, baseDir)
123+
appRoutes = result.appRoutes
124+
appRouteHandlers = result.appRouteHandlers
117125

118126
// Process layout routes
119127
layoutRoutes = processLayoutRoutes(mappedAppLayouts, baseDir)
@@ -123,6 +131,8 @@ const nextTypegen = async (
123131
dir: baseDir,
124132
pageRoutes,
125133
appRoutes,
134+
appRouteHandlers,
135+
pageApiRoutes,
126136
layoutRoutes,
127137
slots,
128138
redirects: nextConfig.redirects,
@@ -135,6 +145,8 @@ const nextTypegen = async (
135145
nextConfig
136146
)
137147

148+
await writeValidatorFile(routeTypesManifest, validatorFilePath)
149+
138150
console.log('✓ Route types generated successfully')
139151
}
140152

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)