vinext 0.1.0 → 0.1.1

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 (119) hide show
  1. package/dist/build/assets-ignore.d.ts +32 -0
  2. package/dist/build/assets-ignore.js +48 -0
  3. package/dist/build/client-build-config.d.ts +27 -1
  4. package/dist/build/client-build-config.js +58 -1
  5. package/dist/cli.js +2 -0
  6. package/dist/client/navigation-runtime.d.ts +8 -0
  7. package/dist/client/navigation-runtime.js +1 -1
  8. package/dist/client/vinext-next-data.d.ts +2 -1
  9. package/dist/config/config-matchers.d.ts +20 -1
  10. package/dist/config/config-matchers.js +35 -1
  11. package/dist/config/next-config.d.ts +16 -3
  12. package/dist/config/next-config.js +30 -2
  13. package/dist/deploy.js +40 -304
  14. package/dist/entries/app-rsc-entry.d.ts +8 -2
  15. package/dist/entries/app-rsc-entry.js +54 -4
  16. package/dist/entries/app-rsc-manifest.js +20 -2
  17. package/dist/entries/pages-server-entry.js +9 -1
  18. package/dist/index.js +162 -217
  19. package/dist/plugins/postcss.js +18 -14
  20. package/dist/plugins/require-context.d.ts +6 -0
  21. package/dist/plugins/require-context.js +184 -0
  22. package/dist/routing/app-route-graph.d.ts +12 -1
  23. package/dist/routing/app-route-graph.js +137 -5
  24. package/dist/routing/route-pattern.d.ts +2 -1
  25. package/dist/routing/route-pattern.js +16 -1
  26. package/dist/server/api-handler.js +4 -0
  27. package/dist/server/app-browser-entry.js +84 -39
  28. package/dist/server/app-browser-interception-context.d.ts +2 -1
  29. package/dist/server/app-browser-interception-context.js +15 -2
  30. package/dist/server/app-browser-navigation-controller.d.ts +11 -1
  31. package/dist/server/app-browser-navigation-controller.js +77 -1
  32. package/dist/server/app-browser-popstate.d.ts +12 -3
  33. package/dist/server/app-browser-popstate.js +19 -4
  34. package/dist/server/app-browser-state.d.ts +3 -0
  35. package/dist/server/app-browser-state.js +6 -3
  36. package/dist/server/app-browser-visible-commit.js +9 -7
  37. package/dist/server/app-history-state.d.ts +45 -1
  38. package/dist/server/app-history-state.js +109 -1
  39. package/dist/server/app-page-boundary-render.js +41 -19
  40. package/dist/server/app-page-dispatch.d.ts +6 -0
  41. package/dist/server/app-page-dispatch.js +3 -1
  42. package/dist/server/app-page-element-builder.d.ts +1 -0
  43. package/dist/server/app-page-element-builder.js +22 -10
  44. package/dist/server/app-page-render.d.ts +6 -0
  45. package/dist/server/app-page-render.js +5 -3
  46. package/dist/server/app-page-request.d.ts +8 -6
  47. package/dist/server/app-page-request.js +12 -9
  48. package/dist/server/app-page-response.d.ts +2 -2
  49. package/dist/server/app-page-response.js +1 -1
  50. package/dist/server/app-page-route-wiring.js +2 -1
  51. package/dist/server/app-page-stream.d.ts +37 -2
  52. package/dist/server/app-page-stream.js +36 -3
  53. package/dist/server/app-pages-bridge.d.ts +16 -0
  54. package/dist/server/app-pages-bridge.js +23 -3
  55. package/dist/server/app-route-handler-cache.d.ts +1 -0
  56. package/dist/server/app-route-handler-cache.js +1 -0
  57. package/dist/server/app-route-handler-dispatch.d.ts +1 -0
  58. package/dist/server/app-route-handler-dispatch.js +2 -0
  59. package/dist/server/app-route-handler-execution.d.ts +1 -0
  60. package/dist/server/app-route-handler-execution.js +1 -0
  61. package/dist/server/app-route-handler-runtime.d.ts +1 -0
  62. package/dist/server/app-route-handler-runtime.js +3 -2
  63. package/dist/server/app-rsc-handler.d.ts +1 -0
  64. package/dist/server/app-rsc-handler.js +4 -3
  65. package/dist/server/app-rsc-route-matching.d.ts +20 -1
  66. package/dist/server/app-rsc-route-matching.js +29 -4
  67. package/dist/server/app-server-action-execution.d.ts +11 -1
  68. package/dist/server/app-server-action-execution.js +68 -10
  69. package/dist/server/app-ssr-entry.d.ts +6 -0
  70. package/dist/server/app-ssr-entry.js +17 -1
  71. package/dist/server/dev-server.d.ts +1 -1
  72. package/dist/server/dev-server.js +54 -31
  73. package/dist/server/isr-cache.d.ts +37 -1
  74. package/dist/server/isr-cache.js +85 -1
  75. package/dist/server/navigation-planner.js +5 -3
  76. package/dist/server/navigation-trace.d.ts +1 -1
  77. package/dist/server/pages-node-compat.d.ts +2 -0
  78. package/dist/server/pages-node-compat.js +4 -0
  79. package/dist/server/pages-page-data.d.ts +10 -7
  80. package/dist/server/pages-page-data.js +4 -2
  81. package/dist/server/pages-page-handler.d.ts +9 -2
  82. package/dist/server/pages-page-handler.js +29 -16
  83. package/dist/server/pages-page-response.d.ts +11 -2
  84. package/dist/server/pages-page-response.js +8 -1
  85. package/dist/server/pages-readiness.d.ts +36 -0
  86. package/dist/server/pages-readiness.js +21 -0
  87. package/dist/server/pages-request-pipeline.d.ts +99 -0
  88. package/dist/server/pages-request-pipeline.js +209 -0
  89. package/dist/server/pages-revalidate.d.ts +15 -0
  90. package/dist/server/pages-revalidate.js +19 -0
  91. package/dist/server/prod-server.d.ts +6 -2
  92. package/dist/server/prod-server.js +101 -217
  93. package/dist/server/socket-error-backstop.d.ts +19 -1
  94. package/dist/server/socket-error-backstop.js +77 -4
  95. package/dist/shims/app-router-scroll.js +22 -4
  96. package/dist/shims/cache-runtime.js +31 -1
  97. package/dist/shims/error-boundary.d.ts +21 -11
  98. package/dist/shims/error-boundary.js +8 -1
  99. package/dist/shims/fetch-cache.d.ts +14 -1
  100. package/dist/shims/fetch-cache.js +18 -1
  101. package/dist/shims/hash-scroll.d.ts +1 -0
  102. package/dist/shims/hash-scroll.js +3 -1
  103. package/dist/shims/internal/link-status-registry.d.ts +43 -0
  104. package/dist/shims/internal/link-status-registry.js +42 -0
  105. package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
  106. package/dist/shims/internal/route-pattern-for-warning.js +40 -0
  107. package/dist/shims/internal/utils.d.ts +1 -0
  108. package/dist/shims/link.js +20 -6
  109. package/dist/shims/navigation.d.ts +2 -2
  110. package/dist/shims/navigation.js +63 -7
  111. package/dist/shims/router-state.d.ts +1 -0
  112. package/dist/shims/router-state.js +2 -0
  113. package/dist/shims/router.d.ts +6 -3
  114. package/dist/shims/router.js +128 -21
  115. package/dist/utils/client-build-manifest.d.ts +8 -1
  116. package/dist/utils/client-build-manifest.js +30 -5
  117. package/dist/utils/client-entry-manifest.d.ts +11 -0
  118. package/dist/utils/client-entry-manifest.js +29 -0
  119. package/package.json +5 -1
