vinext 0.0.22 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/build/static-export.d.ts.map +1 -1
  2. package/dist/build/static-export.js +9 -7
  3. package/dist/build/static-export.js.map +1 -1
  4. package/dist/config/config-matchers.d.ts.map +1 -1
  5. package/dist/config/config-matchers.js +13 -3
  6. package/dist/config/config-matchers.js.map +1 -1
  7. package/dist/config/next-config.d.ts +4 -1
  8. package/dist/config/next-config.d.ts.map +1 -1
  9. package/dist/config/next-config.js +10 -5
  10. package/dist/config/next-config.js.map +1 -1
  11. package/dist/deploy.d.ts.map +1 -1
  12. package/dist/deploy.js +83 -24
  13. package/dist/deploy.js.map +1 -1
  14. package/dist/index.d.ts +36 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +230 -40
  17. package/dist/index.js.map +1 -1
  18. package/dist/routing/app-router.d.ts +2 -1
  19. package/dist/routing/app-router.d.ts.map +1 -1
  20. package/dist/routing/app-router.js +73 -66
  21. package/dist/routing/app-router.js.map +1 -1
  22. package/dist/routing/file-matcher.d.ts +24 -0
  23. package/dist/routing/file-matcher.d.ts.map +1 -0
  24. package/dist/routing/file-matcher.js +75 -0
  25. package/dist/routing/file-matcher.js.map +1 -0
  26. package/dist/routing/pages-router.d.ts +3 -2
  27. package/dist/routing/pages-router.d.ts.map +1 -1
  28. package/dist/routing/pages-router.js +24 -17
  29. package/dist/routing/pages-router.js.map +1 -1
  30. package/dist/server/app-dev-server.d.ts.map +1 -1
  31. package/dist/server/app-dev-server.js +110 -64
  32. package/dist/server/app-dev-server.js.map +1 -1
  33. package/dist/server/dev-server.d.ts +2 -1
  34. package/dist/server/dev-server.d.ts.map +1 -1
  35. package/dist/server/dev-server.js +16 -14
  36. package/dist/server/dev-server.js.map +1 -1
  37. package/dist/server/prod-server.d.ts +8 -2
  38. package/dist/server/prod-server.d.ts.map +1 -1
  39. package/dist/server/prod-server.js +71 -16
  40. package/dist/server/prod-server.js.map +1 -1
  41. package/dist/server/worker-utils.d.ts +15 -0
  42. package/dist/server/worker-utils.d.ts.map +1 -0
  43. package/dist/server/worker-utils.js +41 -0
  44. package/dist/server/worker-utils.js.map +1 -0
  45. package/dist/shims/cache.d.ts +1 -1
  46. package/dist/shims/cache.d.ts.map +1 -1
  47. package/dist/shims/cache.js +8 -3
  48. package/dist/shims/cache.js.map +1 -1
  49. package/dist/shims/headers.d.ts +6 -0
  50. package/dist/shims/headers.d.ts.map +1 -1
  51. package/dist/shims/headers.js +8 -0
  52. package/dist/shims/headers.js.map +1 -1
  53. package/dist/shims/metadata.d.ts +1 -0
  54. package/dist/shims/metadata.d.ts.map +1 -1
  55. package/dist/shims/metadata.js +5 -1
  56. package/dist/shims/metadata.js.map +1 -1
  57. package/dist/utils/project.d.ts +13 -1
  58. package/dist/utils/project.d.ts.map +1 -1
  59. package/dist/utils/project.js +63 -13
  60. package/dist/utils/project.js.map +1 -1
  61. package/package.json +6 -1
