@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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 +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -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 +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- 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/app-version.ts +14 -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 +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -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 +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- 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 +76 -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 +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +167 -309
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- 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 +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- package/src/router/content-negotiation.ts +215 -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 +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -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 +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- 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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -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 +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -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 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -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 +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -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 +462 -0
- package/src/vite/router-discovery.ts +918 -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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- 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/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,618 @@
|
|
|
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
|
+
* Result of extracting URL patterns from a router file.
|
|
161
|
+
* - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
|
|
162
|
+
* - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
|
|
163
|
+
*/
|
|
164
|
+
export type UrlsExtractionResult =
|
|
165
|
+
| { kind: "variable"; name: string }
|
|
166
|
+
| { kind: "inline"; block: string };
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract the url patterns from a router file using AST.
|
|
170
|
+
* Detects four patterns:
|
|
171
|
+
* 1. createRouter(...).routes(variableName)
|
|
172
|
+
* 2. createRouter({ urls: variableName, ... })
|
|
173
|
+
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
174
|
+
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
175
|
+
* Returns either a variable name or an inline code block.
|
|
176
|
+
*/
|
|
177
|
+
export function extractUrlsFromRouter(
|
|
178
|
+
code: string,
|
|
179
|
+
): UrlsExtractionResult | null {
|
|
180
|
+
const sourceFile = ts.createSourceFile(
|
|
181
|
+
"router.tsx",
|
|
182
|
+
code,
|
|
183
|
+
ts.ScriptTarget.Latest,
|
|
184
|
+
true,
|
|
185
|
+
ts.ScriptKind.TSX,
|
|
186
|
+
);
|
|
187
|
+
let result: UrlsExtractionResult | null = null;
|
|
188
|
+
|
|
189
|
+
function isCreateRouterCall(node: ts.Node): boolean {
|
|
190
|
+
if (!ts.isCallExpression(node)) return false;
|
|
191
|
+
const callee = node.expression;
|
|
192
|
+
return ts.isIdentifier(callee) && callee.text === "createRouter";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Check if a node is an arrow/function expression (inline builder). */
|
|
196
|
+
function isInlineBuilder(node: ts.Node): boolean {
|
|
197
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Check if a .routes() call chains from createRouter(). */
|
|
201
|
+
function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
|
|
202
|
+
if (
|
|
203
|
+
!ts.isPropertyAccessExpression(node.expression) ||
|
|
204
|
+
node.expression.name.text !== "routes"
|
|
205
|
+
)
|
|
206
|
+
return false;
|
|
207
|
+
let inner: ts.Expression = node.expression.expression;
|
|
208
|
+
while (
|
|
209
|
+
ts.isCallExpression(inner) &&
|
|
210
|
+
ts.isPropertyAccessExpression(inner.expression)
|
|
211
|
+
) {
|
|
212
|
+
inner = inner.expression.expression;
|
|
213
|
+
}
|
|
214
|
+
return isCreateRouterCall(inner);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function visit(node: ts.Node) {
|
|
218
|
+
if (result) return;
|
|
219
|
+
|
|
220
|
+
// Pattern 1 & 3: createRouter(...).routes(variableName | builder)
|
|
221
|
+
if (
|
|
222
|
+
ts.isCallExpression(node) &&
|
|
223
|
+
node.arguments.length >= 1 &&
|
|
224
|
+
isRoutesOnCreateRouter(node)
|
|
225
|
+
) {
|
|
226
|
+
const arg = node.arguments[0];
|
|
227
|
+
if (ts.isIdentifier(arg)) {
|
|
228
|
+
result = { kind: "variable", name: arg.text };
|
|
229
|
+
} else if (isInlineBuilder(arg)) {
|
|
230
|
+
result = { kind: "inline", block: arg.getText(sourceFile) };
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
|
|
236
|
+
if (isCreateRouterCall(node)) {
|
|
237
|
+
const callExpr = node as ts.CallExpression;
|
|
238
|
+
for (const callArg of callExpr.arguments) {
|
|
239
|
+
if (ts.isObjectLiteralExpression(callArg)) {
|
|
240
|
+
for (const prop of callArg.properties) {
|
|
241
|
+
if (
|
|
242
|
+
ts.isPropertyAssignment(prop) &&
|
|
243
|
+
ts.isIdentifier(prop.name) &&
|
|
244
|
+
prop.name.text === "urls"
|
|
245
|
+
) {
|
|
246
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
247
|
+
result = { kind: "variable", name: prop.initializer.text };
|
|
248
|
+
} else if (isInlineBuilder(prop.initializer)) {
|
|
249
|
+
result = {
|
|
250
|
+
kind: "inline",
|
|
251
|
+
block: prop.initializer.getText(sourceFile),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ts.forEachChild(node, visit);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
visit(sourceFile);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract the `basename` string literal from createRouter({ basename: "..." }).
|
|
270
|
+
* Returns the basename value or undefined if not present.
|
|
271
|
+
*/
|
|
272
|
+
export function extractBasenameFromRouter(code: string): string | undefined {
|
|
273
|
+
const sourceFile = ts.createSourceFile(
|
|
274
|
+
"router.tsx",
|
|
275
|
+
code,
|
|
276
|
+
ts.ScriptTarget.Latest,
|
|
277
|
+
true,
|
|
278
|
+
ts.ScriptKind.TSX,
|
|
279
|
+
);
|
|
280
|
+
let result: string | undefined;
|
|
281
|
+
|
|
282
|
+
function visit(node: ts.Node) {
|
|
283
|
+
if (result !== undefined) return;
|
|
284
|
+
if (
|
|
285
|
+
ts.isCallExpression(node) &&
|
|
286
|
+
ts.isIdentifier(node.expression) &&
|
|
287
|
+
node.expression.text === "createRouter"
|
|
288
|
+
) {
|
|
289
|
+
for (const arg of node.arguments) {
|
|
290
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
291
|
+
for (const prop of arg.properties) {
|
|
292
|
+
if (
|
|
293
|
+
ts.isPropertyAssignment(prop) &&
|
|
294
|
+
ts.isIdentifier(prop.name) &&
|
|
295
|
+
prop.name.text === "basename" &&
|
|
296
|
+
ts.isStringLiteral(prop.initializer)
|
|
297
|
+
) {
|
|
298
|
+
result = prop.initializer.text;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
ts.forEachChild(node, visit);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
visit(sourceFile);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** @deprecated Use extractUrlsFromRouter instead */
|
|
313
|
+
export function extractUrlsVariableFromRouter(code: string): string | null {
|
|
314
|
+
const result = extractUrlsFromRouter(code);
|
|
315
|
+
return result?.kind === "variable" ? result.name : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Apply a basename prefix to all route patterns in a result set. */
|
|
319
|
+
function applyBasenameToRoutes(
|
|
320
|
+
result: {
|
|
321
|
+
routes: Record<string, string>;
|
|
322
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
323
|
+
},
|
|
324
|
+
basename: string,
|
|
325
|
+
): {
|
|
326
|
+
routes: Record<string, string>;
|
|
327
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
328
|
+
} {
|
|
329
|
+
const prefixed: Record<string, string> = {};
|
|
330
|
+
for (const [name, pattern] of Object.entries(result.routes)) {
|
|
331
|
+
if (pattern === "/") {
|
|
332
|
+
prefixed[name] = basename;
|
|
333
|
+
} else if (basename.endsWith("/") && pattern.startsWith("/")) {
|
|
334
|
+
prefixed[name] = basename + pattern.slice(1);
|
|
335
|
+
} else {
|
|
336
|
+
prefixed[name] = basename + pattern;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { routes: prefixed, searchSchemas: result.searchSchemas };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Resolve routes and search schemas from a router source file by following the
|
|
344
|
+
* variable passed to `.routes(...)` or `urls: ...` in createRouter options,
|
|
345
|
+
* or by parsing an inline builder function directly.
|
|
346
|
+
*/
|
|
347
|
+
export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
|
|
348
|
+
routes: Record<string, string>;
|
|
349
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
350
|
+
} {
|
|
351
|
+
let routerSource: string;
|
|
352
|
+
try {
|
|
353
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
354
|
+
} catch {
|
|
355
|
+
return { routes: {}, searchSchemas: {} };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const extraction = extractUrlsFromRouter(routerSource);
|
|
359
|
+
if (!extraction) {
|
|
360
|
+
return { routes: {}, searchSchemas: {} };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Detect basename from createRouter({ basename: "..." })
|
|
364
|
+
const rawBasename = extractBasenameFromRouter(routerSource);
|
|
365
|
+
const basename = rawBasename
|
|
366
|
+
? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
|
|
367
|
+
: undefined;
|
|
368
|
+
|
|
369
|
+
let result: {
|
|
370
|
+
routes: Record<string, string>;
|
|
371
|
+
searchSchemas: Record<string, Record<string, string>>;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Inline builder: extract routes directly from the function body
|
|
375
|
+
if (extraction.kind === "inline") {
|
|
376
|
+
result = buildCombinedRouteMapWithSearch(
|
|
377
|
+
routerFilePath,
|
|
378
|
+
undefined,
|
|
379
|
+
undefined,
|
|
380
|
+
undefined,
|
|
381
|
+
extraction.block,
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
// Variable reference: follow imports or same-file declaration
|
|
385
|
+
const imported = resolveImportedVariable(routerSource, extraction.name);
|
|
386
|
+
if (imported) {
|
|
387
|
+
const targetFile = resolveImportPath(imported.specifier, routerFilePath);
|
|
388
|
+
if (!targetFile) {
|
|
389
|
+
return { routes: {}, searchSchemas: {} };
|
|
390
|
+
}
|
|
391
|
+
result = buildCombinedRouteMapWithSearch(
|
|
392
|
+
targetFile,
|
|
393
|
+
imported.exportedName,
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Apply basename prefix to all extracted route patterns
|
|
401
|
+
if (basename) {
|
|
402
|
+
result = applyBasenameToRoutes(result, basename);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Unresolvable include detection (full include tree walk)
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Walk the full include tree starting from a router file and detect
|
|
414
|
+
* all includes that the static parser cannot resolve.
|
|
415
|
+
* Returns an array of diagnostics; empty means fully resolvable.
|
|
416
|
+
*/
|
|
417
|
+
export function detectUnresolvableIncludes(
|
|
418
|
+
routerFilePath: string,
|
|
419
|
+
): UnresolvableInclude[] {
|
|
420
|
+
const realPath = resolve(routerFilePath);
|
|
421
|
+
let source: string;
|
|
422
|
+
try {
|
|
423
|
+
source = readFileSync(realPath, "utf-8");
|
|
424
|
+
} catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Extract the urls source from the router file
|
|
429
|
+
const extraction = extractUrlsFromRouter(source);
|
|
430
|
+
if (!extraction) return [];
|
|
431
|
+
|
|
432
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
433
|
+
|
|
434
|
+
if (extraction.kind === "inline") {
|
|
435
|
+
// Inline builder: parse directly
|
|
436
|
+
buildCombinedRouteMapWithSearch(
|
|
437
|
+
realPath,
|
|
438
|
+
undefined,
|
|
439
|
+
new Set(),
|
|
440
|
+
diagnostics,
|
|
441
|
+
extraction.block,
|
|
442
|
+
);
|
|
443
|
+
return diagnostics;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Variable reference: resolve where it comes from
|
|
447
|
+
const imported = resolveImportedVariable(source, extraction.name);
|
|
448
|
+
let targetFile: string;
|
|
449
|
+
let exportedName: string | undefined;
|
|
450
|
+
|
|
451
|
+
if (imported) {
|
|
452
|
+
const resolved = resolveImportPath(imported.specifier, realPath);
|
|
453
|
+
if (!resolved) {
|
|
454
|
+
return [
|
|
455
|
+
{
|
|
456
|
+
pathPrefix: "/",
|
|
457
|
+
namePrefix: null,
|
|
458
|
+
reason: "file-not-found",
|
|
459
|
+
sourceFile: realPath,
|
|
460
|
+
detail: `import "${imported.specifier}" resolved to no file`,
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
targetFile = resolved;
|
|
465
|
+
exportedName = imported.exportedName;
|
|
466
|
+
} else {
|
|
467
|
+
// Same-file urls() definition
|
|
468
|
+
targetFile = realPath;
|
|
469
|
+
exportedName = extraction.name;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
buildCombinedRouteMapWithSearch(
|
|
473
|
+
targetFile,
|
|
474
|
+
exportedName,
|
|
475
|
+
new Set(),
|
|
476
|
+
diagnostics,
|
|
477
|
+
);
|
|
478
|
+
return diagnostics;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Walk the include tree for a standalone urls() module file and detect
|
|
483
|
+
* all unresolvable includes. Mirrors detectUnresolvableIncludes() but
|
|
484
|
+
* operates on urls() variable declarations instead of going through
|
|
485
|
+
* createRouter().
|
|
486
|
+
*/
|
|
487
|
+
export function detectUnresolvableIncludesForUrlsFile(
|
|
488
|
+
filePath: string,
|
|
489
|
+
): UnresolvableInclude[] {
|
|
490
|
+
const realPath = resolve(filePath);
|
|
491
|
+
let source: string;
|
|
492
|
+
try {
|
|
493
|
+
source = readFileSync(realPath, "utf-8");
|
|
494
|
+
} catch {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const varNames = findUrlsVariableNames(source);
|
|
499
|
+
if (varNames.length === 0) return [];
|
|
500
|
+
|
|
501
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
502
|
+
for (const varName of varNames) {
|
|
503
|
+
buildCombinedRouteMapWithSearch(realPath, varName, new Set(), diagnostics);
|
|
504
|
+
}
|
|
505
|
+
return diagnostics;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Per-router named-routes.gen.ts writer
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Scan for files containing createRouter() and return their paths.
|
|
514
|
+
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
515
|
+
*/
|
|
516
|
+
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
517
|
+
const result: string[] = [];
|
|
518
|
+
findRouterFilesRecursive(root, filter, result);
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Write named-routes.gen.ts files from static source parsing.
|
|
524
|
+
* Dev-only: provides initial .gen.ts files for IDE types before runtime
|
|
525
|
+
* discovery runs. Must NOT be called during production builds -- runtime
|
|
526
|
+
* discovery in buildStart produces the definitive file.
|
|
527
|
+
*/
|
|
528
|
+
export function writeCombinedRouteTypes(
|
|
529
|
+
root: string,
|
|
530
|
+
knownRouterFiles?: string[],
|
|
531
|
+
opts?: { preserveIfLarger?: boolean },
|
|
532
|
+
): void {
|
|
533
|
+
// Delete old combined named-routes.gen.ts if it exists (stale from older versions)
|
|
534
|
+
try {
|
|
535
|
+
const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
|
|
536
|
+
if (existsSync(oldCombinedPath)) {
|
|
537
|
+
unlinkSync(oldCombinedPath);
|
|
538
|
+
console.log(
|
|
539
|
+
`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
} catch {}
|
|
543
|
+
|
|
544
|
+
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
545
|
+
if (routerFilePaths.length === 0) return;
|
|
546
|
+
|
|
547
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
|
|
548
|
+
if (nestedRouterConflict) {
|
|
549
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const routerFilePath of routerFilePaths) {
|
|
553
|
+
const result = buildCombinedRouteMapForRouterFile(routerFilePath);
|
|
554
|
+
if (
|
|
555
|
+
Object.keys(result.routes).length === 0 &&
|
|
556
|
+
Object.keys(result.searchSchemas).length === 0
|
|
557
|
+
) {
|
|
558
|
+
// Check if the file even has a createRouter call — if not, skip entirely.
|
|
559
|
+
// If it does, fall through to write an empty placeholder below.
|
|
560
|
+
let routerSource: string;
|
|
561
|
+
try {
|
|
562
|
+
routerSource = readFileSync(routerFilePath, "utf-8");
|
|
563
|
+
} catch {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (!extractUrlsFromRouter(routerSource)) continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const routerBasename = pathBasename(routerFilePath).replace(
|
|
570
|
+
/\.(tsx?|jsx?)$/,
|
|
571
|
+
"",
|
|
572
|
+
);
|
|
573
|
+
const outPath = join(
|
|
574
|
+
dirname(routerFilePath),
|
|
575
|
+
`${routerBasename}.named-routes.gen.ts`,
|
|
576
|
+
);
|
|
577
|
+
const existing = existsSync(outPath)
|
|
578
|
+
? readFileSync(outPath, "utf-8")
|
|
579
|
+
: null;
|
|
580
|
+
|
|
581
|
+
// When the static parser can't extract routes (e.g. callback-style urls()),
|
|
582
|
+
// write an empty placeholder so the build-time transform's injected import
|
|
583
|
+
// resolves. Runtime discovery will overwrite this with the real routes.
|
|
584
|
+
if (Object.keys(result.routes).length === 0) {
|
|
585
|
+
if (!existing) {
|
|
586
|
+
const emptySource = generateRouteTypesSource({});
|
|
587
|
+
writeFileSync(outPath, emptySource);
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const hasSearchSchemas = Object.keys(result.searchSchemas).length > 0;
|
|
593
|
+
const source = generateRouteTypesSource(
|
|
594
|
+
result.routes,
|
|
595
|
+
hasSearchSchemas ? result.searchSchemas : undefined,
|
|
596
|
+
);
|
|
597
|
+
if (existing !== source) {
|
|
598
|
+
// On initial dev startup, don't overwrite a file from runtime discovery
|
|
599
|
+
// (which has all dynamic routes) with a smaller set from the static
|
|
600
|
+
// parser. The static parser can't see routes generated by Array.from()
|
|
601
|
+
// or other dynamic code. During HMR (file watcher), always write so
|
|
602
|
+
// newly added routes appear immediately.
|
|
603
|
+
if (opts?.preserveIfLarger && existing) {
|
|
604
|
+
const existingCount = countPublicRouteEntries(existing);
|
|
605
|
+
const newCount = Object.keys(result.routes).filter(
|
|
606
|
+
(name) => !isAutoGeneratedRouteName(name),
|
|
607
|
+
).length;
|
|
608
|
+
if (existingCount > newCount) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
writeFileSync(outPath, source);
|
|
613
|
+
console.log(
|
|
614
|
+
`[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 (
|
|
65
|
+
entry.name === "node_modules" ||
|
|
66
|
+
entry.name.startsWith(".") ||
|
|
67
|
+
entry.name === "dist" ||
|
|
68
|
+
entry.name === "build" ||
|
|
69
|
+
entry.name === "coverage"
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
72
|
+
results.push(...findTsFiles(fullPath, filter));
|
|
73
|
+
} else if (
|
|
74
|
+
(entry.name.endsWith(".ts") ||
|
|
75
|
+
entry.name.endsWith(".tsx") ||
|
|
76
|
+
entry.name.endsWith(".js") ||
|
|
77
|
+
entry.name.endsWith(".jsx")) &&
|
|
78
|
+
!entry.name.includes(".gen.")
|
|
79
|
+
) {
|
|
80
|
+
if (filter && !filter(fullPath)) continue;
|
|
81
|
+
results.push(fullPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|