@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2154 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- 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 +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +243 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +128 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +121 -0
- package/skills/testing/e2e-parity.md +124 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +127 -0
- package/skills/testing/loader.md +108 -0
- package/skills/testing/middleware.md +97 -0
- package/skills/testing/render-handler.md +102 -0
- package/skills/testing/response-routes.md +94 -0
- package/skills/testing/reverse-and-types.md +83 -0
- package/skills/testing/server-actions.md +89 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- 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 +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +104 -68
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +183 -44
- package/src/browser/prefetch/fetch.ts +228 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- 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-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +32 -1
- package/src/browser/rsc-router.tsx +69 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -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 +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +95 -25
- 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 +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +32 -14
- 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 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +54 -17
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +25 -7
- package/src/loader.ts +16 -9
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +27 -6
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -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/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -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 +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- 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 +58 -2
- 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 +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- 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/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +52 -30
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +57 -61
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +67 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -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 +326 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +51 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +323 -0
- package/src/testing/render-route.tsx +590 -0
- package/src/testing/run-loader.ts +363 -0
- package/src/testing/run-middleware.ts +205 -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 +285 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- 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 +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- 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-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- 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 +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -37,6 +37,8 @@ import { track, type MetricsStore } from "./context.js";
|
|
|
37
37
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
38
38
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
39
39
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
40
|
+
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
|
|
41
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
40
42
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
41
43
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
42
44
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
@@ -58,22 +60,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
|
58
60
|
export interface RequestContext<
|
|
59
61
|
TEnv = DefaultEnv,
|
|
60
62
|
TParams = Record<string, string>,
|
|
61
|
-
> {
|
|
62
|
-
/** Platform bindings (Cloudflare env, etc.) */
|
|
63
|
-
env: TEnv;
|
|
64
|
-
/** Original HTTP request */
|
|
65
|
-
request: Request;
|
|
66
|
-
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
67
|
-
url: URL;
|
|
68
|
-
/**
|
|
69
|
-
* The original request URL with all parameters intact, including
|
|
70
|
-
* internal `_rsc*` transport params.
|
|
71
|
-
*/
|
|
72
|
-
originalUrl: URL;
|
|
73
|
-
/** URL pathname */
|
|
74
|
-
pathname: string;
|
|
75
|
-
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
76
|
-
searchParams: URLSearchParams;
|
|
63
|
+
> extends RequestScope<TEnv> {
|
|
77
64
|
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
78
65
|
_variables: Record<string, any>;
|
|
79
66
|
/** Get a variable set by middleware */
|
|
@@ -159,20 +146,6 @@ export interface RequestContext<
|
|
|
159
146
|
import("../cache/profile-registry.js").CacheProfile
|
|
160
147
|
>;
|
|
161
148
|
|
|
162
|
-
/**
|
|
163
|
-
* Schedule work to run after the response is sent.
|
|
164
|
-
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
165
|
-
* On Node.js, runs as fire-and-forget.
|
|
166
|
-
*
|
|
167
|
-
* @example
|
|
168
|
-
* ```typescript
|
|
169
|
-
* ctx.waitUntil(async () => {
|
|
170
|
-
* await cacheStore.set(key, data, ttl);
|
|
171
|
-
* });
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
waitUntil(fn: () => Promise<void>): void;
|
|
175
|
-
|
|
176
149
|
/**
|
|
177
150
|
* Register a callback to run when the response is created.
|
|
178
151
|
* Callbacks are sync and receive the response. They can:
|
|
@@ -300,7 +273,9 @@ export interface RequestContext<
|
|
|
300
273
|
|
|
301
274
|
/**
|
|
302
275
|
* @internal Set to true when the matched entry tree contains any `loading()`
|
|
303
|
-
* entries (streaming).
|
|
276
|
+
* entries (streaming). On a streaming tree rendered() waits for the streaming
|
|
277
|
+
* handlers to settle (via handleStore.settled) before resolving, and the
|
|
278
|
+
* deadlock guard state is kept live until that wait completes.
|
|
304
279
|
*/
|
|
305
280
|
_treeHasStreaming?: boolean;
|
|
306
281
|
|
|
@@ -324,6 +299,18 @@ export interface RequestContext<
|
|
|
324
299
|
*/
|
|
325
300
|
_renderBarrierHandleSnapshot?: HandleData;
|
|
326
301
|
|
|
302
|
+
/**
|
|
303
|
+
* @internal The deadlock guard window is closed (no further handler-awaits-
|
|
304
|
+
* loader cycle is possible). For non-streaming trees this is set when the
|
|
305
|
+
* barrier resolves. For streaming trees the window stays open until
|
|
306
|
+
* handleStore.settled — rendered() keeps waiting past the barrier and a
|
|
307
|
+
* loading() handler can still resume and await a still-waiting loader — so it
|
|
308
|
+
* is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
|
|
309
|
+
* reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
|
|
310
|
+
* during the streaming settle wait.
|
|
311
|
+
*/
|
|
312
|
+
_renderBarrierGuardClosed?: boolean;
|
|
313
|
+
|
|
327
314
|
/** @internal Per-request error dedup set for onError reporting */
|
|
328
315
|
_reportedErrors: WeakSet<object>;
|
|
329
316
|
|
|
@@ -349,6 +336,15 @@ export interface RequestContext<
|
|
|
349
336
|
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
350
337
|
*/
|
|
351
338
|
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @internal Coarse route-level cache signal for the X-Rango-Cache debug
|
|
342
|
+
* header. Populated by match/matchPartial only when the debug cache signal
|
|
343
|
+
* gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
|
|
344
|
+
* the response-finalization path (createResponseWithMergedHeaders). Undefined
|
|
345
|
+
* when the gate is off, so no header is emitted.
|
|
346
|
+
*/
|
|
347
|
+
_cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
|
|
352
348
|
}
|
|
353
349
|
|
|
354
350
|
/**
|
|
@@ -382,6 +378,7 @@ export type PublicRequestContext<
|
|
|
382
378
|
| "_renderBarrierWaiters"
|
|
383
379
|
| "_handlerLoaderDeps"
|
|
384
380
|
| "_renderBarrierHandleSnapshot"
|
|
381
|
+
| "_renderBarrierGuardClosed"
|
|
385
382
|
| "_reportBackgroundError"
|
|
386
383
|
| "_debugPerformance"
|
|
387
384
|
| "_metricsStore"
|
|
@@ -389,6 +386,7 @@ export type PublicRequestContext<
|
|
|
389
386
|
| "_setStatus"
|
|
390
387
|
| "_variables"
|
|
391
388
|
| "_classifiedRoute"
|
|
389
|
+
| "_cacheSignal"
|
|
392
390
|
| "res"
|
|
393
391
|
>;
|
|
394
392
|
|
|
@@ -498,13 +496,7 @@ export function requireRequestContext<
|
|
|
498
496
|
return getRequestContext<TEnv>();
|
|
499
497
|
}
|
|
500
498
|
|
|
501
|
-
|
|
502
|
-
* Cloudflare Workers ExecutionContext (subset we need)
|
|
503
|
-
*/
|
|
504
|
-
export interface ExecutionContext {
|
|
505
|
-
waitUntil(promise: Promise<any>): void;
|
|
506
|
-
passThroughOnException(): void;
|
|
507
|
-
}
|
|
499
|
+
export type { ExecutionContext };
|
|
508
500
|
|
|
509
501
|
/**
|
|
510
502
|
* Options for creating a request context
|
|
@@ -768,16 +760,14 @@ export function createRequestContext<TEnv>(
|
|
|
768
760
|
|
|
769
761
|
waitUntil(fn: () => Promise<void>): void {
|
|
770
762
|
if (executionContext?.waitUntil) {
|
|
771
|
-
// Cloudflare Workers: use native waitUntil
|
|
772
763
|
executionContext.waitUntil(fn());
|
|
773
764
|
} else {
|
|
774
|
-
|
|
775
|
-
fn().catch((err) =>
|
|
776
|
-
console.error("[waitUntil] Background task failed:", err),
|
|
777
|
-
);
|
|
765
|
+
fireAndForgetWaitUntil(fn);
|
|
778
766
|
}
|
|
779
767
|
},
|
|
780
768
|
|
|
769
|
+
executionContext,
|
|
770
|
+
|
|
781
771
|
_onResponseCallbacks: [],
|
|
782
772
|
|
|
783
773
|
onResponse(callback: (response: Response) => Response): void {
|
|
@@ -832,14 +822,37 @@ export function createRequestContext<TEnv>(
|
|
|
832
822
|
.filter((s) => s.type !== "loader")
|
|
833
823
|
.map((s) => s.id);
|
|
834
824
|
ctx._renderBarrierSegmentOrder = segOrder;
|
|
835
|
-
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
825
|
+
|
|
826
|
+
// Closing the guard window means no handler can still form a deadlock cycle
|
|
827
|
+
// with a rendered() loader: drop the dependency-tracking state and mark it
|
|
828
|
+
// closed. WHEN this runs is the only streaming/non-streaming difference.
|
|
829
|
+
const closeGuard = () => {
|
|
830
|
+
ctx._renderBarrierWaiters = undefined;
|
|
831
|
+
ctx._handlerLoaderDeps = undefined;
|
|
832
|
+
ctx._renderBarrierGuardClosed = true;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
if (ctx._treeHasStreaming) {
|
|
836
|
+
// Streaming: rendered() keeps waiting on handleStore.settled past this
|
|
837
|
+
// point, and loading() handlers are still in flight. The eager snapshot
|
|
838
|
+
// here would be incomplete, so leave it unset — rendered() builds and
|
|
839
|
+
// caches the complete one after settled. Keep the guard window OPEN so a
|
|
840
|
+
// handler that resumes and awaits a still-waiting rendered() loader is
|
|
841
|
+
// still caught; close it once settled (every tracked handler has finished
|
|
842
|
+
// then, so none can await a loader anymore). settled resolves after
|
|
843
|
+
// rendered() seals; if no loader used rendered(), nothing seals and the
|
|
844
|
+
// (empty) guard state is simply GC'd at request end.
|
|
845
|
+
handleStore.settled.then(closeGuard);
|
|
846
|
+
} else {
|
|
847
|
+
// Non-streaming: all handlers have settled by now. Build and cache the
|
|
848
|
+
// snapshot so loader ctx.use(handle) calls don't rebuild it, and close the
|
|
849
|
+
// guard window immediately.
|
|
850
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
851
|
+
handleStore,
|
|
852
|
+
segOrder,
|
|
853
|
+
);
|
|
854
|
+
closeGuard();
|
|
855
|
+
}
|
|
843
856
|
if (resolveBarrier) resolveBarrier();
|
|
844
857
|
};
|
|
845
858
|
Object.defineProperty(ctx, "_renderBarrier", {
|
|
@@ -1043,7 +1056,10 @@ export function createUseFunction<TEnv>(
|
|
|
1043
1056
|
search: (ctx as any).search ?? {},
|
|
1044
1057
|
pathname: ctx.pathname,
|
|
1045
1058
|
url: ctx.url,
|
|
1059
|
+
originalUrl: ctx.originalUrl,
|
|
1046
1060
|
env: ctx.env as any,
|
|
1061
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
1062
|
+
executionContext: ctx.executionContext,
|
|
1047
1063
|
get: ctx.get as any,
|
|
1048
1064
|
use: (<TDep, TDepParams = any>(
|
|
1049
1065
|
dep: LoaderDefinition<TDep, TDepParams>,
|
package/src/ssr/index.tsx
CHANGED
|
@@ -162,9 +162,13 @@ function createSsrEventController(opts: {
|
|
|
162
162
|
}): EventController {
|
|
163
163
|
const location = new URL(opts.pathname, "http://localhost");
|
|
164
164
|
let params = opts.params ?? {};
|
|
165
|
+
const rawMatched = opts.matched ?? [];
|
|
165
166
|
const handleState = {
|
|
166
167
|
data: opts.handleData ?? {},
|
|
167
|
-
segmentOrder: filterSegmentOrder(
|
|
168
|
+
segmentOrder: filterSegmentOrder(rawMatched),
|
|
169
|
+
routeSegmentIds: rawMatched.filter(
|
|
170
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
171
|
+
),
|
|
168
172
|
};
|
|
169
173
|
const state: DerivedNavigationState = {
|
|
170
174
|
state: "idle",
|
package/src/static-handler.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { Handler } from "./types.js";
|
|
|
35
35
|
import type { StaticBuildContext } from "./prerender.js";
|
|
36
36
|
import type { UseItems, HandlerUseItem } from "./route-types.js";
|
|
37
37
|
import { isCachedFunction } from "./cache/taint.js";
|
|
38
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
38
39
|
|
|
39
40
|
// -- Types ------------------------------------------------------------------
|
|
40
41
|
|
|
@@ -63,6 +64,11 @@ export interface StaticHandlerDefinition<
|
|
|
63
64
|
|
|
64
65
|
// -- Function ---------------------------------------------------------------
|
|
65
66
|
|
|
67
|
+
// Process-stable fallback id counter (mirrors createHandle / createLoader /
|
|
68
|
+
// Prerender). Only assigned in a bare unit test where the Vite plugin did not
|
|
69
|
+
// inject an id; never fires in a real build (the plugin always injects).
|
|
70
|
+
let runtimeStaticIdCounter = 0;
|
|
71
|
+
|
|
66
72
|
export function Static<TParams extends Record<string, any> = {}>(
|
|
67
73
|
handler: (ctx: StaticBuildContext) => ReactNode | Promise<ReactNode>,
|
|
68
74
|
options?: StaticHandlerOptions,
|
|
@@ -94,12 +100,28 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
94
100
|
id = maybeId ?? "";
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
// Throw unless under a test runner. The plugin always injects $$id for a
|
|
104
|
+
// supported `export const` Static on every build, so a missing id means either
|
|
105
|
+
// no plugin (a bare test — fall back below) or an UNSUPPORTED shape the plugin
|
|
106
|
+
// silently skipped (dev OR a real build — fail loud; a synthetic id would
|
|
107
|
+
// degrade to a silent static/prerender miss). The message is already small (no
|
|
108
|
+
// stack-parsing diagnostic), so it ships as-is. isUnderTestRunner() is
|
|
109
|
+
// runtime-safe — never a bare `process.env` access.
|
|
110
|
+
if (!id && !isUnderTestRunner()) {
|
|
98
111
|
throw new Error(
|
|
99
|
-
"[
|
|
100
|
-
"
|
|
112
|
+
"[rango] Static: missing $$id. Use `export const X = Static(...)` and " +
|
|
113
|
+
"ensure the exposeInternalIds Vite plugin is configured.",
|
|
101
114
|
);
|
|
102
115
|
}
|
|
116
|
+
// Under vitest with no plugin id: assign a process-stable runtime id so a
|
|
117
|
+
// whole-app router with Static() routes constructs in a bare test. Never
|
|
118
|
+
// reached in a real build (the throw above fires there); staticHandlerId is
|
|
119
|
+
// read only during RSC serving (never in dispatch / assertGeneratedRoutesMatch),
|
|
120
|
+
// and the build static manifest keys on the plugin id. Mirrors createHandle /
|
|
121
|
+
// createLoader / Prerender.
|
|
122
|
+
if (!id) {
|
|
123
|
+
id = `__rango_runtime_static_${runtimeStaticIdCounter++}`;
|
|
124
|
+
}
|
|
103
125
|
|
|
104
126
|
return {
|
|
105
127
|
__brand: "staticHandler" as const,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-status testing primitives for @rangojs/router consumers.
|
|
3
|
+
*
|
|
4
|
+
* Two complementary paths, both DEVELOPMENT/TEST ONLY:
|
|
5
|
+
*
|
|
6
|
+
* 1. Header path — `parseCacheHeader` / `assertCacheStatus` read the
|
|
7
|
+
* `X-Rango-Cache` response header. The header is emitted only when the
|
|
8
|
+
* router's debug cache signal gate is on (the `debugCacheSignal` option or
|
|
9
|
+
* `RANGO_TEST_SIGNALS=1`). With the gate off there is no header and these
|
|
10
|
+
* helpers throw a clear "header missing" error.
|
|
11
|
+
*
|
|
12
|
+
* 2. Telemetry path — `createCacheSink` returns a `{ sink, events }` pair the
|
|
13
|
+
* consumer wires via `createRouter({ telemetry: sink })`. This has ZERO
|
|
14
|
+
* production surface: no header, just structured `cache.decision` events
|
|
15
|
+
* (which carry the same coarse `segments` cache signal).
|
|
16
|
+
*
|
|
17
|
+
* v1 cache status is COARSE (route-level): the router reports a single entry
|
|
18
|
+
* keyed by the route key (the route NAME), not per individual segment.
|
|
19
|
+
*
|
|
20
|
+
* Import path: from a Vitest unit/integration test use `@rangojs/router/testing`;
|
|
21
|
+
* from a Playwright e2e use `@rangojs/router/testing/e2e` (the barrel pulls a
|
|
22
|
+
* build-only virtual that does not resolve in a plain Playwright runner).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
CacheDecisionEvent,
|
|
27
|
+
CacheSegmentStatus,
|
|
28
|
+
TelemetryEvent,
|
|
29
|
+
TelemetrySink,
|
|
30
|
+
} from "../router/telemetry.js";
|
|
31
|
+
|
|
32
|
+
const CACHE_HEADER = "X-Rango-Cache";
|
|
33
|
+
|
|
34
|
+
/** Expected cache status passed to assertCacheStatus. */
|
|
35
|
+
export type ExpectedCacheStatus = CacheSegmentStatus;
|
|
36
|
+
|
|
37
|
+
/** A target carrying response headers (a Response or a `{ headers }` object). */
|
|
38
|
+
export type CacheStatusTarget = Response | { headers: Headers };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse an `X-Rango-Cache` header value into a `{ routeKey: status }` map.
|
|
42
|
+
*
|
|
43
|
+
* Header format: `<routeKey>=<status>, <routeKey2>=<status2>`. The key is the
|
|
44
|
+
* route NAME (ctx.routeKey, e.g. `product.detail`), NOT the URL pattern —
|
|
45
|
+
* see assertCacheStatus. Whitespace around entries and the `=` is tolerated.
|
|
46
|
+
* Entries without a status are ignored.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* parseCacheHeader("product.detail=hit, shop.layout=stale")
|
|
50
|
+
* // => { "product.detail": "hit", "shop.layout": "stale" }
|
|
51
|
+
*/
|
|
52
|
+
export function parseCacheHeader(
|
|
53
|
+
headerValue: string | null | undefined,
|
|
54
|
+
): Record<string, string> {
|
|
55
|
+
const result: Record<string, string> = {};
|
|
56
|
+
if (!headerValue) return result;
|
|
57
|
+
for (const rawEntry of headerValue.split(",")) {
|
|
58
|
+
const entry = rawEntry.trim();
|
|
59
|
+
if (entry.length === 0) continue;
|
|
60
|
+
const eq = entry.indexOf("=");
|
|
61
|
+
if (eq === -1) continue;
|
|
62
|
+
const id = entry.slice(0, eq).trim();
|
|
63
|
+
const status = entry.slice(eq + 1).trim();
|
|
64
|
+
if (id.length === 0 || status.length === 0) continue;
|
|
65
|
+
result[id] = status;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getHeaders(target: CacheStatusTarget): Headers {
|
|
71
|
+
return target.headers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assert that the `X-Rango-Cache` header reports `expected` status for the
|
|
76
|
+
* given route. Throws a descriptive error when the header is missing (gate
|
|
77
|
+
* off), the route is absent, or the status differs.
|
|
78
|
+
*
|
|
79
|
+
* `routeKey` is the route NAME (e.g. `product.detail`), the same id the header
|
|
80
|
+
* carries — NOT the URL pattern (`/products/:id`). The signal is built from
|
|
81
|
+
* ctx.routeKey (telemetry.ts), so a pattern-shaped key never matches.
|
|
82
|
+
*
|
|
83
|
+
* The header is produced by the RSC render pipeline, so get the Response from
|
|
84
|
+
* the router's real fetch path (`router.fetch(...)`), with the debug cache
|
|
85
|
+
* signal gate enabled (`debugCacheSignal: true` or `RANGO_TEST_SIGNALS=1`).
|
|
86
|
+
* NOTE: `dispatch()` is the non-RSC primitive and never emits this header.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // debugCacheSignal must be enabled on the router under test.
|
|
90
|
+
* const res = await router.fetch(new Request("https://app/products/42"));
|
|
91
|
+
* assertCacheStatus(res, "product.detail", "hit");
|
|
92
|
+
*/
|
|
93
|
+
export function assertCacheStatus(
|
|
94
|
+
target: CacheStatusTarget,
|
|
95
|
+
segment: string,
|
|
96
|
+
expected: ExpectedCacheStatus,
|
|
97
|
+
): void {
|
|
98
|
+
const headerValue = getHeaders(target).get(CACHE_HEADER);
|
|
99
|
+
if (headerValue === null) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`assertCacheStatus: response has no ${CACHE_HEADER} header. ` +
|
|
102
|
+
`Enable the debug cache signal via createRouter({ debugCacheSignal: true }) ` +
|
|
103
|
+
`or RANGO_TEST_SIGNALS=1.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const map = parseCacheHeader(headerValue);
|
|
107
|
+
const actual = map[segment];
|
|
108
|
+
if (actual === undefined) {
|
|
109
|
+
const known = Object.keys(map);
|
|
110
|
+
throw new Error(
|
|
111
|
+
`assertCacheStatus: segment "${segment}" not found in ${CACHE_HEADER} ` +
|
|
112
|
+
`("${headerValue}"). Known segments: ${
|
|
113
|
+
known.length > 0 ? known.join(", ") : "(none)"
|
|
114
|
+
}.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (actual !== expected) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`assertCacheStatus: segment "${segment}" expected "${expected}" but got "${actual}".`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A telemetry sink paired with the array it records events into.
|
|
126
|
+
*/
|
|
127
|
+
export interface CacheSink {
|
|
128
|
+
/** Wire into `createRouter({ telemetry: sink })`. */
|
|
129
|
+
sink: TelemetrySink;
|
|
130
|
+
/** All telemetry events captured so far, in emit order. */
|
|
131
|
+
events: TelemetryEvent[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a capturing telemetry sink for asserting on `cache.decision` events.
|
|
136
|
+
*
|
|
137
|
+
* This is the ZERO-production-surface path: no response header is emitted, the
|
|
138
|
+
* consumer just inspects the captured events.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const { sink, events } = createCacheSink();
|
|
142
|
+
* const router = createRouter({ telemetry: sink, ... });
|
|
143
|
+
* // ...send a request through the router's RSC fetch path...
|
|
144
|
+
* const decisions = filterCacheDecisions(events);
|
|
145
|
+
* expect(decisions[0].segments?.[0].cacheStatus).toBe("hit");
|
|
146
|
+
*/
|
|
147
|
+
export function createCacheSink(): CacheSink {
|
|
148
|
+
const events: TelemetryEvent[] = [];
|
|
149
|
+
const sink: TelemetrySink = {
|
|
150
|
+
emit(event: TelemetryEvent): void {
|
|
151
|
+
events.push(event);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return { sink, events };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Filter captured telemetry events down to `cache.decision` events.
|
|
159
|
+
*/
|
|
160
|
+
export function filterCacheDecisions(
|
|
161
|
+
events: readonly TelemetryEvent[],
|
|
162
|
+
): CacheDecisionEvent[] {
|
|
163
|
+
return events.filter(
|
|
164
|
+
(e): e is CacheDecisionEvent => e.type === "cache.decision",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collectHandle — unit-test a handle's `collect`/accumulator function directly.
|
|
3
|
+
*
|
|
4
|
+
* A handle's collect function (the `createHandle(collect)` argument that maps the
|
|
5
|
+
* per-segment pushed values into the accumulated result) is otherwise not
|
|
6
|
+
* directly reachable: createHandle keeps it in a private registry keyed by the
|
|
7
|
+
* handle's `$$id` and returns only `{ __brand, $$id }`. This primitive runs that
|
|
8
|
+
* REAL registered collect on per-segment values you provide and returns the
|
|
9
|
+
* accumulated result — so the mapper/accumulator is unit-testable without a full
|
|
10
|
+
* route match.
|
|
11
|
+
*
|
|
12
|
+
* It relies on createHandle registering the collect even in a bare test (it
|
|
13
|
+
* assigns a runtime fallback id when the Vite plugin did not inject one). If a
|
|
14
|
+
* handle's module was never imported (so createHandle never ran), the collect is
|
|
15
|
+
* unregistered and this falls back to a flat array with a warning.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getCollectFn, type Handle } from "../handle.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run a handle's collect function on per-segment pushed values.
|
|
22
|
+
*
|
|
23
|
+
* @param handle - The handle whose collect to run.
|
|
24
|
+
* @param segments - Per-segment pushed values: each entry is the array of values
|
|
25
|
+
* one route segment pushed for this handle, in parent -> child order. Empty
|
|
26
|
+
* per-segment arrays are dropped before the collect runs, matching production
|
|
27
|
+
* collectHandleData (a segment that pushed nothing is not passed through).
|
|
28
|
+
* @returns The accumulated value the handle's collect produces.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Default flatten
|
|
33
|
+
* collectHandle(Breadcrumbs, [[{ label: "Home", href: "/" }], [{ label: "P", href: "/p" }]]);
|
|
34
|
+
* // -> [{ label: "Home", href: "/" }, { label: "P", href: "/p" }]
|
|
35
|
+
*
|
|
36
|
+
* // Custom "last wins"
|
|
37
|
+
* const PageTitle = createHandle<string, string>((s) => s.flat().at(-1) ?? "");
|
|
38
|
+
* collectHandle(PageTitle, [["Home"], ["Product"]]); // -> "Product"
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function collectHandle<TData, TAccumulated>(
|
|
42
|
+
handle: Handle<TData, TAccumulated>,
|
|
43
|
+
segments: ReadonlyArray<ReadonlyArray<TData>>,
|
|
44
|
+
): TAccumulated {
|
|
45
|
+
const collectFn = getCollectFn(handle.$$id) as
|
|
46
|
+
| ((segments: TData[][]) => TAccumulated)
|
|
47
|
+
| undefined;
|
|
48
|
+
|
|
49
|
+
if (!collectFn) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`[rango] collectHandle: handle "${handle.$$id}" has no registered collect ` +
|
|
52
|
+
`function. Import the handle's module so createHandle() runs. Falling ` +
|
|
53
|
+
`back to a flat array.`,
|
|
54
|
+
);
|
|
55
|
+
return segments.flat() as unknown as TAccumulated;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Match production collectHandleData (handle.ts): segments that pushed
|
|
59
|
+
// nothing (empty arrays) are dropped before the collect runs, so a collect
|
|
60
|
+
// that inspects segment count or indices sees the same input as at runtime.
|
|
61
|
+
const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
|
|
62
|
+
return collectFn(nonEmpty);
|
|
63
|
+
}
|