@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847
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 +884 -4
- package/dist/bin/rango.js +1531 -212
- package/dist/vite/index.js +3995 -2489
- package/package.json +57 -52
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +85 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/hooks/SKILL.md +328 -70
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +62 -15
- package/skills/loader/SKILL.md +368 -42
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +14 -10
- package/skills/parallel/SKILL.md +137 -1
- package/skills/prerender/SKILL.md +366 -28
- package/skills/rango/SKILL.md +85 -21
- package/skills/response-routes/SKILL.md +136 -83
- package/skills/route/SKILL.md +195 -21
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +240 -102
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +266 -558
- package/src/browser/navigation-client.ts +132 -75
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +303 -309
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +144 -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 +190 -70
- package/src/browser/react/NavigationProvider.tsx +78 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +29 -70
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- 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 +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +188 -57
- package/src/browser/scroll-restoration.ts +117 -44
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +488 -606
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +116 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +63 -21
- package/src/build/generate-route-types.ts +36 -1038
- package/src/build/index.ts +2 -5
- package/src/build/route-trie.ts +38 -12
- 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 +342 -0
- package/src/cache/cache-scope.ts +122 -303
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- 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 +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +84 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +12 -7
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +104 -40
- package/src/index.ts +122 -67
- package/src/internal-debug.ts +9 -3
- package/src/loader.rsc.ts +18 -93
- package/src/loader.ts +26 -9
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +121 -17
- package/src/prerender.ts +325 -20
- package/src/reverse.ts +144 -124
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +959 -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 -1450
- package/src/route-map-builder.ts +87 -133
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +41 -6
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +324 -116
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +179 -133
- package/src/router/logging.ts +112 -6
- package/src/router/manifest.ts +58 -19
- package/src/router/match-api.ts +89 -88
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +86 -89
- package/src/router/match-middleware/cache-lookup.ts +295 -49
- package/src/router/match-middleware/cache-store.ts +56 -13
- package/src/router/match-middleware/intercept-resolution.ts +45 -22
- package/src/router/match-middleware/segment-resolution.ts +20 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +44 -21
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +327 -369
- package/src/router/pattern-matching.ts +169 -31
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +105 -14
- package/src/router/router-context.ts +40 -21
- 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 +677 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1296 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1354
- 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 +96 -29
- package/src/router/types.ts +15 -9
- package/src/router.ts +642 -2366
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +639 -1027
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -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 +38 -11
- package/src/search-params.ts +66 -54
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +237 -54
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +438 -71
- package/src/server.ts +26 -164
- package/src/ssr/index.tsx +101 -31
- package/src/static-handler.ts +22 -4
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +773 -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 +150 -0
- package/src/types.ts +1 -1795
- 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 -1323
- package/src/use-loader.tsx +85 -77
- 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 +11 -2259
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
- 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/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- 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/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
- 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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -1,1038 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const valid = routes.filter(({ name }) => {
|
|
38
|
-
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
39
|
-
console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Deduplicate by name (last definition wins for same name)
|
|
46
|
-
const deduped = new Map<string, { pattern: string; search?: Record<string, string> }>();
|
|
47
|
-
for (const { name, pattern, search } of valid) {
|
|
48
|
-
deduped.set(name, { pattern, search });
|
|
49
|
-
}
|
|
50
|
-
const sorted = [...deduped.entries()]
|
|
51
|
-
.sort(([a], [b]) => a.localeCompare(b));
|
|
52
|
-
const body = sorted
|
|
53
|
-
.map(([name, { pattern, search }]) => {
|
|
54
|
-
// Quote names that aren't valid bare identifiers (dots, dashes, etc.)
|
|
55
|
-
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
56
|
-
if (search && Object.keys(search).length > 0) {
|
|
57
|
-
const searchBody = Object.entries(search)
|
|
58
|
-
.map(([k, v]) => `${k}: "${v}"`)
|
|
59
|
-
.join(", ");
|
|
60
|
-
return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
|
|
61
|
-
}
|
|
62
|
-
return ` ${key}: "${pattern}",`;
|
|
63
|
-
})
|
|
64
|
-
.join("\n");
|
|
65
|
-
return `// Auto-generated by @rangojs/router - do not edit\nexport const routes = {\n${body}\n} as const;\nexport type routes = typeof routes;\n`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Mini-parser internals
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
function isWhitespace(ch: string): boolean {
|
|
73
|
-
return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Read a single- or double-quoted string literal starting at pos. */
|
|
77
|
-
function readString(
|
|
78
|
-
code: string,
|
|
79
|
-
pos: number
|
|
80
|
-
): { value: string; end: number } | null {
|
|
81
|
-
const quote = code[pos];
|
|
82
|
-
if (quote !== '"' && quote !== "'") return null;
|
|
83
|
-
|
|
84
|
-
let value = "";
|
|
85
|
-
pos++;
|
|
86
|
-
while (pos < code.length) {
|
|
87
|
-
if (code[pos] === "\\") {
|
|
88
|
-
pos++;
|
|
89
|
-
if (pos < code.length) {
|
|
90
|
-
value += code[pos];
|
|
91
|
-
pos++;
|
|
92
|
-
}
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (code[pos] === quote) {
|
|
96
|
-
return { value, end: pos + 1 };
|
|
97
|
-
}
|
|
98
|
-
value += code[pos];
|
|
99
|
-
pos++;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Skip past any string literal (single, double, or template). */
|
|
105
|
-
function skipStringLiteral(code: string, pos: number): number {
|
|
106
|
-
const quote = code[pos];
|
|
107
|
-
|
|
108
|
-
if (quote === "`") {
|
|
109
|
-
pos++;
|
|
110
|
-
while (pos < code.length) {
|
|
111
|
-
if (code[pos] === "\\") {
|
|
112
|
-
pos += 2;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (code[pos] === "`") return pos + 1;
|
|
116
|
-
if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
|
|
117
|
-
pos += 2;
|
|
118
|
-
let braceDepth = 1;
|
|
119
|
-
while (pos < code.length && braceDepth > 0) {
|
|
120
|
-
if (code[pos] === "{") braceDepth++;
|
|
121
|
-
else if (code[pos] === "}") braceDepth--;
|
|
122
|
-
else if (code[pos] === "\\") pos++;
|
|
123
|
-
else if (
|
|
124
|
-
code[pos] === '"' ||
|
|
125
|
-
code[pos] === "'" ||
|
|
126
|
-
code[pos] === "`"
|
|
127
|
-
) {
|
|
128
|
-
pos = skipStringLiteral(code, pos);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (braceDepth > 0) pos++;
|
|
132
|
-
}
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
pos++;
|
|
136
|
-
}
|
|
137
|
-
return pos;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Simple single/double quoted string
|
|
141
|
-
pos++;
|
|
142
|
-
while (pos < code.length) {
|
|
143
|
-
if (code[pos] === "\\") {
|
|
144
|
-
pos += 2;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (code[pos] === quote) return pos + 1;
|
|
148
|
-
pos++;
|
|
149
|
-
}
|
|
150
|
-
return pos;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Check if code at pos starts with `name` as a standalone identifier
|
|
155
|
-
* followed by `:` (an object property).
|
|
156
|
-
*/
|
|
157
|
-
function matchesNameColon(code: string, pos: number): boolean {
|
|
158
|
-
if (code.slice(pos, pos + 4) !== "name") return false;
|
|
159
|
-
if (pos > 0 && /\w/.test(code[pos - 1])) return false;
|
|
160
|
-
const afterName = pos + 4;
|
|
161
|
-
if (afterName < code.length && /\w/.test(code[afterName])) return false;
|
|
162
|
-
let checkPos = afterName;
|
|
163
|
-
while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
|
|
164
|
-
return code[checkPos] === ":";
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Extract the string value after `name:` starting at the `n` of `name`. */
|
|
168
|
-
function extractNameValue(
|
|
169
|
-
code: string,
|
|
170
|
-
pos: number
|
|
171
|
-
): { value: string; end: number } | null {
|
|
172
|
-
pos += 4; // skip 'name'
|
|
173
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
174
|
-
pos++; // skip ':'
|
|
175
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
176
|
-
return readString(code, pos);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Parse a single path() call starting right after the opening paren.
|
|
181
|
-
* Returns { name, pattern } or null if the call is unnamed.
|
|
182
|
-
*/
|
|
183
|
-
/**
|
|
184
|
-
* Check if code at pos starts with `search` as a standalone identifier
|
|
185
|
-
* followed by `:` (an object property).
|
|
186
|
-
*/
|
|
187
|
-
function matchesSearchColon(code: string, pos: number): boolean {
|
|
188
|
-
if (code.slice(pos, pos + 6) !== "search") return false;
|
|
189
|
-
if (pos > 0 && /\w/.test(code[pos - 1])) return false;
|
|
190
|
-
const afterSearch = pos + 6;
|
|
191
|
-
if (afterSearch < code.length && /\w/.test(code[afterSearch])) return false;
|
|
192
|
-
let checkPos = afterSearch;
|
|
193
|
-
while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
|
|
194
|
-
return code[checkPos] === ":";
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Extract a search schema object literal after `search:`.
|
|
199
|
-
* Parses { key: "type", key2: "type2" } at the position after `search:`.
|
|
200
|
-
* Returns the parsed schema and end position, or null if not an object literal.
|
|
201
|
-
*/
|
|
202
|
-
function extractSearchValue(
|
|
203
|
-
code: string,
|
|
204
|
-
pos: number
|
|
205
|
-
): { value: Record<string, string>; end: number } | null {
|
|
206
|
-
pos += 6; // skip 'search'
|
|
207
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
208
|
-
pos++; // skip ':'
|
|
209
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
210
|
-
|
|
211
|
-
if (code[pos] !== "{") return null;
|
|
212
|
-
pos++; // skip '{'
|
|
213
|
-
|
|
214
|
-
const schema: Record<string, string> = {};
|
|
215
|
-
|
|
216
|
-
while (pos < code.length) {
|
|
217
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
218
|
-
if (code[pos] === "}") return { value: schema, end: pos + 1 };
|
|
219
|
-
if (code[pos] === ",") { pos++; continue; }
|
|
220
|
-
|
|
221
|
-
// Parse key (identifier or string)
|
|
222
|
-
let key: string;
|
|
223
|
-
if (code[pos] === '"' || code[pos] === "'") {
|
|
224
|
-
const keyStr = readString(code, pos);
|
|
225
|
-
if (!keyStr) return null;
|
|
226
|
-
key = keyStr.value;
|
|
227
|
-
pos = keyStr.end;
|
|
228
|
-
} else {
|
|
229
|
-
const keyStart = pos;
|
|
230
|
-
while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
|
|
231
|
-
if (pos === keyStart) return null;
|
|
232
|
-
key = code.slice(keyStart, pos);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Skip colon
|
|
236
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
237
|
-
if (code[pos] !== ":") return null;
|
|
238
|
-
pos++;
|
|
239
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
240
|
-
|
|
241
|
-
// Parse value (must be a string literal)
|
|
242
|
-
const valStr = readString(code, pos);
|
|
243
|
-
if (!valStr) return null;
|
|
244
|
-
schema[key] = valStr.value;
|
|
245
|
-
pos = valStr.end;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function parsePathCall(
|
|
252
|
-
code: string,
|
|
253
|
-
pos: number
|
|
254
|
-
): { name: string; pattern: string; search?: Record<string, string> } | null {
|
|
255
|
-
// Skip whitespace to first argument
|
|
256
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
257
|
-
|
|
258
|
-
// First argument must be a string literal (the pattern)
|
|
259
|
-
const patternStr = readString(code, pos);
|
|
260
|
-
if (!patternStr) return null;
|
|
261
|
-
const pattern = patternStr.value;
|
|
262
|
-
pos = patternStr.end;
|
|
263
|
-
|
|
264
|
-
// Scan the rest of the call tracking depth.
|
|
265
|
-
// depth=1: inside path(), depth=2: inside an object/paren at top level of call.
|
|
266
|
-
// We look for `name: "..."` and `search: { ... }` at depth 2 (options object properties).
|
|
267
|
-
let depth = 1;
|
|
268
|
-
let name: string | null = null;
|
|
269
|
-
let search: Record<string, string> | undefined;
|
|
270
|
-
|
|
271
|
-
while (pos < code.length && depth > 0) {
|
|
272
|
-
const ch = code[pos];
|
|
273
|
-
|
|
274
|
-
if (isWhitespace(ch)) {
|
|
275
|
-
pos++;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Line comment
|
|
280
|
-
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
281
|
-
pos += 2;
|
|
282
|
-
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Block comment
|
|
287
|
-
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
288
|
-
pos += 2;
|
|
289
|
-
while (
|
|
290
|
-
pos < code.length - 1 &&
|
|
291
|
-
!(code[pos] === "*" && code[pos + 1] === "/")
|
|
292
|
-
)
|
|
293
|
-
pos++;
|
|
294
|
-
pos += 2;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// At depth 2 (inside an object at call top-level), look for name: "..." and search: { ... }
|
|
299
|
-
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
300
|
-
const nameResult = extractNameValue(code, pos);
|
|
301
|
-
if (nameResult) {
|
|
302
|
-
name = nameResult.value;
|
|
303
|
-
pos = nameResult.end;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (depth === 2 && ch === "s" && matchesSearchColon(code, pos)) {
|
|
309
|
-
const searchResult = extractSearchValue(code, pos);
|
|
310
|
-
if (searchResult) {
|
|
311
|
-
search = searchResult.value;
|
|
312
|
-
pos = searchResult.end;
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Skip string literals.
|
|
318
|
-
// Treat ' preceded by a word char as an apostrophe (e.g. "shouldn't"),
|
|
319
|
-
// not a string delimiter. In valid JS/TS, opening ' is never preceded
|
|
320
|
-
// by a word character.
|
|
321
|
-
if (ch === '"' || ch === "`" || (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))) {
|
|
322
|
-
pos = skipStringLiteral(code, pos);
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Track depth
|
|
327
|
-
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
328
|
-
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
329
|
-
|
|
330
|
-
pos++;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (name === null) return null;
|
|
334
|
-
return { name, pattern, ...(search ? { search } : {}) };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Generates a .ts file that augments RSCRouter.GeneratedRouteMap
|
|
339
|
-
* with route name -> pattern mappings. This enables Handler<"routeName">
|
|
340
|
-
* without circular references since the file has no imports from the app.
|
|
341
|
-
*/
|
|
342
|
-
export function generateRouteTypesSource(
|
|
343
|
-
routeManifest: Record<string, string>,
|
|
344
|
-
searchSchemas?: Record<string, Record<string, string>>
|
|
345
|
-
): string {
|
|
346
|
-
const entries = Object.entries(routeManifest).sort(([a], [b]) =>
|
|
347
|
-
a.localeCompare(b)
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
const objectBody = entries
|
|
351
|
-
.map(([name, pattern]) => {
|
|
352
|
-
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
353
|
-
const search = searchSchemas?.[name];
|
|
354
|
-
if (search && Object.keys(search).length > 0) {
|
|
355
|
-
const searchBody = Object.entries(search)
|
|
356
|
-
.map(([k, v]) => `${k}: "${v}"`)
|
|
357
|
-
.join(", ");
|
|
358
|
-
return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
|
|
359
|
-
}
|
|
360
|
-
return ` ${key}: "${pattern}",`;
|
|
361
|
-
})
|
|
362
|
-
.join("\n");
|
|
363
|
-
|
|
364
|
-
return `// Auto-generated by @rangojs/router - do not edit
|
|
365
|
-
export const NamedRoutes = {
|
|
366
|
-
${objectBody}
|
|
367
|
-
} as const;
|
|
368
|
-
|
|
369
|
-
declare global {
|
|
370
|
-
namespace RSCRouter {
|
|
371
|
-
interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** Default exclude patterns for route type scanning. */
|
|
378
|
-
export const DEFAULT_EXCLUDE_PATTERNS: string[] = [
|
|
379
|
-
"**/__tests__/**",
|
|
380
|
-
"**/__mocks__/**",
|
|
381
|
-
"**/dist/**",
|
|
382
|
-
"**/coverage/**",
|
|
383
|
-
"**/*.test.{ts,tsx}",
|
|
384
|
-
"**/*.spec.{ts,tsx}",
|
|
385
|
-
];
|
|
386
|
-
|
|
387
|
-
export type ScanFilter = (absolutePath: string) => boolean;
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Compile include/exclude glob patterns into a single predicate.
|
|
391
|
-
* Paths are made root-relative before matching.
|
|
392
|
-
* Returns undefined when no filtering is needed (no include, default exclude).
|
|
393
|
-
*/
|
|
394
|
-
export function createScanFilter(
|
|
395
|
-
root: string,
|
|
396
|
-
opts: { include?: string[]; exclude?: string[] },
|
|
397
|
-
): ScanFilter | undefined {
|
|
398
|
-
const { include, exclude } = opts;
|
|
399
|
-
const hasInclude = include && include.length > 0;
|
|
400
|
-
const hasCustomExclude = exclude !== undefined;
|
|
401
|
-
|
|
402
|
-
if (!hasInclude && !hasCustomExclude) return undefined;
|
|
403
|
-
|
|
404
|
-
const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
405
|
-
const includeMatcher = hasInclude ? picomatch(include) : null;
|
|
406
|
-
const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
|
|
407
|
-
|
|
408
|
-
return (absolutePath: string) => {
|
|
409
|
-
const rel = relative(root, absolutePath);
|
|
410
|
-
if (excludeMatcher && excludeMatcher(rel)) return false;
|
|
411
|
-
if (includeMatcher) return includeMatcher(rel);
|
|
412
|
-
return true;
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Recursively find .ts/.tsx files under a directory, skipping node_modules
|
|
418
|
-
* and .gen. files.
|
|
419
|
-
*/
|
|
420
|
-
export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
421
|
-
const results: string[] = [];
|
|
422
|
-
let entries;
|
|
423
|
-
try {
|
|
424
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
425
|
-
} catch (err) {
|
|
426
|
-
console.warn(`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`);
|
|
427
|
-
return results;
|
|
428
|
-
}
|
|
429
|
-
for (const entry of entries) {
|
|
430
|
-
const fullPath = join(dir, entry.name);
|
|
431
|
-
if (entry.isDirectory()) {
|
|
432
|
-
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
433
|
-
results.push(...findTsFiles(fullPath, filter));
|
|
434
|
-
} else if (
|
|
435
|
-
(entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
|
|
436
|
-
!entry.name.includes(".gen.")
|
|
437
|
-
) {
|
|
438
|
-
if (filter && !filter(fullPath)) continue;
|
|
439
|
-
results.push(fullPath);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return results;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Generate per-module route type files by statically parsing url module source.
|
|
447
|
-
* Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
|
|
448
|
-
* extracted route name/pattern pairs. Only writes when content has changed.
|
|
449
|
-
*/
|
|
450
|
-
export function writePerModuleRouteTypes(root: string, filter?: ScanFilter): void {
|
|
451
|
-
const files = findTsFiles(root, filter);
|
|
452
|
-
for (const filePath of files) {
|
|
453
|
-
writePerModuleRouteTypesForFile(filePath);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Generate per-module route types for a single url module file.
|
|
459
|
-
* No-ops if the file doesn't contain `urls(` or has no named routes.
|
|
460
|
-
*/
|
|
461
|
-
export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
462
|
-
try {
|
|
463
|
-
const source = readFileSync(filePath, "utf-8");
|
|
464
|
-
if (!source.includes("urls(")) return;
|
|
465
|
-
|
|
466
|
-
const routes = extractRoutesFromSource(source);
|
|
467
|
-
if (routes.length === 0) return;
|
|
468
|
-
|
|
469
|
-
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
470
|
-
const genSource = generatePerModuleTypesSource(routes);
|
|
471
|
-
const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
|
|
472
|
-
if (existing !== genSource) {
|
|
473
|
-
writeFileSync(genPath, genSource);
|
|
474
|
-
console.log(`[rsc-router] Generated route types -> ${genPath}`);
|
|
475
|
-
}
|
|
476
|
-
} catch (err) {
|
|
477
|
-
console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ---------------------------------------------------------------------------
|
|
482
|
-
// Static include() parsing
|
|
483
|
-
// ---------------------------------------------------------------------------
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Extract include() calls from source code by statically parsing.
|
|
487
|
-
* Returns the path prefix, variable name, and optional name prefix for each.
|
|
488
|
-
*/
|
|
489
|
-
export function extractIncludesFromSource(
|
|
490
|
-
code: string
|
|
491
|
-
): Array<{ pathPrefix: string; variableName: string; namePrefix: string | null }> {
|
|
492
|
-
const results: Array<{
|
|
493
|
-
pathPrefix: string;
|
|
494
|
-
variableName: string;
|
|
495
|
-
namePrefix: string | null;
|
|
496
|
-
}> = [];
|
|
497
|
-
const regex = /\binclude\s*\(/g;
|
|
498
|
-
let match;
|
|
499
|
-
|
|
500
|
-
while ((match = regex.exec(code)) !== null) {
|
|
501
|
-
const result = parseIncludeCall(code, match.index + match[0].length);
|
|
502
|
-
if (result) results.push(result);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return results;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Parse a single include() call starting right after the opening paren.
|
|
510
|
-
* Expects: include("prefix", variableName, { name: "prefix" })
|
|
511
|
-
*/
|
|
512
|
-
function parseIncludeCall(
|
|
513
|
-
code: string,
|
|
514
|
-
pos: number
|
|
515
|
-
): {
|
|
516
|
-
pathPrefix: string;
|
|
517
|
-
variableName: string;
|
|
518
|
-
namePrefix: string | null;
|
|
519
|
-
} | null {
|
|
520
|
-
// Skip whitespace to first argument
|
|
521
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
522
|
-
|
|
523
|
-
// First arg: string literal (pathPrefix)
|
|
524
|
-
const prefixStr = readString(code, pos);
|
|
525
|
-
if (!prefixStr) return null;
|
|
526
|
-
const pathPrefix = prefixStr.value;
|
|
527
|
-
pos = prefixStr.end;
|
|
528
|
-
|
|
529
|
-
// Comma
|
|
530
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
531
|
-
if (pos >= code.length || code[pos] !== ",") return null;
|
|
532
|
-
pos++;
|
|
533
|
-
while (pos < code.length && isWhitespace(code[pos])) pos++;
|
|
534
|
-
|
|
535
|
-
// Second arg: identifier (variableName)
|
|
536
|
-
const varStart = pos;
|
|
537
|
-
while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
|
|
538
|
-
if (pos === varStart) return null;
|
|
539
|
-
const variableName = code.slice(varStart, pos);
|
|
540
|
-
|
|
541
|
-
// Scan rest of call for optional { name: "..." }
|
|
542
|
-
let namePrefix: string | null = null;
|
|
543
|
-
let depth = 1; // inside include()
|
|
544
|
-
|
|
545
|
-
while (pos < code.length && depth > 0) {
|
|
546
|
-
const ch = code[pos];
|
|
547
|
-
|
|
548
|
-
if (isWhitespace(ch)) {
|
|
549
|
-
pos++;
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Line comment
|
|
554
|
-
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
555
|
-
pos += 2;
|
|
556
|
-
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Block comment
|
|
561
|
-
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
562
|
-
pos += 2;
|
|
563
|
-
while (
|
|
564
|
-
pos < code.length - 1 &&
|
|
565
|
-
!(code[pos] === "*" && code[pos + 1] === "/")
|
|
566
|
-
)
|
|
567
|
-
pos++;
|
|
568
|
-
pos += 2;
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// At depth 2 (inside options object), look for name: "..."
|
|
573
|
-
if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
|
|
574
|
-
const nameResult = extractNameValue(code, pos);
|
|
575
|
-
if (nameResult) {
|
|
576
|
-
namePrefix = nameResult.value;
|
|
577
|
-
pos = nameResult.end;
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Skip string literals
|
|
583
|
-
if (
|
|
584
|
-
ch === '"' ||
|
|
585
|
-
ch === "`" ||
|
|
586
|
-
(ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
|
|
587
|
-
) {
|
|
588
|
-
pos = skipStringLiteral(code, pos);
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Track depth
|
|
593
|
-
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
594
|
-
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
595
|
-
|
|
596
|
-
pos++;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return { pathPrefix, variableName, namePrefix };
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// ---------------------------------------------------------------------------
|
|
603
|
-
// Import resolution
|
|
604
|
-
// ---------------------------------------------------------------------------
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Find the import statement for a local variable name.
|
|
608
|
-
* Returns the import specifier and the exported name from the source module.
|
|
609
|
-
*/
|
|
610
|
-
function resolveImportedVariable(
|
|
611
|
-
code: string,
|
|
612
|
-
localName: string
|
|
613
|
-
): { specifier: string; exportedName: string } | null {
|
|
614
|
-
const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
|
|
615
|
-
let match;
|
|
616
|
-
|
|
617
|
-
while ((match = importRegex.exec(code)) !== null) {
|
|
618
|
-
const imports = match[1];
|
|
619
|
-
const specifier = match[2];
|
|
620
|
-
|
|
621
|
-
const parts = imports
|
|
622
|
-
.split(",")
|
|
623
|
-
.map((s) => s.trim())
|
|
624
|
-
.filter(Boolean);
|
|
625
|
-
for (const part of parts) {
|
|
626
|
-
const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
627
|
-
if (asMatch && asMatch[2] === localName)
|
|
628
|
-
return { specifier, exportedName: asMatch[1] };
|
|
629
|
-
if (part === localName) return { specifier, exportedName: localName };
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Resolve an import specifier relative to the importing file.
|
|
638
|
-
* Strips .js/.mjs extensions and tries .ts/.tsx candidates.
|
|
639
|
-
*/
|
|
640
|
-
function resolveImportPath(
|
|
641
|
-
importSpec: string,
|
|
642
|
-
fromFile: string
|
|
643
|
-
): string | null {
|
|
644
|
-
if (!importSpec.startsWith(".")) return null;
|
|
645
|
-
|
|
646
|
-
const dir = dirname(fromFile);
|
|
647
|
-
let base = importSpec;
|
|
648
|
-
if (base.endsWith(".js")) base = base.slice(0, -3);
|
|
649
|
-
else if (base.endsWith(".mjs")) base = base.slice(0, -4);
|
|
650
|
-
|
|
651
|
-
const candidates = [
|
|
652
|
-
resolve(dir, base + ".ts"),
|
|
653
|
-
resolve(dir, base + ".tsx"),
|
|
654
|
-
resolve(dir, base + "/index.ts"),
|
|
655
|
-
resolve(dir, base + "/index.tsx"),
|
|
656
|
-
];
|
|
657
|
-
|
|
658
|
-
for (const candidate of candidates) {
|
|
659
|
-
if (existsSync(candidate)) return candidate;
|
|
660
|
-
}
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// ---------------------------------------------------------------------------
|
|
665
|
-
// urls() block extraction for same-file variables
|
|
666
|
-
// ---------------------------------------------------------------------------
|
|
667
|
-
|
|
668
|
-
function escapeRegExp(s: string): string {
|
|
669
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Extract the source of a specific `const varName = urls(...)` block.
|
|
674
|
-
* Used for same-file variables where include() references a urls() defined
|
|
675
|
-
* in the same module rather than imported.
|
|
676
|
-
*/
|
|
677
|
-
function extractUrlsBlockForVariable(
|
|
678
|
-
code: string,
|
|
679
|
-
varName: string
|
|
680
|
-
): string | null {
|
|
681
|
-
const pattern = new RegExp(
|
|
682
|
-
`(?:export\\s+)?(?:const|let|var)\\s+${escapeRegExp(varName)}\\s*=\\s*urls\\s*\\(`
|
|
683
|
-
);
|
|
684
|
-
const match = pattern.exec(code);
|
|
685
|
-
if (!match) return null;
|
|
686
|
-
|
|
687
|
-
// Start from the opening paren of urls(
|
|
688
|
-
const openParen = match.index + match[0].length - 1;
|
|
689
|
-
let depth = 1;
|
|
690
|
-
let pos = openParen + 1;
|
|
691
|
-
|
|
692
|
-
while (pos < code.length && depth > 0) {
|
|
693
|
-
const ch = code[pos];
|
|
694
|
-
|
|
695
|
-
// Skip strings
|
|
696
|
-
if (
|
|
697
|
-
ch === '"' ||
|
|
698
|
-
ch === "`" ||
|
|
699
|
-
(ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
|
|
700
|
-
) {
|
|
701
|
-
pos = skipStringLiteral(code, pos);
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Line comment
|
|
706
|
-
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
|
|
707
|
-
pos += 2;
|
|
708
|
-
while (pos < code.length && code[pos] !== "\n") pos++;
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Block comment
|
|
713
|
-
if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
|
|
714
|
-
pos += 2;
|
|
715
|
-
while (
|
|
716
|
-
pos < code.length - 1 &&
|
|
717
|
-
!(code[pos] === "*" && code[pos + 1] === "/")
|
|
718
|
-
)
|
|
719
|
-
pos++;
|
|
720
|
-
pos += 2;
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
725
|
-
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
726
|
-
|
|
727
|
-
pos++;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return code.slice(openParen, pos);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ---------------------------------------------------------------------------
|
|
734
|
-
// Combined route map building
|
|
735
|
-
// ---------------------------------------------------------------------------
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Recursively build a route map from a urls module file.
|
|
739
|
-
* Extracts local path() routes and follows include() calls to sub-modules.
|
|
740
|
-
* Handles both imported and same-file variables.
|
|
741
|
-
*/
|
|
742
|
-
export function buildCombinedRouteMap(
|
|
743
|
-
filePath: string,
|
|
744
|
-
variableName?: string,
|
|
745
|
-
visited?: Set<string>
|
|
746
|
-
): Record<string, string> {
|
|
747
|
-
visited = visited ?? new Set();
|
|
748
|
-
const realPath = resolve(filePath);
|
|
749
|
-
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
750
|
-
if (visited.has(key)) return {};
|
|
751
|
-
visited.add(key);
|
|
752
|
-
|
|
753
|
-
let source: string;
|
|
754
|
-
try {
|
|
755
|
-
source = readFileSync(realPath, "utf-8");
|
|
756
|
-
} catch {
|
|
757
|
-
return {};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// If a specific variable is requested, extract just its urls() block
|
|
761
|
-
let block: string;
|
|
762
|
-
if (variableName) {
|
|
763
|
-
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
764
|
-
if (!extracted) return {};
|
|
765
|
-
block = extracted;
|
|
766
|
-
} else {
|
|
767
|
-
block = source;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
return buildRouteMapFromBlock(block, source, realPath, visited);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function buildRouteMapFromBlock(
|
|
774
|
-
block: string,
|
|
775
|
-
fullSource: string,
|
|
776
|
-
filePath: string,
|
|
777
|
-
visited: Set<string>,
|
|
778
|
-
searchSchemasOut?: Record<string, Record<string, string>>
|
|
779
|
-
): Record<string, string> {
|
|
780
|
-
const routeMap: Record<string, string> = {};
|
|
781
|
-
|
|
782
|
-
// Extract local path() routes
|
|
783
|
-
const localRoutes = extractRoutesFromSource(block);
|
|
784
|
-
for (const { name, pattern, search } of localRoutes) {
|
|
785
|
-
routeMap[name] = pattern;
|
|
786
|
-
if (search && searchSchemasOut) {
|
|
787
|
-
searchSchemasOut[name] = search;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Extract include() calls
|
|
792
|
-
const includes = extractIncludesFromSource(block);
|
|
793
|
-
for (const { pathPrefix, variableName, namePrefix } of includes) {
|
|
794
|
-
let childResult: { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> };
|
|
795
|
-
|
|
796
|
-
// Try import resolution first
|
|
797
|
-
const imported = resolveImportedVariable(fullSource, variableName);
|
|
798
|
-
if (imported) {
|
|
799
|
-
const targetFile = resolveImportPath(imported.specifier, filePath);
|
|
800
|
-
if (!targetFile) continue;
|
|
801
|
-
childResult = buildCombinedRouteMapWithSearch(
|
|
802
|
-
targetFile,
|
|
803
|
-
imported.exportedName,
|
|
804
|
-
visited
|
|
805
|
-
);
|
|
806
|
-
} else {
|
|
807
|
-
// Same-file variable
|
|
808
|
-
childResult = buildCombinedRouteMapWithSearch(filePath, variableName, visited);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Apply prefixes
|
|
812
|
-
for (const [name, pattern] of Object.entries(childResult.routes)) {
|
|
813
|
-
const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
|
|
814
|
-
let prefixedPattern: string;
|
|
815
|
-
if (pattern === "/") {
|
|
816
|
-
prefixedPattern = pathPrefix || "/";
|
|
817
|
-
} else if (pathPrefix.endsWith("/") && pattern.startsWith("/")) {
|
|
818
|
-
prefixedPattern = pathPrefix + pattern.slice(1);
|
|
819
|
-
} else {
|
|
820
|
-
prefixedPattern = pathPrefix + pattern;
|
|
821
|
-
}
|
|
822
|
-
routeMap[prefixedName] = prefixedPattern;
|
|
823
|
-
// Propagate search schemas with prefix
|
|
824
|
-
if (childResult.searchSchemas[name] && searchSchemasOut) {
|
|
825
|
-
searchSchemasOut[prefixedName] = childResult.searchSchemas[name];
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
return routeMap;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Build route map and search schemas together.
|
|
835
|
-
* Internal helper used by the include resolution path.
|
|
836
|
-
*/
|
|
837
|
-
function buildCombinedRouteMapWithSearch(
|
|
838
|
-
filePath: string,
|
|
839
|
-
variableName?: string,
|
|
840
|
-
visited?: Set<string>
|
|
841
|
-
): { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> } {
|
|
842
|
-
visited = visited ?? new Set();
|
|
843
|
-
const realPath = resolve(filePath);
|
|
844
|
-
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
845
|
-
if (visited.has(key)) return { routes: {}, searchSchemas: {} };
|
|
846
|
-
visited.add(key);
|
|
847
|
-
|
|
848
|
-
let source: string;
|
|
849
|
-
try {
|
|
850
|
-
source = readFileSync(realPath, "utf-8");
|
|
851
|
-
} catch {
|
|
852
|
-
return { routes: {}, searchSchemas: {} };
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
let block: string;
|
|
856
|
-
if (variableName) {
|
|
857
|
-
const extracted = extractUrlsBlockForVariable(source, variableName);
|
|
858
|
-
if (!extracted) return { routes: {}, searchSchemas: {} };
|
|
859
|
-
block = extracted;
|
|
860
|
-
} else {
|
|
861
|
-
block = source;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const searchSchemas: Record<string, Record<string, string>> = {};
|
|
865
|
-
const routes = buildRouteMapFromBlock(block, source, realPath, visited, searchSchemas);
|
|
866
|
-
return { routes, searchSchemas };
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// ---------------------------------------------------------------------------
|
|
870
|
-
// Router file URL extraction
|
|
871
|
-
// ---------------------------------------------------------------------------
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* Extract the url patterns variable from a router file.
|
|
875
|
-
* Looks for patterns like:
|
|
876
|
-
* .routes(variableName)
|
|
877
|
-
* urls: variableName
|
|
878
|
-
* Returns the local variable name and optional import info.
|
|
879
|
-
*/
|
|
880
|
-
function extractUrlsVariableFromRouter(
|
|
881
|
-
code: string
|
|
882
|
-
): string | null {
|
|
883
|
-
// Pattern 1: .routes(variableName) where variableName is an identifier (not a string)
|
|
884
|
-
const routesCallMatch = code.match(/\.routes\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/);
|
|
885
|
-
if (routesCallMatch) return routesCallMatch[1];
|
|
886
|
-
|
|
887
|
-
// Pattern 2: urls: variableName in createRouter options
|
|
888
|
-
const urlsOptionMatch = code.match(/urls\s*:\s*([a-zA-Z_$][\w$]*)/);
|
|
889
|
-
if (urlsOptionMatch) return urlsOptionMatch[1];
|
|
890
|
-
|
|
891
|
-
return null;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Resolve routes and search schemas from a router source file by following the
|
|
896
|
-
* variable passed to `.routes(...)` or `urls: ...` in createRouter options.
|
|
897
|
-
*/
|
|
898
|
-
export function buildCombinedRouteMapForRouterFile(
|
|
899
|
-
routerFilePath: string,
|
|
900
|
-
): { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> } {
|
|
901
|
-
let routerSource: string;
|
|
902
|
-
try {
|
|
903
|
-
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
904
|
-
} catch {
|
|
905
|
-
return { routes: {}, searchSchemas: {} };
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
909
|
-
if (!urlsVarName) {
|
|
910
|
-
return { routes: {}, searchSchemas: {} };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
914
|
-
if (imported) {
|
|
915
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
916
|
-
if (!targetFile) {
|
|
917
|
-
return { routes: {}, searchSchemas: {} };
|
|
918
|
-
}
|
|
919
|
-
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// ---------------------------------------------------------------------------
|
|
926
|
-
// Per-router named-routes.gen.ts writer
|
|
927
|
-
// ---------------------------------------------------------------------------
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Scan for files containing createRouter() and return their paths.
|
|
931
|
-
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
932
|
-
*/
|
|
933
|
-
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
934
|
-
const files = findTsFiles(root, filter);
|
|
935
|
-
const result: string[] = [];
|
|
936
|
-
for (const filePath of files) {
|
|
937
|
-
if (filePath.includes(".gen.")) continue;
|
|
938
|
-
try {
|
|
939
|
-
const source = readFileSync(filePath, "utf-8");
|
|
940
|
-
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
941
|
-
result.push(filePath);
|
|
942
|
-
}
|
|
943
|
-
} catch {
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
return result;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Generate per-router named-routes.gen.ts files from known router file paths.
|
|
952
|
-
* Re-reads each router file and resolves url patterns via static source parsing.
|
|
953
|
-
*
|
|
954
|
-
* Pass `knownRouterFiles` from a previous `findRouterFiles()` call to skip the
|
|
955
|
-
* full directory scan. If omitted, falls back to scanning (startup path).
|
|
956
|
-
*/
|
|
957
|
-
/**
|
|
958
|
-
* Write named-routes.gen.ts files from static source parsing.
|
|
959
|
-
* Dev-only: provides initial .gen.ts files for IDE types before runtime
|
|
960
|
-
* discovery runs. Must NOT be called during production builds — runtime
|
|
961
|
-
* discovery in buildStart produces the definitive file.
|
|
962
|
-
*/
|
|
963
|
-
export function writeCombinedRouteTypes(root: string, knownRouterFiles?: string[], opts?: { preserveIfLarger?: boolean }): void {
|
|
964
|
-
// Delete old combined named-routes.gen.ts if it exists (stale from older versions)
|
|
965
|
-
try {
|
|
966
|
-
const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
|
|
967
|
-
if (existsSync(oldCombinedPath)) {
|
|
968
|
-
unlinkSync(oldCombinedPath);
|
|
969
|
-
console.log(`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`);
|
|
970
|
-
}
|
|
971
|
-
} catch {}
|
|
972
|
-
|
|
973
|
-
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
974
|
-
if (routerFilePaths.length === 0) return;
|
|
975
|
-
|
|
976
|
-
for (const routerFilePath of routerFilePaths) {
|
|
977
|
-
let routerSource: string;
|
|
978
|
-
try {
|
|
979
|
-
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
980
|
-
} catch {
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
// Extract the urls variable name from .routes(varName) or urls: varName
|
|
984
|
-
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
985
|
-
if (!urlsVarName) continue;
|
|
986
|
-
|
|
987
|
-
// Resolve the variable to its source module
|
|
988
|
-
let result: { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> };
|
|
989
|
-
|
|
990
|
-
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
991
|
-
if (imported) {
|
|
992
|
-
// Variable is imported from another module
|
|
993
|
-
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
994
|
-
if (!targetFile) continue;
|
|
995
|
-
result = buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
996
|
-
} else {
|
|
997
|
-
// Variable is defined in the same file
|
|
998
|
-
result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const routerBasename = pathBasename(routerFilePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
1002
|
-
const outPath = join(dirname(routerFilePath), `${routerBasename}.named-routes.gen.ts`);
|
|
1003
|
-
const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
|
|
1004
|
-
|
|
1005
|
-
// When the static parser can't extract routes (e.g. callback-style urls()),
|
|
1006
|
-
// write an empty placeholder so the build-time transform's injected import
|
|
1007
|
-
// resolves. Runtime discovery will overwrite this with the real routes.
|
|
1008
|
-
if (Object.keys(result.routes).length === 0) {
|
|
1009
|
-
if (!existing) {
|
|
1010
|
-
const emptySource = generateRouteTypesSource({});
|
|
1011
|
-
writeFileSync(outPath, emptySource);
|
|
1012
|
-
}
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
const hasSearchSchemas = Object.keys(result.searchSchemas).length > 0;
|
|
1017
|
-
const source = generateRouteTypesSource(
|
|
1018
|
-
result.routes,
|
|
1019
|
-
hasSearchSchemas ? result.searchSchemas : undefined
|
|
1020
|
-
);
|
|
1021
|
-
if (existing !== source) {
|
|
1022
|
-
// On initial dev startup, don't overwrite a file from runtime discovery
|
|
1023
|
-
// (which has all dynamic routes) with a smaller set from the static
|
|
1024
|
-
// parser. The static parser can't see routes generated by Array.from()
|
|
1025
|
-
// or other dynamic code. During HMR (file watcher), always write so
|
|
1026
|
-
// newly added routes appear immediately.
|
|
1027
|
-
if (opts?.preserveIfLarger && existing) {
|
|
1028
|
-
const existingCount = (existing.match(/^\s+["a-zA-Z_$][^:]*:\s*"/gm) || []).length;
|
|
1029
|
-
const newCount = Object.keys(result.routes).length;
|
|
1030
|
-
if (existingCount > newCount) {
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
writeFileSync(outPath, source);
|
|
1035
|
-
console.log(`[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1
|
+
// Barrel re-export -- see route-types/ for implementations.
|
|
2
|
+
export {
|
|
3
|
+
extractParamsFromPattern,
|
|
4
|
+
formatRouteEntry,
|
|
5
|
+
} from "./route-types/param-extraction.js";
|
|
6
|
+
export { extractRoutesFromSource } from "./route-types/ast-route-extraction.js";
|
|
7
|
+
export {
|
|
8
|
+
generatePerModuleTypesSource,
|
|
9
|
+
generateRouteTypesSource,
|
|
10
|
+
} from "./route-types/codegen.js";
|
|
11
|
+
export {
|
|
12
|
+
DEFAULT_EXCLUDE_PATTERNS,
|
|
13
|
+
type ScanFilter,
|
|
14
|
+
createScanFilter,
|
|
15
|
+
findTsFiles,
|
|
16
|
+
} from "./route-types/scan-filter.js";
|
|
17
|
+
export {
|
|
18
|
+
writePerModuleRouteTypes,
|
|
19
|
+
writePerModuleRouteTypesForFile,
|
|
20
|
+
} from "./route-types/per-module-writer.js";
|
|
21
|
+
export {
|
|
22
|
+
type UnresolvableReason,
|
|
23
|
+
type UnresolvableInclude,
|
|
24
|
+
extractIncludesWithDiagnostics,
|
|
25
|
+
} from "./route-types/include-resolution.js";
|
|
26
|
+
export {
|
|
27
|
+
extractUrlsVariableFromRouter,
|
|
28
|
+
buildCombinedRouteMapForRouterFile,
|
|
29
|
+
detectUnresolvableIncludes,
|
|
30
|
+
detectUnresolvableIncludesForUrlsFile,
|
|
31
|
+
findNestedRouterConflict,
|
|
32
|
+
formatNestedRouterConflictError,
|
|
33
|
+
findRouterFiles,
|
|
34
|
+
writeCombinedRouteTypes,
|
|
35
|
+
} from "./route-types/router-processing.js";
|
|
36
|
+
export { findUrlsVariableNames } from "./route-types/per-module-writer.js";
|