@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- 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 +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -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 +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- 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/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/browser/types.ts
CHANGED
|
@@ -121,7 +121,7 @@ export interface NavigationState {
|
|
|
121
121
|
/** Whether RSC data is currently streaming (initial load or navigation) */
|
|
122
122
|
isStreaming: boolean;
|
|
123
123
|
|
|
124
|
-
/** Current location
|
|
124
|
+
/** Current location */
|
|
125
125
|
location: NavigationLocation;
|
|
126
126
|
|
|
127
127
|
/** URL being navigated to (null when idle) */
|
|
@@ -178,7 +178,7 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* Cache interface for storing segments
|
|
181
|
-
* Compatible with
|
|
181
|
+
* Compatible with Map
|
|
182
182
|
*
|
|
183
183
|
* @internal This type is an implementation detail and may change without notice.
|
|
184
184
|
*/
|
|
@@ -215,7 +215,7 @@ export interface NavigationUpdate {
|
|
|
215
215
|
/**
|
|
216
216
|
* State value for navigate/Link
|
|
217
217
|
* - LocationStateEntry[]: Type-safe state entries (recommended)
|
|
218
|
-
* - unknown:
|
|
218
|
+
* - unknown: Plain state format (object or getter function)
|
|
219
219
|
*/
|
|
220
220
|
export type HistoryState =
|
|
221
221
|
| import("./react/location-state-shared.js").LocationStateEntry[]
|
|
@@ -234,14 +234,22 @@ export interface NavigateOptions {
|
|
|
234
234
|
* @example
|
|
235
235
|
* ```tsx
|
|
236
236
|
* // Type-safe state (recommended)
|
|
237
|
-
* const ProductState = createLocationState<{ name: string }>(
|
|
237
|
+
* const ProductState = createLocationState<{ name: string }>();
|
|
238
238
|
* navigate("/product/123", { state: [ProductState({ name: "Widget" })] });
|
|
239
239
|
*
|
|
240
|
+
* // Type-safe just-in-time state (getter called at navigation time)
|
|
241
|
+
* navigate("/product/123", {
|
|
242
|
+
* state: [ProductState(() => ({ name: computeName() }))],
|
|
243
|
+
* });
|
|
244
|
+
*
|
|
240
245
|
* // Multiple states
|
|
241
246
|
* navigate("/checkout", { state: [ProductState(p), CartState(c)] });
|
|
242
247
|
*
|
|
243
|
-
* //
|
|
248
|
+
* // Plain static state
|
|
244
249
|
* navigate("/product", { state: { from: "list" } });
|
|
250
|
+
*
|
|
251
|
+
* // Plain just-in-time state
|
|
252
|
+
* navigate("/product", { state: () => ({ from: window.location.pathname }) });
|
|
245
253
|
* ```
|
|
246
254
|
*/
|
|
247
255
|
state?: HistoryState;
|
|
@@ -394,35 +402,6 @@ export interface NavigationStore {
|
|
|
394
402
|
): () => void;
|
|
395
403
|
}
|
|
396
404
|
|
|
397
|
-
// ============================================================================
|
|
398
|
-
// Request Controller Types
|
|
399
|
-
// ============================================================================
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Disposable abort controller with automatic cleanup
|
|
403
|
-
*/
|
|
404
|
-
export interface DisposableAbortController extends Disposable {
|
|
405
|
-
controller: AbortController;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Request controller for managing concurrent requests
|
|
410
|
-
*
|
|
411
|
-
* Separates navigation requests (aborted on new navigation) from
|
|
412
|
-
* action requests (complete independently of navigation).
|
|
413
|
-
*/
|
|
414
|
-
export interface RequestController {
|
|
415
|
-
create(): AbortController;
|
|
416
|
-
createDisposable(): DisposableAbortController;
|
|
417
|
-
/** Create a disposable controller for actions (not aborted by navigation) */
|
|
418
|
-
createActionDisposable(): DisposableAbortController;
|
|
419
|
-
/** Abort all navigation requests (not actions) */
|
|
420
|
-
abortAll(): void;
|
|
421
|
-
/** Abort all action requests (used for error handling) */
|
|
422
|
-
abortAllActions(): void;
|
|
423
|
-
remove(controller: AbortController): void;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
405
|
// ============================================================================
|
|
427
406
|
// Navigation Client Types
|
|
428
407
|
// ============================================================================
|
|
@@ -480,7 +459,6 @@ export interface LinkInterceptorOptions {
|
|
|
480
459
|
*/
|
|
481
460
|
export interface ServerActionBridge {
|
|
482
461
|
register(): void;
|
|
483
|
-
unregister(): void;
|
|
484
462
|
}
|
|
485
463
|
|
|
486
464
|
/**
|
|
@@ -526,3 +504,11 @@ export interface NavigationBridgeConfig {
|
|
|
526
504
|
|
|
527
505
|
// Re-export ResolvedSegment for convenience
|
|
528
506
|
export type { ResolvedSegment };
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Token for tracking an active stream.
|
|
510
|
+
* Call end() when the stream completes.
|
|
511
|
+
*/
|
|
512
|
+
export interface StreamingToken {
|
|
513
|
+
end(): void;
|
|
514
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate that a client-consumed redirect URL (from headers or Flight payload)
|
|
3
|
+
* targets the same origin as the current page. Prevents open-redirect attacks
|
|
4
|
+
* via crafted responses.
|
|
5
|
+
*
|
|
6
|
+
* @returns The canonical (normalized) URL string on success, or null if blocked.
|
|
7
|
+
*/
|
|
8
|
+
export function validateRedirectOrigin(
|
|
9
|
+
url: string,
|
|
10
|
+
currentOrigin: string,
|
|
11
|
+
): string | null {
|
|
12
|
+
try {
|
|
13
|
+
const target = new URL(url, currentOrigin);
|
|
14
|
+
if (target.origin !== currentOrigin) {
|
|
15
|
+
console.error(
|
|
16
|
+
`[rango] Redirect blocked: origin mismatch (${target.origin})`,
|
|
17
|
+
);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Return pathname+search+hash for relative inputs, full href for absolute.
|
|
21
|
+
// This normalizes protocol-relative and other ambiguous forms.
|
|
22
|
+
return target.href.startsWith(currentOrigin)
|
|
23
|
+
? target.href
|
|
24
|
+
: target.pathname + target.search + target.hash;
|
|
25
|
+
} catch {
|
|
26
|
+
console.error(`[rango] Redirect blocked: invalid URL "${url}"`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -51,8 +51,6 @@ export interface GeneratedManifest {
|
|
|
51
51
|
responseTypeRoutes?: Record<string, string>;
|
|
52
52
|
/** Route name -> search schema descriptor for typed URL helpers */
|
|
53
53
|
routeSearchSchemas?: Record<string, Record<string, string>>;
|
|
54
|
-
/** Generation timestamp */
|
|
55
|
-
generatedAt: string;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
/**
|
|
@@ -202,6 +200,11 @@ function buildPrefixTreeNode(
|
|
|
202
200
|
}
|
|
203
201
|
}
|
|
204
202
|
|
|
203
|
+
// Remove from visited so sibling branches can reuse the same patterns
|
|
204
|
+
// without false circular-include detection. Only ancestors in the current
|
|
205
|
+
// recursion path should trigger the cycle guard.
|
|
206
|
+
visited.delete(patterns);
|
|
207
|
+
|
|
205
208
|
return {
|
|
206
209
|
staticPrefix: extractStaticPrefix(urlPrefix),
|
|
207
210
|
fullPrefix: urlPrefix,
|
|
@@ -232,11 +235,20 @@ function captureAncestry(
|
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
/**
|
|
235
|
-
*
|
|
238
|
+
* Internal manifest result including build-pipeline-only fields.
|
|
239
|
+
* Not part of the public API — use generateManifest() for the public surface.
|
|
240
|
+
*/
|
|
241
|
+
export interface FullManifest extends GeneratedManifest {
|
|
242
|
+
_routeAncestry: Record<string, string[]>;
|
|
243
|
+
_prerenderDefs?: Record<string, any>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate manifest from UrlPatterns (public API).
|
|
236
248
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
249
|
+
* Returns only the public GeneratedManifest fields. Internal build pipeline
|
|
250
|
+
* consumers that need _routeAncestry or _prerenderDefs should use
|
|
251
|
+
* generateManifestFull() instead.
|
|
240
252
|
*
|
|
241
253
|
* @example
|
|
242
254
|
* ```typescript
|
|
@@ -254,10 +266,26 @@ function captureAncestry(
|
|
|
254
266
|
export function generateManifest<TEnv>(
|
|
255
267
|
urlpatterns: UrlPatterns<TEnv, any>,
|
|
256
268
|
mountIndex: number = 0,
|
|
257
|
-
): GeneratedManifest
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
): GeneratedManifest {
|
|
270
|
+
const {
|
|
271
|
+
_routeAncestry: _,
|
|
272
|
+
_prerenderDefs: __,
|
|
273
|
+
...publicManifest
|
|
274
|
+
} = generateManifestFull(urlpatterns, mountIndex);
|
|
275
|
+
return publicManifest;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Generate manifest with internal build-pipeline fields.
|
|
280
|
+
*
|
|
281
|
+
* Used by the Vite plugin (discover-routers via dynamic import through
|
|
282
|
+
* @rangojs/router/build), manifest-init (direct import), and trie
|
|
283
|
+
* building. Not intended for external use.
|
|
284
|
+
*/
|
|
285
|
+
export function generateManifestFull<TEnv>(
|
|
286
|
+
urlpatterns: UrlPatterns<TEnv, any>,
|
|
287
|
+
mountIndex: number = 0,
|
|
288
|
+
): FullManifest {
|
|
261
289
|
const routeManifest: Record<string, string> = {};
|
|
262
290
|
const routeAncestry: Record<string, string[]> = {};
|
|
263
291
|
const prefixTree: Record<string, PrefixTreeNode> = {};
|
|
@@ -376,8 +404,6 @@ export function generateManifest<TEnv>(
|
|
|
376
404
|
Object.keys(routeSearchSchemas).length > 0
|
|
377
405
|
? routeSearchSchemas
|
|
378
406
|
: undefined,
|
|
379
|
-
generatedAt: new Date().toISOString(),
|
|
380
|
-
// Internal: routeAncestry is used only for trie building, not exported
|
|
381
407
|
_routeAncestry: routeAncestry,
|
|
382
408
|
// Internal: prerender handler definitions for build-time getParams() access
|
|
383
409
|
_prerenderDefs:
|
|
@@ -401,7 +427,6 @@ export function generateManifestCode<TEnv>(
|
|
|
401
427
|
|
|
402
428
|
return `/**
|
|
403
429
|
* Auto-generated route manifest
|
|
404
|
-
* Generated at: ${manifest.generatedAt}
|
|
405
430
|
*
|
|
406
431
|
* DO NOT EDIT - This file is generated by @rangojs/router
|
|
407
432
|
*/
|
|
@@ -27,6 +27,10 @@ export {
|
|
|
27
27
|
extractUrlsVariableFromRouter,
|
|
28
28
|
buildCombinedRouteMapForRouterFile,
|
|
29
29
|
detectUnresolvableIncludes,
|
|
30
|
+
detectUnresolvableIncludesForUrlsFile,
|
|
31
|
+
findNestedRouterConflict,
|
|
32
|
+
formatNestedRouterConflictError,
|
|
30
33
|
findRouterFiles,
|
|
31
34
|
writeCombinedRouteTypes,
|
|
32
35
|
} from "./route-types/router-processing.js";
|
|
36
|
+
export { findUrlsVariableNames } from "./route-types/per-module-writer.js";
|
package/src/build/index.ts
CHANGED
package/src/build/route-trie.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface TrieNode {
|
|
|
47
47
|
s?: Record<string, TrieNode>;
|
|
48
48
|
/** Param child: { n: paramName, c: child node } */
|
|
49
49
|
p?: { n: string; c: TrieNode };
|
|
50
|
+
/** Suffix-param children keyed by suffix (e.g., ".html" → { n: "productId", c: ... }) */
|
|
51
|
+
xp?: Record<string, { n: string; c: TrieNode }>;
|
|
50
52
|
/** Wildcard terminal: leaf + paramName */
|
|
51
53
|
w?: TrieLeaf & { pn: string };
|
|
52
54
|
}
|
|
@@ -158,6 +160,11 @@ export function extractAncestryFromTrie(
|
|
|
158
160
|
visit(child);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
163
|
+
if (node.xp) {
|
|
164
|
+
for (const child of Object.values(node.xp)) {
|
|
165
|
+
visit(child.c);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
161
168
|
if (node.p) {
|
|
162
169
|
visit(node.p.c);
|
|
163
170
|
}
|
|
@@ -235,10 +242,19 @@ function insertSegments(
|
|
|
235
242
|
mergeLeaf(node, leaf);
|
|
236
243
|
// AND continue with param child (param present)
|
|
237
244
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
245
|
+
if (segment.suffix) {
|
|
246
|
+
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
247
|
+
if (!node.xp) node.xp = {};
|
|
248
|
+
if (!node.xp[segment.suffix]) {
|
|
249
|
+
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
|
+
}
|
|
251
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leaf);
|
|
252
|
+
} else {
|
|
253
|
+
if (!node.p) {
|
|
254
|
+
node.p = { n: segment.value, c: {} };
|
|
255
|
+
}
|
|
256
|
+
insertSegments(node.p.c, segments, index + 1, leaf);
|
|
240
257
|
}
|
|
241
|
-
insertSegments(node.p.c, segments, index + 1, leaf);
|
|
242
258
|
} else if (segment.type === "wildcard") {
|
|
243
259
|
// Wildcard consumes all remaining segments
|
|
244
260
|
const wildLeaf = { ...leaf, pn: "*" };
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractParamsFromPattern,
|
|
3
3
|
formatRouteEntry,
|
|
4
4
|
} from "./param-extraction.js";
|
|
5
|
+
import { isAutoGeneratedRouteName } from "../../route-name.js";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Code generation
|
|
@@ -66,15 +67,23 @@ export function generateRouteTypesSource(
|
|
|
66
67
|
routeManifest: Record<string, string>,
|
|
67
68
|
searchSchemas?: Record<string, Record<string, string>>,
|
|
68
69
|
): string {
|
|
69
|
-
const entries = Object.entries(routeManifest)
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const entries = Object.entries(routeManifest)
|
|
71
|
+
.filter(([name]) => !isAutoGeneratedRouteName(name))
|
|
72
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
73
|
+
|
|
74
|
+
const filteredSearchSchemas = searchSchemas
|
|
75
|
+
? Object.fromEntries(
|
|
76
|
+
Object.entries(searchSchemas).filter(
|
|
77
|
+
([name]) => !isAutoGeneratedRouteName(name),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
: undefined;
|
|
72
81
|
|
|
73
82
|
const objectBody = entries
|
|
74
83
|
.map(([name, pattern]) => {
|
|
75
84
|
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
76
85
|
const params = extractParamsFromPattern(pattern);
|
|
77
|
-
const search =
|
|
86
|
+
const search = filteredSearchSchemas?.[name];
|
|
78
87
|
return formatRouteEntry(key, pattern, params, search);
|
|
79
88
|
})
|
|
80
89
|
.join("\n");
|
|
@@ -325,6 +325,13 @@ function buildRouteMapFromBlock(
|
|
|
325
325
|
);
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
// Includes without a name keep their child names private to the mounted
|
|
329
|
+
// module. They remain active at runtime via an internal scope prefix, but
|
|
330
|
+
// they are intentionally omitted from generated public route maps.
|
|
331
|
+
if (namePrefix === null) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
328
335
|
// Apply prefixes
|
|
329
336
|
for (const [name, pattern] of Object.entries(childResult.routes)) {
|
|
330
337
|
const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
|
|
@@ -394,5 +401,11 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
394
401
|
searchSchemas,
|
|
395
402
|
diagnosticsOut,
|
|
396
403
|
);
|
|
404
|
+
|
|
405
|
+
// Remove from visited so sibling branches can include the same variable
|
|
406
|
+
// without false circular-include detection. Only ancestors in the current
|
|
407
|
+
// recursion path should trigger the cycle guard.
|
|
408
|
+
visited.delete(key);
|
|
409
|
+
|
|
397
410
|
return { routes, searchSchemas };
|
|
398
411
|
}
|
|
@@ -26,7 +26,7 @@ export function writePerModuleRouteTypes(
|
|
|
26
26
|
* Find all variable names assigned to urls() calls in source code.
|
|
27
27
|
* e.g. `export const patterns = urls(...)` -> ["patterns"]
|
|
28
28
|
*/
|
|
29
|
-
function findUrlsVariableNames(code: string): string[] {
|
|
29
|
+
export function findUrlsVariableNames(code: string): string[] {
|
|
30
30
|
const sourceFile = ts.createSourceFile(
|
|
31
31
|
"input.tsx",
|
|
32
32
|
code,
|
|
@@ -97,9 +97,21 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
97
97
|
routes = extractRoutesFromSource(source);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
if (routes.length === 0) return;
|
|
101
|
-
|
|
102
100
|
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
101
|
+
|
|
102
|
+
// When a urls() variable was found but static resolution yields zero
|
|
103
|
+
// routes, write an empty placeholder so generated imports stay
|
|
104
|
+
// resolvable until runtime discovery fills them in.
|
|
105
|
+
if (routes.length === 0) {
|
|
106
|
+
if (varNames.length > 0 && !existsSync(genPath)) {
|
|
107
|
+
writeFileSync(genPath, generatePerModuleTypesSource([]));
|
|
108
|
+
console.log(
|
|
109
|
+
`[rsc-router] Generated route types (placeholder) -> ${genPath}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
const genSource = generatePerModuleTypesSource(routes);
|
|
104
116
|
const existing = existsSync(genPath)
|
|
105
117
|
? readFileSync(genPath, "utf-8")
|
|
@@ -1,15 +1,146 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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";
|
|
3
15
|
import ts from "typescript";
|
|
4
16
|
import { generateRouteTypesSource } from "./codegen.js";
|
|
5
17
|
import type { ScanFilter } from "./scan-filter.js";
|
|
6
|
-
import { findTsFiles } from "./scan-filter.js";
|
|
7
18
|
import {
|
|
8
19
|
resolveImportedVariable,
|
|
9
20
|
resolveImportPath,
|
|
10
21
|
buildCombinedRouteMapWithSearch,
|
|
11
22
|
type UnresolvableInclude,
|
|
12
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
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findRouterFilesRecursive(
|
|
53
|
+
dir: string,
|
|
54
|
+
filter: ScanFilter | undefined,
|
|
55
|
+
results: string[],
|
|
56
|
+
): void {
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const childDirs: string[] = [];
|
|
68
|
+
const routerFilesInDir: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
74
|
+
childDirs.push(fullPath);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isRoutableSourceFile(entry.name)) continue;
|
|
79
|
+
if (filter && !filter(fullPath)) continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const source = readFileSync(fullPath, "utf-8");
|
|
83
|
+
if (ROUTER_CALL_PATTERN.test(source)) {
|
|
84
|
+
routerFilesInDir.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A directory that contains a router file is treated as a router root.
|
|
92
|
+
// Once found, deeper directories are skipped to avoid redundant scans.
|
|
93
|
+
if (routerFilesInDir.length > 0) {
|
|
94
|
+
results.push(...routerFilesInDir);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const childDir of childDirs) {
|
|
99
|
+
findRouterFilesRecursive(childDir, filter, results);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function findNestedRouterConflict(
|
|
104
|
+
routerFiles: string[],
|
|
105
|
+
): { ancestor: string; nested: string } | null {
|
|
106
|
+
const routerDirs = [
|
|
107
|
+
...new Set(routerFiles.map((filePath) => dirname(resolve(filePath)))),
|
|
108
|
+
].sort((a, b) => a.length - b.length);
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < routerDirs.length; i++) {
|
|
111
|
+
const ancestorDir = routerDirs[i];
|
|
112
|
+
const prefix = ancestorDir.endsWith(sep)
|
|
113
|
+
? ancestorDir
|
|
114
|
+
: `${ancestorDir}${sep}`;
|
|
115
|
+
for (let j = i + 1; j < routerDirs.length; j++) {
|
|
116
|
+
const nestedDir = routerDirs[j];
|
|
117
|
+
if (!nestedDir.startsWith(prefix)) continue;
|
|
118
|
+
const ancestorFile = routerFiles.find(
|
|
119
|
+
(filePath) => dirname(resolve(filePath)) === ancestorDir,
|
|
120
|
+
);
|
|
121
|
+
const nestedFile = routerFiles.find(
|
|
122
|
+
(filePath) => dirname(resolve(filePath)) === nestedDir,
|
|
123
|
+
);
|
|
124
|
+
if (ancestorFile && nestedFile) {
|
|
125
|
+
return { ancestor: ancestorFile, nested: nestedFile };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatNestedRouterConflictError(
|
|
134
|
+
conflict: { ancestor: string; nested: string },
|
|
135
|
+
prefix = "[rsc-router]",
|
|
136
|
+
): string {
|
|
137
|
+
return (
|
|
138
|
+
`${prefix} Nested router roots are not supported.\n` +
|
|
139
|
+
`Router root: ${conflict.ancestor}\n` +
|
|
140
|
+
`Nested router: ${conflict.nested}\n` +
|
|
141
|
+
`Move the nested router into a sibling directory or configure it as a separate app root.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
13
144
|
|
|
14
145
|
// ---------------------------------------------------------------------------
|
|
15
146
|
// Router file URL extraction
|
|
@@ -184,6 +315,33 @@ export function detectUnresolvableIncludes(
|
|
|
184
315
|
return diagnostics;
|
|
185
316
|
}
|
|
186
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Walk the include tree for a standalone urls() module file and detect
|
|
320
|
+
* all unresolvable includes. Mirrors detectUnresolvableIncludes() but
|
|
321
|
+
* operates on urls() variable declarations instead of going through
|
|
322
|
+
* createRouter().
|
|
323
|
+
*/
|
|
324
|
+
export function detectUnresolvableIncludesForUrlsFile(
|
|
325
|
+
filePath: string,
|
|
326
|
+
): UnresolvableInclude[] {
|
|
327
|
+
const realPath = resolve(filePath);
|
|
328
|
+
let source: string;
|
|
329
|
+
try {
|
|
330
|
+
source = readFileSync(realPath, "utf-8");
|
|
331
|
+
} catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const varNames = findUrlsVariableNames(source);
|
|
336
|
+
if (varNames.length === 0) return [];
|
|
337
|
+
|
|
338
|
+
const diagnostics: UnresolvableInclude[] = [];
|
|
339
|
+
for (const varName of varNames) {
|
|
340
|
+
buildCombinedRouteMapWithSearch(realPath, varName, new Set(), diagnostics);
|
|
341
|
+
}
|
|
342
|
+
return diagnostics;
|
|
343
|
+
}
|
|
344
|
+
|
|
187
345
|
// ---------------------------------------------------------------------------
|
|
188
346
|
// Per-router named-routes.gen.ts writer
|
|
189
347
|
// ---------------------------------------------------------------------------
|
|
@@ -193,19 +351,8 @@ export function detectUnresolvableIncludes(
|
|
|
193
351
|
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
194
352
|
*/
|
|
195
353
|
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
196
|
-
const files = findTsFiles(root, filter);
|
|
197
354
|
const result: string[] = [];
|
|
198
|
-
|
|
199
|
-
if (filePath.includes(".gen.")) continue;
|
|
200
|
-
try {
|
|
201
|
-
const source = readFileSync(filePath, "utf-8");
|
|
202
|
-
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
203
|
-
result.push(filePath);
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
355
|
+
findRouterFilesRecursive(root, filter, result);
|
|
209
356
|
return result;
|
|
210
357
|
}
|
|
211
358
|
|
|
@@ -234,6 +381,11 @@ export function writeCombinedRouteTypes(
|
|
|
234
381
|
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
235
382
|
if (routerFilePaths.length === 0) return;
|
|
236
383
|
|
|
384
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
|
|
385
|
+
if (nestedRouterConflict) {
|
|
386
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
387
|
+
}
|
|
388
|
+
|
|
237
389
|
for (const routerFilePath of routerFilePaths) {
|
|
238
390
|
let routerSource: string;
|
|
239
391
|
try {
|
|
@@ -300,10 +452,10 @@ export function writeCombinedRouteTypes(
|
|
|
300
452
|
// or other dynamic code. During HMR (file watcher), always write so
|
|
301
453
|
// newly added routes appear immediately.
|
|
302
454
|
if (opts?.preserveIfLarger && existing) {
|
|
303
|
-
const existingCount = (
|
|
304
|
-
|
|
455
|
+
const existingCount = countPublicRouteEntries(existing);
|
|
456
|
+
const newCount = Object.keys(result.routes).filter(
|
|
457
|
+
(name) => !isAutoGeneratedRouteName(name),
|
|
305
458
|
).length;
|
|
306
|
-
const newCount = Object.keys(result.routes).length;
|
|
307
459
|
if (existingCount > newCount) {
|
|
308
460
|
continue;
|
|
309
461
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
generateRouteTypesSource,
|
|
5
5
|
buildCombinedRouteMapForRouterFile,
|
|
6
6
|
} from "./generate-route-types.ts";
|
|
7
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
7
8
|
|
|
8
9
|
export interface RuntimeDiscoveryOptions {
|
|
9
10
|
/** Project root directory (where package.json / node_modules live). */
|
|
@@ -140,7 +141,18 @@ export async function discoverAndWriteRouteTypes(
|
|
|
140
141
|
const manifest = generateManifest(router.urlpatterns, routerMountIndex);
|
|
141
142
|
routerMountIndex++;
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
// Filter out auto-generated route names that the runtime creates for
|
|
145
|
+
// unnamed routes (path() with no name option). These get names like
|
|
146
|
+
// "$path__health" at root level or "docs.$path__health" under include().
|
|
147
|
+
// Match the Vite discovery writer's predicate: any name starting with "$"
|
|
148
|
+
// is internal. For prefixed names, check each dot-separated segment.
|
|
149
|
+
const rawManifest: Record<string, string> = manifest.routeManifest;
|
|
150
|
+
const routeManifest: Record<string, string> = {};
|
|
151
|
+
for (const [name, pattern] of Object.entries(rawManifest)) {
|
|
152
|
+
if (!isAutoGeneratedRouteName(name)) {
|
|
153
|
+
routeManifest[name] = pattern;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
144
156
|
let routeSearchSchemas:
|
|
145
157
|
| Record<string, Record<string, string>>
|
|
146
158
|
| undefined = manifest.routeSearchSchemas;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Task Runner
|
|
3
|
+
*
|
|
4
|
+
* Unified helper for scheduling async work via waitUntil.
|
|
5
|
+
* When waitUntil is unavailable, falls back to blocking or skipping.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface WaitUntilHost {
|
|
9
|
+
waitUntil?: (fn: () => Promise<void>) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Schedule an async task in the background via waitUntil.
|
|
14
|
+
*
|
|
15
|
+
* @param host - Object with optional waitUntil (request context or similar)
|
|
16
|
+
* @param task - Async function to execute
|
|
17
|
+
* @param blockWhenNoWaitUntil - If true, awaits the task when waitUntil is
|
|
18
|
+
* unavailable (e.g., Node.js dev server). If false (default), the task
|
|
19
|
+
* is silently skipped when waitUntil is unavailable.
|
|
20
|
+
* @returns A promise when blocking fallback is used, void otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export function runBackground(
|
|
23
|
+
host: WaitUntilHost | null | undefined,
|
|
24
|
+
task: () => Promise<void>,
|
|
25
|
+
blockWhenNoWaitUntil = false,
|
|
26
|
+
): Promise<void> | void {
|
|
27
|
+
if (host?.waitUntil) {
|
|
28
|
+
host.waitUntil(task);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (blockWhenNoWaitUntil) {
|
|
32
|
+
return task();
|
|
33
|
+
}
|
|
34
|
+
}
|