@@ -0,0 +1,184 @@
1
+ import { forEachAstChild, hasRange, isAstRecord, nodeArray } from "./ast-utils.js";
2
+ import { parseAst } from "vite";
3
+ import MagicString from "magic-string";
4
+ //#region src/plugins/require-context.ts
5
+ const TRANSFORMABLE_EXTENSIONS = new Set([
6
+ ".js",
7
+ ".jsx",
8
+ ".ts",
9
+ ".tsx",
10
+ ".mjs",
11
+ ".cjs",
12
+ ".mts",
13
+ ".cts"
14
+ ]);
15
+ function createRequireContextPlugin() {
16
+ return {
17
+ name: "vinext:require-context",
18
+ enforce: "pre",
19
+ transform(code, id) {
20
+ if (!mayContainRequireContext(code)) return null;
21
+ const lang = langForId(id);
22
+ if (!lang) return null;
23
+ let ast;
24
+ try {
25
+ ast = parseAst(code, { lang });
26
+ } catch {
27
+ return null;
28
+ }
29
+ const calls = collectRequireContextCalls(ast);
30
+ if (calls.length === 0) return null;
31
+ const output = new MagicString(code);
32
+ for (const call of calls) output.overwrite(call.range.start, call.range.end, buildReplacement(call));
33
+ return {
34
+ code: output.toString(),
35
+ map: output.generateMap({ hires: "boundary" })
36
+ };
37
+ }
38
+ };
39
+ }
40
+ function mayContainRequireContext(code) {
41
+ return code.includes("require") && code.includes(".context");
42
+ }
43
+ function langForId(id) {
44
+ const clean = id.split("?", 1)[0];
45
+ const dot = clean.lastIndexOf(".");
46
+ if (dot < 0) return null;
47
+ const ext = clean.slice(dot).toLowerCase();
48
+ if (!TRANSFORMABLE_EXTENSIONS.has(ext)) return null;
49
+ switch (ext) {
50
+ case ".ts":
51
+ case ".cts":
52
+ case ".mts": return "ts";
53
+ case ".tsx": return "tsx";
54
+ case ".jsx": return "jsx";
55
+ default: return "jsx";
56
+ }
57
+ }
58
+ function collectRequireContextCalls(ast) {
59
+ const calls = [];
60
+ function visit(value) {
61
+ if (!isAstRecord(value)) return;
62
+ const parsed = parseRequireContextCall(value);
63
+ if (parsed) {
64
+ calls.push(parsed);
65
+ return;
66
+ }
67
+ forEachAstChild(value, visit);
68
+ }
69
+ visit(ast);
70
+ return calls;
71
+ }
72
+ function parseRequireContextCall(node) {
73
+ if (node.type !== "CallExpression" || !hasRange(node)) return null;
74
+ const callee = node.callee;
75
+ if (!isAstRecord(callee) || callee.type !== "MemberExpression" || callee.computed === true || callee.optional === true) return null;
76
+ if (!isPropertyNamed(callee.property, "context")) return null;
77
+ if (!isRequireExpression(callee.object)) return null;
78
+ const args = nodeArray(node.arguments);
79
+ const dir = stringLiteralValue(args[0]);
80
+ if (dir == null || !(dir.startsWith("./") || dir.startsWith("../"))) return null;
81
+ let recursive = true;
82
+ if (args.length >= 2) {
83
+ const value = booleanLiteralValue(args[1]);
84
+ if (value == null) return null;
85
+ recursive = value;
86
+ }
87
+ let pattern = "";
88
+ let flags = "";
89
+ if (args.length >= 3) {
90
+ const regex = regexLiteralValue(args[2]);
91
+ if (regex == null) return null;
92
+ pattern = regex.pattern;
93
+ flags = regex.flags;
94
+ } else if (args.length > 3) return null;
95
+ return {
96
+ range: node,
97
+ dir,
98
+ recursive,
99
+ pattern,
100
+ flags
101
+ };
102
+ }
103
+ function isRequireExpression(value) {
104
+ let node = value;
105
+ while (isAstRecord(node)) {
106
+ if (node.type === "Identifier") return node.name === "require";
107
+ if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") {
108
+ node = node.expression;
109
+ continue;
110
+ }
111
+ if (node.type === "TSNonNullExpression") {
112
+ node = node.expression;
113
+ continue;
114
+ }
115
+ if (node.type === "ParenthesizedExpression") {
116
+ node = node.expression;
117
+ continue;
118
+ }
119
+ return false;
120
+ }
121
+ return false;
122
+ }
123
+ function isPropertyNamed(value, name) {
124
+ return isAstRecord(value) && value.type === "Identifier" && value.name === name;
125
+ }
126
+ function stringLiteralValue(value) {
127
+ if (isAstRecord(value) && value.type === "Literal" && typeof value.value === "string") return value.value;
128
+ return null;
129
+ }
130
+ function booleanLiteralValue(value) {
131
+ if (isAstRecord(value) && value.type === "Literal" && typeof value.value === "boolean") return value.value;
132
+ return null;
133
+ }
134
+ function regexLiteralValue(value) {
135
+ if (!isAstRecord(value) || value.type !== "Literal") return null;
136
+ const regex = value.regex;
137
+ if (typeof regex === "object" && regex !== null && typeof regex.pattern === "string" && typeof regex.flags === "string") return {
138
+ pattern: regex.pattern,
139
+ flags: regex.flags
140
+ };
141
+ return null;
142
+ }
143
+ function buildReplacement(call) {
144
+ const globPattern = globPatternFor(call.dir, call.recursive);
145
+ const glob = `import.meta.glob(${JSON.stringify(globPattern)}, { eager: true })`;
146
+ const base = JSON.stringify(stripTrailingSlash(call.dir));
147
+ const filterFlags = call.flags.replace(/[gy]/g, "");
148
+ const regexArgs = `${JSON.stringify(call.pattern)}, ${JSON.stringify(filterFlags)}`;
149
+ return [
150
+ "(() => {",
151
+ ` const __modules = ${glob};`,
152
+ ` const __base = ${base};`,
153
+ ` const __re = ${call.pattern ? `new RegExp(${regexArgs})` : "null"};`,
154
+ " const __prefix = __base.endsWith('/') ? __base : __base + '/';",
155
+ " const __map = Object.create(null);",
156
+ " for (const __abs in __modules) {",
157
+ " if (!__abs.startsWith(__prefix)) continue;",
158
+ " const __key = './' + __abs.slice(__prefix.length);",
159
+ " if (__re && !__re.test(__key)) continue;",
160
+ " __map[__key] = __modules[__abs];",
161
+ " }",
162
+ " const __keys = Object.keys(__map).sort();",
163
+ " const __ctx = (__key) => {",
164
+ " if (__key in __map) return __map[__key];",
165
+ " const __err = new Error('Cannot find module \\'' + __key + '\\'');",
166
+ " __err.code = 'MODULE_NOT_FOUND';",
167
+ " throw __err;",
168
+ " };",
169
+ " __ctx.keys = () => __keys.slice();",
170
+ " __ctx.resolve = (__key) => __key;",
171
+ ` __ctx.id = __base;`,
172
+ " return __ctx;",
173
+ "})()"
174
+ ].join("\n");
175
+ }
176
+ function globPatternFor(dir, recursive) {
177
+ const base = stripTrailingSlash(dir);
178
+ return recursive ? `${base}/**/*` : `${base}/*`;
179
+ }
180
+ function stripTrailingSlash(value) {
181
+ return value.endsWith("/") ? value.slice(0, -1) : value;
182
+ }
183
+ //#endregion
184
+ export { createRequireContextPlugin };
@@ -24,6 +24,11 @@ type InterceptingRoute = {
24
24
  pagePath: string; /** Absolute layout paths inside the intercepting route tree, outermost to innermost */
25
25
  layoutPaths: string[]; /** Parameter names for dynamic segments */
26
26
  params: string[];
27
+ /**
28
+ * Synthetic page-carrier slot id for sibling (slot-less) interception.
29
+ * Set only when the marker has no `@slot` wrapper; undefined for slot intercepts.
30
+ */
31
+ slotId?: string;
27
32
  };
