@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,479 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
join,
|
|
10
|
+
dirname,
|
|
11
|
+
resolve,
|
|
12
|
+
sep,
|
|
13
|
+
basename as pathBasename,
|
|
14
|
+
} from "node:path";
|
|
15
|
+
import ts from "typescript";
|
|
16
|
+
import { generateRouteTypesSource } from "./codegen.js";
|
|
17
|
+
import type { ScanFilter } from "./scan-filter.js";
|
|
18
|
+
import {
|
|
19
|
+
resolveImportedVariable,
|
|
20
|
+
resolveImportPath,
|
|
21
|
+
buildCombinedRouteMapWithSearch,
|
|
22
|
+
type UnresolvableInclude,
|
|
23
|
+
} from "./include-resolution.js";
|
|
24
|
+
import { findUrlsVariableNames } from "./per-module-writer.js";
|
|
25
|
+
import { isAutoGeneratedRouteName } from "../../route-name.js";
|
|
26
|
+
|
|
27
|
+
function countPublicRouteEntries(source: string): number {
|
|
28
|
+
const matches =
|
|
29
|
+
source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const match of matches) {
|
|
32
|
+
const routeName = match[1] || match[2];
|
|
33
|
+
if (routeName && !isAutoGeneratedRouteName(routeName.trim())) {
|
|
34
|
+
count++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
41
|
+
|
|
42
|
+
function isRoutableSourceFile(name: string): boolean {
|
|
43
|
+
return (
|
|
44
|
+
(name.endsWith(".ts") ||
|
|
45
|
+
name.endsWith(".tsx") ||
|
|
46
|
+
name.endsWith(".js") ||
|
|
47
|
+
name.endsWith(".jsx")) &&
|
|
48
|
+
!name.includes(".gen.") &&
|
|
49
|
+
!name.includes(".test.") &&
|
|
50
|
+
!name.includes(".spec.")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findRouterFilesRecursive(
|
|
55
|
+
dir: string,
|
|
56
|
+
filter: ScanFilter | undefined,
|
|
57
|
+
results: string[],
|
|
58
|
+
): void {
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const childDirs: string[] = [];
|
|
70
|
+
const routerFilesInDir: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const fullPath = join(dir, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (
|
|
76
|
+
entry.name === "node_modules" ||
|
|
77
|
+
entry.name === "dist" ||
|
|
78
|
+
entry.name === "coverage" ||
|
|
79
|
+
entry.name === "__tests__" ||
|
|
80
|
+
entry.name === "__mocks__" ||
|
|
81
|
+
entry.name.startsWith(".")
|
|
82
|
+
)
|
|
83
|
+
continue;
|
|
84
|
+
childDirs.push(fullPath);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isRoutableSourceFile(entry.name)) continue;
|
|
89
|
+
if (filter && !filter(fullPath)) continue;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const source = readFileSync(fullPath, "utf-8");
|
|
93
|
+
if (ROUTER_CALL_PATTERN.test(source)) {
|
|
94
|
+
routerFilesInDir.push(fullPath);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A directory that contains a router file is treated as a router root.
|
|
102
|
+
// Once found, deeper directories are skipped to avoid redundant scans.
|
|
103
|
+
if (routerFilesInDir.length > 0) {
|
|
104
|
+
results.push(...routerFilesInDir);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const childDir of childDirs) {
|
|
109
|
+
findRouterFilesRecursive(childDir, filter, results);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function findNestedRouterConflict(
|
|
114
|
+
routerFiles: string[],
|
|
115
|
+
): { ancestor: string; nested: string } | null {
|
|
116
|
+
const routerDirs = [
|
|
117
|
+
...new Set(routerFiles.map((filePath) => dirname(resolve(filePath)))),
|
|
118
|
+
].sort((a, b) => a.length - b.length);
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < routerDirs.length; i++) {
|
|
121
|
+
const ancestorDir = routerDirs[i];
|
|
122
|
+
const prefix = ancestorDir.endsWith(sep)
|
|
123
|
+
? ancestorDir
|
|
124
|
+
: `${ancestorDir}${sep}`;
|
|
125
|
+
for (let j = i + 1; j < routerDirs.length; j++) {
|
|
126
|
+
const nestedDir = routerDirs[j];
|
|
127
|
+
if (!nestedDir.startsWith(prefix)) continue;
|
|
128
|
+
const ancestorFile = routerFiles.find(
|
|
129
|
+
(filePath) => dirname(resolve(filePath)) === ancestorDir,
|
|
130
|
+
);
|
|
131
|
+
const nestedFile = routerFiles.find(
|
|
132
|
+
(filePath) => dirname(resolve(filePath)) === nestedDir,
|
|
133
|
+
);
|
|
134
|
+
if (ancestorFile && nestedFile) {
|
|
135
|
+
return { ancestor: ancestorFile, nested: nestedFile };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatNestedRouterConflictError(
|
|
144
|
+
conflict: { ancestor: string; nested: string },
|
|
145
|
+
prefix = "[rsc-router]",
|
|
146
|
+
): string {
|
|
147
|
+
return (
|
|
148
|
+
`${prefix} Nested router roots are not supported.\n` +
|
|
149
|
+
`Router root: ${conflict.ancestor}\n` +
|
|
150
|
+
`Nested router: ${conflict.nested}\n` +
|
|
151
|
+
`Move the nested router into a sibling directory or configure it as a separate app root.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Router file URL extraction
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extract the url patterns variable from a router file using AST.
|
|
161
|
+
* Detects two patterns:
|
|
162
|
+
* 1. createRouter(...).routes(variableName)
|
|
163
|
+
* 2. createRouter({ urls: variableName, ... })
|
|
164
|
+
* Returns the local variable name.
|
|
165
|
+
*/
|
|
166
|
+
export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
167
|
+
const sourceFile = ts.createSourceFile(
|
|
168
|
+
"router.tsx",
|
|
169
|
+
code,
|
|
170
|
+
ts.ScriptTarget.Latest,
|
|
171
|
+
true,
|
|
172
|
+
ts.ScriptKind.TSX,
|
|
173
|
+
);
|
|
174
|
+
let result: string | null = null;
|
|
175
|
+
|
|
176
|
+
function isCreateRouterCall(node: ts.Node): boolean {
|
|
177
|
+
if (!ts.isCallExpression(node)) return false;
|
|
178
|
+
const callee = node.expression;
|
|
179
|
+
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function visit(node: ts.Node) {
|
|
183
|
+
if (result) return;
|
|
184
|
+
|
|
185
|
+
// Pattern 1: createRouter(...).routes(variableName)
|
|
186
|
+
// The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
|
|
187
|
+
if (
|
|
188
|
+
ts.isCallExpression(node) &&
|
|
189
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
190
|
+
node.expression.name.text === "routes" &&
|
|
191
|
+
node.arguments.length >= 1 &&
|
|
192
|
+
ts.isIdentifier(node.arguments[0])
|
|
193
|
+
) {
|
|
194
|
+
// Walk up the chain: createRouter().middleware(...).routes(x) etc.
|
|
195
|
+
// The innermost call should be createRouter(...)
|
|
196
|
+
let inner: ts.Expression = node.expression.expression;
|
|
197
|
+
while (
|
|
198
|
+
ts.isCallExpression(inner) &&
|
|
199
|
+
ts.isPropertyAccessExpression(inner.expression)
|
|
200
|
+
) {
|
|
201
|
+
inner = inner.expression.expression;
|
|
202
|
+
}
|
|
203
|
+
if (isCreateRouterCall(inner)) {
|
|
204
|
+
result = (node.arguments[0] as ts.Identifier).text;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Pattern 2: createRouter({ urls: variableName, ... })
|
|
210
|
+
if (isCreateRouterCall(node)) {
|
|
211
|
+
const callExpr = node as ts.CallExpression;
|
|
212
|
+
for (const arg of callExpr.arguments) {
|
|
213
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
214
|
+
for (const prop of arg.properties) {
|
|
215
|
+
if (
|
|
216
|
+
ts.isPropertyAssignment(prop) &&
|
|
217
|
+
ts.isIdentifier(prop.name) &&
|
|
218
|
+
prop.name.text === "urls" &&
|
|
219
|
+
ts.isIdentifier(prop.initializer)
|
|
220
|
+
) {
|
|
221
|
+
result = prop.initializer.text;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ts.forEachChild(node, visit);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
visit(sourceFile);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve routes and search schemas from a router source file by following the
|
|
238
|
+
* variable passed to `.routes(...)` or `urls: ...` in createRouter options.
|
|
239
|
+
*/
|
|
240
|
+
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
241
|
+
routes: Record<string, string>;
|
|
242
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
243
|
+
} {
|
|
244
|
+
let routerSource: string;
|
|
245
|
+
try {
|
|
246
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
247
|
+
} catch {
|
|
248
|
+
return { routes: {}, searchSchemas: {} };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
252
|
+
if (!urlsVarName) {
|
|
253
|
+
return { routes: {}, searchSchemas: {} };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
257
|
+
if (imported) {
|
|
258
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
259
|
+
if (!targetFile) {
|
|
260
|
+
return { routes: {}, searchSchemas: {} };
|
|
261
|
+
}
|
|
262
|
+
return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Unresolvable include detection (full include tree walk)
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Walk the full include tree starting from a router file and detect
|
|
274
|
+
* all includes that the static parser cannot resolve.
|
|
275
|
+
* Returns an array of diagnostics; empty means fully resolvable.
|
|
276
|
+
*/
|
|
277
|
+
export function detectUnresolvableIncludes(
|
|
278
|
+
routerFilePath: string,
|
|
279
|
+
): UnresolvableInclude[] {
|
|
280
|
+
const realPath = resolve(routerFilePath);
|
|
281
|
+
let source: string;
|
|
282
|
+
try {
|
|
283
|
+
source = readFileSync(realPath, "utf-8");
|
|
284
|
+
} catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Extract the urls variable from the router file
|
|
289
|
+
const urlsVarName = extractUrlsVariableFromRouter(source);
|
|
290
|
+
if (!urlsVarName) return [];
|
|
291
|
+
|
|
292
|
+
// Resolve where the urls variable comes from
|
|
293
|
+
const imported = resolveImportedVariable(source, urlsVarName);
|
|
294
|
+
let targetFile: string;
|
|
295
|
+
let exportedName: string | undefined;
|
|
296
|
+
|
|
297
|
+
if (imported) {
|
|
298
|
+
const resolved = resolveImportPath(imported.specifier, realPath);
|
|
299
|
+
if (!resolved) {
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
pathPrefix: "/",
|
|
303
|
+
namePrefix: null,
|
|
304
|
+
reason: "file-not-found",
|
|
305
|
+
sourceFile: realPath,
|
|
306
|
+
detail: `import "${imported.specifier}" resolved to no file`,
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
targetFile = resolved;
|
|
311
|
+
exportedName = imported.exportedName;
|
|
312
|
+
} else {
|
|
313
|
+
// Same-file urls() definition
|
|
314
|
+
targetFile = realPath;
|
|
315
|
+
exportedName = urlsVarName;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
319
|
+
buildCombinedRouteMapWithSearch(
|
|
320
|
+
targetFile,
|
|
321
|
+
exportedName,
|
|
322
|
+
new Set(),
|
|
323
|
+
diagnostics,
|
|
324
|
+
);
|
|
325
|
+
return diagnostics;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Walk the include tree for a standalone urls() module file and detect
|
|
330
|
+
* all unresolvable includes. Mirrors detectUnresolvableIncludes() but
|
|
331
|
+
* operates on urls() variable declarations instead of going through
|
|
332
|
+
* createRouter().
|
|
333
|
+
*/
|
|
334
|
+
export function detectUnresolvableIncludesForUrlsFile(
|
|
335
|
+
filePath: string,
|
|
336
|
+
): UnresolvableInclude[] {
|
|
337
|
+
const realPath = resolve(filePath);
|
|
338
|
+
let source: string;
|
|
339
|
+
try {
|
|
340
|
+
source = readFileSync(realPath, "utf-8");
|
|
341
|
+
} catch {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const varNames = findUrlsVariableNames(source);
|
|
346
|
+
if (varNames.length === 0) return [];
|
|
347
|
+
|
|
348
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
349
|
+
for (const varName of varNames) {
|
|
350
|
+
buildCombinedRouteMapWithSearch(realPath, varName, new Set(), diagnostics);
|
|
351
|
+
}
|
|
352
|
+
return diagnostics;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Per-router named-routes.gen.ts writer
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Scan for files containing createRouter() and return their paths.
|
|
361
|
+
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
362
|
+
*/
|
|
363
|
+
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
364
|
+
const result: string[] = [];
|
|
365
|
+
findRouterFilesRecursive(root, filter, result);
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Write named-routes.gen.ts files from static source parsing.
|
|
371
|
+
* Dev-only: provides initial .gen.ts files for IDE types before runtime
|
|
372
|
+
* discovery runs. Must NOT be called during production builds -- runtime
|
|
373
|
+
* discovery in buildStart produces the definitive file.
|
|
374
|
+
*/
|
|
375
|
+
export function writeCombinedRouteTypes(
|
|
376
|
+
root: string,
|
|
377
|
+
knownRouterFiles?: string[],
|
|
378
|
+
opts?: { preserveIfLarger?: boolean },
|
|
379
|
+
): void {
|
|
380
|
+
// Delete old combined named-routes.gen.ts if it exists (stale from older versions)
|
|
381
|
+
try {
|
|
382
|
+
const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
|
|
383
|
+
if (existsSync(oldCombinedPath)) {
|
|
384
|
+
unlinkSync(oldCombinedPath);
|
|
385
|
+
console.log(
|
|
386
|
+
`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} catch {}
|
|
390
|
+
|
|
391
|
+
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
392
|
+
if (routerFilePaths.length === 0) return;
|
|
393
|
+
|
|
394
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
|
|
395
|
+
if (nestedRouterConflict) {
|
|
396
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const routerFilePath of routerFilePaths) {
|
|
400
|
+
let routerSource: string;
|
|
401
|
+
try {
|
|
402
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
403
|
+
} catch {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Extract the urls variable name from .routes(varName) or urls: varName
|
|
407
|
+
const urlsVarName = extractUrlsVariableFromRouter(routerSource);
|
|
408
|
+
if (!urlsVarName) continue;
|
|
409
|
+
|
|
410
|
+
// Resolve the variable to its source module
|
|
411
|
+
let result: {
|
|
412
|
+
routes: Record<string, string>;
|
|
413
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const imported = resolveImportedVariable(routerSource, urlsVarName);
|
|
417
|
+
if (imported) {
|
|
418
|
+
// Variable is imported from another module
|
|
419
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
420
|
+
if (!targetFile) continue;
|
|
421
|
+
result = buildCombinedRouteMapWithSearch(
|
|
422
|
+
targetFile,
|
|
423
|
+
imported.exportedName,
|
|
424
|
+
);
|
|
425
|
+
} else {
|
|
426
|
+
// Variable is defined in the same file
|
|
427
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const routerBasename = pathBasename(routerFilePath).replace(
|
|
431
|
+
/\.(tsx?|jsx?)$/,
|
|
432
|
+
"",
|
|
433
|
+
);
|
|
434
|
+
const outPath = join(
|
|
435
|
+
dirname(routerFilePath),
|
|
436
|
+
`${routerBasename}.named-routes.gen.ts`,
|
|
437
|
+
);
|
|
438
|
+
const existing = existsSync(outPath)
|
|
439
|
+
? readFileSync(outPath, "utf-8")
|
|
440
|
+
: null;
|
|
441
|
+
|
|
442
|
+
// When the static parser can't extract routes (e.g. callback-style urls()),
|
|
443
|
+
// write an empty placeholder so the build-time transform's injected import
|
|
444
|
+
// resolves. Runtime discovery will overwrite this with the real routes.
|
|
445
|
+
if (Object.keys(result.routes).length === 0) {
|
|
446
|
+
if (!existing) {
|
|
447
|
+
const emptySource = generateRouteTypesSource({});
|
|
448
|
+
writeFileSync(outPath, emptySource);
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const hasSearchSchemas = Object.keys(result.searchSchemas).length > 0;
|
|
454
|
+
const source = generateRouteTypesSource(
|
|
455
|
+
result.routes,
|
|
456
|
+
hasSearchSchemas ? result.searchSchemas : undefined,
|
|
457
|
+
);
|
|
458
|
+
if (existing !== source) {
|
|
459
|
+
// On initial dev startup, don't overwrite a file from runtime discovery
|
|
460
|
+
// (which has all dynamic routes) with a smaller set from the static
|
|
461
|
+
// parser. The static parser can't see routes generated by Array.from()
|
|
462
|
+
// or other dynamic code. During HMR (file watcher), always write so
|
|
463
|
+
// newly added routes appear immediately.
|
|
464
|
+
if (opts?.preserveIfLarger && existing) {
|
|
465
|
+
const existingCount = countPublicRouteEntries(existing);
|
|
466
|
+
const newCount = Object.keys(result.routes).filter(
|
|
467
|
+
(name) => !isAutoGeneratedRouteName(name),
|
|
468
|
+
).length;
|
|
469
|
+
if (existingCount > newCount) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
writeFileSync(outPath, source);
|
|
474
|
+
console.log(
|
|
475
|
+
`[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
// @ts-ignore -- picomatch ships no .d.ts; types are trivial
|
|
4
|
+
import picomatch from "picomatch";
|
|
5
|
+
|
|
6
|
+
/** Default exclude patterns for route type scanning. */
|
|
7
|
+
export const DEFAULT_EXCLUDE_PATTERNS: string[] = [
|
|
8
|
+
"**/__tests__/**",
|
|
9
|
+
"**/__mocks__/**",
|
|
10
|
+
"**/dist/**",
|
|
11
|
+
"**/coverage/**",
|
|
12
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
13
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export type ScanFilter = (absolutePath: string) => boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compile include/exclude glob patterns into a single predicate.
|
|
20
|
+
* Paths are made root-relative before matching.
|
|
21
|
+
* Returns undefined when no filtering is needed (no include, default exclude).
|
|
22
|
+
*/
|
|
23
|
+
export function createScanFilter(
|
|
24
|
+
root: string,
|
|
25
|
+
opts: { include?: string[]; exclude?: string[] },
|
|
26
|
+
): ScanFilter | undefined {
|
|
27
|
+
const { include, exclude } = opts;
|
|
28
|
+
const hasInclude = include && include.length > 0;
|
|
29
|
+
const hasCustomExclude = exclude !== undefined;
|
|
30
|
+
|
|
31
|
+
if (!hasInclude && !hasCustomExclude) return undefined;
|
|
32
|
+
|
|
33
|
+
const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
34
|
+
const includeMatcher = hasInclude ? picomatch(include) : null;
|
|
35
|
+
const excludeMatcher =
|
|
36
|
+
effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
|
|
37
|
+
|
|
38
|
+
return (absolutePath: string) => {
|
|
39
|
+
const rel = relative(root, absolutePath);
|
|
40
|
+
if (excludeMatcher && excludeMatcher(rel)) return false;
|
|
41
|
+
if (includeMatcher) return includeMatcher(rel);
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Recursively find .ts/.tsx files under a directory, skipping node_modules
|
|
48
|
+
* and .gen. files.
|
|
49
|
+
*/
|
|
50
|
+
export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
51
|
+
const results: string[] = [];
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.warn(
|
|
57
|
+
`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
58
|
+
);
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const fullPath = join(dir, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
65
|
+
results.push(...findTsFiles(fullPath, filter));
|
|
66
|
+
} else if (
|
|
67
|
+
(entry.name.endsWith(".ts") ||
|
|
68
|
+
entry.name.endsWith(".tsx") ||
|
|
69
|
+
entry.name.endsWith(".js") ||
|
|
70
|
+
entry.name.endsWith(".jsx")) &&
|
|
71
|
+
!entry.name.includes(".gen.")
|
|
72
|
+
) {
|
|
73
|
+
if (filter && !filter(fullPath)) continue;
|
|
74
|
+
results.push(fullPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|