@@ -1,3 +1,4 @@
1
+ import { type ValidFileMatcher } from "./file-matcher.js";
1
2
  export interface InterceptingRoute {
2
3
  /** The interception convention: "." | ".." | "../.." | "..." */
3
4
  convention: string;
@@ -85,7 +86,7 @@ export declare function invalidateAppRouteCache(): void;
85
86
  /**
86
87
  * Scan the app/ directory and return a list of routes.
87
88
  */
88
- export declare function appRouter(appDir: string): Promise<AppRoute[]>;
89
+ export declare function appRouter(appDir: string, pageExtensions?: readonly string[], matcher?: ValidFileMatcher): Promise<AppRoute[]>;
89
90
  /**
90
91
  * Match a URL against App Router routes.
91
92
  */
@@ -1 +1 @@
1
- {"version":3,"file":"app-router.d.ts","sourceRoot":"","sources":["../../src/routing/app-router.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uDAAuD;IACvD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oDAAoD;IACpD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kDAAkD;IAClD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2CAA2C;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qDAAqD;IACrD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,mFAAmF;IACnF,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,6BAA6B;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,mEAAmE;IACnE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACjC,qCAAqC;IACrC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wCAAwC;IACxC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;;;OAMG;IACH,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAMD,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAoCnE;AA+yBD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,QAAQ,EAAE,GACjB;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CAiBvE"}
1
+ {"version":3,"file":"app-router.d.ts","sourceRoot":"","sources":["../../src/routing/app-router.ts"],"names":[],"mappings":"AAkBA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uDAAuD;IACvD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oDAAoD;IACpD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kDAAkD;IAClD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2CAA2C;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qDAAqD;IACrD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,mFAAmF;IACnF,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,6BAA6B;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,mEAAmE;IACnE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACjC,qCAAqC;IACrC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wCAAwC;IACxC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;;;OAMG;IACH,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAOD,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,EAClC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAuDrB;AAk2BD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,QAAQ,EAAE,GACjB;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CAiBvE"}
@@ -15,34 +15,41 @@
15
15
  */
16
16
  import path from "node:path";
17
17
  import fs from "node:fs";
18
- import { glob } from "node:fs/promises";
19
18
  import { routePrecedence } from "./utils.js";
19
+ import { createValidFileMatcher, scanWithExtensions, } from "./file-matcher.js";
20
20
  // Cache for app routes
21
21
  let cachedRoutes = null;
22
22
  let cachedAppDir = null;
23
+ let cachedPageExtensionsKey = null;
23
24
  export function invalidateAppRouteCache() {
24
25
  cachedRoutes = null;
25
26
  cachedAppDir = null;
27
+ cachedPageExtensionsKey = null;
26
28
  }
27
29
  /**
28
30
  * Scan the app/ directory and return a list of routes.
29
31
  */
30
- export async function appRouter(appDir) {
31
- if (cachedRoutes && cachedAppDir === appDir)
32
+ export async function appRouter(appDir, pageExtensions, matcher) {
33
+ matcher ??= createValidFileMatcher(pageExtensions);
34
+ const pageExtensionsKey = JSON.stringify(matcher.extensions);
35
+ if (cachedRoutes &&
36
+ cachedAppDir === appDir &&
37
+ cachedPageExtensionsKey === pageExtensionsKey) {
32
38
  return cachedRoutes;
39
+ }
33
40
  // Find all page.tsx and route.ts files, excluding @slot directories
34
41
  // (slot pages are not standalone routes — they're rendered as props of their parent layout)
35
42
  const routes = [];
36
43
  // Process page files in a single pass
37
44
  // Use function form of exclude for Node < 22.14 compatibility (string arrays require >= 22.14)
38
- for await (const file of glob("**/page.{tsx,ts,jsx,js}", { cwd: appDir, exclude: (name) => name.startsWith("@") })) {
39
- const route = fileToAppRoute(file, appDir, "page");
45
+ for await (const file of scanWithExtensions("**/page", appDir, matcher.extensions, (name) => name.startsWith("@"))) {
46
+ const route = fileToAppRoute(file, appDir, "page", matcher);
40
47
  if (route)
41
48
  routes.push(route);
42
49
  }
43
50
  // Process route handler files (API routes) in a single pass
44
- for await (const file of glob("**/route.{tsx,ts,jsx,js}", { cwd: appDir, exclude: (name) => name.startsWith("@") })) {
45
- const route = fileToAppRoute(file, appDir, "route");
51
+ for await (const file of scanWithExtensions("**/route", appDir, matcher.extensions, (name) => name.startsWith("@"))) {
52
+ const route = fileToAppRoute(file, appDir, "route", matcher);
46
53
  if (route)
47
54
  routes.push(route);
48
55
  }
@@ -50,7 +57,7 @@ export async function appRouter(appDir) {
50
57
  // In Next.js, pages nested inside @slot directories create additional URL routes.
51
58
  // For example, @audience/demographics/page.tsx at app/parallel-routes/ creates
52
59
  // a route at /parallel-routes/demographics.
53
- const slotSubRoutes = discoverSlotSubRoutes(routes, appDir);
60
+ const slotSubRoutes = discoverSlotSubRoutes(routes, appDir, matcher);
54
61
  routes.push(...slotSubRoutes);
55
62
  // Sort: static routes first, then dynamic, then catch-all
56
63
  routes.sort((a, b) => {
@@ -59,6 +66,7 @@ export async function appRouter(appDir) {
59
66
  });
60
67
  cachedRoutes = routes;
61
68
  cachedAppDir = appDir;
69
+ cachedPageExtensionsKey = pageExtensionsKey;
62
70
  return routes;
63
71
  }
64
72
  /**
@@ -72,7 +80,7 @@ export async function appRouter(appDir) {
72
80
  * - @audience slot → @audience/demographics/page.tsx (matched)
73
81
  * - other slots → their default.tsx (fallback)
74
82
  */
75
- function discoverSlotSubRoutes(routes, _appDir) {
83
+ function discoverSlotSubRoutes(routes, _appDir, matcher) {
76
84
  const syntheticRoutes = [];
77
85
  const existingPatterns = new Set(routes.map((r) => r.pattern));
78
86
  for (const parentRoute of routes) {
@@ -88,7 +96,7 @@ function discoverSlotSubRoutes(routes, _appDir) {
88
96
  const slotDir = path.join(parentPageDir, `@${slot.name}`);
89
97
  if (!fs.existsSync(slotDir))
90
98
  continue;
91
- const subPages = findSlotSubPages(slotDir);
99
+ const subPages = findSlotSubPages(slotDir, matcher);
92
100
  for (const { relativePath, pagePath } of subPages) {
93
101
  if (!subPathMap.has(relativePath)) {
94
102
  subPathMap.set(relativePath, new Map());
@@ -99,7 +107,7 @@ function discoverSlotSubRoutes(routes, _appDir) {
99
107
  if (subPathMap.size === 0)
100
108
  continue;
101
109
  // Find the default.tsx for the children slot at the parent directory
102
- const childrenDefault = findFile(parentPageDir, "default");
110
+ const childrenDefault = findFile(parentPageDir, "default", matcher);
103
111
  for (const [subPath, slotPages] of subPathMap) {
104
112
  // Convert sub-path segments to URL pattern parts
105
113
  const subSegments = subPath.split(path.sep);
@@ -176,7 +184,7 @@ function discoverSlotSubRoutes(routes, _appDir) {
176
184
  * Skips the root page.tsx (already handled as the slot's main page)
177
185
  * and intercepting route directories.
178
186
  */
179
- function findSlotSubPages(slotDir) {
187
+ function findSlotSubPages(slotDir, matcher) {
180
188
  const results = [];
181
189
  function scan(dir) {
182
190
  if (!fs.existsSync(dir))
@@ -192,7 +200,7 @@ function findSlotSubPages(slotDir) {
192
200
  if (entry.name.startsWith("_"))
193
201
  continue;
194
202
  const subDir = path.join(dir, entry.name);
195
- const page = findFile(subDir, "page");
203
+ const page = findFile(subDir, "page", matcher);
196
204
  if (page) {
197
205
  const relativePath = path.relative(slotDir, subDir);
198
206
  results.push({ relativePath, pagePath: page });
@@ -207,7 +215,7 @@ function findSlotSubPages(slotDir) {
207
215
  /**
208
216
  * Convert a file path relative to app/ into an AppRoute.
209
217
  */
210
- function fileToAppRoute(file, appDir, type) {
218
+ function fileToAppRoute(file, appDir, type, matcher) {
211
219
  // Remove the filename (page.tsx or route.ts)
212
220
  const dir = path.dirname(file);
213
221
  const segments = dir === "." ? [] : dir.split(path.sep);
@@ -257,33 +265,33 @@ function fileToAppRoute(file, appDir, type) {
257
265
  }
258
266
  const pattern = "/" + urlSegments.join("/");
259
267
  // Discover layouts and templates from root to leaf
260
- const layouts = discoverLayouts(segments, appDir);
261
- const templates = discoverTemplates(segments, appDir);
268
+ const layouts = discoverLayouts(segments, appDir, matcher);
269
+ const templates = discoverTemplates(segments, appDir, matcher);
262
270
  // Compute the URL segment depth for each layout.
263
271
  // Each layout corresponds to a directory level. We need to count how many
264
272
  // of the filesystem segments up to that layout's level contribute URL segments
265
273
  // (i.e., are not route groups or parallel slots).
266
- const layoutSegmentDepths = computeLayoutSegmentDepths(segments, appDir, layouts);
274
+ const layoutSegmentDepths = computeLayoutSegmentDepths(segments, appDir, layouts, matcher);
267
275
  // Discover per-layout error boundaries (aligned with layouts array).
268
276
  // In Next.js, each segment independently wraps its children with an ErrorBoundary.
269
277
  // This array enables interleaving error boundaries with layouts in the rendering.
270
- const layoutErrorPaths = discoverLayoutAlignedErrors(segments, appDir);
278
+ const layoutErrorPaths = discoverLayoutAlignedErrors(segments, appDir, matcher);
271
279
  // Discover loading, error in the route's directory
272
280
  const routeDir = dir === "." ? appDir : path.join(appDir, dir);
273
- const loadingPath = findFile(routeDir, "loading");
274
- const errorPath = findFile(routeDir, "error");
281
+ const loadingPath = findFile(routeDir, "loading", matcher);
282
+ const errorPath = findFile(routeDir, "error", matcher);
275
283
  // Discover not-found/forbidden/unauthorized: walk from route directory up to root (nearest wins).
276
- const notFoundPath = discoverBoundaryFile(segments, appDir, "not-found");
277
- const forbiddenPath = discoverBoundaryFile(segments, appDir, "forbidden");
278
- const unauthorizedPath = discoverBoundaryFile(segments, appDir, "unauthorized");
284
+ const notFoundPath = discoverBoundaryFile(segments, appDir, "not-found", matcher);
285
+ const forbiddenPath = discoverBoundaryFile(segments, appDir, "forbidden", matcher);
286
+ const unauthorizedPath = discoverBoundaryFile(segments, appDir, "unauthorized", matcher);
279
287
  // Discover per-layout not-found files (one per layout directory).
280
288
  // These are used for per-layout NotFoundBoundary to match Next.js behavior where
281
289
  // notFound() thrown from a layout is caught by the parent layout's boundary.
282
- const notFoundPaths = discoverBoundaryFilePerLayout(layouts, "not-found");
290
+ const notFoundPaths = discoverBoundaryFilePerLayout(layouts, "not-found", matcher);
283
291
  // Discover parallel slots (@team, @analytics, etc.).
284
292
  // Slots at the route's own directory use page.tsx; slots at ancestor directories
285
293
  // (inherited from parent layouts) use default.tsx as fallback.
286
- const parallelSlots = discoverInheritedParallelSlots(segments, appDir, routeDir);
294
+ const parallelSlots = discoverInheritedParallelSlots(segments, appDir, routeDir, matcher);
287
295
  return {
288
296
  pattern: pattern === "/" ? "/" : pattern,
289
297
  pagePath: type === "page" ? path.join(appDir, file) : null,
@@ -308,12 +316,12 @@ function fileToAppRoute(file, appDir, type) {
308
316
  * Root layout = 0, then each directory level that contributes a URL segment
309
317
  * increments the depth. Route groups and parallel slots don't contribute.
310
318
  */
311
- function computeLayoutSegmentDepths(segments, appDir, layouts) {
319
+ function computeLayoutSegmentDepths(segments, appDir, layouts, matcher) {
312
320
  // Build a map: layout file path → depth in URL segments
313
321
  // Walk the segments directory-by-directory, tracking cumulative URL depth
314
322
  const depthMap = new Map();
315
323
  // Root layout (at appDir) always has depth 0
316
- const rootLayout = findFile(appDir, "layout");
324
+ const rootLayout = findFile(appDir, "layout", matcher);
317
325
  if (rootLayout)
318
326
  depthMap.set(rootLayout, 0);
319
327
  let urlDepth = 0;
@@ -326,7 +334,7 @@ function computeLayoutSegmentDepths(segments, appDir, layouts) {
326
334
  if (!isRouteGroup && !isParallelSlot) {
327
335
  urlDepth++;
328
336
  }
329
- const layout = findFile(currentDir, "layout");
337
+ const layout = findFile(currentDir, "layout", matcher);
330
338
  if (layout) {
331
339
  depthMap.set(layout, urlDepth);
332
340
  }
@@ -338,17 +346,17 @@ function computeLayoutSegmentDepths(segments, appDir, layouts) {
338
346
  * Discover all layout files from root to the given directory.
339
347
  * Each level of the directory tree may have a layout.tsx.
340
348
  */
341
- function discoverLayouts(segments, appDir) {
349
+ function discoverLayouts(segments, appDir, matcher) {
342
350
  const layouts = [];
343
351
  // Check root layout
344
- const rootLayout = findFile(appDir, "layout");
352
+ const rootLayout = findFile(appDir, "layout", matcher);
345
353
  if (rootLayout)
346
354
  layouts.push(rootLayout);
347
355
  // Check each directory level
348
356
  let currentDir = appDir;
349
357
  for (const segment of segments) {
350
358
  currentDir = path.join(currentDir, segment);
351
- const layout = findFile(currentDir, "layout");
359
+ const layout = findFile(currentDir, "layout", matcher);
352
360
  if (layout)
353
361
  layouts.push(layout);
354
362
  }
@@ -359,17 +367,17 @@ function discoverLayouts(segments, appDir) {
359
367
  * Each level of the directory tree may have a template.tsx.
360
368
  * Templates are like layouts but re-mount on navigation.
361
369
  */
362
- function discoverTemplates(segments, appDir) {
370
+ function discoverTemplates(segments, appDir, matcher) {
363
371
  const templates = [];
364
372
  // Check root template
365
- const rootTemplate = findFile(appDir, "template");
373
+ const rootTemplate = findFile(appDir, "template", matcher);
366
374
  if (rootTemplate)
367
375
  templates.push(rootTemplate);
368
376
  // Check each directory level
369
377
  let currentDir = appDir;
370
378
  for (const segment of segments) {
371
379
  currentDir = path.join(currentDir, segment);
372
- const template = findFile(currentDir, "template");
380
+ const template = findFile(currentDir, "template", matcher);
373
381
  if (template)
374
382
  templates.push(template);
375
383
  }
@@ -386,20 +394,20 @@ function discoverTemplates(segments, appDir) {
386
394
  * rendering tree, matching Next.js behavior where each segment independently
387
395
  * wraps its children with an error boundary.
388
396
  */
389
- function discoverLayoutAlignedErrors(segments, appDir) {
397
+ function discoverLayoutAlignedErrors(segments, appDir, matcher) {
390
398
  const errors = [];
391
399
  // Root level (only if root has a layout — matching discoverLayouts logic)
392
- const rootLayout = findFile(appDir, "layout");
400
+ const rootLayout = findFile(appDir, "layout", matcher);
393
401
  if (rootLayout) {
394
- errors.push(findFile(appDir, "error"));
402
+ errors.push(findFile(appDir, "error", matcher));
395
403
  }
396
404
  // Check each directory level
397
405
  let currentDir = appDir;
398
406
  for (const segment of segments) {
399
407
  currentDir = path.join(currentDir, segment);
400
- const layout = findFile(currentDir, "layout");
408
+ const layout = findFile(currentDir, "layout", matcher);
401
409
  if (layout) {
402
- errors.push(findFile(currentDir, "error"));
410
+ errors.push(findFile(currentDir, "error", matcher));
403
411
  }
404
412
  }
405
413
  return errors;
@@ -409,7 +417,7 @@ function discoverLayoutAlignedErrors(segments, appDir) {
409
417
  * by walking from the route's directory up to the app root.
410
418
  * Returns the first (closest) file found, or null.
411
419
  */
412
- function discoverBoundaryFile(segments, appDir, fileName) {
420
+ function discoverBoundaryFile(segments, appDir, fileName, matcher) {
413
421
  // Build all directory paths from leaf to root
414
422
  const dirs = [];
415
423
  let dir = appDir;
@@ -420,7 +428,7 @@ function discoverBoundaryFile(segments, appDir, fileName) {
420
428
  }
421
429
  // Walk from leaf (last) to root (first)
422
430
  for (let i = dirs.length - 1; i >= 0; i--) {
423
- const f = findFile(dirs[i], fileName);
431
+ const f = findFile(dirs[i], fileName, matcher);
424
432
  if (f)
425
433
  return f;
426
434
  }
@@ -435,10 +443,10 @@ function discoverBoundaryFile(segments, appDir, fileName) {
435
443
  * has its own boundary that wraps the layout's children. When notFound() is thrown
436
444
  * from a layout, it propagates up to the parent layout's boundary.
437
445
  */
438
- function discoverBoundaryFilePerLayout(layouts, fileName) {
446
+ function discoverBoundaryFilePerLayout(layouts, fileName, matcher) {
439
447
  return layouts.map((layoutPath) => {
440
448
  const layoutDir = path.dirname(layoutPath);
441
- return findFile(layoutDir, fileName);
449
+ return findFile(layoutDir, fileName, matcher);
442
450
  });
443
451
  }
444
452
  /**
@@ -453,25 +461,25 @@ function discoverBoundaryFilePerLayout(layouts, fileName) {
453
461
  * that has @slot dirs, collect them. Slots at the route's own directory level
454
462
  * use page.tsx; slots at ancestor levels use default.tsx only.
455
463
  */
456
- function discoverInheritedParallelSlots(segments, appDir, routeDir) {
464
+ function discoverInheritedParallelSlots(segments, appDir, routeDir, matcher) {
457
465
  const slotMap = new Map();
458
466
  // Walk from appDir through each segment, tracking layout indices.
459
467
  // layoutIndex tracks which position in the route's layouts[] array corresponds
460
468
  // to a given directory. Only directories with a layout.tsx file increment.
461
469
  let currentDir = appDir;
462
470
  const dirsToCheck = [];
463
- let layoutIdx = findFile(appDir, "layout") ? 0 : -1;
471
+ let layoutIdx = findFile(appDir, "layout", matcher) ? 0 : -1;
464
472
  dirsToCheck.push({ dir: appDir, layoutIdx: Math.max(layoutIdx, 0) });
465
473
  for (const segment of segments) {
466
474
  currentDir = path.join(currentDir, segment);
467
- if (findFile(currentDir, "layout")) {
475
+ if (findFile(currentDir, "layout", matcher)) {
468
476
  layoutIdx++;
469
477
  }
470
478
  dirsToCheck.push({ dir: currentDir, layoutIdx: Math.max(layoutIdx, 0) });
471
479
  }
472
480
  for (const { dir, layoutIdx: lvlLayoutIdx } of dirsToCheck) {
473
481
  const isOwnDir = dir === routeDir;
474
- const slotsAtLevel = discoverParallelSlots(dir, appDir);
482
+ const slotsAtLevel = discoverParallelSlots(dir, appDir, matcher);
475
483
  for (const slot of slotsAtLevel) {
476
484
  if (isOwnDir) {
477
485
  // At the route's own directory: use page.tsx (normal behavior)
@@ -500,7 +508,7 @@ function discoverInheritedParallelSlots(segments, appDir, routeDir) {
500
508
  * Discover parallel route slots (@team, @analytics, etc.) in a directory.
501
509
  * Returns a ParallelSlot for each @-prefixed subdirectory that has a page or default component.
502
510
  */
503
- function discoverParallelSlots(dir, appDir) {
511
+ function discoverParallelSlots(dir, appDir, matcher) {
504
512
  if (!fs.existsSync(dir))
505
513
  return [];
506
514
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -510,9 +518,9 @@ function discoverParallelSlots(dir, appDir) {
510
518
  continue;
511
519
  const slotName = entry.name.slice(1); // "@team" -> "team"
512
520
  const slotDir = path.join(dir, entry.name);
513
- const pagePath = findFile(slotDir, "page");
514
- const defaultPath = findFile(slotDir, "default");
515
- const interceptingRoutes = discoverInterceptingRoutes(slotDir, dir, appDir);
521
+ const pagePath = findFile(slotDir, "page", matcher);
522
+ const defaultPath = findFile(slotDir, "default", matcher);
523
+ const interceptingRoutes = discoverInterceptingRoutes(slotDir, dir, appDir, matcher);
516
524
  // Only include slots that have at least a page, default, or intercepting route
517
525
  if (!pagePath && !defaultPath && interceptingRoutes.length === 0)
518
526
  continue;
@@ -520,9 +528,9 @@ function discoverParallelSlots(dir, appDir) {
520
528
  name: slotName,
521
529
  pagePath,
522
530
  defaultPath,
523
- layoutPath: findFile(slotDir, "layout"),
524
- loadingPath: findFile(slotDir, "loading"),
525
- errorPath: findFile(slotDir, "error"),
531
+ layoutPath: findFile(slotDir, "layout", matcher),
532
+ loadingPath: findFile(slotDir, "loading", matcher),
533
+ errorPath: findFile(slotDir, "error", matcher),
526
534
  interceptingRoutes,
527
535
  layoutIndex: -1, // Will be set by discoverInheritedParallelSlots
528
536
  });
@@ -549,19 +557,19 @@ const INTERCEPT_PATTERNS = [
549
557
  * @param routeDir - The directory of the route that owns this slot (e.g. app/feed)
550
558
  * @param appDir - The root app directory
551
559
  */
552
- function discoverInterceptingRoutes(slotDir, routeDir, appDir) {
560
+ function discoverInterceptingRoutes(slotDir, routeDir, appDir, matcher) {
553
561
  if (!fs.existsSync(slotDir))
554
562
  return [];
555
563
  const results = [];
556
564
  // Recursively scan for page files inside intercepting directories
557
- scanForInterceptingPages(slotDir, routeDir, appDir, results);
565
+ scanForInterceptingPages(slotDir, routeDir, appDir, results, matcher);
558
566
  return results;
559
567
  }
560
568
  /**
561
569
  * Recursively scan a directory tree for page.tsx files that are inside
562
570
  * intercepting route directories.
563
571
  */
564
- function scanForInterceptingPages(currentDir, routeDir, appDir, results) {
572
+ function scanForInterceptingPages(currentDir, routeDir, appDir, results, matcher) {
565
573
  if (!fs.existsSync(currentDir))
566
574
  return;
567
575
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
@@ -576,11 +584,11 @@ function scanForInterceptingPages(currentDir, routeDir, appDir, results) {
576
584
  const restOfName = entry.name.slice(interceptMatch.prefix.length);
577
585
  const interceptDir = path.join(currentDir, entry.name);
578
586
  // Find page files within this intercepting directory tree
579
- collectInterceptingPages(interceptDir, interceptDir, interceptMatch.convention, restOfName, routeDir, appDir, results);
587
+ collectInterceptingPages(interceptDir, interceptDir, interceptMatch.convention, restOfName, routeDir, appDir, results, matcher);
580
588
  }
581
589
  else {
582
590
  // Regular subdirectory — keep scanning for intercepting dirs
583
- scanForInterceptingPages(path.join(currentDir, entry.name), routeDir, appDir, results);
591
+ scanForInterceptingPages(path.join(currentDir, entry.name), routeDir, appDir, results, matcher);
584
592
  }
585
593
  }
586
594
  }
@@ -599,9 +607,9 @@ function matchInterceptConvention(name) {
599
607
  * Collect page.tsx files inside an intercepting route directory tree
600
608
  * and compute their target URL patterns.
601
609
  */
602
- function collectInterceptingPages(currentDir, interceptRoot, convention, interceptSegment, routeDir, appDir, results) {
610
+ function collectInterceptingPages(currentDir, interceptRoot, convention, interceptSegment, routeDir, appDir, results, matcher) {
603
611
  // Check for page.tsx in current directory
604
- const page = findFile(currentDir, "page");
612
+ const page = findFile(currentDir, "page", matcher);
605
613
  if (page) {
606
614
  const targetPattern = computeInterceptTarget(convention, interceptSegment, currentDir, interceptRoot, routeDir, appDir);
607
615
  if (targetPattern) {
@@ -620,7 +628,7 @@ function collectInterceptingPages(currentDir, interceptRoot, convention, interce
620
628
  for (const entry of entries) {
621
629
  if (!entry.isDirectory())
622
630
  continue;
623
- collectInterceptingPages(path.join(currentDir, entry.name), interceptRoot, convention, interceptSegment, routeDir, appDir, results);
631
+ collectInterceptingPages(path.join(currentDir, entry.name), interceptRoot, convention, interceptSegment, routeDir, appDir, results, matcher);
624
632
  }
625
633
  }
626
634
  /**
@@ -704,11 +712,10 @@ function computeInterceptTarget(convention, interceptSegment, currentDir, interc
704
712
  }
705
713
  /**
706
714
  * Find a file by name (without extension) in a directory.
707
- * Checks .tsx, .ts, .jsx, .js extensions.
715
+ * Checks configured pageExtensions.
708
716
  */
709
- function findFile(dir, name) {
710
- const extensions = [".tsx", ".ts", ".jsx", ".js"];
711
- for (const ext of extensions) {
717
+ function findFile(dir, name, matcher) {
718
+ for (const ext of matcher.dottedExtensions) {
712
719
  const filePath = path.join(dir, name + ext);
713
720
  if (fs.existsSync(filePath))
714
721
  return filePath;