@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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 +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -128,7 +128,7 @@ export interface BrowserAppContext {
|
|
|
128
128
|
let browserAppContext: BrowserAppContext | null = null;
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Initialize the browser app. Must be called before rendering
|
|
131
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
132
132
|
*
|
|
133
133
|
* This function:
|
|
134
134
|
* - Loads the initial RSC payload from the stream
|
|
@@ -325,11 +325,11 @@ export async function initBrowserApp(
|
|
|
325
325
|
// full lifecycle (fetching + streaming, before commit) without
|
|
326
326
|
// blocking on server actions.
|
|
327
327
|
if (eventController.getState().isNavigating) {
|
|
328
|
-
console.log("[
|
|
328
|
+
console.log("[Rango] HMR: Skipping — navigation in progress");
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
console.log("[
|
|
332
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
333
333
|
|
|
334
334
|
const abort = new AbortController();
|
|
335
335
|
hmrAbort = abort;
|
|
@@ -364,11 +364,18 @@ export async function initBrowserApp(
|
|
|
364
364
|
// Update version BEFORE rebuilding state so that
|
|
365
365
|
// clearHistoryCache() runs first, then the fresh segment
|
|
366
366
|
// cache entry we create below survives.
|
|
367
|
+
//
|
|
368
|
+
// Compare against the bridge's live version, not the init-time
|
|
369
|
+
// `version` const: after the first HMR bump the const is stale, so a
|
|
370
|
+
// later update with an unchanged version would otherwise re-clear the
|
|
371
|
+
// cache and re-broadcast across tabs/apps. The live read fires only
|
|
372
|
+
// on a genuine version change.
|
|
367
373
|
const newVersion = payload.metadata.version;
|
|
368
|
-
|
|
374
|
+
const currentVersion = navigationBridge.getVersion();
|
|
375
|
+
if (newVersion && newVersion !== currentVersion) {
|
|
369
376
|
console.log(
|
|
370
|
-
"[
|
|
371
|
-
|
|
377
|
+
"[Rango] HMR: version changed",
|
|
378
|
+
currentVersion,
|
|
372
379
|
"→",
|
|
373
380
|
newVersion,
|
|
374
381
|
"clearing caches",
|
|
@@ -376,6 +383,13 @@ export async function initBrowserApp(
|
|
|
376
383
|
navigationBridge.updateVersion(newVersion);
|
|
377
384
|
}
|
|
378
385
|
|
|
386
|
+
// Apply only partial segment updates. A non-partial payload during
|
|
387
|
+
// HMR is transient: the worker route table is still rebuilding after
|
|
388
|
+
// the edit, so the URL momentarily resolves to not-found/catch-all.
|
|
389
|
+
// Skip it -- the debounced follow-up refetch returns the settled
|
|
390
|
+
// route's partial payload and renders it below. We never reload here:
|
|
391
|
+
// a paramless document GET would run the SSR path and surface the
|
|
392
|
+
// not-found page during that same transient.
|
|
379
393
|
if (payload.metadata?.isPartial) {
|
|
380
394
|
const segments = payload.metadata.segments || [];
|
|
381
395
|
const matched = payload.metadata.matched || [];
|
|
@@ -415,10 +429,10 @@ export async function initBrowserApp(
|
|
|
415
429
|
|
|
416
430
|
await streamComplete;
|
|
417
431
|
handle.complete(new URL(window.location.href));
|
|
418
|
-
console.log("[
|
|
432
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
419
433
|
} catch (err) {
|
|
420
434
|
if (abort.signal.aborted) return;
|
|
421
|
-
console.warn("[
|
|
435
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
422
436
|
window.location.reload();
|
|
423
437
|
return;
|
|
424
438
|
} finally {
|
|
@@ -430,7 +444,7 @@ export async function initBrowserApp(
|
|
|
430
444
|
});
|
|
431
445
|
}
|
|
432
446
|
|
|
433
|
-
// Store context for
|
|
447
|
+
// Store context for Rango component
|
|
434
448
|
const context: BrowserAppContext = {
|
|
435
449
|
store,
|
|
436
450
|
eventController,
|
|
@@ -454,7 +468,7 @@ export async function initBrowserApp(
|
|
|
454
468
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
455
469
|
if (!browserAppContext) {
|
|
456
470
|
throw new Error(
|
|
457
|
-
"
|
|
471
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
458
472
|
);
|
|
459
473
|
}
|
|
460
474
|
return browserAppContext;
|
|
@@ -468,18 +482,18 @@ export function resetBrowserAppContext(): void {
|
|
|
468
482
|
}
|
|
469
483
|
|
|
470
484
|
/**
|
|
471
|
-
* Props for the
|
|
485
|
+
* Props for the Rango component
|
|
472
486
|
*/
|
|
473
|
-
export interface
|
|
487
|
+
export interface RangoProps {}
|
|
474
488
|
|
|
475
489
|
/**
|
|
476
|
-
*
|
|
490
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
477
491
|
*
|
|
478
492
|
* Must be called after initBrowserApp() has completed.
|
|
479
493
|
*
|
|
480
494
|
* @example
|
|
481
495
|
* ```tsx
|
|
482
|
-
* import { initBrowserApp,
|
|
496
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
483
497
|
* import { rscStream } from "rsc-html-stream/client";
|
|
484
498
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
485
499
|
*
|
|
@@ -489,14 +503,14 @@ export interface RSCRouterProps {}
|
|
|
489
503
|
* hydrateRoot(
|
|
490
504
|
* document,
|
|
491
505
|
* <React.StrictMode>
|
|
492
|
-
* <
|
|
506
|
+
* <Rango />
|
|
493
507
|
* </React.StrictMode>
|
|
494
508
|
* );
|
|
495
509
|
* }
|
|
496
510
|
* main();
|
|
497
511
|
* ```
|
|
498
512
|
*/
|
|
499
|
-
export function
|
|
513
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
500
514
|
const {
|
|
501
515
|
store,
|
|
502
516
|
eventController,
|
|
@@ -332,6 +332,8 @@ export function scrollToHash(): boolean {
|
|
|
332
332
|
* Scroll to top of page
|
|
333
333
|
*/
|
|
334
334
|
export function scrollToTop(): void {
|
|
335
|
+
if (typeof window === "undefined") return;
|
|
336
|
+
if (typeof window.scrollTo !== "function") return;
|
|
335
337
|
window.scrollTo(0, 0);
|
|
336
338
|
}
|
|
337
339
|
|
|
@@ -374,20 +376,26 @@ export function handleNavigationEnd(options: {
|
|
|
374
376
|
// Fall through to hash or top if no saved position
|
|
375
377
|
}
|
|
376
378
|
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
379
|
+
// scrollToHash / scrollToTop run synchronously here.
|
|
380
|
+
// handleNavigationEnd is invoked from NavigationProvider's
|
|
381
|
+
// useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
|
|
382
|
+
// captured by the upcoming paint AND by startViewTransition's snapshot.
|
|
383
|
+
// Deferring via rAF here pushed the call past the snapshot capture,
|
|
384
|
+
// making forward navigations wrapped in a layout/route view transition
|
|
385
|
+
// skip scroll-to-top — the live DOM scrolled but the captured snapshot
|
|
386
|
+
// was at the previous scroll position, so the user-facing page stayed
|
|
387
|
+
// visually clamped at the source page's scrollY (often the new tree's
|
|
388
|
+
// max scroll for tall→short navs). Y=0 / a hash element are robust
|
|
389
|
+
// against unmeasured layout, so sync scroll is correct here even
|
|
390
|
+
// before the new tree's scrollHeight settles.
|
|
391
|
+
//
|
|
392
|
+
// (The restore branch above keeps deferToNextPaint because savedY
|
|
393
|
+
// depends on the new tree's max scroll; sync scrollTo against an
|
|
394
|
+
// unmeasured DOM would clamp savedY to whatever the old/zero max was.)
|
|
395
|
+
if (scrollToHash()) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
scrollToTop();
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
/**
|
|
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
|
|
|
48
48
|
|
|
49
49
|
if (cachedCategory !== incomingCategory) {
|
|
50
50
|
console.warn(
|
|
51
|
-
`[
|
|
51
|
+
`[Rango] Tree structure mismatch detected in ${context} ` +
|
|
52
52
|
`for segment "${cached.id}": loading category changed from ` +
|
|
53
53
|
`"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
|
|
54
54
|
`"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
|
|
@@ -64,7 +64,7 @@ export function assertSegmentStructure(
|
|
|
64
64
|
const incomingHasMount = !!incoming.mountPath;
|
|
65
65
|
if (cachedHasMount !== incomingHasMount) {
|
|
66
66
|
console.warn(
|
|
67
|
-
`[
|
|
67
|
+
`[Rango] MountContextProvider mismatch detected in ${context} ` +
|
|
68
68
|
`for segment "${cached.id}": mountPath changed from ` +
|
|
69
69
|
`${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
|
|
70
70
|
`${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
|
|
@@ -25,6 +25,7 @@ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
|
25
25
|
import {
|
|
26
26
|
extractRscHeaderUrl,
|
|
27
27
|
emptyResponse,
|
|
28
|
+
handleReloadHeader,
|
|
28
29
|
teeWithCompletion,
|
|
29
30
|
} from "./response-adapter.js";
|
|
30
31
|
import { mergeLocationState } from "./history-state.js";
|
|
@@ -77,6 +78,20 @@ export function createServerActionBridge(
|
|
|
77
78
|
onNavigate,
|
|
78
79
|
} = config;
|
|
79
80
|
|
|
81
|
+
// SPA-navigate when onNavigate is set, else hard-reload. state is omitted (not
|
|
82
|
+
// passed as undefined) to match the header path's prior call shape.
|
|
83
|
+
async function dispatchRedirect(url: string, state?: unknown): Promise<void> {
|
|
84
|
+
if (onNavigate) {
|
|
85
|
+
await onNavigate(url, {
|
|
86
|
+
...(state !== undefined ? { state } : {}),
|
|
87
|
+
replace: true,
|
|
88
|
+
_skipCache: true,
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
window.location.href = url;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
let isRegistered = false;
|
|
81
96
|
|
|
82
97
|
const fetchPartialUpdate = createPartialUpdater({
|
|
@@ -222,18 +237,12 @@ export function createServerActionBridge(
|
|
|
222
237
|
handle.signal.removeEventListener("abort", onHandleAbort);
|
|
223
238
|
|
|
224
239
|
// Check for version mismatch - server wants us to reload
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
if (
|
|
231
|
-
log("version mismatch on action, reloading", {
|
|
232
|
-
reloadUrl: reload.url,
|
|
233
|
-
});
|
|
234
|
-
window.location.href = reload.url;
|
|
235
|
-
return new Promise<Response>(() => {});
|
|
236
|
-
}
|
|
240
|
+
const reloadResult = handleReloadHeader(response, {
|
|
241
|
+
onBlocked: resolveStreamComplete,
|
|
242
|
+
onReload: (url) =>
|
|
243
|
+
log("version mismatch on action, reloading", { reloadUrl: url }),
|
|
244
|
+
});
|
|
245
|
+
if (reloadResult) return reloadResult;
|
|
237
246
|
|
|
238
247
|
// Simple redirect from action (no state, no RSC payload).
|
|
239
248
|
// Short-circuits before createFromFetch — no Flight deserialization needed.
|
|
@@ -243,14 +252,7 @@ export function createServerActionBridge(
|
|
|
243
252
|
if (redirect && redirect !== "blocked" && !handle.signal.aborted) {
|
|
244
253
|
log("action simple redirect", { url: redirect.url });
|
|
245
254
|
handle.complete(undefined);
|
|
246
|
-
|
|
247
|
-
await onNavigate(redirect.url, {
|
|
248
|
-
replace: true,
|
|
249
|
-
_skipCache: true,
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
window.location.href = redirect.url;
|
|
253
|
-
}
|
|
255
|
+
await dispatchRedirect(redirect.url);
|
|
254
256
|
return new Promise<Response>(() => {});
|
|
255
257
|
}
|
|
256
258
|
if (redirect === "blocked") {
|
|
@@ -339,18 +341,9 @@ export function createServerActionBridge(
|
|
|
339
341
|
handle.complete(returnValue?.data);
|
|
340
342
|
return returnValue?.data;
|
|
341
343
|
}
|
|
342
|
-
const redirectState = metadata.locationState;
|
|
343
344
|
log("action redirect", { url: redirectUrl });
|
|
344
345
|
handle.complete(returnValue?.data);
|
|
345
|
-
|
|
346
|
-
await onNavigate(redirectUrl, {
|
|
347
|
-
state: redirectState,
|
|
348
|
-
replace: true,
|
|
349
|
-
_skipCache: true,
|
|
350
|
-
});
|
|
351
|
-
} else {
|
|
352
|
-
window.location.href = redirectUrl;
|
|
353
|
-
}
|
|
346
|
+
await dispatchRedirect(redirectUrl, metadata.locationState);
|
|
354
347
|
return returnValue?.data;
|
|
355
348
|
}
|
|
356
349
|
|
package/src/browser/types.ts
CHANGED
|
@@ -552,6 +552,8 @@ export interface NavigationBridge {
|
|
|
552
552
|
refresh(): Promise<void>;
|
|
553
553
|
handlePopstate(): Promise<void>;
|
|
554
554
|
registerLinkInterception(): () => void;
|
|
555
|
+
/** Current RSC version (live, reflects the latest updateVersion). */
|
|
556
|
+
getVersion(): string | undefined;
|
|
555
557
|
/** Update the RSC version (e.g. after HMR). Clears prefetch cache. */
|
|
556
558
|
updateVersion(newVersion: string): void;
|
|
557
559
|
/**
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Collect the `"use client"` client-reference keys reachable from an error /
|
|
2
|
+
// notFound boundary registration, for routing them into the dedicated
|
|
3
|
+
// `app-fallback` chunk (see vite/utils/client-chunks.ts).
|
|
4
|
+
//
|
|
5
|
+
// A boundary registration is not always a bare client element. The common,
|
|
6
|
+
// load-bearing pattern wraps the client boundary in providers a thrown handler
|
|
7
|
+
// needs (the layout that would normally supply them did not mount):
|
|
8
|
+
//
|
|
9
|
+
// defaultErrorBoundary: ({ error }) => (
|
|
10
|
+
// <FallbackIntl locales={...}>
|
|
11
|
+
// <ThemedError error={error} /> // <- the real "use client" boundary
|
|
12
|
+
// </FallbackIntl>
|
|
13
|
+
// )
|
|
14
|
+
//
|
|
15
|
+
// So the value may be (a) a handler FUNCTION returning a tree, or (b) an element
|
|
16
|
+
// tree with the client boundary nested below server wrappers. We:
|
|
17
|
+
// 1. If it's a function, CALL it with synthetic props to get the returned tree.
|
|
18
|
+
// This only constructs JSX — the inner components are element `type`s, never
|
|
19
|
+
// invoked — so no hooks run. Guarded: a boundary that needs a real render
|
|
20
|
+
// context (request globals, etc.) throws and is skipped (graceful: it simply
|
|
21
|
+
// stays on the default grouping, as before).
|
|
22
|
+
// 2. Walk the resulting tree and report every element whose `.type` is a
|
|
23
|
+
// plugin-rsc client reference.
|
|
24
|
+
//
|
|
25
|
+
// Limit: a boundary that *conditionally* renders different client components based
|
|
26
|
+
// on the runtime error cannot be resolved statically — only the branch taken with
|
|
27
|
+
// the synthetic error is seen. Such cases fall back to the default chunk; the
|
|
28
|
+
// custom `clientChunks` function is the escape hatch.
|
|
29
|
+
|
|
30
|
+
const CLIENT_REF = Symbol.for("react.client.reference");
|
|
31
|
+
const MAX_DEPTH = 40;
|
|
32
|
+
|
|
33
|
+
// Synthetic props covering the error-boundary (`{ error, reset }`) and notFound
|
|
34
|
+
// (`{ pathname }`) handler shapes. The handler destructures what it needs.
|
|
35
|
+
const SYNTHETIC_PROPS = {
|
|
36
|
+
error: new Error("rango: build-time fallback-chunk discovery"),
|
|
37
|
+
reset: () => {},
|
|
38
|
+
pathname: "/",
|
|
39
|
+
info: { componentStack: "" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface MaybeElement {
|
|
43
|
+
type?: { $$typeof?: symbol; $$id?: string };
|
|
44
|
+
props?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isReactNodeLike(v: unknown): boolean {
|
|
48
|
+
return (
|
|
49
|
+
Array.isArray(v) ||
|
|
50
|
+
(typeof v === "object" && v !== null && "$$typeof" in (v as object))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walkElementTree(
|
|
55
|
+
node: unknown,
|
|
56
|
+
report: (refKey: string) => void,
|
|
57
|
+
depth: number,
|
|
58
|
+
): void {
|
|
59
|
+
if (node == null || depth > MAX_DEPTH) return;
|
|
60
|
+
if (Array.isArray(node)) {
|
|
61
|
+
for (const child of node) walkElementTree(child, report, depth + 1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof node !== "object") return;
|
|
65
|
+
|
|
66
|
+
const el = node as MaybeElement;
|
|
67
|
+
const type = el.type;
|
|
68
|
+
if (type?.$$typeof === CLIENT_REF && typeof type.$$id === "string") {
|
|
69
|
+
// $$id is `<referenceKey>#<exportName>` in build mode — keep the referenceKey.
|
|
70
|
+
report(type.$$id.split("#")[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = el.props;
|
|
74
|
+
if (props && typeof props === "object") {
|
|
75
|
+
// Children are always nodes; other props are followed only when they look
|
|
76
|
+
// like React nodes (slots/icons), never arbitrary data objects.
|
|
77
|
+
walkElementTree(props.children, report, depth + 1);
|
|
78
|
+
for (const key in props) {
|
|
79
|
+
if (key === "children") continue;
|
|
80
|
+
const value = props[key];
|
|
81
|
+
if (isReactNodeLike(value)) walkElementTree(value, report, depth + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Report every `"use client"` client-reference key reachable from a single
|
|
88
|
+
* error/notFound boundary registration (handler function or element tree).
|
|
89
|
+
*/
|
|
90
|
+
export function collectFallbackClientRefs(
|
|
91
|
+
boundary: unknown,
|
|
92
|
+
report: (refKey: string) => void,
|
|
93
|
+
): void {
|
|
94
|
+
try {
|
|
95
|
+
let node = boundary;
|
|
96
|
+
if (typeof node === "function") {
|
|
97
|
+
node = (node as (props: unknown) => unknown)(SYNTHETIC_PROPS);
|
|
98
|
+
}
|
|
99
|
+
walkElementTree(node, report, 0);
|
|
100
|
+
} catch {
|
|
101
|
+
// The boundary needs a real render context (request globals, hooks at the
|
|
102
|
+
// top level) or its tree has hostile getters. Its client refs can't be
|
|
103
|
+
// resolved statically — skip. It stays on the default grouping (no
|
|
104
|
+
// regression vs. not collecting), and the custom clientChunks fn is the
|
|
105
|
+
// escape hatch for such cases.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
import type { UrlPatterns } from "../urls.js";
|
|
12
12
|
import type { AllUseItems } from "../route-types.js";
|
|
13
13
|
import { extractStaticPrefix } from "../router/pattern-matching.js";
|
|
14
|
-
import {
|
|
14
|
+
import { RangoContext, runWithPrefixes } from "../server/context.js";
|
|
15
15
|
import type { EntryData, TrackedInclude } from "../server/context.js";
|
|
16
16
|
import type { TrailingSlashMode } from "../types.js";
|
|
17
17
|
import { createRouteHelpers } from "../route-definition.js";
|
|
18
18
|
import MapRootLayout from "../server/root-layout.js";
|
|
19
|
+
import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Node in the prefix tree
|
|
@@ -57,6 +58,26 @@ export interface GeneratedManifest {
|
|
|
57
58
|
* Build prefix tree node by running the patterns with proper context.
|
|
58
59
|
* Uses a visited set to detect circular includes and prevent infinite recursion.
|
|
59
60
|
*/
|
|
61
|
+
// Merge tracked nested includes into `target`. Multiple includes can share a
|
|
62
|
+
// fullPrefix (e.g. include("/", a), include("/", b)) — concat their routes and
|
|
63
|
+
// Object.assign children rather than overwrite.
|
|
64
|
+
function mergeIncludeNodes(
|
|
65
|
+
target: Record<string, PrefixTreeNode>,
|
|
66
|
+
includes: TrackedInclude[],
|
|
67
|
+
buildChild: (include: TrackedInclude) => PrefixTreeNode,
|
|
68
|
+
): void {
|
|
69
|
+
for (const include of includes) {
|
|
70
|
+
const node = buildChild(include);
|
|
71
|
+
const existing = target[include.fullPrefix];
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.routes.push(...node.routes);
|
|
74
|
+
Object.assign(existing.children, node.children);
|
|
75
|
+
} else {
|
|
76
|
+
target[include.fullPrefix] = node;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
function buildPrefixTreeNode(
|
|
61
82
|
urlPrefix: string,
|
|
62
83
|
namePrefix: string | undefined,
|
|
@@ -93,7 +114,7 @@ function buildPrefixTreeNode(
|
|
|
93
114
|
const searchSchemasMap = new Map<string, Record<string, string>>();
|
|
94
115
|
const trackedIncludes: TrackedInclude[] = [];
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
RangoContext.run(
|
|
97
118
|
{
|
|
98
119
|
manifest,
|
|
99
120
|
patterns: patternsMap,
|
|
@@ -166,13 +187,9 @@ function buildPrefixTreeNode(
|
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
// Build children from tracked nested includes.
|
|
170
|
-
// Multiple includes can share the same fullPrefix (e.g., include("/", patternsA),
|
|
171
|
-
// include("/", patternsB)). Merge their routes instead of overwriting.
|
|
172
190
|
const children: Record<string, PrefixTreeNode> = {};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const childNode = buildPrefixTreeNode(
|
|
191
|
+
mergeIncludeNodes(children, trackedIncludes, (include) =>
|
|
192
|
+
buildPrefixTreeNode(
|
|
176
193
|
include.fullPrefix,
|
|
177
194
|
include.namePrefix,
|
|
178
195
|
include.patterns as UrlPatterns<any>,
|
|
@@ -186,16 +203,8 @@ function buildPrefixTreeNode(
|
|
|
186
203
|
passthroughRoutes,
|
|
187
204
|
responseTypeRoutes,
|
|
188
205
|
routeSearchSchemas,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
const existing = children[include.fullPrefix];
|
|
192
|
-
if (existing) {
|
|
193
|
-
existing.routes.push(...childNode.routes);
|
|
194
|
-
Object.assign(existing.children, childNode.children);
|
|
195
|
-
} else {
|
|
196
|
-
children[include.fullPrefix] = childNode;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
206
|
+
),
|
|
207
|
+
);
|
|
199
208
|
|
|
200
209
|
// Remove from visited so sibling branches can reuse the same patterns
|
|
201
210
|
// without false circular-include detection. Only ancestors in the current
|
|
@@ -282,7 +291,17 @@ export function generateManifest<TEnv>(
|
|
|
282
291
|
export function generateManifestFull<TEnv>(
|
|
283
292
|
urlpatterns: UrlPatterns<TEnv, any>,
|
|
284
293
|
mountIndex: number = 0,
|
|
285
|
-
options?: {
|
|
294
|
+
options?: {
|
|
295
|
+
urlPrefix?: string;
|
|
296
|
+
/**
|
|
297
|
+
* Called once per `"use client"` component registered as an
|
|
298
|
+
* errorBoundary/notFoundBoundary fallback, with its client-reference key
|
|
299
|
+
* (`$$id`). Lets the build collect fallback module ids for dedicated
|
|
300
|
+
* chunking without exposing the otherwise-discarded EntryData tree. The
|
|
301
|
+
* EntryData map built below is local; this is the only seam that surfaces it.
|
|
302
|
+
*/
|
|
303
|
+
collectClientFallbackRef?: (refKey: string) => void;
|
|
304
|
+
},
|
|
286
305
|
): FullManifest {
|
|
287
306
|
const routeManifest: Record<string, string> = {};
|
|
288
307
|
const routeAncestry: Record<string, string[]> = {};
|
|
@@ -296,7 +315,7 @@ export function generateManifestFull<TEnv>(
|
|
|
296
315
|
const searchSchemasMap = new Map<string, Record<string, string>>();
|
|
297
316
|
const trackedIncludes: TrackedInclude[] = [];
|
|
298
317
|
|
|
299
|
-
|
|
318
|
+
RangoContext.run(
|
|
300
319
|
{
|
|
301
320
|
manifest,
|
|
302
321
|
patterns: patternsMap,
|
|
@@ -320,6 +339,22 @@ export function generateManifestFull<TEnv>(
|
|
|
320
339
|
},
|
|
321
340
|
);
|
|
322
341
|
|
|
342
|
+
// Surface the "use client" components registered as error/notFound fallbacks
|
|
343
|
+
// (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
|
|
344
|
+
// The boundary may be a handler function and/or wrap the client boundary in
|
|
345
|
+
// server providers, so walk the whole tree (see collectFallbackClientRefs).
|
|
346
|
+
if (options?.collectClientFallbackRef) {
|
|
347
|
+
const report = options.collectClientFallbackRef;
|
|
348
|
+
const collect = (boundary: unknown[] | undefined) => {
|
|
349
|
+
for (const item of boundary ?? [])
|
|
350
|
+
collectFallbackClientRefs(item, report);
|
|
351
|
+
};
|
|
352
|
+
for (const entry of manifest.values()) {
|
|
353
|
+
collect(entry.errorBoundary);
|
|
354
|
+
collect(entry.notFoundBoundary);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
323
358
|
// Collect root-level routes and trailing slash config
|
|
324
359
|
const routeTrailingSlash: Record<string, string> = {};
|
|
325
360
|
for (const [name, pattern] of patternsMap.entries()) {
|
|
@@ -356,12 +391,10 @@ export function generateManifestFull<TEnv>(
|
|
|
356
391
|
}
|
|
357
392
|
}
|
|
358
393
|
|
|
359
|
-
//
|
|
360
|
-
// Multiple includes can share the same fullPrefix (e.g., include("/", patternsA),
|
|
361
|
-
// include("/", patternsB)). Merge their routes instead of overwriting.
|
|
394
|
+
// Shared visited set for cycle detection across all root-level includes.
|
|
362
395
|
const visited = new Set<unknown>();
|
|
363
|
-
|
|
364
|
-
|
|
396
|
+
mergeIncludeNodes(prefixTree, trackedIncludes, (include) =>
|
|
397
|
+
buildPrefixTreeNode(
|
|
365
398
|
include.fullPrefix,
|
|
366
399
|
include.namePrefix,
|
|
367
400
|
include.patterns as UrlPatterns<any>,
|
|
@@ -375,16 +408,8 @@ export function generateManifestFull<TEnv>(
|
|
|
375
408
|
passthroughRoutes,
|
|
376
409
|
responseTypeRoutes,
|
|
377
410
|
routeSearchSchemas,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
const existing = prefixTree[include.fullPrefix];
|
|
381
|
-
if (existing) {
|
|
382
|
-
existing.routes.push(...node.routes);
|
|
383
|
-
Object.assign(existing.children, node.children);
|
|
384
|
-
} else {
|
|
385
|
-
prefixTree[include.fullPrefix] = node;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
411
|
+
),
|
|
412
|
+
);
|
|
388
413
|
|
|
389
414
|
return {
|
|
390
415
|
prefixTree,
|
package/src/build/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function generatePerModuleTypesSource(
|
|
|
23
23
|
const valid = routes.filter(({ name }) => {
|
|
24
24
|
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
25
25
|
console.warn(
|
|
26
|
-
`[
|
|
26
|
+
`[rango] Skipping route with invalid name: ${JSON.stringify(name)}`,
|
|
27
27
|
);
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
@@ -42,7 +42,7 @@ export function generatePerModuleTypesSource(
|
|
|
42
42
|
for (const { name, pattern, params, search } of valid) {
|
|
43
43
|
if (deduped.has(name)) {
|
|
44
44
|
console.warn(
|
|
45
|
-
`[
|
|
45
|
+
`[rango] Duplicate route name "${name}" — keeping first definition`,
|
|
46
46
|
);
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
@@ -59,7 +59,7 @@ export function generatePerModuleTypesSource(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Generates a .ts file that augments
|
|
62
|
+
* Generates a .ts file that augments Rango.GeneratedRouteMap
|
|
63
63
|
* with route name -> pattern mappings. This enables Handler<"routeName">
|
|
64
64
|
* without circular references since the file has no imports from the app.
|
|
65
65
|
*/
|
|
@@ -94,7 +94,7 @@ ${objectBody}
|
|
|
94
94
|
} as const;
|
|
95
95
|
|
|
96
96
|
declare global {
|
|
97
|
-
namespace
|
|
97
|
+
namespace Rango {
|
|
98
98
|
interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -376,7 +376,7 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
376
376
|
const realPath = resolve(filePath);
|
|
377
377
|
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
378
378
|
if (visited.has(key)) {
|
|
379
|
-
console.warn(`[
|
|
379
|
+
console.warn(`[rango] Circular include detected, skipping: ${key}`);
|
|
380
380
|
return { routes: {}, searchSchemas: {} };
|
|
381
381
|
}
|
|
382
382
|
visited.add(key);
|
|
@@ -97,7 +97,10 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
97
97
|
routes = extractRoutesFromSource(source);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// Match .ts/.tsx/.js/.jsx (same as router-processing.ts / router-transform.ts).
|
|
101
|
+
// Without the jsx? branch a .jsx/.js source produced genPath === filePath,
|
|
102
|
+
// overwriting the source file instead of writing a sibling .gen.ts.
|
|
103
|
+
const genPath = filePath.replace(/\.(tsx?|jsx?)$/, ".gen.ts");
|
|
101
104
|
|
|
102
105
|
// When a urls() variable was found but static resolution yields zero
|
|
103
106
|
// routes, write an empty placeholder so generated imports stay
|
|
@@ -106,7 +109,7 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
106
109
|
if (varNames.length > 0 && !existsSync(genPath)) {
|
|
107
110
|
writeFileSync(genPath, generatePerModuleTypesSource([]));
|
|
108
111
|
console.log(
|
|
109
|
-
`[
|
|
112
|
+
`[rango] Generated route types (placeholder) -> ${genPath}`,
|
|
110
113
|
);
|
|
111
114
|
}
|
|
112
115
|
return;
|
|
@@ -118,11 +121,11 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
118
121
|
: null;
|
|
119
122
|
if (existing !== genSource) {
|
|
120
123
|
writeFileSync(genPath, genSource);
|
|
121
|
-
console.log(`[
|
|
124
|
+
console.log(`[rango] Generated route types -> ${genPath}`);
|
|
122
125
|
}
|
|
123
126
|
} catch (err) {
|
|
124
127
|
console.warn(
|
|
125
|
-
`[
|
|
128
|
+
`[rango] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
|
|
126
129
|
);
|
|
127
130
|
}
|
|
128
131
|
}
|