@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
createScanFilter,
|
|
20
20
|
} from "../build/generate-route-types.js";
|
|
21
21
|
import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
|
|
22
|
+
import {
|
|
23
|
+
injectClientDebugFlag,
|
|
24
|
+
internalDebugNoCacheMiddleware,
|
|
25
|
+
} from "./inject-client-debug.js";
|
|
22
26
|
import { createVersionPlugin } from "./plugins/version-plugin.js";
|
|
23
27
|
import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
|
|
24
28
|
import {
|
|
@@ -42,6 +46,12 @@ import {
|
|
|
42
46
|
peekSelfGenWrite,
|
|
43
47
|
} from "./discovery/self-gen-tracking.js";
|
|
44
48
|
import { discoverRouters } from "./discovery/discover-routers.js";
|
|
49
|
+
import { describeDiscoveryFailure } from "./discovery/discovery-errors.js";
|
|
50
|
+
import {
|
|
51
|
+
createDevPrerenderCache,
|
|
52
|
+
devPrerenderCacheKey,
|
|
53
|
+
payloadBodiesFromResult,
|
|
54
|
+
} from "./discovery/dev-prerender-cache.js";
|
|
45
55
|
import {
|
|
46
56
|
writeCombinedRouteTypesWithTracking,
|
|
47
57
|
writeRouteTypesFiles,
|
|
@@ -310,19 +320,13 @@ export function createRouterDiscoveryPlugin(
|
|
|
310
320
|
return {
|
|
311
321
|
name: "@rangojs/router:discovery",
|
|
312
322
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// rest of the RSC entry. A previous design forced them into dedicated
|
|
321
|
-
// __prerender-handlers / __static-handlers chunks via manualChunks,
|
|
322
|
-
// but Rollup hoisted all shared dependencies into those chunks,
|
|
323
|
-
// inflating them to ~1 MB with active runtime code. Handler code is
|
|
324
|
-
// evicted in closeBundle regardless of which chunk it lands in.
|
|
325
|
-
return config;
|
|
323
|
+
// Make INTERNAL_RANGO_DEBUG reach the CLIENT debug logs by just setting the
|
|
324
|
+
// env var. See injectClientDebugFlag: bakes the resolved flag into the
|
|
325
|
+
// internal-debug module so FE debug no longer depends on Vite delivering the
|
|
326
|
+
// `__RANGO_DEBUG__` define to the client (which it does only as an injected
|
|
327
|
+
// global whose presence varies across consumer setups). Runs in dev and build.
|
|
328
|
+
transform(_code, id) {
|
|
329
|
+
return injectClientDebugFlag(id);
|
|
326
330
|
},
|
|
327
331
|
|
|
328
332
|
configResolved(config) {
|
|
@@ -391,6 +395,11 @@ export function createRouterDiscoveryPlugin(
|
|
|
391
395
|
if ((globalThis as any).__rscRouterDiscoveryActive) return;
|
|
392
396
|
s.devServer = server;
|
|
393
397
|
|
|
398
|
+
// Serve the internal-debug module no-cache: consumers resolve it into
|
|
399
|
+
// node_modules, where dev's immutable `?v=` caching pinned browsers to a
|
|
400
|
+
// stale baked INTERNAL_RANGO_DEBUG. See internalDebugNoCacheMiddleware.
|
|
401
|
+
server.middlewares.use(internalDebugNoCacheMiddleware());
|
|
402
|
+
|
|
394
403
|
// Discovery promise that the handler can await if requests arrive
|
|
395
404
|
// before discovery completes
|
|
396
405
|
let resolveDiscovery: () => void;
|
|
@@ -549,6 +558,14 @@ export function createRouterDiscoveryPlugin(
|
|
|
549
558
|
);
|
|
550
559
|
console.warn(`[rango] Failed to create temp runner: ${err.message}`);
|
|
551
560
|
}
|
|
561
|
+
// Reached only on failure (runner unavailable, or create/import threw
|
|
562
|
+
// AFTER the server was created). Close the just-created server so a
|
|
563
|
+
// failed discovery does not leak it until the next call or dev shutdown,
|
|
564
|
+
// and null the refs so the reuse path above starts clean. Mirrors the
|
|
565
|
+
// close pattern used when an existing server is discarded (above).
|
|
566
|
+
await prerenderTempServer?.close().catch(() => {});
|
|
567
|
+
prerenderTempServer = null;
|
|
568
|
+
prerenderNodeRegistry = null;
|
|
552
569
|
return null;
|
|
553
570
|
}
|
|
554
571
|
|
|
@@ -650,6 +667,30 @@ export function createRouterDiscoveryPlugin(
|
|
|
650
667
|
return tempRscEnv;
|
|
651
668
|
}
|
|
652
669
|
|
|
670
|
+
// Surface a discovery failure on either dev path (Node RSC runner or the
|
|
671
|
+
// Cloudflare temp Node server). `hashBefore`/`hashAfter` are the discovering
|
|
672
|
+
// environment's dep-optimizer browserHash snapshots: a change across the
|
|
673
|
+
// attempt means a reload-causing re-optimization landed mid-flight, so an
|
|
674
|
+
// empty registry was the transient race (downgraded to a warning) rather
|
|
675
|
+
// than a genuine misconfig (loud, actionable error). Shared so both catch
|
|
676
|
+
// sites frame the same failure identically.
|
|
677
|
+
const emitDiscoveryFailure = (
|
|
678
|
+
err: unknown,
|
|
679
|
+
hashBefore: string | undefined,
|
|
680
|
+
hashAfter: string | undefined,
|
|
681
|
+
): void => {
|
|
682
|
+
const reoptimizeObserved =
|
|
683
|
+
hashBefore !== undefined &&
|
|
684
|
+
hashAfter !== undefined &&
|
|
685
|
+
hashBefore !== hashAfter;
|
|
686
|
+
const report = describeDiscoveryFailure(err, { reoptimizeObserved });
|
|
687
|
+
if (report.level === "warn") {
|
|
688
|
+
console.warn(report.message);
|
|
689
|
+
} else {
|
|
690
|
+
console.error(report.message);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
653
694
|
const discover = async () => {
|
|
654
695
|
const discoverStart = performance.now();
|
|
655
696
|
const rscEnv = (server.environments as any)?.rsc;
|
|
@@ -665,18 +706,26 @@ export function createRouterDiscoveryPlugin(
|
|
|
665
706
|
|
|
666
707
|
// Create a temp Node.js server to run runtime discovery and generate
|
|
667
708
|
// named route types (static parser can't resolve factory calls).
|
|
709
|
+
// The temp server is a separate Vite instance with its own dep
|
|
710
|
+
// optimizer; snapshot ITS browserHash (hoisted so the catch can tell a
|
|
711
|
+
// transient re-optimization apart from a genuine empty registry, the
|
|
712
|
+
// same way the Node path below does).
|
|
713
|
+
let tempRscEnv: any;
|
|
714
|
+
let optimizerHashBefore: string | undefined;
|
|
668
715
|
try {
|
|
669
716
|
// Acquire build-time env bindings for dev prerender
|
|
670
717
|
await timed(debugDiscovery, "acquireBuildEnv", () =>
|
|
671
718
|
acquireBuildEnv(s, viteCommand, viteMode),
|
|
672
719
|
);
|
|
673
720
|
|
|
674
|
-
|
|
721
|
+
tempRscEnv = await timed(
|
|
675
722
|
debugDiscovery,
|
|
676
723
|
"getOrCreateTempServer",
|
|
677
724
|
() => getOrCreateTempServer(),
|
|
678
725
|
);
|
|
679
726
|
if (tempRscEnv) {
|
|
727
|
+
optimizerHashBefore =
|
|
728
|
+
tempRscEnv.depsOptimizer?.metadata?.browserHash;
|
|
680
729
|
await timed(debugDiscovery, "discoverRouters (cloudflare)", () =>
|
|
681
730
|
discoverRouters(s, tempRscEnv),
|
|
682
731
|
);
|
|
@@ -685,8 +734,10 @@ export function createRouterDiscoveryPlugin(
|
|
|
685
734
|
);
|
|
686
735
|
}
|
|
687
736
|
} catch (err: any) {
|
|
688
|
-
|
|
689
|
-
|
|
737
|
+
emitDiscoveryFailure(
|
|
738
|
+
err,
|
|
739
|
+
optimizerHashBefore,
|
|
740
|
+
tempRscEnv?.depsOptimizer?.metadata?.browserHash,
|
|
690
741
|
);
|
|
691
742
|
}
|
|
692
743
|
|
|
@@ -698,6 +749,13 @@ export function createRouterDiscoveryPlugin(
|
|
|
698
749
|
return;
|
|
699
750
|
}
|
|
700
751
|
|
|
752
|
+
// Snapshot the dep-optimizer hash before discovery so the catch can tell
|
|
753
|
+
// a transient re-optimization race apart from a genuine empty registry.
|
|
754
|
+
// A reload-causing re-optimization regenerates browserHash; if it changed
|
|
755
|
+
// across the attempt, an empty read was almost certainly the race below.
|
|
756
|
+
const optimizerHashBefore: string | undefined =
|
|
757
|
+
rscEnv.depsOptimizer?.metadata?.browserHash;
|
|
758
|
+
|
|
701
759
|
try {
|
|
702
760
|
// Acquire build-time env bindings for dev prerender (Node.js path)
|
|
703
761
|
debugDiscovery?.("dev: node path start");
|
|
@@ -705,21 +763,31 @@ export function createRouterDiscoveryPlugin(
|
|
|
705
763
|
acquireBuildEnv(s, viteCommand, viteMode),
|
|
706
764
|
);
|
|
707
765
|
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
766
|
+
// Discover routers FIRST, then arm the manifest-readiness gate on the
|
|
767
|
+
// server module discovery actually read the registry from.
|
|
768
|
+
//
|
|
769
|
+
// We deliberately do NOT pre-import "@rangojs/router/server" before the
|
|
770
|
+
// entry to arm the gate early. During a Vite dependency re-optimization
|
|
771
|
+
// (dev boot after a lockfile change, or `vite dev --force`), a module
|
|
772
|
+
// imported here before the entry resolves to the pre-optimize copy of
|
|
773
|
+
// the runner's module graph, while discoverRouters' entry import — which
|
|
774
|
+
// awaits the in-flight re-optimization — resolves to the post-optimize
|
|
775
|
+
// copy. createRouter() then populates RouterRegistry on the fresh copy,
|
|
776
|
+
// but a stale pre-imported "@rangojs/router/server" reads the other
|
|
777
|
+
// copy's empty Map and discovery throws a spurious "No routers found"
|
|
778
|
+
// even though the app is configured correctly. discoverRouters imports
|
|
779
|
+
// the entry first and reads the registry off the same instance, keeping
|
|
780
|
+
// read and write on one copy. The virtual manifest module's own gate
|
|
781
|
+
// (s.discoveryDone, armed by beginDiscoveryGate) already blocks early
|
|
782
|
+
// requests during discovery on the Node path, so arming
|
|
783
|
+
// manifestReadyPromise after discovery is sufficient here.
|
|
784
|
+
const serverMod = await timed(debugDiscovery, "discoverRouters", () =>
|
|
785
|
+
discoverRouters(s, rscEnv),
|
|
714
786
|
);
|
|
715
787
|
if (serverMod?.setManifestReadyPromise) {
|
|
716
788
|
serverMod.setManifestReadyPromise(discoveryPromise);
|
|
717
789
|
}
|
|
718
790
|
|
|
719
|
-
await timed(debugDiscovery, "discoverRouters", () =>
|
|
720
|
-
discoverRouters(s, rscEnv),
|
|
721
|
-
);
|
|
722
|
-
|
|
723
791
|
// Store server origin for dev prerender endpoint (virtual module injection)
|
|
724
792
|
s.devServerOrigin = getDevServerOrigin();
|
|
725
793
|
|
|
@@ -737,8 +805,10 @@ export function createRouterDiscoveryPlugin(
|
|
|
737
805
|
propagateDiscoveryState(rscEnv),
|
|
738
806
|
);
|
|
739
807
|
} catch (err: any) {
|
|
740
|
-
|
|
741
|
-
|
|
808
|
+
emitDiscoveryFailure(
|
|
809
|
+
err,
|
|
810
|
+
optimizerHashBefore,
|
|
811
|
+
rscEnv.depsOptimizer?.metadata?.browserHash,
|
|
742
812
|
);
|
|
743
813
|
} finally {
|
|
744
814
|
debugDiscovery?.(
|
|
@@ -771,6 +841,15 @@ export function createRouterDiscoveryPlugin(
|
|
|
771
841
|
// Registry from the main server's RSC environment (populated by discoverRouters)
|
|
772
842
|
let mainRegistry: Map<string, any> | null = null;
|
|
773
843
|
|
|
844
|
+
// Memoized /__rsc_prerender render results, keyed by router-instance
|
|
845
|
+
// identity (#654). The per-request entry re-import below is what makes
|
|
846
|
+
// identity a valid freshness key: an HMR-invalidated chain re-runs
|
|
847
|
+
// createRouter() and replaces the registry instance, so cached bodies
|
|
848
|
+
// for the old instance become unreachable; an untouched chain returns
|
|
849
|
+
// the same instance and the cached body is byte-identical to a fresh
|
|
850
|
+
// render. See dev-prerender-cache.ts for the full invariant.
|
|
851
|
+
const devPrerenderCache = createDevPrerenderCache();
|
|
852
|
+
|
|
774
853
|
// Push discovery state (manifest, trie, precomputed entries) to the
|
|
775
854
|
// server module so runtime request handling uses the current routes.
|
|
776
855
|
// Shared by initial discovery and HMR-triggered re-discovery.
|
|
@@ -861,9 +940,28 @@ export function createRouterDiscoveryPlugin(
|
|
|
861
940
|
|
|
862
941
|
if (!registry) {
|
|
863
942
|
// No main registry: the RSC env has no module runner (Cloudflare dev).
|
|
864
|
-
// Lazily create a Node.js temp server for prerender evaluation
|
|
865
|
-
|
|
866
|
-
|
|
943
|
+
// Lazily create a Node.js temp server for prerender evaluation, and
|
|
944
|
+
// re-import the entry through it on EVERY request — the temp server
|
|
945
|
+
// has its own file watcher, so a handler-only edit (a file without
|
|
946
|
+
// urls()/createRouter() that the main watcher's route-file sniff
|
|
947
|
+
// ignores) invalidates its module graph; the re-import re-evaluates
|
|
948
|
+
// exactly the dirty subgraph and re-registers fresh router
|
|
949
|
+
// instances. Before #654 the cached registry was only refreshed on
|
|
950
|
+
// route-file edits, so handler-only edits served stale prerender
|
|
951
|
+
// content on this path. Warm-cache re-imports are module-cache hits.
|
|
952
|
+
const tempRscEnv = await getOrCreateTempServer();
|
|
953
|
+
if (tempRscEnv) {
|
|
954
|
+
try {
|
|
955
|
+
await importEntryAndRegistry(tempRscEnv);
|
|
956
|
+
} catch (err: any) {
|
|
957
|
+
console.warn(
|
|
958
|
+
`[rango] Dev prerender module refresh failed: ${err.message}`,
|
|
959
|
+
);
|
|
960
|
+
res.statusCode = 500;
|
|
961
|
+
res.end(`Prerender handler error: ${err.message}`);
|
|
962
|
+
logResult(500, "temp module refresh failed");
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
867
965
|
}
|
|
868
966
|
registry = prerenderNodeRegistry;
|
|
869
967
|
}
|
|
@@ -879,8 +977,38 @@ export function createRouterDiscoveryPlugin(
|
|
|
879
977
|
const wantRouteName = url.searchParams.get("routeName");
|
|
880
978
|
const wantPassthrough = url.searchParams.get("passthrough") === "1";
|
|
881
979
|
|
|
980
|
+
// One render warms BOTH variant keys (matchForPrerender computes the
|
|
981
|
+
// intercept segments unconditionally), so a route's main and modal
|
|
982
|
+
// variants cost a single render per HMR generation.
|
|
983
|
+
const variantDims = {
|
|
984
|
+
passthrough: wantPassthrough,
|
|
985
|
+
routeName: wantRouteName,
|
|
986
|
+
};
|
|
987
|
+
const keyMain = devPrerenderCacheKey(pathname, {
|
|
988
|
+
intercept: false,
|
|
989
|
+
...variantDims,
|
|
990
|
+
});
|
|
991
|
+
const keyIntercept = devPrerenderCacheKey(pathname, {
|
|
992
|
+
intercept: true,
|
|
993
|
+
...variantDims,
|
|
994
|
+
});
|
|
995
|
+
const requestedKey = wantIntercept ? keyIntercept : keyMain;
|
|
996
|
+
|
|
882
997
|
for (const [, routerInstance] of registry) {
|
|
883
998
|
if (!routerInstance.matchForPrerender) continue;
|
|
999
|
+
// Cache is consulted per router IN LOOP ORDER so multi-router
|
|
1000
|
+
// fall-through semantics are identical to the uncached path: a
|
|
1001
|
+
// router that never produced a payload for this key still runs
|
|
1002
|
+
// its matchForPrerender (cheap trie miss / intentionally-uncached
|
|
1003
|
+
// error retry) before the next router is considered.
|
|
1004
|
+
const cached = devPrerenderCache.get(routerInstance, requestedKey);
|
|
1005
|
+
if (cached !== undefined) {
|
|
1006
|
+
res.setHeader("content-type", "application/json");
|
|
1007
|
+
res.setHeader("x-rango-prerender-cache", "HIT");
|
|
1008
|
+
res.end(cached);
|
|
1009
|
+
logResult(200, "cache hit");
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
884
1012
|
try {
|
|
885
1013
|
const result = await routerInstance.matchForPrerender(
|
|
886
1014
|
pathname,
|
|
@@ -896,19 +1024,19 @@ export function createRouterDiscoveryPlugin(
|
|
|
896
1024
|
// This prevents returning the wrong entry when multiple routers
|
|
897
1025
|
// have prerenderable routes sharing the same pathname.
|
|
898
1026
|
if (wantRouteName && result.routeName !== wantRouteName) continue;
|
|
1027
|
+
// Pre-encoded MERGED handle string in the intercept body comes
|
|
1028
|
+
// from the producer (handles are Flight-encoded so
|
|
1029
|
+
// Promise/ReactNode values survive the wire).
|
|
1030
|
+
const bodies = payloadBodiesFromResult(result);
|
|
1031
|
+
devPrerenderCache.set(routerInstance, keyMain, bodies.main);
|
|
1032
|
+
devPrerenderCache.set(
|
|
1033
|
+
routerInstance,
|
|
1034
|
+
keyIntercept,
|
|
1035
|
+
bodies.intercept,
|
|
1036
|
+
);
|
|
899
1037
|
res.setHeader("content-type", "application/json");
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
payload = {
|
|
903
|
-
segments: [...result.segments, ...result.interceptSegments],
|
|
904
|
-
// Pre-encoded MERGED handle string from the producer (handles are
|
|
905
|
-
// Flight-encoded so Promise/ReactNode values survive the wire).
|
|
906
|
-
handles: result.interceptHandles ?? "",
|
|
907
|
-
};
|
|
908
|
-
} else {
|
|
909
|
-
payload = { segments: result.segments, handles: result.handles };
|
|
910
|
-
}
|
|
911
|
-
res.end(JSON.stringify(payload));
|
|
1038
|
+
res.setHeader("x-rango-prerender-cache", "MISS");
|
|
1039
|
+
res.end(wantIntercept ? bodies.intercept : bodies.main);
|
|
912
1040
|
logResult(200, `match ${result.routeName}`);
|
|
913
1041
|
return;
|
|
914
1042
|
} catch (err: any) {
|
|
@@ -10,12 +10,10 @@ import {
|
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { escapeRegExp } from "../../regex-escape.js";
|
|
13
|
+
import { encodePathRemainder } from "../../router/url-params.js";
|
|
13
14
|
|
|
14
15
|
export function encodePathParam(value: unknown): string {
|
|
15
|
-
return String(value)
|
|
16
|
-
.split("/")
|
|
17
|
-
.map((segment) => encodeURIComponent(segment))
|
|
18
|
-
.join("/");
|
|
16
|
+
return encodePathRemainder(String(value), encodeURIComponent);
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export function substituteRouteParams(
|
|
@@ -28,6 +26,21 @@ export function substituteRouteParams(
|
|
|
28
26
|
|
|
29
27
|
for (const [key, value] of Object.entries(params)) {
|
|
30
28
|
const escaped = escapeRegExp(key);
|
|
29
|
+
// Named catch-all `:key+` / `:key*` (issue #634). Like the runtime reverse,
|
|
30
|
+
// the value is multi-segment: encode each segment and keep the `/` separators
|
|
31
|
+
// (a whole-value encode would turn `a/b` into `a%2Fb`), and consume the
|
|
32
|
+
// trailing modifier so it does not leak into the URL. `*` (zero-or-more)
|
|
33
|
+
// absent collapses via the trailing cleanup below.
|
|
34
|
+
const catchAllRe = new RegExp(`:${escaped}[+*]`);
|
|
35
|
+
if (catchAllRe.test(result)) {
|
|
36
|
+
if (value === "") {
|
|
37
|
+
result = result.replace(catchAllRe, "");
|
|
38
|
+
hadOmittedOptional = true;
|
|
39
|
+
} else {
|
|
40
|
+
result = result.replace(catchAllRe, encodePathRemainder(value, encode));
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
31
44
|
if (value === "") {
|
|
32
45
|
result = result.replace(
|
|
33
46
|
new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
2
|
import * as Vite from "vite";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { getPublishedPackageName } from "./package-resolution.js";
|
|
4
5
|
import { performanceTracksOptimizeDepsPlugin } from "../plugins/performance-tracks.js";
|
|
5
6
|
import {
|
|
@@ -55,6 +56,52 @@ export const sharedRolldownOptions: {
|
|
|
55
56
|
plugins: [versionRolldownPlugin, performanceTracksOptimizeDepsPlugin()],
|
|
56
57
|
};
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Normalize an explicit `hostRouter` option into the specifier emitted as the
|
|
61
|
+
* host entry's `import ... from "<path>"`, and — for the forms that resolve
|
|
62
|
+
* unambiguously against the project root — verify the file exists (failing with
|
|
63
|
+
* a rango message instead of a downstream bundler "failed to resolve").
|
|
64
|
+
*
|
|
65
|
+
* Only a BARE specifier ("src/worker.rsc.tsx") that exists under the root is
|
|
66
|
+
* rewritten (to "./src/...") so it is not read as a package. A bare specifier
|
|
67
|
+
* that does NOT exist under the root is passed through VERBATIM: it may be a
|
|
68
|
+
* Vite alias ("@/worker.rsc.tsx"), an imports-map entry ("#app/worker"), or a
|
|
69
|
+
* workspace package the bundler resolves — rewriting or rejecting those was a
|
|
70
|
+
* regression (aliases worked before normalization existed). "./"/"../", a
|
|
71
|
+
* leading-slash (Vite root-relative), and a filesystem-absolute path are
|
|
72
|
+
* emitted unchanged — Vite resolves each. A leading-slash path and a
|
|
73
|
+
* filesystem-absolute path are indistinguishable on POSIX, so both are treated
|
|
74
|
+
* as "rooted": passed through WITHOUT an existence check (guessing between the
|
|
75
|
+
* two would wrongly reject a valid file — this was a regression the first time
|
|
76
|
+
* the check was added). Only the explicitly-relative forms, which resolve
|
|
77
|
+
* unambiguously against the root, fail fast with a rango message.
|
|
78
|
+
*
|
|
79
|
+
* `exists` is injected so the logic is unit-testable without a real filesystem.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeHostRouterEntry(
|
|
82
|
+
rawInput: string,
|
|
83
|
+
root: string,
|
|
84
|
+
exists: (absPath: string) => boolean,
|
|
85
|
+
): string {
|
|
86
|
+
const raw = rawInput.replaceAll("\\", "/");
|
|
87
|
+
const isRelative = raw.startsWith("./") || raw.startsWith("../");
|
|
88
|
+
const isRooted = raw.startsWith("/") || isAbsolute(rawInput);
|
|
89
|
+
if (isRooted) return raw;
|
|
90
|
+
if (isRelative) {
|
|
91
|
+
if (!exists(resolve(root, raw))) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[rango] hostRouter entry not found: "${rawInput}" (resolved under ` +
|
|
94
|
+
`${root}). Point it at your createHostRouter() module, e.g. ` +
|
|
95
|
+
`rango({ hostRouter: "./src/worker.rsc.tsx" }).`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
// Bare specifier: an existing root-relative file gets the explicit "./";
|
|
101
|
+
// anything else (alias, imports-map, package) is the bundler's to resolve.
|
|
102
|
+
return exists(resolve(root, raw)) ? "./" + raw : raw;
|
|
103
|
+
}
|
|
104
|
+
|
|
58
105
|
/**
|
|
59
106
|
* Create a virtual modules plugin for default entry files.
|
|
60
107
|
* Provides virtual module content when entries use VIRTUAL_IDS (no custom entry configured).
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import type { NetworkError } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
interface NetworkErrorThrowerProps {
|
|
7
|
-
error: NetworkError;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Client component that throws a NetworkError during render.
|
|
12
|
-
* Errors thrown during render are caught by error boundaries; async errors are not.
|
|
13
|
-
*/
|
|
14
|
-
export function NetworkErrorThrower({
|
|
15
|
-
error,
|
|
16
|
-
}: NetworkErrorThrowerProps): ReactNode {
|
|
17
|
-
throw error;
|
|
18
|
-
}
|