@@ -12,7 +12,7 @@ import type {
12
12
import type { LoadedEnvFiles } from '@next/env'
13
13
import type { AppLoaderOptions } from './webpack/loaders/next-app-loader'
14
14
15
- import { posix , join , dirname , extname , normalize } from 'path'
15
+ import { posix , join , dirname , extname , normalize , relative } from 'path'
16
16
import { stringify } from 'querystring'
17
17
import fs from 'fs'
18
18
import {
@@ -76,37 +76,36 @@ import type { MappedPages } from './build-context'
76
76
import { PAGE_TYPES } from '../lib/page-types'
77
77
import { isAppPageRoute } from '../lib/is-app-page-route'
78
78
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'
80
80
import { isReservedPage } from './utils'
81
81
import { isParallelRouteSegment } from '../shared/lib/segment'
82
82
import { ensureLeadingSlash } from '../shared/lib/page-path/ensure-leading-slash'
83
83
84
84
/**
85
- * Collect app pages and layouts from the app directory
85
+ * Collect app pages, layouts, and default files from the app directory
86
86
* @param appDir - The app directory path
87
- * @param pageExtensions - The configured page extensions
88
- * @param options - Optional configuration
89
- * @returns Object containing appPaths and layoutPaths arrays
87
+ * @param validFileMatcher - File matcher object
88
+ * @returns Object containing appPaths, layoutPaths, and defaultPaths arrays
90
89
*/
91
90
export async function collectAppFiles (
92
91
appDir : string ,
93
- pageExtensions : PageExtensions
92
+ validFileMatcher : ReturnType < typeof createValidFileMatcher >
94
93
) : Promise < {
95
94
appPaths : string [ ]
96
95
layoutPaths : string [ ]
96
+ defaultPaths : string [ ]
97
97
} > {
98
- const validFileMatcher = createValidFileMatcher ( pageExtensions , appDir )
99
-
100
- // Collect both app pages and layouts in a single directory traversal
98
+ // Collect app pages, layouts, and default files in a single directory traversal
101
99
const allAppFiles = await recursiveReadDir ( appDir , {
102
100
pathnameFilter : ( absolutePath ) =>
103
101
validFileMatcher . isAppRouterPage ( absolutePath ) ||
104
102
validFileMatcher . isRootNotFound ( absolutePath ) ||
105
- validFileMatcher . isAppLayoutPage ( absolutePath ) ,
103
+ validFileMatcher . isAppLayoutPage ( absolutePath ) ||
104
+ validFileMatcher . isAppDefaultPage ( absolutePath ) ,
106
105
ignorePartFilter : ( part ) => part . startsWith ( '_' ) ,
107
106
} )
108
107
109
- // Separate app pages from layouts
108
+ // Separate app pages, layouts, and defaults
110
109
const appPaths = allAppFiles . filter (
111
110
( absolutePath ) =>
112
111
validFileMatcher . isAppRouterPage ( absolutePath ) ||
@@ -115,26 +114,25 @@ export async function collectAppFiles(
115
114
const layoutPaths = allAppFiles . filter ( ( absolutePath ) =>
116
115
validFileMatcher . isAppLayoutPage ( absolutePath )
117
116
)
117
+ const defaultPaths = allAppFiles . filter ( ( absolutePath ) =>
118
+ validFileMatcher . isAppDefaultPage ( absolutePath )
119
+ )
118
120
119
- return { appPaths, layoutPaths }
121
+ return { appPaths, layoutPaths, defaultPaths }
120
122
}
121
123
122
124
/**
123
125
* Collect pages from the pages directory
124
126
* @param pagesDir - The pages directory path
125
- * @param pageExtensions - The configured page extensions
127
+ * @param validFileMatcher - File matcher object
126
128
* @returns Array of page file paths
127
129
*/
128
130
export async function collectPagesFiles (
129
131
pagesDir : string ,
130
- pageExtensions : PageExtensions
132
+ validFileMatcher : ReturnType < typeof createValidFileMatcher >
131
133
) : Promise < string [ ] > {
132
134
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 ( '_' ) ,
135
+ pathnameFilter : validFileMatcher . isPageFile ,
138
136
} )
139
137
}
140
138
@@ -154,30 +152,39 @@ export type SlotInfo = {
154
152
* @param baseDir - The base directory path
155
153
* @param filePath - The mapped file path (with private prefix)
156
154
* @param prefix - The directory prefix ('pages' or 'app')
155
+ * @param isSrcDir - Whether the project uses src directory structure
157
156
* @returns The relative file path
158
157
*/
159
158
export function createRelativeFilePath (
160
159
baseDir : string ,
161
160
filePath : string ,
162
- prefix : 'pages' | 'app'
161
+ prefix : 'pages' | 'app' ,
162
+ isSrcDir : boolean
163
163
) : string {
164
164
const privatePrefix =
165
165
prefix === 'pages' ? 'private-next-pages' : 'private-next-app-dir'
166
- return join (
166
+ const srcPrefix = isSrcDir ? 'src/' : ''
167
+ const absolutePath = join (
167
168
baseDir ,
168
- filePath . replace ( new RegExp ( `^${ privatePrefix } /` ) , `${ prefix } /` )
169
+ filePath . replace ( new RegExp ( `^${ privatePrefix } /` ) , `${ srcPrefix } ${ prefix } /` )
169
170
)
171
+
172
+ // For validator.ts generation, calculate path relative to .next/types/ directory
173
+ const validatorDir = join ( baseDir , '.next' , 'types' )
174
+ return relative ( validatorDir , absolutePath )
170
175
}
171
176
172
177
/**
173
178
* Process pages routes from mapped pages
174
179
* @param mappedPages - The mapped pages object
175
180
* @param baseDir - The base directory path
181
+ * @param isSrcDir - Whether the project uses src directory structure
176
182
* @returns Object containing pageRoutes and pageApiRoutes
177
183
*/
178
184
export function processPageRoutes (
179
185
mappedPages : { [ page : string ] : string } ,
180
- baseDir : string
186
+ baseDir : string ,
187
+ isSrcDir : boolean
181
188
) : {
182
189
pageRoutes : RouteInfo [ ]
183
190
pageApiRoutes : RouteInfo [ ]
@@ -186,7 +193,12 @@ export function processPageRoutes(
186
193
const pageApiRoutes : RouteInfo [ ] = [ ]
187
194
188
195
for ( const [ route , filePath ] of Object . entries ( mappedPages ) ) {
189
- const relativeFilePath = createRelativeFilePath ( baseDir , filePath , 'pages' )
196
+ const relativeFilePath = createRelativeFilePath (
197
+ baseDir ,
198
+ filePath ,
199
+ 'pages' ,
200
+ isSrcDir
201
+ )
190
202
191
203
if ( route . startsWith ( '/api/' ) ) {
192
204
pageApiRoutes . push ( {
@@ -243,46 +255,129 @@ export function extractSlotsFromAppRoutes(mappedAppPages: {
243
255
return slots
244
256
}
245
257
258
+ /**
259
+ * Extract slots from default files
260
+ * @param mappedDefaultFiles - The mapped default files object
261
+ * @returns Array of slot information
262
+ */
263
+ export function extractSlotsFromDefaultFiles ( mappedDefaultFiles : {
264
+ [ page : string ] : string
265
+ } ) : SlotInfo [ ] {
266
+ const slots : SlotInfo [ ] = [ ]
267
+
268
+ for ( const [ route ] of Object . entries ( mappedDefaultFiles ) ) {
269
+ const segments = route . split ( '/' )
270
+ for ( let i = segments . length - 1 ; i >= 0 ; i -- ) {
271
+ const segment = segments [ i ]
272
+ if ( isParallelRouteSegment ( segment ) ) {
273
+ const parentPath = normalizeAppPath ( segments . slice ( 0 , i ) . join ( '/' ) )
274
+ const slotName = segment . slice ( 1 )
275
+
276
+ // Check if the slot already exists
277
+ if ( slots . some ( ( s ) => s . name === slotName && s . parent === parentPath ) )
278
+ continue
279
+
280
+ slots . push ( {
281
+ name : slotName ,
282
+ parent : parentPath ,
283
+ } )
284
+ break
285
+ }
286
+ }
287
+ }
288
+
289
+ return slots
290
+ }
291
+
292
+ /**
293
+ * Combine and deduplicate slot arrays using a Set
294
+ * @param slotArrays - Arrays of slot information to combine
295
+ * @returns Deduplicated array of slots
296
+ */
297
+ export function combineSlots ( ...slotArrays : SlotInfo [ ] [ ] ) : SlotInfo [ ] {
298
+ const slotSet = new Set < string > ( )
299
+ const result : SlotInfo [ ] = [ ]
300
+
301
+ for ( const slots of slotArrays ) {
302
+ for ( const slot of slots ) {
303
+ const key = `${ slot . name } :${ slot . parent } `
304
+ if ( ! slotSet . has ( key ) ) {
305
+ slotSet . add ( key )
306
+ result . push ( slot )
307
+ }
308
+ }
309
+ }
310
+
311
+ return result
312
+ }
313
+
246
314
/**
247
315
* Process app routes from mapped app pages
248
316
* @param mappedAppPages - The mapped app pages object
317
+ * @param validFileMatcher - File matcher object
249
318
* @param baseDir - The base directory path
319
+ * @param isSrcDir - Whether the project uses src directory structure
250
320
* @returns Array of route information
251
321
*/
252
322
export function processAppRoutes (
253
323
mappedAppPages : { [ page : string ] : string } ,
254
- baseDir : string
255
- ) : RouteInfo [ ] {
324
+ validFileMatcher : ReturnType < typeof createValidFileMatcher > ,
325
+ baseDir : string ,
326
+ isSrcDir : boolean
327
+ ) : {
328
+ appRoutes : RouteInfo [ ]
329
+ appRouteHandlers : RouteInfo [ ]
330
+ } {
256
331
const appRoutes : RouteInfo [ ] = [ ]
332
+ const appRouteHandlers : RouteInfo [ ] = [ ]
257
333
258
334
for ( const [ route , filePath ] of Object . entries ( mappedAppPages ) ) {
259
335
if ( route === '/_not-found/page' ) continue
260
336
261
- const relativeFilePath = createRelativeFilePath ( baseDir , filePath , 'app' )
337
+ const relativeFilePath = createRelativeFilePath (
338
+ baseDir ,
339
+ filePath ,
340
+ 'app' ,
341
+ isSrcDir
342
+ )
262
343
263
- appRoutes . push ( {
264
- route : normalizeAppPath ( normalizePathSep ( route ) ) ,
265
- filePath : relativeFilePath ,
266
- } )
344
+ if ( validFileMatcher . isAppRouterRoute ( filePath ) ) {
345
+ appRouteHandlers . push ( {
346
+ route : normalizeAppPath ( normalizePathSep ( route ) ) ,
347
+ filePath : relativeFilePath ,
348
+ } )
349
+ } else {
350
+ appRoutes . push ( {
351
+ route : normalizeAppPath ( normalizePathSep ( route ) ) ,
352
+ filePath : relativeFilePath ,
353
+ } )
354
+ }
267
355
}
268
356
269
- return appRoutes
357
+ return { appRoutes, appRouteHandlers }
270
358
}
271
359
272
360
/**
273
361
* Process layout routes from mapped app layouts
274
362
* @param mappedAppLayouts - The mapped app layouts object
275
363
* @param baseDir - The base directory path
364
+ * @param isSrcDir - Whether the project uses src directory structure
276
365
* @returns Array of layout route information
277
366
*/
278
367
export function processLayoutRoutes (
279
368
mappedAppLayouts : { [ page : string ] : string } ,
280
- baseDir : string
369
+ baseDir : string ,
370
+ isSrcDir : boolean
281
371
) : RouteInfo [ ] {
282
372
const layoutRoutes : RouteInfo [ ] = [ ]
283
373
284
374
for ( const [ route , filePath ] of Object . entries ( mappedAppLayouts ) ) {
285
- const relativeFilePath = createRelativeFilePath ( baseDir , filePath , 'app' )
375
+ const relativeFilePath = createRelativeFilePath (
376
+ baseDir ,
377
+ filePath ,
378
+ 'app' ,
379
+ isSrcDir
380
+ )
286
381
layoutRoutes . push ( {
287
382
route : ensureLeadingSlash (
288
383
normalizeAppPath ( normalizePathSep ( route ) ) . replace ( / \/ l a y o u t $ / , '' )
0 commit comments