28
33
  type ParallelSlot = {
29
34
  /** Graph-owned semantic slot identity. Required on AppRouteGraphParallelSlot. */id?: string; /** Stable slot identity (name + owning directory), used for route serialization keys. */
@@ -73,7 +78,13 @@ type AppRoute = {
73
78
  routePath: string | null; /** Ordered list of layout files from root to leaf */
74
79
  layouts: string[]; /** Ordered list of all discovered template files from root to leaf (not necessarily aligned 1:1 with layouts) */
75
80
  templates: string[]; /** Parallel route slots (from @slot directories at the route's directory level) */
76
- parallelSlots: ParallelSlot[]; /** Loading component path */
81
+ parallelSlots: ParallelSlot[];
82
+ /**
83
+ * Interception markers not wrapped in an `@slot` directory.
84
+ * On soft-nav, the intercepting page replaces the entire page response.
85
+ * Empty array when there are no sibling-style interception markers.
86
+ */
87
+ siblingIntercepts: InterceptingRoute[]; /** Loading component path */
77
88
  loadingPath: string | null; /** Error component path (leaf directory only) */
78
89
  errorPath: string | null;
79
90
  /**
@@ -33,6 +33,10 @@ function createAppRouteGraphSlotId(slotName, ownerTreePath) {
33
33
  function createAppRouteGraphDefaultId(slotId) {
34
34
  return `default:${slotId}`;
35
35
  }
36
+ const SIBLING_INTERCEPT_SLOT_NAME = "__vinext_sibling_intercept";
37
+ function createAppRouteGraphSiblingInterceptSlotId(sourcePattern) {
38
+ return createAppRouteGraphSlotId(SIBLING_INTERCEPT_SLOT_NAME, sourcePattern);
39
+ }
36
40
  function createAppRouteGraphInterceptionId(slotId, sourcePattern, targetPattern) {
37
41
  return `interception:${slotId}:${sourcePattern}->${targetPattern}`;
38
42
  }
@@ -182,6 +186,21 @@ function createStaticSegmentGraph(routes) {
182
186
  slot
183
187
  });
184
188
  }
189
+ for (const ir of route.siblingIntercepts) {
190
+ if (!ir.slotId) continue;
191
+ const id = createAppRouteGraphInterceptionId(ir.slotId, ir.sourceMatchPattern, ir.targetPattern);
192
+ interceptions.set(id, {
193
+ id,
194
+ sourcePattern: ir.sourceMatchPattern,
195
+ sourcePatternParts: splitRouteManifestPatternParts(ir.sourceMatchPattern),
196
+ targetPattern: ir.targetPattern,
197
+ targetPatternParts: splitRouteManifestPatternParts(ir.targetPattern),
198
+ slotId: ir.slotId,
199
+ ownerLayoutId: null,
200
+ interceptingRouteId: routeIdByPattern.get(ir.sourceMatchPattern) ?? null,
201
+ targetRouteId: routeIdByPattern.get(ir.targetPattern) ?? null
202
+ });
203
+ }
185
204
  }
186
205
  return {
187
206
  routes: routeEntries,
@@ -353,9 +372,10 @@ async function buildAppRouteGraph(appDir, matcher) {
353
372
  }
354
373
  const slotSubRoutes = discoverSlotSubRoutes(routes, matcher, ghostParentRoutes);
355
374
  routes.push(...slotSubRoutes);
375
+ discoverSiblingInterceptingRoutes(routes, appDir, matcher);
356
376
  validatePageRouteConflicts(routes, appDir);
357
377
  validateRoutePatterns(routes.map((route) => route.pattern));
358
- validateRoutePatterns([...new Set(routes.flatMap((route) => route.parallelSlots.flatMap((slot) => slot.interceptingRoutes.map((intercept) => intercept.targetPattern))))]);
378
+ validateRoutePatterns([...new Set(routes.flatMap((route) => [...route.parallelSlots.flatMap((slot) => slot.interceptingRoutes.map((intercept) => intercept.targetPattern)), ...route.siblingIntercepts.map((intercept) => intercept.targetPattern)]))]);
359
379
  routes.sort(compareRoutes);
360
380
  return {
361
381
  routes,
@@ -456,6 +476,8 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
456
476
  if (subPathMap.size === 0) continue;
457
477
  const childrenDefault = findFile(parentPageDir, "default", matcher);
458
478
  if (parentRoute.pagePath && !childrenDefault) continue;
479
+ const childrenCatchAll = childrenDefault ? null : findCatchAllPage(parentPageDir, matcher);
480
+ const childrenFallback = childrenDefault ?? childrenCatchAll;
459
481
  for (const { rawSegments, converted: convertedSubRoute, slotPages } of subPathMap.values()) {
460
482
  const { urlSegments: urlParts, params: subParams, isDynamic: subIsDynamic } = convertedSubRoute;
461
483
  const subUrlPath = urlParts.join("/");
@@ -479,7 +501,7 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
479
501
  const newRoute = {
480
502
  ids: createAppRouteSemanticIds({
481
503
  pattern,
482
- pagePath: childrenDefault,
504
+ pagePath: childrenFallback,
483
505
  routePath: null,
484
506
  routeSegments: [...parentRoute.routeSegments, ...rawSegments],
485
507
  layoutTreePositions: parentRoute.layoutTreePositions,
@@ -487,7 +509,7 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
487
509
  slots: subSlots
488
510
  }),
489
511
  pattern,
490
- pagePath: childrenDefault,
512
+ pagePath: childrenFallback,
491
513
  routePath: null,
492
514
  layouts: parentRoute.layouts,
493
515
  templates: parentRoute.templates,
@@ -507,7 +529,8 @@ function discoverSlotSubRoutes(routes, matcher, ghostParents = []) {
507
529
  isDynamic: parentRoute.isDynamic || subIsDynamic,
508
530
  params: [...parentRoute.params, ...subParams],
509
531
  rootParamNames: parentRoute.rootParamNames,
510
- patternParts: [...parentRoute.patternParts, ...urlParts]
532
+ patternParts: [...parentRoute.patternParts, ...urlParts],
533
+ siblingIntercepts: []
511
534
  };
512
535
  syntheticRoutes.push(newRoute);
513
536
  routesByPattern.set(pattern, newRoute);
@@ -549,6 +572,34 @@ function findSlotSubPages(slotDir, matcher) {
549
572
  return results;
550
573
  }
551
574
  /**
575
+ * Find a sibling catch-all page directly under `dir`, i.e. a `[...slug]` or
576
+ * `[[...slug]]` directory that contains a `page` file. Returns the absolute
577
+ * page path, or null when no catch-all sibling exists.
578
+ *
579
+ * Used as the children fallback for slot-only sub-routes (an explicit `@slot`
580
+ * sub-page with no corresponding children page or `default.tsx`): Next.js
581
+ * serves the children prop from the nearest catch-all, so `/baz` renders
582
+ * `[...catchAll]/page.tsx` for children while `@slot/baz/page.tsx` fills the
583
+ * slot. Optional catch-alls (`[[...slug]]`) qualify because they also match a
584
+ * single extra segment.
585
+ */
586
+ function findCatchAllPage(dir, matcher) {
587
+ let entries;
588
+ try {
589
+ entries = fs.readdirSync(dir, { withFileTypes: true });
590
+ } catch {
591
+ return null;
592
+ }
593
+ for (const entry of entries) {
594
+ if (!entry.isDirectory()) continue;
595
+ const name = entry.name;
596
+ if (!(name.startsWith("[...") && name.endsWith("]") || name.startsWith("[[...") && name.endsWith("]]"))) continue;
597
+ const page = findFile(path.join(dir, name), "page", matcher);
598
+ if (page) return page;
599
+ }
600
+ return null;
601
+ }
602
+ /**
552
603
  * Convert a file path relative to app/ into an AppRoute.
553
604
  */
554
605
  function fileToAppRoute(file, appDir, type, matcher) {
@@ -620,7 +671,8 @@ function directoryToAppRoute(dir, appDir, matcher, pagePath, routePath) {
620
671
  isDynamic,
621
672
  params,
622
673
  rootParamNames: computeRootParamNames(segments, layoutTreePositions),
623
- patternParts: urlSegments
674
+ patternParts: urlSegments,
675
+ siblingIntercepts: []
624
676
  };
625
677
  }
626
678
  function dynamicParamNameFromSegment(segment) {
@@ -1062,6 +1114,86 @@ function discoverInterceptingRoutes(slotDir, routeDir, appDir, matcher) {
1062
1114
  return results;
1063
1115
  }
1064
1116
  /**
1117
+ * Discover sibling-style interception markers — interception marker directories
1118
+ * (e.g. `(..)showcase`, `(..)(..)hoge`) that are NOT wrapped inside an `@slot`
1119
+ * directory. Mutates each matching route's `siblingIntercepts` array.
1120
+ *
1121
+ * Sibling intercepts use the same conventions and target-computation logic as
1122
+ * slot intercepts, but their intercepting page replaces the full page response
1123
+ * (not a slot) during soft navigation.
1124
+ */
1125
+ function discoverSiblingInterceptingRoutes(routes, appDir, matcher) {
1126
+ const routesByDir = /* @__PURE__ */ new Map();
1127
+ for (const route of routes) {
1128
+ const filePath = route.pagePath ?? route.routePath;
1129
+ if (!filePath) continue;
1130
+ const routeDir = path.dirname(filePath);
1131
+ if (!routesByDir.has(routeDir)) routesByDir.set(routeDir, route);
1132
+ }
1133
+ function walk(dir) {
1134
+ let entries;
1135
+ try {
1136
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1137
+ } catch {
1138
+ return;
1139
+ }
1140
+ for (const entry of entries) {
1141
+ if (!entry.isDirectory()) continue;
1142
+ if (entry.name.startsWith("_")) continue;
1143
+ if (entry.name.startsWith("@")) continue;
1144
+ const childDir = path.join(dir, entry.name);
1145
+ const marker = matchInterceptConvention(entry.name);
1146
+ if (marker) {
1147
+ const restOfName = entry.name.slice(marker.prefix.length);
1148
+ const parentDir = dir;
1149
+ const results = [];
1150
+ collectInterceptingPages(childDir, childDir, marker.convention, restOfName, parentDir, appDir, parentDir, results, matcher);
1151
+ for (const ir of results) {
1152
+ ir.slotId = createAppRouteGraphSiblingInterceptSlotId(ir.sourceMatchPattern);
1153
+ const owner = findOwnerRouteForDir(parentDir, appDir, routes, routesByDir);
1154
+ if (owner) owner.siblingIntercepts.push(ir);
1155
+ }
1156
+ continue;
1157
+ }
1158
+ walk(childDir);
1159
+ }
1160
+ }
1161
+ walk(appDir);
1162
+ }
1163
+ /**
1164
+ * Find the best route to attach a sibling intercept to, given the directory
1165
+ * that contains the interception marker.
1166
+ *
1167
+ * 1. Exact hit: a route whose page/handler lives directly in `dir`.
1168
+ * 2. Subtree hit: shallowest route whose page lives anywhere under `dir`
1169
+ * (handles catch-all routes like `/templates/:catchAll+`).
1170
+ * 3. Ancestor walk: walk up the directory tree toward `appDir` looking for
1171
+ * any of the above. This handles the case where the marker directory has
1172
+ * no sibling pages at all (e.g. `deep/path/(...)target` with no
1173
+ * `deep/path/page.tsx`).
1174
+ */
1175
+ function findOwnerRouteForDir(dir, appDir, routes, routesByDir) {
1176
+ let current = dir;
1177
+ while (true) {
1178
+ const exact = routesByDir.get(current);
1179
+ if (exact) return exact;
1180
+ const currentWithSep = current + path.sep;
1181
+ let best = null;
1182
+ for (const route of routes) {
1183
+ const filePath = route.pagePath ?? route.routePath;
1184
+ if (!filePath) continue;
1185
+ if (!filePath.startsWith(currentWithSep)) continue;
1186
+ if (!best || route.patternParts.length < best.patternParts.length) best = route;
1187
+ }
1188
+ if (best) return best;
1189
+ if (current === appDir) break;
1190
+ const parent = path.dirname(current);
1191
+ if (parent === current) break;
1192
+ current = parent;
1193
+ }
1194
+ return null;
1195
+ }
1196
+ /**
1065
1197
  * Recursively scan a directory tree for page.tsx files that are inside
1066
1198
  * intercepting route directories.
1067
1199
  */
@@ -5,6 +5,7 @@ declare function routePattern(pathname: string): string;
5
5
  declare function fillRoutePatternSegments(pathname: string, params: RoutePatternParams): string | null;
6
6
  declare function matchRoutePattern(urlParts: readonly string[], patternParts: readonly string[]): RoutePatternParams | null;
7
7
  declare function matchRoutePatternPrefix(pathParts: readonly string[], patternParts: readonly string[]): boolean;
8
+ declare function matchRoutePatternWithOptionalDynamicSegments(pathParts: readonly string[], patternParts: readonly string[]): boolean;
8
9
  /**
9
10
  * A single entry from `getStaticPaths().paths`.
10
11
  *
@@ -60,4 +61,4 @@ declare function normalizeStaticPathname(pathname: string): string;
60
61
  */
61
62
  declare function normalizeStaticPathsEntry(entry: StaticPathsEntry, routePattern: string): NormalizedStaticPathsEntry;
62
63
  //#endregion
63
- export { RoutePatternParams, StaticPathsEntry, fillRoutePatternSegments, matchRoutePattern, matchRoutePatternPrefix, normalizeStaticPathname, normalizeStaticPathsEntry, routePattern, routePatternParts };
64
+ export { RoutePatternParams, StaticPathsEntry, fillRoutePatternSegments, matchRoutePattern, matchRoutePatternPrefix, matchRoutePatternWithOptionalDynamicSegments, normalizeStaticPathname, normalizeStaticPathsEntry, routePattern, routePatternParts };
@@ -104,6 +104,21 @@ function matchRoutePatternPrefix(pathParts, patternParts) {
104
104
  }
105
105
  return true;
106
106
  }
107
+ function matchRoutePatternWithOptionalDynamicSegments(pathParts, patternParts) {
108
+ function matchFrom(pathIndex, patternIndex) {
109
+ if (patternIndex === patternParts.length) return pathIndex === pathParts.length;
110
+ const patternPart = patternParts[patternIndex];
111
+ if (patternPart.startsWith(":") && (patternPart.endsWith("+") || patternPart.endsWith("*"))) {
112
+ const minLength = patternPart.endsWith("+") ? 1 : 0;
113
+ for (let endIndex = pathIndex + minLength; endIndex <= pathParts.length; endIndex++) if (matchFrom(endIndex, patternIndex + 1)) return true;
114
+ return false;
115
+ }
116
+ if (patternPart.startsWith(":")) return matchFrom(pathIndex, patternIndex + 1) || pathIndex < pathParts.length && matchFrom(pathIndex + 1, patternIndex + 1);
117
+ if (pathIndex >= pathParts.length || pathParts[pathIndex] !== patternPart) return false;
118
+ return matchFrom(pathIndex + 1, patternIndex + 1);
119
+ }
120
+ return matchFrom(0, 0);
121
+ }
107
122
  /**
108
123
  * Strip query string and a single trailing slash from a pathname.
109
124
  *
@@ -147,4 +162,4 @@ function normalizeStaticPathsEntry(entry, routePattern) {
147
162
  return { params };
148
163
  }
149
164
  //#endregion
150
- export { fillRoutePatternSegments, matchRoutePattern, matchRoutePatternPrefix, normalizeStaticPathname, normalizeStaticPathsEntry, routePattern, routePatternParts };
165
+ export { fillRoutePatternSegments, matchRoutePattern, matchRoutePatternPrefix, matchRoutePatternWithOptionalDynamicSegments, normalizeStaticPathname, normalizeStaticPathsEntry, routePattern, routePatternParts };
@@ -7,6 +7,7 @@ import { PagesBodyParseError, getMediaType, isJsonMediaType } from "./pages-medi
7
7
  import { isEdgeApiRuntime } from "./edge-api-runtime.js";
8
8
  import { DEFAULT_PAGES_API_BODY_SIZE_LIMIT, resolveBodyParserConfig } from "./pages-body-parser-config.js";
9
9
  import { resolveRequestHost, resolveRequestProtocol } from "./proxy-trust.js";
10
+ import { performOnDemandRevalidate } from "./pages-revalidate.js";
10
11
  import { decode } from "node:querystring";
11
12
  import { Buffer } from "node:buffer";
12
13
  //#region src/server/api-handler.ts
@@ -198,6 +199,9 @@ function enhanceApiObjects(req, res, query, body) {
198
199
  if (typeof statusOrUrl === "string") this.writeHead(307, { Location: statusOrUrl });
199
200
  else this.writeHead(statusOrUrl, { Location: url ?? "" });
200
201
  this.end();
202
+ },
203
+ async revalidate(urlPath, opts) {
204
+ await performOnDemandRevalidate(req, urlPath, opts);
201
205
  }
202
206
  })
203
207
  };