@rangojs/router 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +62 -0
- package/dist/types/client-urls/types.d.ts +144 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +10 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/vite/index.js +5313 -2365
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +698 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +579 -0
- package/src/client-urls/types.ts +195 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +47 -0
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +14 -24
- package/src/router/segment-resolution/revalidation.ts +20 -1
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +14 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { normalizePath, parseAst } from "vite";
|
|
4
|
+
import {
|
|
5
|
+
serializeClientUrlPatterns,
|
|
6
|
+
type ClientUrlProjection,
|
|
7
|
+
} from "../../client-urls/server-projection.js";
|
|
8
|
+
import type { ClientUrlPatterns } from "../../client-urls/types.js";
|
|
9
|
+
import {
|
|
10
|
+
computeProductionHash,
|
|
11
|
+
hashRefKey,
|
|
12
|
+
} from "../plugins/client-ref-hashing.js";
|
|
13
|
+
import { hasUseClientDirective } from "../utils/directive-prologue.js";
|
|
14
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
15
|
+
import { findTsFiles } from "../../build/route-types/scan-filter.js";
|
|
16
|
+
import type { DiscoveryState } from "./state.js";
|
|
17
|
+
|
|
18
|
+
const debug = createRangoDebugger(NS.discovery);
|
|
19
|
+
|
|
20
|
+
interface ClientUrlSsrEnvironment {
|
|
21
|
+
readonly runner?: {
|
|
22
|
+
import(id: string): Promise<Record<string, unknown>>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ClientUrlProjectionServerModule {
|
|
27
|
+
clearClientUrlProjections(): void;
|
|
28
|
+
setClientUrlProjection(
|
|
29
|
+
referenceId: string,
|
|
30
|
+
projection: ClientUrlProjection,
|
|
31
|
+
): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasDefaultExport(code: string): boolean {
|
|
35
|
+
let program: { body?: any[] };
|
|
36
|
+
try {
|
|
37
|
+
program = parseAst(code, { lang: "tsx" }) as { body?: any[] };
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const node of program.body ?? []) {
|
|
43
|
+
if (node?.type === "ExportDefaultDeclaration") return true;
|
|
44
|
+
if (node?.type !== "ExportNamedDeclaration") continue;
|
|
45
|
+
for (const specifier of node.specifiers ?? []) {
|
|
46
|
+
const exportedName =
|
|
47
|
+
specifier?.exported?.name ?? specifier?.exported?.value;
|
|
48
|
+
if (exportedName === "default") return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeSource(state: DiscoveryState, id: string): string {
|
|
55
|
+
const cleanId = id.split("?", 1)[0];
|
|
56
|
+
const absoluteId = isAbsolute(cleanId)
|
|
57
|
+
? cleanId
|
|
58
|
+
: resolve(state.projectRoot, cleanId);
|
|
59
|
+
return normalizePath(absoluteId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function referenceId(referenceKey: string): string {
|
|
63
|
+
return `${referenceKey}#default`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function withoutReferenceQuery(reference: string): string {
|
|
67
|
+
const exportIndex = reference.lastIndexOf("#");
|
|
68
|
+
const moduleId =
|
|
69
|
+
exportIndex === -1 ? reference : reference.slice(0, exportIndex);
|
|
70
|
+
const exportName = exportIndex === -1 ? "" : reference.slice(exportIndex);
|
|
71
|
+
const queryIndex = moduleId.indexOf("?");
|
|
72
|
+
return `${queryIndex === -1 ? moduleId : moduleId.slice(0, queryIndex)}${exportName}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function recordClientUrlsModule(
|
|
76
|
+
state: DiscoveryState,
|
|
77
|
+
code: string,
|
|
78
|
+
id: string,
|
|
79
|
+
): void {
|
|
80
|
+
if (
|
|
81
|
+
!code.includes("clientUrls(") ||
|
|
82
|
+
!hasUseClientDirective(code) ||
|
|
83
|
+
!hasDefaultExport(code)
|
|
84
|
+
) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const source = normalizeSource(state, id);
|
|
89
|
+
const projectRoot = normalizePath(resolve(state.projectRoot));
|
|
90
|
+
const relativeId = normalizePath(relative(projectRoot, source));
|
|
91
|
+
const outsideProject =
|
|
92
|
+
relativeId === ".." ||
|
|
93
|
+
relativeId.startsWith("../") ||
|
|
94
|
+
isAbsolute(relativeId);
|
|
95
|
+
const devReferenceKey = outsideProject ? `/@fs${source}` : `/${relativeId}`;
|
|
96
|
+
|
|
97
|
+
const sourceByReferenceId = (state.clientUrlSourceByReferenceId ??=
|
|
98
|
+
new Map());
|
|
99
|
+
sourceByReferenceId.set(referenceId(devReferenceKey), source);
|
|
100
|
+
|
|
101
|
+
const productionReferenceKeys = new Set([
|
|
102
|
+
hashRefKey(relativeId),
|
|
103
|
+
computeProductionHash(projectRoot, devReferenceKey),
|
|
104
|
+
]);
|
|
105
|
+
for (const productionReferenceKey of productionReferenceKeys) {
|
|
106
|
+
sourceByReferenceId.set(referenceId(productionReferenceKey), source);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveClientUrlsSource(
|
|
111
|
+
state: DiscoveryState,
|
|
112
|
+
referenceId: string,
|
|
113
|
+
): string | undefined {
|
|
114
|
+
return state.clientUrlSourceByReferenceId?.get(
|
|
115
|
+
withoutReferenceQuery(referenceId),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Deterministic filesystem discovery of clientUrls modules. Transform-based
|
|
121
|
+
* recording only sees modules that some environment actually loads — a
|
|
122
|
+
* clientUrls module not yet reachable from any transformed graph at discovery
|
|
123
|
+
* time (cold ordering, a mount added before its first import is served) would
|
|
124
|
+
* never be recorded and its include would fail with empty projections. Walking
|
|
125
|
+
* the project source (the same walk static route-type generation performs)
|
|
126
|
+
* makes recording independent of module-graph timing. The transform hooks
|
|
127
|
+
* still record too — they refresh dev HMR edits between scans.
|
|
128
|
+
*/
|
|
129
|
+
export function scanForClientUrlModules(state: DiscoveryState): void {
|
|
130
|
+
if (!state.projectRoot) return;
|
|
131
|
+
for (const filePath of findTsFiles(state.projectRoot, state.scanFilter)) {
|
|
132
|
+
let code: string;
|
|
133
|
+
try {
|
|
134
|
+
code = readFileSync(filePath, "utf-8");
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
recordClientUrlsModule(state, code, filePath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function projectionError(
|
|
143
|
+
referenceId: string,
|
|
144
|
+
source: string | undefined,
|
|
145
|
+
message: string,
|
|
146
|
+
cause?: unknown,
|
|
147
|
+
): Error {
|
|
148
|
+
const sourceDescription = source ? ` from source "${source}"` : "";
|
|
149
|
+
const errorMessage = `Cannot discover client URL projection for reference "${referenceId}"${sourceDescription}: ${message}`;
|
|
150
|
+
return cause === undefined
|
|
151
|
+
? new Error(errorMessage)
|
|
152
|
+
: new Error(errorMessage, { cause });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Lenient pre-entry projection refresh. Re-serializes every RECORDED clientUrls
|
|
157
|
+
* module before the discovery pass imports the entry, so routers re-created by
|
|
158
|
+
* that import materialize against the CURRENT module contents.
|
|
159
|
+
*
|
|
160
|
+
* Scar: node-preset HMR served stale client-urls routes after a route-shape
|
|
161
|
+
* edit — the clientUrls module is an HMR-accepted client boundary in the rsc
|
|
162
|
+
* graph, and the strict registry-driven pass below only runs AFTER the entry
|
|
163
|
+
* import, so `.routes(reference)` materialized the previous pass's projection.
|
|
164
|
+
*
|
|
165
|
+
* Errors are deliberately swallowed per source (a module mid-edit keeps its
|
|
166
|
+
* last-known projection); the strict discoverClientUrlProjections() pass still
|
|
167
|
+
* surfaces real failures afterwards. No-ops on cold start (nothing recorded
|
|
168
|
+
* yet) and when the SSR runner is unavailable.
|
|
169
|
+
*/
|
|
170
|
+
export async function refreshRecordedClientUrlProjections(
|
|
171
|
+
state: DiscoveryState,
|
|
172
|
+
ssrEnv: ClientUrlSsrEnvironment | undefined,
|
|
173
|
+
serverMod: ClientUrlProjectionServerModule,
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
const sourceByReferenceId = state.clientUrlSourceByReferenceId;
|
|
176
|
+
if (
|
|
177
|
+
!sourceByReferenceId?.size ||
|
|
178
|
+
typeof ssrEnv?.runner?.import !== "function"
|
|
179
|
+
) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const referenceIdsBySource = new Map<string, string[]>();
|
|
184
|
+
for (const [refId, source] of sourceByReferenceId) {
|
|
185
|
+
const ids = referenceIdsBySource.get(source);
|
|
186
|
+
if (ids) {
|
|
187
|
+
ids.push(refId);
|
|
188
|
+
} else {
|
|
189
|
+
referenceIdsBySource.set(source, [refId]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [source, referenceIds] of referenceIdsBySource) {
|
|
194
|
+
let definition: unknown;
|
|
195
|
+
try {
|
|
196
|
+
definition = (await ssrEnv.runner.import(source)).default;
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
(typeof definition !== "object" && typeof definition !== "function") ||
|
|
202
|
+
definition === null ||
|
|
203
|
+
(definition as { __brand?: unknown }).__brand !== "client-urls"
|
|
204
|
+
) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const projection = serializeClientUrlPatterns(
|
|
208
|
+
definition as ClientUrlPatterns,
|
|
209
|
+
);
|
|
210
|
+
debug?.(
|
|
211
|
+
"pre-entry refresh %s -> [%s]",
|
|
212
|
+
source,
|
|
213
|
+
projection.routes.map((route) => route.pattern).join(", "),
|
|
214
|
+
);
|
|
215
|
+
for (const referenceId of referenceIds) {
|
|
216
|
+
serverMod.setClientUrlProjection(referenceId, projection);
|
|
217
|
+
// The eager routes-manifest virtual module REPLAYS this state map
|
|
218
|
+
// (clearClientUrlProjections + set) whenever it re-evaluates — and the
|
|
219
|
+
// watcher's gen-file write invalidates it right before the entry import,
|
|
220
|
+
// so a stale map here would clobber the registry install above before
|
|
221
|
+
// the router materializes its mount. Keep both in sync.
|
|
222
|
+
state.clientUrlProjectionMap?.set(referenceId, projection);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Strict projection discovery over every RECORDED clientUrls module. Recorded
|
|
229
|
+
* modules — not router references — are the source of truth: include() mounts
|
|
230
|
+
* materialize lazily from the projection registry, so there is no router-level
|
|
231
|
+
* reference list to consult. Serializes every source before atomically
|
|
232
|
+
* installing the new projections (a failure preserves last-known state).
|
|
233
|
+
*
|
|
234
|
+
* A recorded source whose file no longer exists is dropped silently
|
|
235
|
+
* (recordClientUrlsModule never un-records, so deletions must self-heal here).
|
|
236
|
+
* A present-but-broken module is a hard error: it is part of the app graph.
|
|
237
|
+
*/
|
|
238
|
+
export async function discoverClientUrlProjections(
|
|
239
|
+
state: DiscoveryState,
|
|
240
|
+
ssrEnv: ClientUrlSsrEnvironment | undefined,
|
|
241
|
+
serverMod: ClientUrlProjectionServerModule,
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
scanForClientUrlModules(state);
|
|
244
|
+
const sourceByReferenceId = state.clientUrlSourceByReferenceId;
|
|
245
|
+
if (!sourceByReferenceId?.size) {
|
|
246
|
+
serverMod.clearClientUrlProjections();
|
|
247
|
+
state.clientUrlProjectionMap = new Map();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const referenceIdsBySource = new Map<string, string[]>();
|
|
252
|
+
for (const [referenceId, source] of sourceByReferenceId) {
|
|
253
|
+
const ids = referenceIdsBySource.get(source);
|
|
254
|
+
if (ids) {
|
|
255
|
+
ids.push(referenceId);
|
|
256
|
+
} else {
|
|
257
|
+
referenceIdsBySource.set(source, [referenceId]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof ssrEnv?.runner?.import !== "function") {
|
|
262
|
+
const [source, ids] = referenceIdsBySource.entries().next().value as [
|
|
263
|
+
string,
|
|
264
|
+
string[],
|
|
265
|
+
];
|
|
266
|
+
throw projectionError(
|
|
267
|
+
ids[0],
|
|
268
|
+
source,
|
|
269
|
+
"the SSR module runner is unavailable",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const nextProjections = new Map<string, ClientUrlProjection>();
|
|
274
|
+
const removedSources: string[] = [];
|
|
275
|
+
for (const [source, referenceIds] of referenceIdsBySource) {
|
|
276
|
+
let module: Record<string, unknown>;
|
|
277
|
+
try {
|
|
278
|
+
module = await ssrEnv.runner.import(source);
|
|
279
|
+
} catch (cause) {
|
|
280
|
+
if (!existsSync(source)) {
|
|
281
|
+
removedSources.push(source);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
throw projectionError(
|
|
285
|
+
referenceIds[0],
|
|
286
|
+
source,
|
|
287
|
+
"failed to import the source module",
|
|
288
|
+
cause,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const definition = module.default;
|
|
292
|
+
if (
|
|
293
|
+
(typeof definition !== "object" && typeof definition !== "function") ||
|
|
294
|
+
definition === null ||
|
|
295
|
+
(definition as { __brand?: unknown }).__brand !== "client-urls"
|
|
296
|
+
) {
|
|
297
|
+
throw projectionError(
|
|
298
|
+
referenceIds[0],
|
|
299
|
+
source,
|
|
300
|
+
'the module default export must be created by clientUrls() and have __brand === "client-urls"',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const projection = serializeClientUrlPatterns(
|
|
304
|
+
definition as ClientUrlPatterns,
|
|
305
|
+
);
|
|
306
|
+
for (const referenceId of referenceIds) {
|
|
307
|
+
nextProjections.set(referenceId, projection);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const source of removedSources) {
|
|
312
|
+
for (const [referenceId, recorded] of sourceByReferenceId) {
|
|
313
|
+
if (recorded === source) sourceByReferenceId.delete(referenceId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
serverMod.clearClientUrlProjections();
|
|
318
|
+
for (const [id, projection] of nextProjections) {
|
|
319
|
+
serverMod.setClientUrlProjection(id, projection);
|
|
320
|
+
}
|
|
321
|
+
state.clientUrlProjectionMap = nextProjections;
|
|
322
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
// RangoContext from the runner realm) — see the runner.import below.
|
|
22
22
|
import { buildPerRouterTrie } from "../../build/route-trie.js";
|
|
23
23
|
import { collectFallbackClientRefs } from "../../build/collect-fallback-refs.js";
|
|
24
|
+
import { mergeFullManifests } from "../../build/merge-full-manifests.js";
|
|
24
25
|
import { flattenLeafEntries } from "../utils/manifest-utils.js";
|
|
25
26
|
import type { DiscoveryState, PrecomputedEntry } from "./state.js";
|
|
26
27
|
import {
|
|
@@ -34,6 +35,10 @@ import {
|
|
|
34
35
|
} from "./discovery-errors.js";
|
|
35
36
|
import { createRangoDebugger, timed, NS } from "../debug.js";
|
|
36
37
|
import { computeProductionHash } from "../plugins/client-ref-hashing.js";
|
|
38
|
+
import {
|
|
39
|
+
discoverClientUrlProjections,
|
|
40
|
+
refreshRecordedClientUrlProjections,
|
|
41
|
+
} from "./client-urls-projection.js";
|
|
37
42
|
|
|
38
43
|
const debug = createRangoDebugger(NS.discovery);
|
|
39
44
|
|
|
@@ -48,9 +53,25 @@ const debug = createRangoDebugger(NS.discovery);
|
|
|
48
53
|
export async function discoverRouters(
|
|
49
54
|
state: DiscoveryState,
|
|
50
55
|
rscEnv: any,
|
|
56
|
+
ssrEnv?: any,
|
|
51
57
|
): Promise<any> {
|
|
52
58
|
if (!state.resolvedEntryPath) return;
|
|
53
59
|
|
|
60
|
+
// Import the router package FIRST (leaf module, no side effects on user
|
|
61
|
+
// code) so recorded clientUrls projections can be refreshed BEFORE the entry
|
|
62
|
+
// import: routers re-created by that import call `.routes(reference)` at
|
|
63
|
+
// module scope and materialize whatever projection is installed at that
|
|
64
|
+
// moment. On HMR re-discovery that must be the CURRENT module contents, not
|
|
65
|
+
// the previous pass's snapshot (see refreshRecordedClientUrlProjections).
|
|
66
|
+
const serverMod = await timed(
|
|
67
|
+
debug,
|
|
68
|
+
"inner: import @rangojs/router/server",
|
|
69
|
+
() => rscEnv.runner.import("@rangojs/router/server"),
|
|
70
|
+
);
|
|
71
|
+
await timed(debug, "inner: refresh recorded client URL projections", () =>
|
|
72
|
+
refreshRecordedClientUrlProjections(state, ssrEnv, serverMod),
|
|
73
|
+
);
|
|
74
|
+
|
|
54
75
|
// Import the entry file via RSC environment.
|
|
55
76
|
// For node preset: this is the router file (createRouter() registers in RouterRegistry).
|
|
56
77
|
// For cloudflare preset: this is the worker entry (which imports the router).
|
|
@@ -58,12 +79,6 @@ export async function discoverRouters(
|
|
|
58
79
|
rscEnv.runner.import(state.resolvedEntryPath),
|
|
59
80
|
);
|
|
60
81
|
|
|
61
|
-
// Import the router package to access the registry
|
|
62
|
-
const serverMod = await timed(
|
|
63
|
-
debug,
|
|
64
|
-
"inner: import @rangojs/router/server",
|
|
65
|
-
() => rscEnv.runner.import("@rangojs/router/server"),
|
|
66
|
-
);
|
|
67
82
|
let registry: Map<string, any> = serverMod.RouterRegistry;
|
|
68
83
|
|
|
69
84
|
if (!registry || registry.size === 0) {
|
|
@@ -114,6 +129,10 @@ export async function discoverRouters(
|
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
await timed(debug, "inner: discover client URL projections", () =>
|
|
133
|
+
discoverClientUrlProjections(state, ssrEnv, serverMod),
|
|
134
|
+
);
|
|
135
|
+
|
|
117
136
|
// generateManifestFull must run in the RSC runner realm: it invokes the
|
|
118
137
|
// user's urlpatterns.handler() via RangoContext, consuming router instances
|
|
119
138
|
// from the runner. The trie/fallback-ref builders are pure transforms over
|
|
@@ -176,20 +195,27 @@ export async function discoverRouters(
|
|
|
176
195
|
|
|
177
196
|
const manifestGenStart = debug ? performance.now() : 0;
|
|
178
197
|
for (const [id, router] of registry) {
|
|
179
|
-
if (!
|
|
198
|
+
if (!generateManifestFull) {
|
|
180
199
|
continue;
|
|
181
200
|
}
|
|
182
201
|
|
|
183
|
-
const
|
|
184
|
-
router.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
const mounts =
|
|
203
|
+
router.__urlpatternMounts ??
|
|
204
|
+
(router.urlpatterns ? [{ patterns: router.urlpatterns }] : []);
|
|
205
|
+
if (mounts.length === 0) continue;
|
|
206
|
+
|
|
207
|
+
const mountManifests = [];
|
|
208
|
+
for (const mount of mounts) {
|
|
209
|
+
mountManifests.push(
|
|
210
|
+
await generateManifestFull(mount.patterns, routerMountIndex, {
|
|
211
|
+
routerId: id,
|
|
212
|
+
...(router.__basename ? { urlPrefix: router.__basename } : {}),
|
|
213
|
+
...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
routerMountIndex++;
|
|
217
|
+
}
|
|
218
|
+
const manifest = mergeFullManifests(mountManifests);
|
|
193
219
|
allManifests.push({ id, manifest });
|
|
194
220
|
|
|
195
221
|
// Router-level "use client" boundary defaults -> app-fallback (the
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ScanFilter } from "../../build/generate-route-types.js";
|
|
10
|
+
import type { ClientUrlProjection } from "../../client-urls/server-projection.js";
|
|
10
11
|
|
|
11
12
|
export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
|
|
12
13
|
|
|
@@ -79,6 +80,11 @@ export interface ShellPrerenderCandidate {
|
|
|
79
80
|
| { ttl?: number; swr?: number; tags?: string[]; captureTimeout?: number };
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
export interface ClientUrlDiscoveryState {
|
|
84
|
+
clientUrlSourceByReferenceId: Map<string, string>;
|
|
85
|
+
clientUrlProjectionMap: Map<string, ClientUrlProjection>;
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
export interface DiscoveryState {
|
|
83
89
|
resolvedEntryPath: string | undefined;
|
|
84
90
|
projectRoot: string;
|
|
@@ -109,6 +115,8 @@ export interface DiscoveryState {
|
|
|
109
115
|
perRouterTrieMap: Map<string, any>;
|
|
110
116
|
perRouterPrecomputedMap: Map<string, PrecomputedEntry[]>;
|
|
111
117
|
perRouterManifestDataMap: Map<string, Record<string, string>>;
|
|
118
|
+
clientUrlSourceByReferenceId?: Map<string, string>;
|
|
119
|
+
clientUrlProjectionMap?: Map<string, ClientUrlProjection>;
|
|
112
120
|
|
|
113
121
|
prerenderManifestEntries: Record<string, string> | null;
|
|
114
122
|
staticManifestEntries: Record<string, string> | null;
|
|
@@ -164,7 +172,7 @@ export interface DiscoveryState {
|
|
|
164
172
|
export function createDiscoveryState(
|
|
165
173
|
entryPath: string | undefined,
|
|
166
174
|
opts: PluginOptions | undefined,
|
|
167
|
-
): DiscoveryState {
|
|
175
|
+
): DiscoveryState & ClientUrlDiscoveryState {
|
|
168
176
|
return {
|
|
169
177
|
resolvedEntryPath: entryPath,
|
|
170
178
|
projectRoot: "",
|
|
@@ -182,6 +190,8 @@ export function createDiscoveryState(
|
|
|
182
190
|
perRouterTrieMap: new Map(),
|
|
183
191
|
perRouterPrecomputedMap: new Map(),
|
|
184
192
|
perRouterManifestDataMap: new Map(),
|
|
193
|
+
clientUrlSourceByReferenceId: new Map(),
|
|
194
|
+
clientUrlProjectionMap: new Map(),
|
|
185
195
|
|
|
186
196
|
prerenderManifestEntries: null,
|
|
187
197
|
staticManifestEntries: null,
|
|
@@ -44,6 +44,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
44
44
|
const hasManifest =
|
|
45
45
|
state.mergedRouteManifest &&
|
|
46
46
|
Object.keys(state.mergedRouteManifest).length > 0;
|
|
47
|
+
const hasClientUrlModules = Boolean(state.clientUrlSourceByReferenceId?.size);
|
|
47
48
|
|
|
48
49
|
if (hasManifest) {
|
|
49
50
|
// Build gen file import statements for each router with a sourceFile.
|
|
@@ -83,6 +84,9 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
83
84
|
"setRouterManifest",
|
|
84
85
|
...(state.isBuildMode ? ["registerRouterManifestLoader"] : []),
|
|
85
86
|
"clearAllRouterData",
|
|
87
|
+
...(hasClientUrlModules
|
|
88
|
+
? ["clearClientUrlProjections", "setClientUrlProjection"]
|
|
89
|
+
: []),
|
|
86
90
|
];
|
|
87
91
|
const lines = [
|
|
88
92
|
`import { ${serverImports.join(", ")} } from "@rangojs/router/server";`,
|
|
@@ -95,6 +99,16 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
95
99
|
`clearAllRouterData();`,
|
|
96
100
|
];
|
|
97
101
|
|
|
102
|
+
if (hasClientUrlModules) {
|
|
103
|
+
lines.push(`clearClientUrlProjections();`);
|
|
104
|
+
for (const [referenceId, projection] of state.clientUrlProjectionMap ??
|
|
105
|
+
[]) {
|
|
106
|
+
lines.push(
|
|
107
|
+
`setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
if (varIdx > 0) {
|
|
99
113
|
lines.push(
|
|
100
114
|
`function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,
|
|
@@ -169,6 +183,12 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
const lines: string[] = [];
|
|
186
|
+
if (hasClientUrlModules) {
|
|
187
|
+
lines.push(
|
|
188
|
+
`import { clearClientUrlProjections } from "@rangojs/router/server";`,
|
|
189
|
+
`clearClientUrlProjections();`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
172
192
|
if (!state.isBuildMode) {
|
|
173
193
|
const origin =
|
|
174
194
|
state.devServerOrigin ||
|
|
@@ -154,14 +154,23 @@ import {
|
|
|
154
154
|
decodeAction,
|
|
155
155
|
decodeFormState,
|
|
156
156
|
} from "@rangojs/router/internal/deps/rsc";
|
|
157
|
-
import { router } from "${routerPath}";
|
|
158
|
-
import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
|
|
159
|
-
import { VERSION } from "@rangojs/router:version";
|
|
160
157
|
|
|
161
158
|
// Startup bootstrap imports (routes + loader manifests). See
|
|
162
159
|
// RSC_ENTRY_BOOTSTRAP_IMPORTS — the same list the custom-entry injector uses.
|
|
160
|
+
//
|
|
161
|
+
// ORDER IS LOAD-BEARING: the bootstrap imports MUST precede the router import.
|
|
162
|
+
// The routes-manifest module installs client URL projections
|
|
163
|
+
// (setClientUrlProjection), and createRouter().routes(clientUrlsReference)
|
|
164
|
+
// consults them at router-module evaluation. Router-first would silently push
|
|
165
|
+
// every clientUrls() app onto the deferred-subscription path and change mount
|
|
166
|
+
// registration order with no error. The custom-entry injector keeps the same
|
|
167
|
+
// guarantee by PREPENDING this list at file top (version-injector.ts).
|
|
163
168
|
${bootstrapImports}
|
|
164
169
|
|
|
170
|
+
import { router } from "${routerPath}";
|
|
171
|
+
import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
|
|
172
|
+
import { VERSION } from "@rangojs/router:version";
|
|
173
|
+
|
|
165
174
|
// Lazily create the handler on first request so that ESM live bindings
|
|
166
175
|
// have resolved by the time we read \`router\`. During HMR the module may
|
|
167
176
|
// re-evaluate before router.tsx finishes, leaving the import undefined.
|