@rangojs/router 0.0.0-experimental.78a48627 → 0.0.0-experimental.79
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/README.md +76 -18
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +853 -435
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +16 -17
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +55 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +37 -5
- package/src/browser/navigation-client.ts +142 -57
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +63 -22
- package/src/browser/prefetch/cache.ts +73 -11
- package/src/browser/prefetch/fetch.ts +98 -27
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +76 -9
- package/src/browser/react/NavigationProvider.tsx +16 -7
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +134 -59
- package/src/browser/scroll-restoration.ts +21 -18
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +50 -24
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +84 -230
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +46 -6
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +25 -1
- package/src/route-definition/dsl-helpers.ts +224 -37
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +82 -23
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +7 -6
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/logging.ts +1 -1
- package/src/router/manifest.ts +28 -15
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +60 -5
- package/src/router/match-result.ts +104 -10
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +4 -6
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +198 -20
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +433 -296
- package/src/router/types.ts +1 -0
- package/src/router.ts +55 -6
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +10 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +166 -17
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +185 -19
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +137 -33
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +2 -0
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -6
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +163 -211
- package/src/vite/router-discovery.ts +178 -45
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +37 -5
- package/src/vite/utils/shared-utils.ts +3 -2
package/src/build/route-trie.ts
CHANGED
|
@@ -98,8 +98,14 @@ export function buildRouteTrie(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Insert a route into the trie
|
|
102
|
-
*
|
|
101
|
+
* Insert a route into the trie. Optional params expand into two branches at
|
|
102
|
+
* registration time (skip-first, then present), so each terminal lives at the
|
|
103
|
+
* correct depth for its number of bound params and carries a branch-local
|
|
104
|
+
* `pa` listing only those names. The trie's single-slot `node.p` is reused
|
|
105
|
+
* across branches because matching ignores `node.p.n` — the leaf's `pa` is
|
|
106
|
+
* the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
|
|
107
|
+
* last-wins rule produce greedy-leftmost semantics for free at any shared
|
|
108
|
+
* terminal depth.
|
|
103
109
|
*/
|
|
104
110
|
function insertRoute(
|
|
105
111
|
node: TrieNode,
|
|
@@ -107,14 +113,13 @@ function insertRoute(
|
|
|
107
113
|
index: number,
|
|
108
114
|
leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
|
|
109
115
|
): void {
|
|
110
|
-
//
|
|
111
|
-
|
|
116
|
+
// op (full optional list) and cv (full constraint map) are route-level and
|
|
117
|
+
// identical on every terminal, so compute them once on the shared base.
|
|
112
118
|
const optionalParams: string[] = [];
|
|
113
119
|
const constraints: Record<string, string[]> = {};
|
|
114
120
|
|
|
115
121
|
for (const seg of segments) {
|
|
116
122
|
if (seg.type === "param") {
|
|
117
|
-
paramNames.push(seg.value);
|
|
118
123
|
if (seg.optional) {
|
|
119
124
|
optionalParams.push(seg.value);
|
|
120
125
|
}
|
|
@@ -124,21 +129,15 @@ function insertRoute(
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
const
|
|
132
|
+
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
128
133
|
...leaf,
|
|
129
|
-
...(paramNames.length > 0 ? { pa: paramNames } : {}),
|
|
130
134
|
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
131
135
|
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
132
136
|
};
|
|
133
137
|
|
|
134
|
-
insertSegments(node, segments, index,
|
|
138
|
+
insertSegments(node, segments, index, leafBase, []);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
/**
|
|
138
|
-
* Recursively insert segments into the trie.
|
|
139
|
-
* For optional params, we add a terminal at the current node (param absent)
|
|
140
|
-
* AND continue inserting into the param child (param present).
|
|
141
|
-
*/
|
|
142
141
|
/**
|
|
143
142
|
* Extract ancestry map from a built trie by visiting all leaf nodes.
|
|
144
143
|
* Returns { routeName: ancestryShortCodes[] } for every route in the trie.
|
|
@@ -218,15 +217,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
|
|
|
218
217
|
node.r = mergeLeaves(node.r, leaf);
|
|
219
218
|
}
|
|
220
219
|
|
|
220
|
+
function buildLeaf(
|
|
221
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
222
|
+
paramNames: string[],
|
|
223
|
+
): TrieLeaf {
|
|
224
|
+
return paramNames.length > 0
|
|
225
|
+
? { ...leafBase, pa: [...paramNames] }
|
|
226
|
+
: { ...leafBase };
|
|
227
|
+
}
|
|
228
|
+
|
|
221
229
|
function insertSegments(
|
|
222
230
|
node: TrieNode,
|
|
223
231
|
segments: ParsedSegment[],
|
|
224
232
|
index: number,
|
|
225
|
-
|
|
233
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
234
|
+
paramNames: string[],
|
|
226
235
|
): void {
|
|
227
|
-
// Base case: all segments consumed, add terminal
|
|
236
|
+
// Base case: all segments consumed, add terminal with branch-local pa
|
|
228
237
|
if (index >= segments.length) {
|
|
229
|
-
mergeLeaf(node,
|
|
238
|
+
mergeLeaf(node, buildLeaf(leafBase, paramNames));
|
|
230
239
|
return;
|
|
231
240
|
}
|
|
232
241
|
|
|
@@ -235,12 +244,19 @@ function insertSegments(
|
|
|
235
244
|
if (segment.type === "static") {
|
|
236
245
|
if (!node.s) node.s = {};
|
|
237
246
|
if (!node.s[segment.value]) node.s[segment.value] = {};
|
|
238
|
-
insertSegments(
|
|
247
|
+
insertSegments(
|
|
248
|
+
node.s[segment.value],
|
|
249
|
+
segments,
|
|
250
|
+
index + 1,
|
|
251
|
+
leafBase,
|
|
252
|
+
paramNames,
|
|
253
|
+
);
|
|
239
254
|
} else if (segment.type === "param") {
|
|
240
255
|
if (segment.optional) {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
//
|
|
256
|
+
// SKIP first: continue at the same node without binding this name.
|
|
257
|
+
// Skip-first ordering means the present-branch's TAKE overwrites any
|
|
258
|
+
// shared terminal later, giving greedy-leftmost semantics.
|
|
259
|
+
insertSegments(node, segments, index + 1, leafBase, paramNames);
|
|
244
260
|
}
|
|
245
261
|
if (segment.suffix) {
|
|
246
262
|
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
@@ -248,16 +264,26 @@ function insertSegments(
|
|
|
248
264
|
if (!node.xp[segment.suffix]) {
|
|
249
265
|
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
266
|
}
|
|
251
|
-
insertSegments(node.xp[segment.suffix].c, segments, index + 1,
|
|
267
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
|
|
268
|
+
...paramNames,
|
|
269
|
+
segment.value,
|
|
270
|
+
]);
|
|
252
271
|
} else {
|
|
253
272
|
if (!node.p) {
|
|
254
273
|
node.p = { n: segment.value, c: {} };
|
|
255
274
|
}
|
|
256
|
-
insertSegments(node.p.c, segments, index + 1,
|
|
275
|
+
insertSegments(node.p.c, segments, index + 1, leafBase, [
|
|
276
|
+
...paramNames,
|
|
277
|
+
segment.value,
|
|
278
|
+
]);
|
|
257
279
|
}
|
|
258
280
|
} else if (segment.type === "wildcard") {
|
|
259
|
-
// Wildcard consumes all remaining segments
|
|
260
|
-
|
|
281
|
+
// Wildcard consumes all remaining segments. Carry any params bound before
|
|
282
|
+
// the wildcard in pa so they zip correctly against paramValues at match.
|
|
283
|
+
const wildLeaf: TrieLeaf & { pn: string } = {
|
|
284
|
+
...buildLeaf(leafBase, paramNames),
|
|
285
|
+
pn: "*",
|
|
286
|
+
};
|
|
261
287
|
const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
|
|
262
288
|
const merged = mergeLeaves(existing, wildLeaf);
|
|
263
289
|
node.w = merged as TrieLeaf & { pn: string };
|
|
@@ -357,12 +357,17 @@ function buildRouteMapFromBlock(
|
|
|
357
357
|
/**
|
|
358
358
|
* Build route map and search schemas together.
|
|
359
359
|
* Internal helper used by the include resolution path.
|
|
360
|
+
*
|
|
361
|
+
* @param inlineBlock - Optional pre-extracted code block (e.g. from an inline
|
|
362
|
+
* builder function). When provided, variableName is ignored and the block
|
|
363
|
+
* is parsed directly for path()/include() calls.
|
|
360
364
|
*/
|
|
361
365
|
export function buildCombinedRouteMapWithSearch(
|
|
362
366
|
filePath: string,
|
|
363
367
|
variableName?: string,
|
|
364
368
|
visited?: Set<string>,
|
|
365
369
|
diagnosticsOut?: UnresolvableInclude[],
|
|
370
|
+
inlineBlock?: string,
|
|
366
371
|
): {
|
|
367
372
|
routes: Record<string, string>;
|
|
368
373
|
searchSchemas: Record<string, Record<string, string>>;
|
|
@@ -384,7 +389,9 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
384
389
|
}
|
|
385
390
|
|
|
386
391
|
let block: string;
|
|
387
|
-
if (
|
|
392
|
+
if (inlineBlock) {
|
|
393
|
+
block = inlineBlock;
|
|
394
|
+
} else if (variableName) {
|
|
388
395
|
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
389
396
|
if (!extracted) return { routes: {}, searchSchemas: {} };
|
|
390
397
|
block = extracted;
|
|
@@ -45,7 +45,9 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
45
45
|
name.endsWith(".tsx") ||
|
|
46
46
|
name.endsWith(".js") ||
|
|
47
47
|
name.endsWith(".jsx")) &&
|
|
48
|
-
!name.includes(".gen.")
|
|
48
|
+
!name.includes(".gen.") &&
|
|
49
|
+
!name.includes(".test.") &&
|
|
50
|
+
!name.includes(".spec.")
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -70,7 +72,15 @@ function findRouterFilesRecursive(
|
|
|
70
72
|
for (const entry of entries) {
|
|
71
73
|
const fullPath = join(dir, entry.name);
|
|
72
74
|
if (entry.isDirectory()) {
|
|
73
|
-
if (
|
|
75
|
+
if (
|
|
76
|
+
entry.name === "node_modules" ||
|
|
77
|
+
entry.name === "dist" ||
|
|
78
|
+
entry.name === "coverage" ||
|
|
79
|
+
entry.name === "__tests__" ||
|
|
80
|
+
entry.name === "__mocks__" ||
|
|
81
|
+
entry.name.startsWith(".")
|
|
82
|
+
)
|
|
83
|
+
continue;
|
|
74
84
|
childDirs.push(fullPath);
|
|
75
85
|
continue;
|
|
76
86
|
}
|
|
@@ -147,13 +157,26 @@ export function formatNestedRouterConflictError(
|
|
|
147
157
|
// ---------------------------------------------------------------------------
|
|
148
158
|
|
|
149
159
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
160
|
+
* Result of extracting URL patterns from a router file.
|
|
161
|
+
* - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
|
|
162
|
+
* - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
|
|
163
|
+
*/
|
|
164
|
+
export type UrlsExtractionResult =
|
|
165
|
+
| { kind: "variable"; name: string }
|
|
166
|
+
| { kind: "inline"; block: string };
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract the url patterns from a router file using AST.
|
|
170
|
+
* Detects four patterns:
|
|
152
171
|
* 1. createRouter(...).routes(variableName)
|
|
153
172
|
* 2. createRouter({ urls: variableName, ... })
|
|
154
|
-
*
|
|
173
|
+
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
174
|
+
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
175
|
+
* Returns either a variable name or an inline code block.
|
|
155
176
|
*/
|
|
156
|
-
export function
|
|
177
|
+
export function extractUrlsFromRouter(
|
|
178
|
+
code: string,
|
|
179
|
+
): UrlsExtractionResult | null {
|
|
157
180
|
const sourceFile = ts.createSourceFile(
|
|
158
181
|
"router.tsx",
|
|
159
182
|
code,
|
|
@@ -161,7 +184,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
161
184
|
true,
|
|
162
185
|
ts.ScriptKind.TSX,
|
|
163
186
|
);
|
|
164
|
-
let result:
|
|
187
|
+
let result: UrlsExtractionResult | null = null;
|
|
165
188
|
|
|
166
189
|
function isCreateRouterCall(node: ts.Node): boolean {
|
|
167
190
|
if (!ts.isCallExpression(node)) return false;
|
|
@@ -169,44 +192,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
169
192
|
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
170
193
|
}
|
|
171
194
|
|
|
195
|
+
/** Check if a node is an arrow/function expression (inline builder). */
|
|
196
|
+
function isInlineBuilder(node: ts.Node): boolean {
|
|
197
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Check if a .routes() call chains from createRouter(). */
|
|
201
|
+
function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
|
|
202
|
+
if (
|
|
203
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
204
|
+
node.expression.name.text !== "routes"
|
|
205
|
+
)
|
|
206
|
+
return false;
|
|
207
|
+
let inner: ts.Expression = node.expression.expression;
|
|
208
|
+
while (
|
|
209
|
+
ts.isCallExpression(inner) &&
|
|
210
|
+
ts.isPropertyAccessExpression(inner.expression)
|
|
211
|
+
) {
|
|
212
|
+
inner = inner.expression.expression;
|
|
213
|
+
}
|
|
214
|
+
return isCreateRouterCall(inner);
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
function visit(node: ts.Node) {
|
|
173
218
|
if (result) return;
|
|
174
219
|
|
|
175
|
-
// Pattern 1: createRouter(...).routes(variableName)
|
|
176
|
-
// The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
|
|
220
|
+
// Pattern 1 & 3: createRouter(...).routes(variableName | builder)
|
|
177
221
|
if (
|
|
178
222
|
ts.isCallExpression(node) &&
|
|
179
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
180
|
-
node.expression.name.text === "routes" &&
|
|
181
223
|
node.arguments.length >= 1 &&
|
|
182
|
-
|
|
224
|
+
isRoutesOnCreateRouter(node)
|
|
183
225
|
) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
ts.isPropertyAccessExpression(inner.expression)
|
|
190
|
-
) {
|
|
191
|
-
inner = inner.expression.expression;
|
|
192
|
-
}
|
|
193
|
-
if (isCreateRouterCall(inner)) {
|
|
194
|
-
result = (node.arguments[0] as ts.Identifier).text;
|
|
195
|
-
return;
|
|
226
|
+
const arg = node.arguments[0];
|
|
227
|
+
if (ts.isIdentifier(arg)) {
|
|
228
|
+
result = { kind: "variable", name: arg.text };
|
|
229
|
+
} else if (isInlineBuilder(arg)) {
|
|
230
|
+
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
196
231
|
}
|
|
232
|
+
return;
|
|
197
233
|
}
|
|
198
234
|
|
|
199
|
-
// Pattern 2: createRouter({ urls: variableName, ... })
|
|
235
|
+
// Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
|
|
200
236
|
if (isCreateRouterCall(node)) {
|
|
201
237
|
const callExpr = node as ts.CallExpression;
|
|
202
|
-
for (const
|
|
238
|
+
for (const callArg of callExpr.arguments) {
|
|
239
|
+
if (ts.isObjectLiteralExpression(callArg)) {
|
|
240
|
+
for (const prop of callArg.properties) {
|
|
241
|
+
if (
|
|
242
|
+
ts.isPropertyAssignment(prop) &&
|
|
243
|
+
ts.isIdentifier(prop.name) &&
|
|
244
|
+
prop.name.text === "urls"
|
|
245
|
+
) {
|
|
246
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
247
|
+
result = { kind: "variable", name: prop.initializer.text };
|
|
248
|
+
} else if (isInlineBuilder(prop.initializer)) {
|
|
249
|
+
result = {
|
|
250
|
+
kind: "inline",
|
|
251
|
+
block: prop.initializer.getText(sourceFile),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ts.forEachChild(node, visit);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
visit(sourceFile);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract the `basename` string literal from createRouter({ basename: "..." }).
|
|
270
|
+
* Returns the basename value or undefined if not present.
|
|
271
|
+
*/
|
|
272
|
+
export function extractBasenameFromRouter(code: string): string | undefined {
|
|
273
|
+
const sourceFile = ts.createSourceFile(
|
|
274
|
+
"router.tsx",
|
|
275
|
+
code,
|
|
276
|
+
ts.ScriptTarget.Latest,
|
|
277
|
+
true,
|
|
278
|
+
ts.ScriptKind.TSX,
|
|
279
|
+
);
|
|
280
|
+
let result: string | undefined;
|
|
281
|
+
|
|
282
|
+
function visit(node: ts.Node) {
|
|
283
|
+
if (result !== undefined) return;
|
|
284
|
+
if (
|
|
285
|
+
ts.isCallExpression(node) &&
|
|
286
|
+
ts.isIdentifier(node.expression) &&
|
|
287
|
+
node.expression.text === "createRouter"
|
|
288
|
+
) {
|
|
289
|
+
for (const arg of node.arguments) {
|
|
203
290
|
if (ts.isObjectLiteralExpression(arg)) {
|
|
204
291
|
for (const prop of arg.properties) {
|
|
205
292
|
if (
|
|
206
293
|
ts.isPropertyAssignment(prop) &&
|
|
207
294
|
ts.isIdentifier(prop.name) &&
|
|
208
|
-
prop.name.text === "
|
|
209
|
-
ts.
|
|
295
|
+
prop.name.text === "basename" &&
|
|
296
|
+
ts.isStringLiteral(prop.initializer)
|
|
210
297
|
) {
|
|
211
298
|
result = prop.initializer.text;
|
|
212
299
|
return;
|
|
@@ -215,7 +302,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
215
302
|
}
|
|
216
303
|
}
|
|
217
304
|
}
|
|
218
|
-
|
|
219
305
|
ts.forEachChild(node, visit);
|
|
220
306
|
}
|
|
221
307
|
|
|
@@ -223,9 +309,40 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
|
223
309
|
return result;
|
|
224
310
|
}
|
|
225
311
|
|
|
312
|
+
/** @deprecated Use extractUrlsFromRouter instead */
|
|
313
|
+
export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
314
|
+
const result = extractUrlsFromRouter(code);
|
|
315
|
+
return result?.kind === "variable" ? result.name : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Apply a basename prefix to all route patterns in a result set. */
|
|
319
|
+
function applyBasenameToRoutes(
|
|
320
|
+
result: {
|
|
321
|
+
routes: Record<string, string>;
|
|
322
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
323
|
+
},
|
|
324
|
+
basename: string,
|
|
325
|
+
): {
|
|
326
|
+
routes: Record<string, string>;
|
|
327
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
328
|
+
} {
|
|
329
|
+
const prefixed: Record<string, string> = {};
|
|
330
|
+
for (const [name, pattern] of Object.entries(result.routes)) {
|
|
331
|
+
if (pattern === "/") {
|
|
332
|
+
prefixed[name] = basename;
|
|
333
|
+
} else if (basename.endsWith("/") && pattern.startsWith("/")) {
|
|
334
|
+
prefixed[name] = basename + pattern.slice(1);
|
|
335
|
+
} else {
|
|
336
|
+
prefixed[name] = basename + pattern;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
340
|
+
}
|
|
341
|
+
|
|
226
342
|
/**
|
|
227
343
|
* Resolve routes and search schemas from a router source file by following the
|
|
228
|
-
* variable passed to `.routes(...)` or `urls: ...` in createRouter options
|
|
344
|
+
* variable passed to `.routes(...)` or `urls: ...` in createRouter options,
|
|
345
|
+
* or by parsing an inline builder function directly.
|
|
229
346
|
*/
|
|
230
347
|
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
231
348
|
routes: Record<string, string>;
|
|
@@ -238,21 +355,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
238
355
|
return { routes: {}, searchSchemas: {} };
|
|
239
356
|
}
|
|
240
357
|
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
358
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
359
|
+
if (!extraction) {
|
|
243
360
|
return { routes: {}, searchSchemas: {} };
|
|
244
361
|
}
|
|
245
362
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
363
|
+
// Detect basename from createRouter({ basename: "..." })
|
|
364
|
+
const rawBasename = extractBasenameFromRouter(routerSource);
|
|
365
|
+
const basename = rawBasename
|
|
366
|
+
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
367
|
+
: undefined;
|
|
368
|
+
|
|
369
|
+
let result: {
|
|
370
|
+
routes: Record<string, string>;
|
|
371
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Inline builder: extract routes directly from the function body
|
|
375
|
+
if (extraction.kind === "inline") {
|
|
376
|
+
result = buildCombinedRouteMapWithSearch(
|
|
377
|
+
routerFilePath,
|
|
378
|
+
undefined,
|
|
379
|
+
undefined,
|
|
380
|
+
undefined,
|
|
381
|
+
extraction.block,
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
// Variable reference: follow imports or same-file declaration
|
|
385
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
386
|
+
if (imported) {
|
|
387
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
388
|
+
if (!targetFile) {
|
|
389
|
+
return { routes: {}, searchSchemas: {} };
|
|
390
|
+
}
|
|
391
|
+
result = buildCombinedRouteMapWithSearch(
|
|
392
|
+
targetFile,
|
|
393
|
+
imported.exportedName,
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
251
397
|
}
|
|
252
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
253
398
|
}
|
|
254
399
|
|
|
255
|
-
|
|
400
|
+
// Apply basename prefix to all extracted route patterns
|
|
401
|
+
if (basename) {
|
|
402
|
+
result = applyBasenameToRoutes(result, basename);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
256
406
|
}
|
|
257
407
|
|
|
258
408
|
// ---------------------------------------------------------------------------
|
|
@@ -275,12 +425,26 @@ export function detectUnresolvableIncludes(
|
|
|
275
425
|
return [];
|
|
276
426
|
}
|
|
277
427
|
|
|
278
|
-
// Extract the urls
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
428
|
+
// Extract the urls source from the router file
|
|
429
|
+
const extraction = extractUrlsFromRouter(source);
|
|
430
|
+
if (!extraction) return [];
|
|
431
|
+
|
|
432
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
433
|
+
|
|
434
|
+
if (extraction.kind === "inline") {
|
|
435
|
+
// Inline builder: parse directly
|
|
436
|
+
buildCombinedRouteMapWithSearch(
|
|
437
|
+
realPath,
|
|
438
|
+
undefined,
|
|
439
|
+
new Set(),
|
|
440
|
+
diagnostics,
|
|
441
|
+
extraction.block,
|
|
442
|
+
);
|
|
443
|
+
return diagnostics;
|
|
444
|
+
}
|
|
281
445
|
|
|
282
|
-
//
|
|
283
|
-
const imported = resolveImportedVariable(source,
|
|
446
|
+
// Variable reference: resolve where it comes from
|
|
447
|
+
const imported = resolveImportedVariable(source, extraction.name);
|
|
284
448
|
let targetFile: string;
|
|
285
449
|
let exportedName: string | undefined;
|
|
286
450
|
|
|
@@ -302,10 +466,9 @@ export function detectUnresolvableIncludes(
|
|
|
302
466
|
} else {
|
|
303
467
|
// Same-file urls() definition
|
|
304
468
|
targetFile = realPath;
|
|
305
|
-
exportedName =
|
|
469
|
+
exportedName = extraction.name;
|
|
306
470
|
}
|
|
307
471
|
|
|
308
|
-
const diagnostics: UnresolvableInclude[] = [];
|
|
309
472
|
buildCombinedRouteMapWithSearch(
|
|
310
473
|
targetFile,
|
|
311
474
|
exportedName,
|
|
@@ -387,34 +550,20 @@ export function writeCombinedRouteTypes(
|
|
|
387
550
|
}
|
|
388
551
|
|
|
389
552
|
for (const routerFilePath of routerFilePaths) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
407
|
-
if (imported) {
|
|
408
|
-
// Variable is imported from another module
|
|
409
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
410
|
-
if (!targetFile) continue;
|
|
411
|
-
result = buildCombinedRouteMapWithSearch(
|
|
412
|
-
targetFile,
|
|
413
|
-
imported.exportedName,
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
// Variable is defined in the same file
|
|
417
|
-
result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
553
|
+
const result = buildCombinedRouteMapForRouterFile(routerFilePath);
|
|
554
|
+
if (
|
|
555
|
+
Object.keys(result.routes).length === 0 &&
|
|
556
|
+
Object.keys(result.searchSchemas).length === 0
|
|
557
|
+
) {
|
|
558
|
+
// Check if the file even has a createRouter call — if not, skip entirely.
|
|
559
|
+
// If it does, fall through to write an empty placeholder below.
|
|
560
|
+
let routerSource: string;
|
|
561
|
+
try {
|
|
562
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
418
567
|
}
|
|
419
568
|
|
|
420
569
|
const routerBasename = pathBasename(routerFilePath).replace(
|
|
@@ -61,7 +61,14 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
61
61
|
for (const entry of entries) {
|
|
62
62
|
const fullPath = join(dir, entry.name);
|
|
63
63
|
if (entry.isDirectory()) {
|
|
64
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
entry.name === "node_modules" ||
|
|
66
|
+
entry.name.startsWith(".") ||
|
|
67
|
+
entry.name === "dist" ||
|
|
68
|
+
entry.name === "build" ||
|
|
69
|
+
entry.name === "coverage"
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
65
72
|
results.push(...findTsFiles(fullPath, filter));
|
|
66
73
|
} else if (
|
|
67
74
|
(entry.name.endsWith(".ts") ||
|
|
@@ -214,11 +214,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
214
214
|
bgStopCapture = c.stop;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// Stamp tainted
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
217
|
+
// Stamp tainted ARGS only — not requestCtx. The args stamp guards
|
|
218
|
+
// direct ctx method calls (ctx.set, ctx.header, ctx.onResponse, etc.)
|
|
219
|
+
// which is sufficient for correctness.
|
|
220
|
+
//
|
|
221
|
+
// We intentionally skip stamping requestCtx here because:
|
|
222
|
+
// 1. runBackground starts the async task synchronously (before the
|
|
223
|
+
// first await), so stampCacheExec would pollute the shared
|
|
224
|
+
// requestCtx while the foreground pipeline is still running.
|
|
225
|
+
// This causes assertNotInsideCacheExec to fire when cache-store
|
|
226
|
+
// later calls requestCtx.onResponse().
|
|
227
|
+
// 2. requestCtx methods are closure-bound to the original ctx, so
|
|
228
|
+
// neither Object.create() nor a proxy can isolate the stamp.
|
|
229
|
+
// 3. The foreground miss path already stamps requestCtx and catches
|
|
230
|
+
// cookies()/headers() misuse on first execution. The background
|
|
231
|
+
// re-runs the same function with the same request.
|
|
222
232
|
const bgTaintedArgs: unknown[] = [];
|
|
223
233
|
for (const arg of args) {
|
|
224
234
|
if (isTainted(arg)) {
|
|
@@ -226,9 +236,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
226
236
|
bgTaintedArgs.push(arg);
|
|
227
237
|
}
|
|
228
238
|
}
|
|
229
|
-
if (requestCtx) {
|
|
230
|
-
stampCacheExec(requestCtx as object);
|
|
231
|
-
}
|
|
232
239
|
|
|
233
240
|
try {
|
|
234
241
|
const freshResult = await fn.apply(this, args);
|
|
@@ -249,9 +256,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
249
256
|
for (const arg of bgTaintedArgs) {
|
|
250
257
|
unstampCacheExec(arg as object);
|
|
251
258
|
}
|
|
252
|
-
if (requestCtx) {
|
|
253
|
-
unstampCacheExec(requestCtx as object);
|
|
254
|
-
}
|
|
255
259
|
// Restore original handle store
|
|
256
260
|
if (originalHandleStore && requestCtx) {
|
|
257
261
|
requestCtx._handleStore = originalHandleStore;
|