@rangojs/router 0.0.0-experimental.002d056c
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/AGENTS.md +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { getStringValue } from "./ast-helpers.js";
|
|
5
|
+
import { extractRoutesFromSource } from "./ast-route-extraction.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Unresolvable include diagnostics
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export type UnresolvableReason =
|
|
12
|
+
| "factory-call"
|
|
13
|
+
| "dynamic-expression"
|
|
14
|
+
| "unresolvable-import"
|
|
15
|
+
| "file-not-found";
|
|
16
|
+
|
|
17
|
+
export interface UnresolvableInclude {
|
|
18
|
+
pathPrefix: string;
|
|
19
|
+
namePrefix: string | null;
|
|
20
|
+
reason: UnresolvableReason;
|
|
21
|
+
sourceFile: string;
|
|
22
|
+
detail: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// AST-based include() parsing
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function extractNamePrefixFromInclude(node: ts.CallExpression): string | null {
|
|
30
|
+
if (node.arguments.length >= 3) {
|
|
31
|
+
const thirdArg = node.arguments[2];
|
|
32
|
+
if (ts.isObjectLiteralExpression(thirdArg)) {
|
|
33
|
+
for (const prop of thirdArg.properties) {
|
|
34
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
35
|
+
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
|
|
36
|
+
if (propName === "name") {
|
|
37
|
+
return getStringValue(prop.initializer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract include() calls with diagnostics for unresolvable ones.
|
|
47
|
+
* Returns both resolved includes (identifier second args) and unresolvable
|
|
48
|
+
* includes (factory calls, etc.) with reasons.
|
|
49
|
+
*/
|
|
50
|
+
export function extractIncludesWithDiagnostics(code: string): {
|
|
51
|
+
resolved: Array<{
|
|
52
|
+
pathPrefix: string;
|
|
53
|
+
variableName: string;
|
|
54
|
+
namePrefix: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
unresolvable: Array<{
|
|
57
|
+
pathPrefix: string;
|
|
58
|
+
namePrefix: string | null;
|
|
59
|
+
reason: UnresolvableReason;
|
|
60
|
+
detail: string;
|
|
61
|
+
}>;
|
|
62
|
+
} {
|
|
63
|
+
const sourceFile = ts.createSourceFile(
|
|
64
|
+
"input.tsx",
|
|
65
|
+
code,
|
|
66
|
+
ts.ScriptTarget.Latest,
|
|
67
|
+
true,
|
|
68
|
+
ts.ScriptKind.TSX,
|
|
69
|
+
);
|
|
70
|
+
const resolved: Array<{
|
|
71
|
+
pathPrefix: string;
|
|
72
|
+
variableName: string;
|
|
73
|
+
namePrefix: string | null;
|
|
74
|
+
}> = [];
|
|
75
|
+
const unresolvable: Array<{
|
|
76
|
+
pathPrefix: string;
|
|
77
|
+
namePrefix: string | null;
|
|
78
|
+
reason: UnresolvableReason;
|
|
79
|
+
detail: string;
|
|
80
|
+
}> = [];
|
|
81
|
+
|
|
82
|
+
function visit(node: ts.Node) {
|
|
83
|
+
if (ts.isCallExpression(node)) {
|
|
84
|
+
const callee = node.expression;
|
|
85
|
+
if (ts.isIdentifier(callee) && callee.text === "include") {
|
|
86
|
+
if (node.arguments.length < 2) {
|
|
87
|
+
ts.forEachChild(node, visit);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pathPrefix = getStringValue(node.arguments[0]);
|
|
92
|
+
if (pathPrefix === null) {
|
|
93
|
+
ts.forEachChild(node, visit);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const secondArg = node.arguments[1];
|
|
98
|
+
const namePrefix = extractNamePrefixFromInclude(node);
|
|
99
|
+
|
|
100
|
+
if (ts.isIdentifier(secondArg)) {
|
|
101
|
+
resolved.push({
|
|
102
|
+
pathPrefix,
|
|
103
|
+
variableName: secondArg.text,
|
|
104
|
+
namePrefix,
|
|
105
|
+
});
|
|
106
|
+
} else if (ts.isCallExpression(secondArg)) {
|
|
107
|
+
const callText = secondArg.expression.getText(sourceFile);
|
|
108
|
+
unresolvable.push({
|
|
109
|
+
pathPrefix,
|
|
110
|
+
namePrefix,
|
|
111
|
+
reason: "factory-call",
|
|
112
|
+
detail: `${callText}()`,
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
unresolvable.push({
|
|
116
|
+
pathPrefix,
|
|
117
|
+
namePrefix,
|
|
118
|
+
reason: "dynamic-expression",
|
|
119
|
+
detail: secondArg.getText(sourceFile),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
ts.forEachChild(node, visit);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
visit(sourceFile);
|
|
128
|
+
return { resolved, unresolvable };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Import resolution
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Find the import statement for a local variable name.
|
|
137
|
+
* Returns the import specifier and the exported name from the source module.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveImportedVariable(
|
|
140
|
+
code: string,
|
|
141
|
+
localName: string,
|
|
142
|
+
): { specifier: string; exportedName: string } | null {
|
|
143
|
+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
144
|
+
let match;
|
|
145
|
+
|
|
146
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
147
|
+
const imports = match[1];
|
|
148
|
+
const specifier = match[2];
|
|
149
|
+
|
|
150
|
+
const parts = imports
|
|
151
|
+
.split(",")
|
|
152
|
+
.map((s) => s.trim())
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
for (const part of parts) {
|
|
155
|
+
const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
156
|
+
if (asMatch && asMatch[2] === localName)
|
|
157
|
+
return { specifier, exportedName: asMatch[1] };
|
|
158
|
+
if (part === localName) return { specifier, exportedName: localName };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve an import specifier relative to the importing file.
|
|
167
|
+
* Strips .js/.mjs/.jsx extensions and tries .ts/.tsx/.js/.jsx candidates.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveImportPath(
|
|
170
|
+
importSpec: string,
|
|
171
|
+
fromFile: string,
|
|
172
|
+
): string | null {
|
|
173
|
+
if (!importSpec.startsWith(".")) return null;
|
|
174
|
+
|
|
175
|
+
const dir = dirname(fromFile);
|
|
176
|
+
let base = importSpec;
|
|
177
|
+
if (base.endsWith(".js")) base = base.slice(0, -3);
|
|
178
|
+
else if (base.endsWith(".mjs")) base = base.slice(0, -4);
|
|
179
|
+
else if (base.endsWith(".jsx")) base = base.slice(0, -4);
|
|
180
|
+
|
|
181
|
+
const candidates = [
|
|
182
|
+
resolve(dir, base + ".ts"),
|
|
183
|
+
resolve(dir, base + ".tsx"),
|
|
184
|
+
resolve(dir, base + ".js"),
|
|
185
|
+
resolve(dir, base + ".jsx"),
|
|
186
|
+
resolve(dir, base + "/index.ts"),
|
|
187
|
+
resolve(dir, base + "/index.tsx"),
|
|
188
|
+
resolve(dir, base + "/index.js"),
|
|
189
|
+
resolve(dir, base + "/index.jsx"),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
for (const candidate of candidates) {
|
|
193
|
+
if (existsSync(candidate)) return candidate;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// urls() block extraction for same-file variables
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract the source of a specific `const varName = urls(...)` call using
|
|
204
|
+
* the TypeScript AST. Returns the full text of the urls() call expression.
|
|
205
|
+
*/
|
|
206
|
+
function extractUrlsBlockForVariable(
|
|
207
|
+
code: string,
|
|
208
|
+
varName: string,
|
|
209
|
+
): string | null {
|
|
210
|
+
const sourceFile = ts.createSourceFile(
|
|
211
|
+
"input.tsx",
|
|
212
|
+
code,
|
|
213
|
+
ts.ScriptTarget.Latest,
|
|
214
|
+
true,
|
|
215
|
+
ts.ScriptKind.TSX,
|
|
216
|
+
);
|
|
217
|
+
let result: string | null = null;
|
|
218
|
+
|
|
219
|
+
function visit(node: ts.Node) {
|
|
220
|
+
if (result) return;
|
|
221
|
+
if (
|
|
222
|
+
ts.isVariableDeclaration(node) &&
|
|
223
|
+
ts.isIdentifier(node.name) &&
|
|
224
|
+
node.name.text === varName &&
|
|
225
|
+
node.initializer &&
|
|
226
|
+
ts.isCallExpression(node.initializer)
|
|
227
|
+
) {
|
|
228
|
+
const callee = node.initializer.expression;
|
|
229
|
+
if (ts.isIdentifier(callee) && callee.text === "urls") {
|
|
230
|
+
result = node.initializer.getText(sourceFile);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
ts.forEachChild(node, visit);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
visit(sourceFile);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Combined route map building
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
function buildRouteMapFromBlock(
|
|
246
|
+
block: string,
|
|
247
|
+
fullSource: string,
|
|
248
|
+
filePath: string,
|
|
249
|
+
visited: Set<string>,
|
|
250
|
+
searchSchemasOut?: Record<string, Record<string, string>>,
|
|
251
|
+
diagnosticsOut?: UnresolvableInclude[],
|
|
252
|
+
): Record<string, string> {
|
|
253
|
+
const routeMap: Record<string, string> = {};
|
|
254
|
+
|
|
255
|
+
// Extract local path() routes
|
|
256
|
+
const localRoutes = extractRoutesFromSource(block);
|
|
257
|
+
for (const { name, pattern, search } of localRoutes) {
|
|
258
|
+
routeMap[name] = pattern;
|
|
259
|
+
if (search && searchSchemasOut) {
|
|
260
|
+
searchSchemasOut[name] = search;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Extract include() calls with diagnostics for unresolvable ones
|
|
265
|
+
const { resolved: includes, unresolvable } =
|
|
266
|
+
extractIncludesWithDiagnostics(block);
|
|
267
|
+
|
|
268
|
+
if (diagnosticsOut) {
|
|
269
|
+
for (const entry of unresolvable) {
|
|
270
|
+
diagnosticsOut.push({ ...entry, sourceFile: filePath });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const { pathPrefix, variableName, namePrefix } of includes) {
|
|
275
|
+
let childResult: {
|
|
276
|
+
routes: Record<string, string>;
|
|
277
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Try import resolution first
|
|
281
|
+
const imported = resolveImportedVariable(fullSource, variableName);
|
|
282
|
+
if (imported) {
|
|
283
|
+
const targetFile = resolveImportPath(imported.specifier, filePath);
|
|
284
|
+
if (!targetFile) {
|
|
285
|
+
if (diagnosticsOut) {
|
|
286
|
+
diagnosticsOut.push({
|
|
287
|
+
pathPrefix,
|
|
288
|
+
namePrefix,
|
|
289
|
+
reason: "file-not-found",
|
|
290
|
+
sourceFile: filePath,
|
|
291
|
+
detail: `import "${imported.specifier}" resolved to no file`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
childResult = buildCombinedRouteMapWithSearch(
|
|
297
|
+
targetFile,
|
|
298
|
+
imported.exportedName,
|
|
299
|
+
visited,
|
|
300
|
+
diagnosticsOut,
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
// Check if variable exists as a same-file urls() definition
|
|
304
|
+
const sameFileBlock = extractUrlsBlockForVariable(
|
|
305
|
+
fullSource,
|
|
306
|
+
variableName,
|
|
307
|
+
);
|
|
308
|
+
if (!sameFileBlock) {
|
|
309
|
+
if (diagnosticsOut) {
|
|
310
|
+
diagnosticsOut.push({
|
|
311
|
+
pathPrefix,
|
|
312
|
+
namePrefix,
|
|
313
|
+
reason: "unresolvable-import",
|
|
314
|
+
sourceFile: filePath,
|
|
315
|
+
detail: `variable "${variableName}" not found in imports or same-file scope`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
childResult = buildCombinedRouteMapWithSearch(
|
|
321
|
+
filePath,
|
|
322
|
+
variableName,
|
|
323
|
+
visited,
|
|
324
|
+
diagnosticsOut,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Includes without a name keep their child names private to the mounted
|
|
329
|
+
// module. They remain active at runtime via an internal scope prefix, but
|
|
330
|
+
// they are intentionally omitted from generated public route maps.
|
|
331
|
+
if (namePrefix === null) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Apply prefixes
|
|
336
|
+
for (const [name, pattern] of Object.entries(childResult.routes)) {
|
|
337
|
+
const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
|
|
338
|
+
let prefixedPattern: string;
|
|
339
|
+
if (pattern === "/") {
|
|
340
|
+
prefixedPattern = pathPrefix || "/";
|
|
341
|
+
} else if (pathPrefix.endsWith("/") && pattern.startsWith("/")) {
|
|
342
|
+
prefixedPattern = pathPrefix + pattern.slice(1);
|
|
343
|
+
} else {
|
|
344
|
+
prefixedPattern = pathPrefix + pattern;
|
|
345
|
+
}
|
|
346
|
+
routeMap[prefixedName] = prefixedPattern;
|
|
347
|
+
// Propagate search schemas with prefix
|
|
348
|
+
if (childResult.searchSchemas[name] && searchSchemasOut) {
|
|
349
|
+
searchSchemasOut[prefixedName] = childResult.searchSchemas[name];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return routeMap;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Build route map and search schemas together.
|
|
359
|
+
* Internal helper used by the include resolution path.
|
|
360
|
+
*/
|
|
361
|
+
export function buildCombinedRouteMapWithSearch(
|
|
362
|
+
filePath: string,
|
|
363
|
+
variableName?: string,
|
|
364
|
+
visited?: Set<string>,
|
|
365
|
+
diagnosticsOut?: UnresolvableInclude[],
|
|
366
|
+
): {
|
|
367
|
+
routes: Record<string, string>;
|
|
368
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
369
|
+
} {
|
|
370
|
+
visited = visited ?? new Set();
|
|
371
|
+
const realPath = resolve(filePath);
|
|
372
|
+
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
373
|
+
if (visited.has(key)) {
|
|
374
|
+
console.warn(`[rsc-router] Circular include detected, skipping: ${key}`);
|
|
375
|
+
return { routes: {}, searchSchemas: {} };
|
|
376
|
+
}
|
|
377
|
+
visited.add(key);
|
|
378
|
+
|
|
379
|
+
let source: string;
|
|
380
|
+
try {
|
|
381
|
+
source = readFileSync(realPath, "utf-8");
|
|
382
|
+
} catch {
|
|
383
|
+
return { routes: {}, searchSchemas: {} };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let block: string;
|
|
387
|
+
if (variableName) {
|
|
388
|
+
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
389
|
+
if (!extracted) return { routes: {}, searchSchemas: {} };
|
|
390
|
+
block = extracted;
|
|
391
|
+
} else {
|
|
392
|
+
block = source;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const searchSchemas: Record<string, Record<string, string>> = {};
|
|
396
|
+
const routes = buildRouteMapFromBlock(
|
|
397
|
+
block,
|
|
398
|
+
source,
|
|
399
|
+
realPath,
|
|
400
|
+
visited,
|
|
401
|
+
searchSchemas,
|
|
402
|
+
diagnosticsOut,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Remove from visited so sibling branches can include the same variable
|
|
406
|
+
// without false circular-include detection. Only ancestors in the current
|
|
407
|
+
// recursion path should trigger the cycle guard.
|
|
408
|
+
visited.delete(key);
|
|
409
|
+
|
|
410
|
+
return { routes, searchSchemas };
|
|
411
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Param extraction from route patterns
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract typed params from a route pattern string.
|
|
7
|
+
* Matches `:paramName` and `:paramName?` (optional).
|
|
8
|
+
* Custom regex constraints like `:id(\d+)` are ignored for type purposes.
|
|
9
|
+
*/
|
|
10
|
+
export function extractParamsFromPattern(
|
|
11
|
+
pattern: string,
|
|
12
|
+
): Record<string, string> | undefined {
|
|
13
|
+
const params: Record<string, string> = {};
|
|
14
|
+
const regex = /:([a-zA-Z_$][\w$]*)(?:\([^)]+\))?(\?)?/g;
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = regex.exec(pattern)) !== null) {
|
|
17
|
+
params[match[1]!] = match[2] ? "string?" : "string";
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Shared route entry formatter
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format a single route entry for codegen output.
|
|
28
|
+
* Routes without search remain plain strings (params are extracted from
|
|
29
|
+
* the pattern at the type level by ExtractParams).
|
|
30
|
+
* Routes with search become objects with path and search fields.
|
|
31
|
+
*/
|
|
32
|
+
export function formatRouteEntry(
|
|
33
|
+
key: string,
|
|
34
|
+
pattern: string,
|
|
35
|
+
_params?: Record<string, string>,
|
|
36
|
+
search?: Record<string, string>,
|
|
37
|
+
): string {
|
|
38
|
+
const hasSearch = search && Object.keys(search).length > 0;
|
|
39
|
+
|
|
40
|
+
if (!hasSearch) {
|
|
41
|
+
return ` ${key}: "${pattern}",`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const searchBody = Object.entries(search!)
|
|
45
|
+
.map(([k, v]) => `${k}: "${v}"`)
|
|
46
|
+
.join(", ");
|
|
47
|
+
return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { extractParamsFromPattern } from "./param-extraction.js";
|
|
4
|
+
import { extractRoutesFromSource } from "./ast-route-extraction.js";
|
|
5
|
+
import { generatePerModuleTypesSource } from "./codegen.js";
|
|
6
|
+
import { buildCombinedRouteMapWithSearch } from "./include-resolution.js";
|
|
7
|
+
import type { ScanFilter } from "./scan-filter.js";
|
|
8
|
+
import { findTsFiles } from "./scan-filter.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate per-module route type files by statically parsing url module source.
|
|
12
|
+
* Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
|
|
13
|
+
* extracted route name/pattern pairs. Only writes when content has changed.
|
|
14
|
+
*/
|
|
15
|
+
export function writePerModuleRouteTypes(
|
|
16
|
+
root: string,
|
|
17
|
+
filter?: ScanFilter,
|
|
18
|
+
): void {
|
|
19
|
+
const files = findTsFiles(root, filter);
|
|
20
|
+
for (const filePath of files) {
|
|
21
|
+
writePerModuleRouteTypesForFile(filePath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Find all variable names assigned to urls() calls in source code.
|
|
27
|
+
* e.g. `export const patterns = urls(...)` -> ["patterns"]
|
|
28
|
+
*/
|
|
29
|
+
export function findUrlsVariableNames(code: string): string[] {
|
|
30
|
+
const sourceFile = ts.createSourceFile(
|
|
31
|
+
"input.tsx",
|
|
32
|
+
code,
|
|
33
|
+
ts.ScriptTarget.Latest,
|
|
34
|
+
true,
|
|
35
|
+
ts.ScriptKind.TSX,
|
|
36
|
+
);
|
|
37
|
+
const names: string[] = [];
|
|
38
|
+
|
|
39
|
+
function visit(node: ts.Node) {
|
|
40
|
+
if (
|
|
41
|
+
ts.isVariableDeclaration(node) &&
|
|
42
|
+
ts.isIdentifier(node.name) &&
|
|
43
|
+
node.initializer &&
|
|
44
|
+
ts.isCallExpression(node.initializer)
|
|
45
|
+
) {
|
|
46
|
+
const callee = node.initializer.expression;
|
|
47
|
+
if (ts.isIdentifier(callee) && callee.text === "urls") {
|
|
48
|
+
names.push(node.name.text);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
ts.forEachChild(node, visit);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
visit(sourceFile);
|
|
55
|
+
return names;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate per-module route types for a single url module file.
|
|
60
|
+
* Follows include() calls recursively to produce the full route tree.
|
|
61
|
+
* No-ops if the file doesn't contain `urls(` or has no named routes.
|
|
62
|
+
*/
|
|
63
|
+
export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
64
|
+
try {
|
|
65
|
+
const source = readFileSync(filePath, "utf-8");
|
|
66
|
+
if (!source.includes("urls(")) return;
|
|
67
|
+
|
|
68
|
+
const varNames = findUrlsVariableNames(source);
|
|
69
|
+
|
|
70
|
+
type Route = {
|
|
71
|
+
name: string;
|
|
72
|
+
pattern: string;
|
|
73
|
+
params?: Record<string, string>;
|
|
74
|
+
search?: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
let routes: Route[];
|
|
77
|
+
|
|
78
|
+
if (varNames.length > 0) {
|
|
79
|
+
// Follow includes recursively via the combined route map builder.
|
|
80
|
+
// The visited set in buildCombinedRouteMapWithSearch prevents infinite loops.
|
|
81
|
+
routes = [];
|
|
82
|
+
for (const varName of varNames) {
|
|
83
|
+
const { routes: routeMap, searchSchemas } =
|
|
84
|
+
buildCombinedRouteMapWithSearch(filePath, varName);
|
|
85
|
+
for (const [name, pattern] of Object.entries(routeMap)) {
|
|
86
|
+
const params = extractParamsFromPattern(pattern);
|
|
87
|
+
routes.push({
|
|
88
|
+
name,
|
|
89
|
+
pattern,
|
|
90
|
+
...(params ? { params } : {}),
|
|
91
|
+
...(searchSchemas[name] ? { search: searchSchemas[name] } : {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback: no urls() variable found, extract path() calls directly
|
|
97
|
+
routes = extractRoutesFromSource(source);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
101
|
+
|
|
102
|
+
// When a urls() variable was found but static resolution yields zero
|
|
103
|
+
// routes, write an empty placeholder so generated imports stay
|
|
104
|
+
// resolvable until runtime discovery fills them in.
|
|
105
|
+
if (routes.length === 0) {
|
|
106
|
+
if (varNames.length > 0 && !existsSync(genPath)) {
|
|
107
|
+
writeFileSync(genPath, generatePerModuleTypesSource([]));
|
|
108
|
+
console.log(
|
|
109
|
+
`[rsc-router] Generated route types (placeholder) -> ${genPath}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const genSource = generatePerModuleTypesSource(routes);
|
|
116
|
+
const existing = existsSync(genPath)
|
|
117
|
+
? readFileSync(genPath, "utf-8")
|
|
118
|
+
: null;
|
|
119
|
+
if (existing !== genSource) {
|
|
120
|
+
writeFileSync(genPath, genSource);
|
|
121
|
+
console.log(`[rsc-router] Generated route types -> ${genPath}`);
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|