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.
- package/dist/build/assets-ignore.d.ts +32 -0
- package/dist/build/assets-ignore.js +48 -0
- package/dist/build/client-build-config.d.ts +27 -1
- package/dist/build/client-build-config.js +58 -1
- package/dist/cli.js +2 -0
- package/dist/client/navigation-runtime.d.ts +8 -0
- package/dist/client/navigation-runtime.js +1 -1
- package/dist/client/vinext-next-data.d.ts +2 -1
- package/dist/config/config-matchers.d.ts +20 -1
- package/dist/config/config-matchers.js +35 -1
- package/dist/config/next-config.d.ts +16 -3
- package/dist/config/next-config.js +30 -2
- package/dist/deploy.js +40 -304
- package/dist/entries/app-rsc-entry.d.ts +8 -2
- package/dist/entries/app-rsc-entry.js +54 -4
- package/dist/entries/app-rsc-manifest.js +20 -2
- package/dist/entries/pages-server-entry.js +9 -1
- package/dist/index.js +162 -217
- package/dist/plugins/postcss.js +18 -14
- package/dist/plugins/require-context.d.ts +6 -0
- package/dist/plugins/require-context.js +184 -0
- package/dist/routing/app-route-graph.d.ts +12 -1
- package/dist/routing/app-route-graph.js +137 -5
- package/dist/routing/route-pattern.d.ts +2 -1
- package/dist/routing/route-pattern.js +16 -1
- package/dist/server/api-handler.js +4 -0
- package/dist/server/app-browser-entry.js +84 -39
- package/dist/server/app-browser-interception-context.d.ts +2 -1
- package/dist/server/app-browser-interception-context.js +15 -2
- package/dist/server/app-browser-navigation-controller.d.ts +11 -1
- package/dist/server/app-browser-navigation-controller.js +77 -1
- package/dist/server/app-browser-popstate.d.ts +12 -3
- package/dist/server/app-browser-popstate.js +19 -4
- package/dist/server/app-browser-state.d.ts +3 -0
- package/dist/server/app-browser-state.js +6 -3
- package/dist/server/app-browser-visible-commit.js +9 -7
- package/dist/server/app-history-state.d.ts +45 -1
- package/dist/server/app-history-state.js +109 -1
- package/dist/server/app-page-boundary-render.js +41 -19
- package/dist/server/app-page-dispatch.d.ts +6 -0
- package/dist/server/app-page-dispatch.js +3 -1
- package/dist/server/app-page-element-builder.d.ts +1 -0
- package/dist/server/app-page-element-builder.js +22 -10
- package/dist/server/app-page-render.d.ts +6 -0
- package/dist/server/app-page-render.js +5 -3
- package/dist/server/app-page-request.d.ts +8 -6
- package/dist/server/app-page-request.js +12 -9
- package/dist/server/app-page-response.d.ts +2 -2
- package/dist/server/app-page-response.js +1 -1
- package/dist/server/app-page-route-wiring.js +2 -1
- package/dist/server/app-page-stream.d.ts +37 -2
- package/dist/server/app-page-stream.js +36 -3
- package/dist/server/app-pages-bridge.d.ts +16 -0
- package/dist/server/app-pages-bridge.js +23 -3
- package/dist/server/app-route-handler-cache.d.ts +1 -0
- package/dist/server/app-route-handler-cache.js +1 -0
- package/dist/server/app-route-handler-dispatch.d.ts +1 -0
- package/dist/server/app-route-handler-dispatch.js +2 -0
- package/dist/server/app-route-handler-execution.d.ts +1 -0
- package/dist/server/app-route-handler-execution.js +1 -0
- package/dist/server/app-route-handler-runtime.d.ts +1 -0
- package/dist/server/app-route-handler-runtime.js +3 -2
- package/dist/server/app-rsc-handler.d.ts +1 -0
- package/dist/server/app-rsc-handler.js +4 -3
- package/dist/server/app-rsc-route-matching.d.ts +20 -1
- package/dist/server/app-rsc-route-matching.js +29 -4
- package/dist/server/app-server-action-execution.d.ts +11 -1
- package/dist/server/app-server-action-execution.js +68 -10
- package/dist/server/app-ssr-entry.d.ts +6 -0
- package/dist/server/app-ssr-entry.js +17 -1
- package/dist/server/dev-server.d.ts +1 -1
- package/dist/server/dev-server.js +54 -31
- package/dist/server/isr-cache.d.ts +37 -1
- package/dist/server/isr-cache.js +85 -1
- package/dist/server/navigation-planner.js +5 -3
- package/dist/server/navigation-trace.d.ts +1 -1
- package/dist/server/pages-node-compat.d.ts +2 -0
- package/dist/server/pages-node-compat.js +4 -0
- package/dist/server/pages-page-data.d.ts +10 -7
- package/dist/server/pages-page-data.js +4 -2
- package/dist/server/pages-page-handler.d.ts +9 -2
- package/dist/server/pages-page-handler.js +29 -16
- package/dist/server/pages-page-response.d.ts +11 -2
- package/dist/server/pages-page-response.js +8 -1
- package/dist/server/pages-readiness.d.ts +36 -0
- package/dist/server/pages-readiness.js +21 -0
- package/dist/server/pages-request-pipeline.d.ts +99 -0
- package/dist/server/pages-request-pipeline.js +209 -0
- package/dist/server/pages-revalidate.d.ts +15 -0
- package/dist/server/pages-revalidate.js +19 -0
- package/dist/server/prod-server.d.ts +6 -2
- package/dist/server/prod-server.js +101 -217
- package/dist/server/socket-error-backstop.d.ts +19 -1
- package/dist/server/socket-error-backstop.js +77 -4
- package/dist/shims/app-router-scroll.js +22 -4
- package/dist/shims/cache-runtime.js +31 -1
- package/dist/shims/error-boundary.d.ts +21 -11
- package/dist/shims/error-boundary.js +8 -1
- package/dist/shims/fetch-cache.d.ts +14 -1
- package/dist/shims/fetch-cache.js +18 -1
- package/dist/shims/hash-scroll.d.ts +1 -0
- package/dist/shims/hash-scroll.js +3 -1
- package/dist/shims/internal/link-status-registry.d.ts +43 -0
- package/dist/shims/internal/link-status-registry.js +42 -0
- package/dist/shims/internal/route-pattern-for-warning.d.ts +27 -0
- package/dist/shims/internal/route-pattern-for-warning.js +40 -0
- package/dist/shims/internal/utils.d.ts +1 -0
- package/dist/shims/link.js +20 -6
- package/dist/shims/navigation.d.ts +2 -2
- package/dist/shims/navigation.js +63 -7
- package/dist/shims/router-state.d.ts +1 -0
- package/dist/shims/router-state.js +2 -0
- package/dist/shims/router.d.ts +6 -3
- package/dist/shims/router.js +128 -21
- package/dist/utils/client-build-manifest.d.ts +8 -1
- package/dist/utils/client-build-manifest.js +30 -5
- package/dist/utils/client-entry-manifest.d.ts +11 -0
- package/dist/utils/client-entry-manifest.js +29 -0
- 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[];
|
|
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:
|
|
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:
|
|
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
|
};
|