@rangojs/router 0.5.2 → 0.7.0
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/bin/rango.js +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +147 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +13 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +6159 -2959
- package/package.json +6 -5
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +776 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +201 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +55 -2
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +34 -23
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +23 -2
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +17 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -77,6 +77,14 @@ function readSourceMemoized(
|
|
|
77
77
|
return source;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function isUrlsDefinitionCall(node: ts.CallExpression): boolean {
|
|
81
|
+
const callee = node.expression;
|
|
82
|
+
return (
|
|
83
|
+
ts.isIdentifier(callee) &&
|
|
84
|
+
(callee.text === "urls" || callee.text === "clientUrls")
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
// ---------------------------------------------------------------------------
|
|
81
89
|
// AST-based include() parsing
|
|
82
90
|
// ---------------------------------------------------------------------------
|
|
@@ -206,10 +214,9 @@ function extractDynamicImportSpecifier(node: ts.Expression): string | null {
|
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
/**
|
|
209
|
-
* Resolve a module's `export default` to either a same-file
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* (`() => import("./mod")`), whose convention is `export default urls(...)`.
|
|
217
|
+
* Resolve a module's `export default` to either a same-file route variable name
|
|
218
|
+
* (`export default shopPatterns`) or an inline urls()/clientUrls() call block.
|
|
219
|
+
* Used for default imports and to walk async include modules.
|
|
213
220
|
*/
|
|
214
221
|
function resolveDefaultExportTarget(
|
|
215
222
|
code: string,
|
|
@@ -223,11 +230,8 @@ function resolveDefaultExportTarget(
|
|
|
223
230
|
const expr = node.expression;
|
|
224
231
|
if (ts.isIdentifier(expr)) {
|
|
225
232
|
result = { variableName: expr.text };
|
|
226
|
-
} else if (ts.isCallExpression(expr)) {
|
|
227
|
-
|
|
228
|
-
if (ts.isIdentifier(callee) && callee.text === "urls") {
|
|
229
|
-
result = { inlineBlock: expr.getText(sourceFile) };
|
|
230
|
-
}
|
|
233
|
+
} else if (ts.isCallExpression(expr) && isUrlsDefinitionCall(expr)) {
|
|
234
|
+
result = { inlineBlock: expr.getText(sourceFile) };
|
|
231
235
|
}
|
|
232
236
|
return;
|
|
233
237
|
}
|
|
@@ -358,6 +362,16 @@ export function resolveImportedVariable(
|
|
|
358
362
|
code: string,
|
|
359
363
|
localName: string,
|
|
360
364
|
): { specifier: string; exportedName: string } | null {
|
|
365
|
+
const defaultImportRegex =
|
|
366
|
+
/import\s+([\w$]+)\s*(?:,\s*\{[^}]*\})?\s+from\s*["']([^"']+)["']/g;
|
|
367
|
+
let defaultMatch;
|
|
368
|
+
|
|
369
|
+
while ((defaultMatch = defaultImportRegex.exec(code)) !== null) {
|
|
370
|
+
if (defaultMatch[1] === localName) {
|
|
371
|
+
return { specifier: defaultMatch[2], exportedName: "default" };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
361
375
|
// Allow an optional leading default binding before the named-import brace so
|
|
362
376
|
// a combined `import Foo, { bar } from "..."` is matched (the named members
|
|
363
377
|
// are the only part we resolve; the default binding is skipped). Without the
|
|
@@ -453,8 +467,7 @@ function extractUrlsBlockForVariable(
|
|
|
453
467
|
node.initializer &&
|
|
454
468
|
ts.isCallExpression(node.initializer)
|
|
455
469
|
) {
|
|
456
|
-
|
|
457
|
-
if (ts.isIdentifier(callee) && callee.text === "urls") {
|
|
470
|
+
if (isUrlsDefinitionCall(node.initializer)) {
|
|
458
471
|
result = node.initializer.getText(sourceFile);
|
|
459
472
|
return;
|
|
460
473
|
}
|
|
@@ -780,6 +793,48 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
780
793
|
let block: string;
|
|
781
794
|
if (inlineBlock) {
|
|
782
795
|
block = inlineBlock;
|
|
796
|
+
} else if (variableName === "default") {
|
|
797
|
+
const defaultTarget = resolveDefaultExportTarget(
|
|
798
|
+
source,
|
|
799
|
+
parseBlock(memo, source),
|
|
800
|
+
);
|
|
801
|
+
if (!defaultTarget) {
|
|
802
|
+
visited.delete(key);
|
|
803
|
+
return { routes: {}, searchSchemas: {} };
|
|
804
|
+
}
|
|
805
|
+
if (defaultTarget.inlineBlock) {
|
|
806
|
+
block = defaultTarget.inlineBlock;
|
|
807
|
+
} else {
|
|
808
|
+
const targetName = defaultTarget.variableName!;
|
|
809
|
+
const imported = resolveImportedVariable(source, targetName);
|
|
810
|
+
if (imported) {
|
|
811
|
+
const targetFile = resolveImportPath(imported.specifier, realPath);
|
|
812
|
+
if (!targetFile) {
|
|
813
|
+
visited.delete(key);
|
|
814
|
+
return { routes: {}, searchSchemas: {} };
|
|
815
|
+
}
|
|
816
|
+
const result = buildCombinedRouteMapWithSearch(
|
|
817
|
+
targetFile,
|
|
818
|
+
imported.exportedName,
|
|
819
|
+
visited,
|
|
820
|
+
diagnosticsOut,
|
|
821
|
+
undefined,
|
|
822
|
+
memo,
|
|
823
|
+
);
|
|
824
|
+
visited.delete(key);
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
const extracted = extractUrlsBlockForVariable(
|
|
828
|
+
source,
|
|
829
|
+
targetName,
|
|
830
|
+
parseBlock(memo, source),
|
|
831
|
+
);
|
|
832
|
+
if (!extracted) {
|
|
833
|
+
visited.delete(key);
|
|
834
|
+
return { routes: {}, searchSchemas: {} };
|
|
835
|
+
}
|
|
836
|
+
block = extracted;
|
|
837
|
+
}
|
|
783
838
|
} else if (variableName) {
|
|
784
839
|
const extracted = extractUrlsBlockForVariable(
|
|
785
840
|
source,
|
|
@@ -12,8 +12,9 @@ import { findTsFiles } from "./scan-filter.js";
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Generate per-module route type files by statically parsing url module source.
|
|
15
|
-
* Scans for files containing `urls(` and writes a sibling
|
|
16
|
-
* extracted route name/pattern pairs. Only writes when
|
|
15
|
+
* Scans for files containing `urls(` or `clientUrls(` and writes a sibling
|
|
16
|
+
* `.gen.ts` with the extracted route name/pattern pairs. Only writes when
|
|
17
|
+
* content has changed.
|
|
17
18
|
*/
|
|
18
19
|
export function writePerModuleRouteTypes(
|
|
19
20
|
root: string,
|
|
@@ -26,7 +27,7 @@ export function writePerModuleRouteTypes(
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Find all variable names assigned to urls() calls in source
|
|
30
|
+
* Find all variable names assigned to urls() or clientUrls() calls in source.
|
|
30
31
|
* e.g. `export const patterns = urls(...)` -> ["patterns"]
|
|
31
32
|
*/
|
|
32
33
|
export function findUrlsVariableNames(code: string): string[] {
|
|
@@ -47,7 +48,10 @@ export function findUrlsVariableNames(code: string): string[] {
|
|
|
47
48
|
ts.isCallExpression(node.initializer)
|
|
48
49
|
) {
|
|
49
50
|
const callee = node.initializer.expression;
|
|
50
|
-
if (
|
|
51
|
+
if (
|
|
52
|
+
ts.isIdentifier(callee) &&
|
|
53
|
+
(callee.text === "urls" || callee.text === "clientUrls")
|
|
54
|
+
) {
|
|
51
55
|
names.push(node.name.text);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -61,12 +65,13 @@ export function findUrlsVariableNames(code: string): string[] {
|
|
|
61
65
|
/**
|
|
62
66
|
* Generate per-module route types for a single url module file.
|
|
63
67
|
* Follows include() calls recursively to produce the full route tree.
|
|
64
|
-
* No-ops if the file doesn't contain `urls(` or has no named
|
|
68
|
+
* No-ops if the file doesn't contain `urls(`/`clientUrls(` or has no named
|
|
69
|
+
* routes.
|
|
65
70
|
*/
|
|
66
71
|
export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
67
72
|
try {
|
|
68
73
|
const source = readFileSync(filePath, "utf-8");
|
|
69
|
-
if (!source.includes("urls(")) return;
|
|
74
|
+
if (!source.includes("urls(") && !source.includes("clientUrls(")) return;
|
|
70
75
|
|
|
71
76
|
const varNames = findUrlsVariableNames(source);
|
|
72
77
|
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
resolveImportedVariable,
|
|
21
21
|
resolveImportPath,
|
|
22
22
|
buildCombinedRouteMapWithSearch,
|
|
23
|
+
createScanMemo,
|
|
24
|
+
type ScanMemo,
|
|
23
25
|
type UnresolvableInclude,
|
|
24
26
|
} from "./include-resolution.js";
|
|
25
27
|
import { findUrlsVariableNames } from "./per-module-writer.js";
|
|
@@ -197,18 +199,43 @@ export type UrlsExtractionResult =
|
|
|
197
199
|
| { kind: "variable"; name: string }
|
|
198
200
|
| { kind: "inline"; block: string };
|
|
199
201
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
export function extractUrlsFromRouter(
|
|
210
|
-
code: string,
|
|
202
|
+
function isCreateRouterCall(node: ts.Node): boolean {
|
|
203
|
+
if (!ts.isCallExpression(node)) return false;
|
|
204
|
+
const callee = node.expression;
|
|
205
|
+
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractUrlsArgument(
|
|
209
|
+
node: ts.Expression,
|
|
210
|
+
sourceFile: ts.SourceFile,
|
|
211
211
|
): UrlsExtractionResult | null {
|
|
212
|
+
if (ts.isIdentifier(node)) {
|
|
213
|
+
return { kind: "variable", name: node.text };
|
|
214
|
+
}
|
|
215
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
216
|
+
return { kind: "inline", block: node.getText(sourceFile) };
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Return a createRouter call chain in runtime registration order. */
|
|
222
|
+
function collectCreateRouterCallChain(
|
|
223
|
+
endpoint: ts.CallExpression,
|
|
224
|
+
): ts.CallExpression[] | null {
|
|
225
|
+
const calls: ts.CallExpression[] = [];
|
|
226
|
+
let current = endpoint;
|
|
227
|
+
|
|
228
|
+
while (true) {
|
|
229
|
+
calls.push(current);
|
|
230
|
+
if (isCreateRouterCall(current)) return calls.reverse();
|
|
231
|
+
if (!ts.isPropertyAccessExpression(current.expression)) return null;
|
|
232
|
+
const receiver = current.expression.expression;
|
|
233
|
+
if (!ts.isCallExpression(receiver)) return null;
|
|
234
|
+
current = receiver;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractAllUrlsFromRouter(code: string): UrlsExtractionResult[] {
|
|
212
239
|
const sourceFile = ts.createSourceFile(
|
|
213
240
|
"router.tsx",
|
|
214
241
|
code,
|
|
@@ -216,85 +243,71 @@ export function extractUrlsFromRouter(
|
|
|
216
243
|
true,
|
|
217
244
|
ts.ScriptKind.TSX,
|
|
218
245
|
);
|
|
219
|
-
let result: UrlsExtractionResult | null = null;
|
|
220
|
-
|
|
221
|
-
function isCreateRouterCall(node: ts.Node): boolean {
|
|
222
|
-
if (!ts.isCallExpression(node)) return false;
|
|
223
|
-
const callee = node.expression;
|
|
224
|
-
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Check if a node is an arrow/function expression (inline builder). */
|
|
228
|
-
function isInlineBuilder(node: ts.Node): boolean {
|
|
229
|
-
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Check if a .routes() call chains from createRouter(). */
|
|
233
|
-
function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
|
|
234
|
-
if (
|
|
235
|
-
!ts.isPropertyAccessExpression(node.expression) ||
|
|
236
|
-
node.expression.name.text !== "routes"
|
|
237
|
-
)
|
|
238
|
-
return false;
|
|
239
|
-
let inner: ts.Expression = node.expression.expression;
|
|
240
|
-
while (
|
|
241
|
-
ts.isCallExpression(inner) &&
|
|
242
|
-
ts.isPropertyAccessExpression(inner.expression)
|
|
243
|
-
) {
|
|
244
|
-
inner = inner.expression.expression;
|
|
245
|
-
}
|
|
246
|
-
return isCreateRouterCall(inner);
|
|
247
|
-
}
|
|
246
|
+
let result: UrlsExtractionResult[] | null = null;
|
|
248
247
|
|
|
249
248
|
function visit(node: ts.Node) {
|
|
250
249
|
if (result) return;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (ts.isIdentifier(arg)) {
|
|
260
|
-
result = { kind: "variable", name: arg.text };
|
|
261
|
-
} else if (isInlineBuilder(arg)) {
|
|
262
|
-
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
263
|
-
}
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
|
|
268
|
-
if (isCreateRouterCall(node)) {
|
|
269
|
-
const callExpr = node as ts.CallExpression;
|
|
270
|
-
for (const callArg of callExpr.arguments) {
|
|
271
|
-
if (ts.isObjectLiteralExpression(callArg)) {
|
|
250
|
+
if (ts.isCallExpression(node)) {
|
|
251
|
+
const chain = collectCreateRouterCallChain(node);
|
|
252
|
+
if (chain) {
|
|
253
|
+
const extracted: UrlsExtractionResult[] = [];
|
|
254
|
+
const createRouterCall = chain[0];
|
|
255
|
+
|
|
256
|
+
for (const callArg of createRouterCall.arguments) {
|
|
257
|
+
if (!ts.isObjectLiteralExpression(callArg)) continue;
|
|
272
258
|
for (const prop of callArg.properties) {
|
|
273
259
|
if (
|
|
274
|
-
ts.isPropertyAssignment(prop)
|
|
275
|
-
ts.isIdentifier(prop.name)
|
|
276
|
-
prop.name.text
|
|
260
|
+
!ts.isPropertyAssignment(prop) ||
|
|
261
|
+
!ts.isIdentifier(prop.name) ||
|
|
262
|
+
prop.name.text !== "urls"
|
|
277
263
|
) {
|
|
278
|
-
|
|
279
|
-
result = { kind: "variable", name: prop.initializer.text };
|
|
280
|
-
} else if (isInlineBuilder(prop.initializer)) {
|
|
281
|
-
result = {
|
|
282
|
-
kind: "inline",
|
|
283
|
-
block: prop.initializer.getText(sourceFile),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
return;
|
|
264
|
+
continue;
|
|
287
265
|
}
|
|
266
|
+
const urls = extractUrlsArgument(prop.initializer, sourceFile);
|
|
267
|
+
if (urls) extracted.push(urls);
|
|
268
|
+
break;
|
|
288
269
|
}
|
|
289
270
|
}
|
|
271
|
+
|
|
272
|
+
for (const call of chain.slice(1)) {
|
|
273
|
+
if (
|
|
274
|
+
!ts.isPropertyAccessExpression(call.expression) ||
|
|
275
|
+
call.expression.name.text !== "routes" ||
|
|
276
|
+
call.arguments.length === 0
|
|
277
|
+
) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const urls = extractUrlsArgument(call.arguments[0], sourceFile);
|
|
281
|
+
if (urls) extracted.push(urls);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (extracted.length > 0) {
|
|
285
|
+
result = extracted;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
290
288
|
}
|
|
291
289
|
}
|
|
292
|
-
|
|
293
290
|
ts.forEachChild(node, visit);
|
|
294
291
|
}
|
|
295
292
|
|
|
296
293
|
visit(sourceFile);
|
|
297
|
-
return result;
|
|
294
|
+
return result ?? [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Extract the url patterns from a router file using AST.
|
|
299
|
+
* Detects four patterns:
|
|
300
|
+
* 1. createRouter(...).routes(variableName)
|
|
301
|
+
* 2. createRouter({ urls: variableName, ... })
|
|
302
|
+
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
303
|
+
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
304
|
+
* Returns either a variable name or an inline code block.
|
|
305
|
+
*/
|
|
306
|
+
export function extractUrlsFromRouter(
|
|
307
|
+
code: string,
|
|
308
|
+
): UrlsExtractionResult | null {
|
|
309
|
+
const extractions = extractAllUrlsFromRouter(code);
|
|
310
|
+
return extractions[extractions.length - 1] ?? null;
|
|
298
311
|
}
|
|
299
312
|
|
|
300
313
|
/**
|
|
@@ -365,6 +378,77 @@ function applyBasenameToRoutes(
|
|
|
365
378
|
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
366
379
|
}
|
|
367
380
|
|
|
381
|
+
type RouteMapWithSearch = {
|
|
382
|
+
routes: Record<string, string>;
|
|
383
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
function resolveRouterUrlsExtraction(
|
|
387
|
+
routerFilePath: string,
|
|
388
|
+
routerSource: string,
|
|
389
|
+
extraction: UrlsExtractionResult,
|
|
390
|
+
diagnosticsOut: UnresolvableInclude[] | undefined,
|
|
391
|
+
memo: ScanMemo,
|
|
392
|
+
): RouteMapWithSearch {
|
|
393
|
+
if (extraction.kind === "inline") {
|
|
394
|
+
return buildCombinedRouteMapWithSearch(
|
|
395
|
+
routerFilePath,
|
|
396
|
+
undefined,
|
|
397
|
+
new Set(),
|
|
398
|
+
diagnosticsOut,
|
|
399
|
+
extraction.block,
|
|
400
|
+
memo,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
405
|
+
if (imported) {
|
|
406
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
407
|
+
if (!targetFile) {
|
|
408
|
+
diagnosticsOut?.push({
|
|
409
|
+
pathPrefix: "/",
|
|
410
|
+
namePrefix: null,
|
|
411
|
+
reason: "file-not-found",
|
|
412
|
+
sourceFile: routerFilePath,
|
|
413
|
+
detail: `import "${imported.specifier}" resolved to no file`,
|
|
414
|
+
});
|
|
415
|
+
return { routes: {}, searchSchemas: {} };
|
|
416
|
+
}
|
|
417
|
+
return buildCombinedRouteMapWithSearch(
|
|
418
|
+
targetFile,
|
|
419
|
+
imported.exportedName,
|
|
420
|
+
new Set(),
|
|
421
|
+
diagnosticsOut,
|
|
422
|
+
undefined,
|
|
423
|
+
memo,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return buildCombinedRouteMapWithSearch(
|
|
428
|
+
routerFilePath,
|
|
429
|
+
extraction.name,
|
|
430
|
+
new Set(),
|
|
431
|
+
diagnosticsOut,
|
|
432
|
+
undefined,
|
|
433
|
+
memo,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mergeRouteMaps(
|
|
438
|
+
target: RouteMapWithSearch,
|
|
439
|
+
source: RouteMapWithSearch,
|
|
440
|
+
): void {
|
|
441
|
+
for (const [name, pattern] of Object.entries(source.routes)) {
|
|
442
|
+
target.routes[name] = pattern;
|
|
443
|
+
const search = source.searchSchemas[name];
|
|
444
|
+
if (search) {
|
|
445
|
+
target.searchSchemas[name] = search;
|
|
446
|
+
} else {
|
|
447
|
+
delete target.searchSchemas[name];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
368
452
|
// Filesystem path of the generated route-types file for a router source file.
|
|
369
453
|
// Native separators — matches the self-gen-tracking Map key the watcher compares.
|
|
370
454
|
export function genFileTsPath(sourceFile: string): string {
|
|
@@ -396,9 +480,8 @@ export function resolveSearchSchemas(
|
|
|
396
480
|
}
|
|
397
481
|
|
|
398
482
|
/**
|
|
399
|
-
* Resolve routes and search schemas from
|
|
400
|
-
*
|
|
401
|
-
* or by parsing an inline builder function directly.
|
|
483
|
+
* Resolve routes and search schemas from every direct mount in a createRouter
|
|
484
|
+
* chain. Registrations are merged in runtime order, with later mounts winning.
|
|
402
485
|
*/
|
|
403
486
|
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
404
487
|
routes: Record<string, string>;
|
|
@@ -411,8 +494,8 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
411
494
|
return { routes: {}, searchSchemas: {} };
|
|
412
495
|
}
|
|
413
496
|
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
497
|
+
const extractions = extractAllUrlsFromRouter(routerSource);
|
|
498
|
+
if (extractions.length === 0) {
|
|
416
499
|
return { routes: {}, searchSchemas: {} };
|
|
417
500
|
}
|
|
418
501
|
|
|
@@ -422,40 +505,24 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
|
422
505
|
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
423
506
|
: undefined;
|
|
424
507
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
extraction.block,
|
|
508
|
+
const result: RouteMapWithSearch = { routes: {}, searchSchemas: {} };
|
|
509
|
+
const memo = createScanMemo();
|
|
510
|
+
for (const extraction of extractions) {
|
|
511
|
+
mergeRouteMaps(
|
|
512
|
+
result,
|
|
513
|
+
resolveRouterUrlsExtraction(
|
|
514
|
+
routerFilePath,
|
|
515
|
+
routerSource,
|
|
516
|
+
extraction,
|
|
517
|
+
undefined,
|
|
518
|
+
memo,
|
|
519
|
+
),
|
|
438
520
|
);
|
|
439
|
-
} else {
|
|
440
|
-
// Variable reference: follow imports or same-file declaration
|
|
441
|
-
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
442
|
-
if (imported) {
|
|
443
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
444
|
-
if (!targetFile) {
|
|
445
|
-
return { routes: {}, searchSchemas: {} };
|
|
446
|
-
}
|
|
447
|
-
result = buildCombinedRouteMapWithSearch(
|
|
448
|
-
targetFile,
|
|
449
|
-
imported.exportedName,
|
|
450
|
-
);
|
|
451
|
-
} else {
|
|
452
|
-
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
453
|
-
}
|
|
454
521
|
}
|
|
455
522
|
|
|
456
523
|
// Apply basename prefix to all extracted route patterns
|
|
457
524
|
if (basename) {
|
|
458
|
-
|
|
525
|
+
return applyBasenameToRoutes(result, basename);
|
|
459
526
|
}
|
|
460
527
|
|
|
461
528
|
return result;
|
|
@@ -481,56 +548,20 @@ export function detectUnresolvableIncludes(
|
|
|
481
548
|
return [];
|
|
482
549
|
}
|
|
483
550
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (!extraction) return [];
|
|
551
|
+
const extractions = extractAllUrlsFromRouter(source);
|
|
552
|
+
if (extractions.length === 0) return [];
|
|
487
553
|
|
|
488
554
|
const diagnostics: UnresolvableInclude[] = [];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
buildCombinedRouteMapWithSearch(
|
|
555
|
+
const memo = createScanMemo();
|
|
556
|
+
for (const extraction of extractions) {
|
|
557
|
+
resolveRouterUrlsExtraction(
|
|
493
558
|
realPath,
|
|
494
|
-
|
|
495
|
-
|
|
559
|
+
source,
|
|
560
|
+
extraction,
|
|
496
561
|
diagnostics,
|
|
497
|
-
|
|
562
|
+
memo,
|
|
498
563
|
);
|
|
499
|
-
return diagnostics;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Variable reference: resolve where it comes from
|
|
503
|
-
const imported = resolveImportedVariable(source, extraction.name);
|
|
504
|
-
let targetFile: string;
|
|
505
|
-
let exportedName: string | undefined;
|
|
506
|
-
|
|
507
|
-
if (imported) {
|
|
508
|
-
const resolved = resolveImportPath(imported.specifier, realPath);
|
|
509
|
-
if (!resolved) {
|
|
510
|
-
return [
|
|
511
|
-
{
|
|
512
|
-
pathPrefix: "/",
|
|
513
|
-
namePrefix: null,
|
|
514
|
-
reason: "file-not-found",
|
|
515
|
-
sourceFile: realPath,
|
|
516
|
-
detail: `import "${imported.specifier}" resolved to no file`,
|
|
517
|
-
},
|
|
518
|
-
];
|
|
519
|
-
}
|
|
520
|
-
targetFile = resolved;
|
|
521
|
-
exportedName = imported.exportedName;
|
|
522
|
-
} else {
|
|
523
|
-
// Same-file urls() definition
|
|
524
|
-
targetFile = realPath;
|
|
525
|
-
exportedName = extraction.name;
|
|
526
564
|
}
|
|
527
|
-
|
|
528
|
-
buildCombinedRouteMapWithSearch(
|
|
529
|
-
targetFile,
|
|
530
|
-
exportedName,
|
|
531
|
-
new Set(),
|
|
532
|
-
diagnostics,
|
|
533
|
-
);
|
|
534
565
|
return diagnostics;
|
|
535
566
|
}
|
|
536
567
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
genFileTsPath,
|
|
6
6
|
resolveSearchSchemas,
|
|
7
7
|
} from "./generate-route-types.ts";
|
|
8
|
+
import { mergeFullManifests } from "./merge-full-manifests.js";
|
|
8
9
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
9
10
|
|
|
10
11
|
export interface RuntimeDiscoveryOptions {
|
|
@@ -126,24 +127,34 @@ export async function discoverAndWriteRouteTypes(
|
|
|
126
127
|
|
|
127
128
|
// Import build utilities for manifest generation
|
|
128
129
|
const buildMod = await rscEnv.runner.import("@rangojs/router/build");
|
|
129
|
-
const
|
|
130
|
+
const generateManifestFull = buildMod.generateManifestFull;
|
|
130
131
|
|
|
131
|
-
if (!
|
|
132
|
-
throw new Error(
|
|
132
|
+
if (!generateManifestFull) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"generateManifestFull not found in @rangojs/router/build",
|
|
135
|
+
);
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
const outputFiles: string[] = [];
|
|
136
139
|
let totalRouteCount = 0;
|
|
137
|
-
let
|
|
140
|
+
let globalMountIndex = 0;
|
|
141
|
+
let routerCount = 0;
|
|
138
142
|
|
|
139
143
|
for (const [id, router] of registry) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
const mounts =
|
|
145
|
+
router.__urlpatternMounts ??
|
|
146
|
+
(router.urlpatterns ? [{ patterns: router.urlpatterns }] : []);
|
|
147
|
+
if (mounts.length === 0) continue;
|
|
148
|
+
|
|
149
|
+
const mountManifests = [];
|
|
150
|
+
for (const mount of mounts) {
|
|
151
|
+
mountManifests.push(
|
|
152
|
+
await generateManifestFull(mount.patterns, globalMountIndex),
|
|
153
|
+
);
|
|
154
|
+
globalMountIndex++;
|
|
155
|
+
}
|
|
156
|
+
const manifest = mergeFullManifests(mountManifests);
|
|
157
|
+
routerCount++;
|
|
147
158
|
|
|
148
159
|
// Filter out auto-generated route names that the runtime creates for
|
|
149
160
|
// unnamed routes (path() with no name option). These get names like
|
|
@@ -211,7 +222,7 @@ export async function discoverAndWriteRouteTypes(
|
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
return {
|
|
214
|
-
routerCount
|
|
225
|
+
routerCount,
|
|
215
226
|
routeCount: totalRouteCount,
|
|
216
227
|
outputFiles,
|
|
217
228
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution-chain scope for "use cache" function bodies.
|
|
3
|
+
*
|
|
4
|
+
* The ambient guards (cookies(), headers(), ctx.set() reached via
|
|
5
|
+
* getRequestContext()) must fire for code running INSIDE a cached body and
|
|
6
|
+
* stay silent for everything else on the request. A property stamped on the
|
|
7
|
+
* shared RequestContext cannot express that: while a slow "use cache" body is
|
|
8
|
+
* in flight, every parallel read on the same request sees the stamp. Scar: a
|
|
9
|
+
* 2s cached product fetch running beside a sibling loader made the loader's
|
|
10
|
+
* cookies() read throw `cannot be called inside a "use cache" function` —
|
|
11
|
+
* same shared-object hazard as issue #684 plan 010, which fixed only the
|
|
12
|
+
* background-revalidation path. AsyncLocalStorage follows the cached body's
|
|
13
|
+
* own async chain, so parallel work is invisible to the guard while genuine
|
|
14
|
+
* in-body reads still throw.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT entered on the store-less bypass paths in cache-runtime.ts:
|
|
17
|
+
* there the body executes for real on every call, side effects take effect,
|
|
18
|
+
* and request-scoped reads are safe (see the bypass comments there).
|
|
19
|
+
*
|
|
20
|
+
* Kept separate from taint.ts so `node:async_hooks` stays out of that module —
|
|
21
|
+
* taint.ts is reachable from browser-bundled DSL helpers. The probe
|
|
22
|
+
* registration below wires assertNotInsideCacheExec to this scope without
|
|
23
|
+
* taint.ts importing it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
|
+
import { _setCacheExecScopeProbe } from "./taint.js";
|
|
28
|
+
|
|
29
|
+
const cacheExecStorage = new AsyncLocalStorage<true>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run fn with the "use cache" execution scope active. Continuations spawned
|
|
33
|
+
* from fn's synchronous kickoff inherit the scope; parallel chains do not.
|
|
34
|
+
*/
|
|
35
|
+
export function runWithCacheExecScope<T>(fn: () => T): T {
|
|
36
|
+
return cacheExecStorage.run(true, fn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when the calling async chain is inside a "use cache" body. */
|
|
40
|
+
export function isInsideCacheExecScope(): boolean {
|
|
41
|
+
return cacheExecStorage.getStore() === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Wire the ambient ctx-method guards (assertNotInsideCacheExec in taint.ts)
|
|
45
|
+
// to this scope. Module-init registration cannot be missed: exec scopes only
|
|
46
|
+
// exist once cache-runtime.ts (which imports this module) is loaded.
|
|
47
|
+
_setCacheExecScopeProbe(isInsideCacheExecScope);
|