@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -45
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +2 -1
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +48 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +34 -0
- package/src/redirect-origin.ts +100 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +4 -14
- package/src/types/handler-context.ts +28 -9
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +2 -108
- package/src/vite/router-discovery.ts +9 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.126",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -157,6 +157,17 @@
|
|
|
157
157
|
"access": "public",
|
|
158
158
|
"tag": "experimental"
|
|
159
159
|
},
|
|
160
|
+
"scripts": {
|
|
161
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
162
|
+
"prepublishOnly": "pnpm build",
|
|
163
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
164
|
+
"test": "playwright test",
|
|
165
|
+
"test:ui": "playwright test --ui",
|
|
166
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
167
|
+
"test:unit": "vitest run",
|
|
168
|
+
"test:unit:watch": "vitest",
|
|
169
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
170
|
+
},
|
|
160
171
|
"dependencies": {
|
|
161
172
|
"@types/debug": "^4.1.12",
|
|
162
173
|
"@vitejs/plugin-rsc": "^0.5.26",
|
|
@@ -168,19 +179,19 @@
|
|
|
168
179
|
},
|
|
169
180
|
"devDependencies": {
|
|
170
181
|
"@playwright/test": "^1.49.1",
|
|
182
|
+
"@shared/e2e": "workspace:*",
|
|
171
183
|
"@testing-library/dom": "^10.4.1",
|
|
172
184
|
"@testing-library/react": "^16.3.2",
|
|
173
185
|
"@types/node": "^24.10.1",
|
|
174
|
-
"@types/react": "
|
|
175
|
-
"@types/react-dom": "
|
|
186
|
+
"@types/react": "catalog:",
|
|
187
|
+
"@types/react-dom": "catalog:",
|
|
176
188
|
"esbuild": "^0.27.0",
|
|
177
189
|
"happy-dom": "^20.10.1",
|
|
178
190
|
"jiti": "^2.6.1",
|
|
179
|
-
"react": "
|
|
180
|
-
"react-dom": "
|
|
191
|
+
"react": "catalog:",
|
|
192
|
+
"react-dom": "catalog:",
|
|
181
193
|
"typescript": "^5.3.0",
|
|
182
|
-
"vitest": "^4.0.0"
|
|
183
|
-
"@shared/e2e": "0.0.1"
|
|
194
|
+
"vitest": "^4.0.0"
|
|
184
195
|
},
|
|
185
196
|
"peerDependencies": {
|
|
186
197
|
"@cloudflare/vite-plugin": "^1.38.0",
|
|
@@ -208,15 +219,5 @@
|
|
|
208
219
|
"vitest": {
|
|
209
220
|
"optional": true
|
|
210
221
|
}
|
|
211
|
-
},
|
|
212
|
-
"scripts": {
|
|
213
|
-
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
214
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
215
|
-
"test": "playwright test",
|
|
216
|
-
"test:ui": "playwright test --ui",
|
|
217
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
218
|
-
"test:unit": "vitest run",
|
|
219
|
-
"test:unit:watch": "vitest",
|
|
220
|
-
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
221
222
|
}
|
|
222
|
-
}
|
|
223
|
+
}
|
|
@@ -81,6 +81,66 @@ path("/product/:id", async (ctx) => {
|
|
|
81
81
|
Async content is a `Promise<ReactNode>`. Resolve it in your component
|
|
82
82
|
with React's `use()` hook wrapped in `<Suspense>`.
|
|
83
83
|
|
|
84
|
+
### Deferred content (decide now, resolve from a deep component)
|
|
85
|
+
|
|
86
|
+
When the handler should DECIDE to push a crumb (it holds `ctx`, so the decision
|
|
87
|
+
must land before the handles stream seals) but the value is produced far away — by
|
|
88
|
+
a deep async component, not the handler — call `.defer()` on the push function.
|
|
89
|
+
`ctx.use(Handle)` returns the push function; `.defer(options)` reserves the crumb's
|
|
90
|
+
slot synchronously and returns a **resolver that is push-equal** — you call it
|
|
91
|
+
later, anywhere in the render, with the same argument you'd have passed to the
|
|
92
|
+
push (a value, a `Promise`, or a thunk). The only added behavior is a timeout, so a
|
|
93
|
+
forgotten resolve can't hold the Flight stream (and the HTTP response) open forever.
|
|
94
|
+
|
|
95
|
+
Reserve the slot in the handler, then resolve it from a nested async component
|
|
96
|
+
that closes over the resolver — no extra wiring (the resolver is a plain closure,
|
|
97
|
+
not outlet context):
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { Breadcrumbs } from "@rangojs/router";
|
|
101
|
+
import { Outlet } from "@rangojs/router/client";
|
|
102
|
+
import { Suspense } from "react";
|
|
103
|
+
|
|
104
|
+
function DocsLayout(ctx) {
|
|
105
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
106
|
+
// Decide now (the slot is reserved before the stream seals); resolve later.
|
|
107
|
+
const resolveCrumb = breadcrumb.defer({ timeoutMs: 5000, else: null });
|
|
108
|
+
|
|
109
|
+
// Deep, async, far from the handler — closes over the resolver, never touches ctx.
|
|
110
|
+
// Same call shape as breadcrumb({ ... }), just deferred:
|
|
111
|
+
async function LiveCrumb() {
|
|
112
|
+
const n = await countOpenIssues();
|
|
113
|
+
resolveCrumb({ label: "Docs", href: "/docs", content: <span>{n}</span> });
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
<Suspense>
|
|
120
|
+
<LiveCrumb />
|
|
121
|
+
</Suspense>
|
|
122
|
+
<Outlet />
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If the resolver is never called, the slot auto-resolves to `else` after
|
|
129
|
+
`timeoutMs` (default 10s) and warns in dev — graceful degradation instead of a
|
|
130
|
+
hung request. `timeoutMs: 0` or `Infinity` disable the timeout intentionally; any
|
|
131
|
+
other non-finite or negative value falls back to the default rather than silently
|
|
132
|
+
disabling the safety net.
|
|
133
|
+
|
|
134
|
+
**Consumer note:** because `.defer()` reserves the slot for the WHOLE item, a
|
|
135
|
+
client reading the handle (`useHandle(Breadcrumbs)`) sees that entry as a
|
|
136
|
+
`Promise` until it resolves. Type such reads with the exported
|
|
137
|
+
`DeferredHandleEntry<BreadcrumbItem>` (from `@rangojs/router/client`); a
|
|
138
|
+
deferred-aware consumer should `use()` thenable entries inside `<Suspense>`, while
|
|
139
|
+
a simple one can skip them (`typeof entry.then === "function"`). Use `.defer()`
|
|
140
|
+
only when even `label`/`href` are unknown at handler time — if you know them and
|
|
141
|
+
only the `content` is async, push a concrete item with a `Promise` `content` field
|
|
142
|
+
instead (no `.defer()` needed).
|
|
143
|
+
|
|
84
144
|
## Consuming Breadcrumbs (Client)
|
|
85
145
|
|
|
86
146
|
Use `useHandle(Breadcrumbs)` in a client component to read the accumulated items:
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -89,8 +89,8 @@ import { useSegments } from "@rangojs/router/client";
|
|
|
89
89
|
function Breadcrumbs() {
|
|
90
90
|
const { path, segmentIds, location } = useSegments();
|
|
91
91
|
|
|
92
|
-
// path: ["
|
|
93
|
-
// segmentIds: ["
|
|
92
|
+
// path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
|
|
93
|
+
// segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
|
|
94
94
|
// location: URL object
|
|
95
95
|
|
|
96
96
|
return <nav>{path.join(" > ")}</nav>;
|
package/skills/route/SKILL.md
CHANGED
|
@@ -296,6 +296,12 @@ path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
|
|
|
296
296
|
path("/moved", () => redirect("/new-location", 301), { name: "moved" });
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
+
> **Redirecting from a route with `loading()`:** an `async` handler that returns
|
|
300
|
+
> a `Response`/`redirect()` on a route that also declares `loading()` is streamed,
|
|
301
|
+
> so the redirect is rendered into the RSC stream instead of becoming an HTTP
|
|
302
|
+
> redirect. Issue the redirect from `middleware`, a loader, or a **synchronous**
|
|
303
|
+
> handler return instead. (Dev logs a warning if this is hit.)
|
|
304
|
+
|
|
299
305
|
### Redirect with location state
|
|
300
306
|
|
|
301
307
|
Carry typed state through redirects (e.g. flash messages):
|
|
@@ -465,7 +465,10 @@ See `/middleware` for the full cross-segment revalidation contract.
|
|
|
465
465
|
|
|
466
466
|
`redirect()` works inside actions. Both `return redirect(...)` and
|
|
467
467
|
`throw redirect(...)` are supported and behave the same way for the
|
|
468
|
-
client. Throwing is clearer when the redirect is conditional
|
|
468
|
+
client. Throwing is clearer when the redirect is conditional, and it keeps
|
|
469
|
+
the action's return type narrow (e.g. `Promise<void>`) — `redirect()` returns
|
|
470
|
+
a `Response`, so the `return` form needs `Promise<Response>` in the signature.
|
|
471
|
+
Prefer `throw redirect(...)`; never cast with `as any`.
|
|
469
472
|
|
|
470
473
|
```typescript
|
|
471
474
|
"use server";
|
|
@@ -490,6 +493,27 @@ Redirects from actions render the **target** route tree's matched segments
|
|
|
490
493
|
source page's — the target is what the user sees next. See `/hooks
|
|
491
494
|
useLocationState` for reading flash state on the target page.
|
|
492
495
|
|
|
496
|
+
### Same-origin by default (open-redirect protection)
|
|
497
|
+
|
|
498
|
+
`redirect()` is same-origin by default on every path — JS, no-JS PE, and
|
|
499
|
+
full-page. A cross-origin target (e.g. from unvalidated user input) is blocked
|
|
500
|
+
and the user is sent to the app root instead, so `redirect(userInput)` can never
|
|
501
|
+
become an open redirect. To intentionally redirect off-host (an OAuth provider,
|
|
502
|
+
say), opt in explicitly:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
throw redirect("https://accounts.google.com/o/oauth2/v2/auth?...", {
|
|
506
|
+
external: true,
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`{ external: true }` is the audit point: passing user input with it re-opens the
|
|
511
|
+
cross-origin risk and is your responsibility (the Rails `allow_other_host: true`
|
|
512
|
+
model). Omit it and off-host targets stay blocked. `external` only waives the
|
|
513
|
+
**same-origin** rule, not scheme safety: the target must be `http(s)` — a
|
|
514
|
+
`javascript:` or `data:` URL is still neutralized, so a forged or mistaken
|
|
515
|
+
`external` target can never become a scriptable navigation.
|
|
516
|
+
|
|
493
517
|
## Error Handling
|
|
494
518
|
|
|
495
519
|
### Validation errors — return them as state
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -81,23 +81,23 @@ The single rule that drives everything:
|
|
|
81
81
|
|
|
82
82
|
Each primitive links to its sub-file (API + recipe + caveats).
|
|
83
83
|
|
|
84
|
-
| The behavior is…
|
|
85
|
-
|
|
|
86
|
-
| a pure function / `reverse` / `href` / a predicate (`revalidate`, `isAction`)
|
|
87
|
-
| one loader's data logic
|
|
88
|
-
| a loader's cookie / header / redirect output (auth-loader pattern)
|
|
89
|
-
| one middleware's ordering / short-circuit / cookie+header merge
|
|
90
|
-
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`)
|
|
91
|
-
| a handle's `collect`/accumulator, or a seeded handle read
|
|
92
|
-
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`)
|
|
93
|
-
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight
|
|
94
|
-
| a real async **Server Component**
|
|
95
|
-
|
|
|
96
|
-
| a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects)
|
|
97
|
-
| navigation, hydration, PE parity, view transitions, real SSR
|
|
98
|
-
| cache hit/miss/stale, prerender (= a cache hit by design)
|
|
99
|
-
| generated route map drift vs runtime
|
|
100
|
-
| a platform binding (`env.DB` / Durable Object / `env.R2`)
|
|
84
|
+
| The behavior is… | Layer | Primitive | Import root |
|
|
85
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
86
|
+
| a pure function / `reverse` / `href` / a predicate (`revalidate`, `isAction`) | unit + types | [`reverse`/`@ts-expect-error`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
87
|
+
| one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
|
|
88
|
+
| a loader's cookie / header / redirect output (auth-loader pattern) | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
|
|
89
|
+
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
90
|
+
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
91
|
+
| a handle's `collect`/accumulator, or a seeded handle read | unit | [`collectHandle` / seeded `handles`](./handles.md) | `@rangojs/router/testing[/dom]` |
|
|
92
|
+
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
93
|
+
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
|
|
94
|
+
| a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
|
|
95
|
+
| the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
|
|
96
|
+
| a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
|
|
97
|
+
| navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
|
|
98
|
+
| cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
|
|
99
|
+
| generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
100
|
+
| a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
|
|
101
101
|
|
|
102
102
|
Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
103
103
|
`/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# Testing cache / SWR / prerender — assertCacheStatus
|
|
2
2
|
|
|
3
|
-
**Layer:** e2e + signal · **Import:** the cache-status helpers (`assertCacheStatus`/`parseCacheHeader`/`createCacheSink`/`filterCacheDecisions`) are re-exported from BOTH entries — use `@rangojs/router/testing` from a Vitest unit/integration test, and `@rangojs/router/testing/e2e` from a plain Playwright runner (the e2e barrel avoids the Vite-only virtuals the main barrel pulls in). · **DSL it tests:** `cache()` / `"use cache"` / loader cache / `Prerender(...)` (see `/caching`, `/prerender`, `/use-cache`)
|
|
3
|
+
**Layer:** e2e + signal · **Import:** the cache-status helpers (`assertCacheStatus`/`parseCacheHeader`/`createCacheSink`/`assertCacheDecision`/`filterCacheDecisions`) are re-exported from BOTH entries — use `@rangojs/router/testing` from a Vitest unit/integration test, and `@rangojs/router/testing/e2e` from a plain Playwright runner (the e2e barrel avoids the Vite-only virtuals the main barrel pulls in). · **DSL it tests:** `cache()` / `"use cache"` / loader cache / `Prerender(...)` (see `/caching`, `/prerender`, `/use-cache`)
|
|
4
4
|
|
|
5
5
|
The router's REAL cache pipeline runs (runtime cache, SWR revalidation, prerender lookup); you SEED nothing — you drive a request through the real fetch path and read the resulting cache decision. The decision surfaces two ways: the `X-Rango-Cache` response header (a debug gate) or a captured `cache.decision` telemetry event.
|
|
6
6
|
|
|
7
|
+
## Which path to use
|
|
8
|
+
|
|
9
|
+
Both report the SAME coarse route-level signal (keyed by the route NAME). Pick by **transport**, not by meaning:
|
|
10
|
+
|
|
11
|
+
| Path | Helper | Transport | Needs the debug gate? | Production surface | Per-segment `shouldRevalidate`? |
|
|
12
|
+
| ------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------------------------- | ------------------------------- |
|
|
13
|
+
| **Header** | `assertCacheStatus(res, routeKey, expected)` / `parseCacheHeader` | the `X-Rango-Cache` response header — the ONLY signal a black-box Playwright `Response` carries | Yes (`debugCacheSignal` / `RANGO_TEST_SIGNALS=1`) | the header (gated off by default) | no |
|
|
14
|
+
| **Telemetry** | `assertCacheDecision(events, routeKey, expected)` / `filterCacheDecisions` | a captured `cache.decision` event off a `createCacheSink()` sink | No | zero | yes (the only path exposing it) |
|
|
15
|
+
|
|
16
|
+
Use the header path when all you have is a black-box `Response` (a Playwright `APIResponse`); use the telemetry path when you can wire `createRouter({ telemetry: sink })` and want zero production surface or per-segment `shouldRevalidate`. `assertCacheDecision` is the one-call counterpart of `assertCacheStatus` (parallel `(…, routeKey, expected)` shape — captured `events` in place of a `Response`); reach for raw `filterCacheDecisions` only when you need the per-segment event fields directly.
|
|
17
|
+
|
|
7
18
|
## API
|
|
8
19
|
|
|
9
20
|
### Options — `assertCacheStatus(target, segment, expected)`
|
|
@@ -38,7 +49,11 @@ parseCacheHeader(headerValue: string | null | undefined): Record<string, string>
|
|
|
38
49
|
// createCacheSink -> a sink to wire via createRouter({ telemetry: sink }), plus the array it records into.
|
|
39
50
|
createCacheSink(): { sink: TelemetrySink; events: TelemetryEvent[] }
|
|
40
51
|
|
|
41
|
-
//
|
|
52
|
+
// assertCacheDecision -> the one-call telemetry assert (counterpart of assertCacheStatus).
|
|
53
|
+
// Throws on mismatch / no matching segment / unknown routeKey; returns void.
|
|
54
|
+
assertCacheDecision(events: readonly TelemetryEvent[], routeKey: string, expected: ExpectedCacheStatus): void
|
|
55
|
+
|
|
56
|
+
// filterCacheDecisions -> narrow captured events to cache.decision events (raw form).
|
|
42
57
|
filterCacheDecisions(events: readonly TelemetryEvent[]): CacheDecisionEvent[]
|
|
43
58
|
```
|
|
44
59
|
|
|
@@ -74,16 +89,27 @@ parityDescribe("product page caches", (f) => {
|
|
|
74
89
|
Zero-prod-surface alternative — the telemetry sink. No header at all; you inspect captured `cache.decision` events:
|
|
75
90
|
|
|
76
91
|
```ts
|
|
77
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
createCacheSink,
|
|
94
|
+
assertCacheDecision,
|
|
95
|
+
filterCacheDecisions,
|
|
96
|
+
} from "@rangojs/router/testing";
|
|
78
97
|
|
|
79
98
|
const { sink, events } = createCacheSink();
|
|
80
99
|
const router = createRouter({ telemetry: sink }).routes(urlpatterns);
|
|
81
100
|
// ...drive a request through the router's RSC fetch path...
|
|
101
|
+
|
|
102
|
+
// One-call assert (counterpart of assertCacheStatus), keyed by the route NAME:
|
|
103
|
+
assertCacheDecision(events, "product.detail", "stale");
|
|
104
|
+
|
|
105
|
+
// Or read the raw event when you need per-segment fields (shouldRevalidate):
|
|
82
106
|
const decision = filterCacheDecisions(events)[0];
|
|
83
107
|
expect(decision.segments?.[0].cacheStatus).toBe("stale");
|
|
84
108
|
expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
85
109
|
```
|
|
86
110
|
|
|
111
|
+
`events` accumulates across requests, so the FIRST matching segment for a `routeKey` wins — slice or recreate the sink between requests for the same route.
|
|
112
|
+
|
|
87
113
|
## Caveats
|
|
88
114
|
|
|
89
115
|
- The `X-Rango-Cache` header is emitted ONLY when the gate is on: `createRouter({ debugCacheSignal: true })` or `process.env.RANGO_TEST_SIGNALS === "1"`. Off by default — zero production surface. With the gate off, `assertCacheStatus` throws a clear "header missing" error.
|
package/skills/testing/flight.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Pinning the Flight wire payload — renderToFlightString
|
|
2
2
|
|
|
3
3
|
**Layer:** RSC unit (react-server project) · **Import:** `@rangojs/router/testing/flight` + `@rangojs/router/testing/flight-matchers` · **DSL it tests:** an async Server Component / Flight output (see `/route`)
|
|
4
4
|
|
|
5
|
+
> **Prefer `renderServerTree` (see [`./server-tree.md`](./server-tree.md)) for assertions on a Flight render** — it deserializes to a traversable tree with TYPED boundary props (a `Date` is a `Date`, not the opaque `$D...` encoding). Reach for `renderToFlightString` + the wire matchers (`toMatchFlight`/`toMatchFlightSnapshot`) only to pin the raw wire payload SHAPE — a `toMatchFlightSnapshot` drift snapshot. That is the niche/escape-hatch case; for "testing an async Server Component (assert what it rendered)" start at `./server-tree.md`.
|
|
6
|
+
|
|
5
7
|
`renderToFlightString` runs the REAL react-server-dom serializer the router uses at runtime — your async Server Component genuinely renders to its Flight wire string in plain node, with a request context active for the render. What you SEED is the request, headers, env, params, routeName, and vars that context exposes.
|
|
6
8
|
|
|
7
9
|
## API
|
|
8
10
|
|
|
9
11
|
### Options — `RenderToFlightStringOptions`
|
|
10
12
|
|
|
11
|
-
| Field | Type | Meaning
|
|
12
|
-
| ----------- | ------------------------ |
|
|
13
|
-
| `request` | `Request \| string` | The request the render runs under: a `Request`, or a URL string (absolute or path). Defaults to `http://localhost/`. A component reading `getRequestContext()` sees this request's url/cookies. When a `Request` is passed, its headers are used and `headers` is ignored.
|
|
14
|
-
| `headers` | `HeadersInit` | Request headers (e.g. Cookie) visible to the server tree, used only when `request` is a string.
|
|
15
|
-
| `env` | `unknown` | Env / bindings exposed as `ctx.env`. Defaults to `{}`.
|
|
16
|
-
| `params` | `Record<string, string>` | Route params exposed via `ctx.params` and loader contexts.
|
|
17
|
-
| `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse).
|
|
18
|
-
| `
|
|
13
|
+
| Field | Type | Meaning |
|
|
14
|
+
| ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `request` | `Request \| string` | The request the render runs under: a `Request`, or a URL string (absolute or path). Defaults to `http://localhost/`. A component reading `getRequestContext()` sees this request's url/cookies. When a `Request` is passed, its headers are used and `headers` is ignored. |
|
|
16
|
+
| `headers` | `HeadersInit` | Request headers (e.g. Cookie) visible to the server tree, used only when `request` is a string. |
|
|
17
|
+
| `env` | `unknown` | Env / bindings exposed as `ctx.env`. Defaults to `{}`. |
|
|
18
|
+
| `params` | `Record<string, string>` | Route params exposed via `ctx.params` and loader contexts. |
|
|
19
|
+
| `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
|
|
20
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map scoping `ctx.reverse()` (like `renderHandler`). Without it, a component that reverses resolves against the GLOBAL route map and is order-dependent on whatever router registered last. Pass the router-under-test's map for deterministic reverse. |
|
|
21
|
+
| `vars` | `VarsInit` | Variables a prior middleware would have set, visible via `ctx.get(...)`. Object form (`{ user }`) or `[key, value]` tuples (`[[userVar, u]]`). |
|
|
19
22
|
|
|
20
23
|
### Context — `RequestContext` (what your component receives)
|
|
21
24
|
|
|
@@ -78,7 +81,7 @@ it("snapshots the normalized payload", async () => {
|
|
|
78
81
|
|
|
79
82
|
- Leaf / server-only: a client island in the tree emits an un-hydratable `I[...]` import row against the empty client manifest. Keep Flight tests to leaf server components; test full pages at e2e.
|
|
80
83
|
- Requires the react-server vitest project (see `./setup.md`): `resolve.conditions` includes `react-server`, the `@rangojs/router -> index.rsc.ts` alias, `NODE_ENV=production`, and the worker `execArgv`. Name files `*.rsc-test.{ts,tsx}` and run `pnpm test:unit:rsc`. The main vitest project must NOT set `react-server` (it would flip React to the no-hooks server build).
|
|
81
|
-
- A component that imports a server API (`getRequestContext`, `cookies`) from the bare `@rangojs/router` barrel works ONLY with the `index.rsc.ts` alias wired (see `./setup.md`); without it the bare import resolves to the throwing out-of-react-server stub. Pure-leaf components that take all data as props need no barrel import and are the simplest case.
|
|
84
|
+
- A component that imports a server API (`getRequestContext`, `cookies`) from the bare `@rangojs/router` barrel works ONLY with the `index.rsc.ts` alias wired (see `./setup.md`); without it the bare import resolves to the throwing out-of-react-server stub. `renderToFlightString` / `renderServerTree` self-diagnose this exact misconfiguration — they reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error. Pure-leaf components that take all data as props need no barrel import and are the simplest case.
|
|
82
85
|
- `toMatchFlight` is containment (substring), not equality — the row framing (prefixes/quoting) is an internal serializer detail, so pin the rendered text/shape, not the framing. `toMatchFlightSnapshot()` snapshots the normalized payload; run under `NODE_ENV=production` for the cleanest, most stable bytes.
|
|
83
86
|
- No hydration / no interaction here — that is the e2e tier. For typed assertions on a client boundary's props (a `Date` back as a `Date`), or to confirm an island actually crossed the boundary, use `renderServerTree` (see `./server-tree.md`).
|
|
84
87
|
|
|
@@ -20,6 +20,8 @@ A Rango route handler is a pure function `(ctx) => rsc` — the function you pas
|
|
|
20
20
|
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
|
|
21
21
|
| `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
|
|
22
22
|
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (the value is `{version}:{timestamp}`, default `"0"`). |
|
|
23
|
+
| `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
|
|
24
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
|
|
23
25
|
|
|
24
26
|
### Context — `HandlerContext` (what your handler receives)
|
|
25
27
|
|
|
@@ -110,6 +112,7 @@ it("asserts the client-cache directives", async () => {
|
|
|
110
112
|
- A `throw redirect()` is captured on `thrown` (with `tree` undefined, since it produced a `Response`) — assert on `thrown`/`response`, no try/catch needed.
|
|
111
113
|
- No hydration and no interaction — for clicks, forms, and navigation use e2e.
|
|
112
114
|
- `renderHandler` runs a handler FUNCTION `(ctx) => rsc`; for a plain ELEMENT `<Page/>` use `renderServerTree` (see [`./server-tree.md`](./server-tree.md)).
|
|
115
|
+
- A handler that calls a `"use cache"` function runs UNCACHED unless you seed `cacheStore` (and `cacheProfiles` for a named profile). With nothing seeded the runtime bypasses to the live body and warns once under the test runner — assert real cache behavior by passing `{ cacheStore: new MemorySegmentCacheStore(), cacheProfiles: { default: { ttl: 60 } } }`.
|
|
113
116
|
|
|
114
117
|
## See also
|
|
115
118
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Layer:** RSC unit (react-server project) · **Import:** `@rangojs/router/testing/flight` · **DSL it tests:** client islands across the boundary + server-rendered host content (see `/route`)
|
|
4
4
|
|
|
5
|
-
`renderServerTree` serializes the real Flight (identical bytes to `renderToFlightString`) and then deserializes it back to an inspectable React element tree you traverse — that serialize/deserialize round-trip is REAL; what you SEED is the element you render plus the request context (`request`/`headers`/`params`/`vars`/`env`). The win over the wire string: a client boundary's props come back as real JS values (a `Date` is a `Date`, not the opaque `$D...` encoding) and you can confirm a `"use client"` component actually crossed the boundary (an `I` row) instead of being inlined. There is NO hydration and NO interaction — boundaries are inert placeholders carrying props.
|
|
5
|
+
`renderServerTree` is the DEFAULT way to assert on a Flight render; `renderToFlightString` (see [`./flight.md`](./flight.md)) is the escape hatch for pinning the raw wire bytes. It serializes the real Flight (identical bytes to `renderToFlightString`) and then deserializes it back to an inspectable React element tree you traverse — that serialize/deserialize round-trip is REAL; what you SEED is the element you render plus the request context (`request`/`headers`/`params`/`vars`/`env`). The win over the wire string: a client boundary's props come back as real JS values (a `Date` is a `Date`, not the opaque `$D...` encoding) and you can confirm a `"use client"` component actually crossed the boundary (an `I` row) instead of being inlined. There is NO hydration and NO interaction — boundaries are inert placeholders carrying props.
|
|
6
6
|
|
|
7
7
|
## API
|
|
8
8
|
|
package/skills/testing/setup.md
CHANGED
|
@@ -106,7 +106,7 @@ Scripts:
|
|
|
106
106
|
|
|
107
107
|
- Node >= 23 requires `rangoTestConfig()`, not bare `rangoTestAliases()`. `@rangojs/router` is consumed as SOURCE (exports -> `./src/*.ts`), and Node >= 23 refuses to type-strip `.ts` under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). `rangoTestConfig` ships as compiled JS AND adds `server.deps.inline: [/@rangojs[/\\]router/]` so Vite (not Node) transpiles rango source. With bare `rangoTestAliases` you must wire `deps.inline` yourself.
|
|
108
108
|
- Two separate projects. The node/DOM project keeps React on its CLIENT build; the Flight project uses the `react-server` condition in a separate `vitest.rsc.config.ts`. The main project must NOT set `react-server` — it flips React to the no-hooks server build and breaks every `renderRoute` / client test.
|
|
109
|
-
- The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub (symptom: `renderHandler` returns `tree: undefined`).
|
|
109
|
+
- The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub (symptom: `renderHandler` returns `tree: undefined`). `renderToFlightString` / `renderServerTree` now self-diagnose this exact misconfiguration — they reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error.
|
|
110
110
|
- `NODE_ENV` must be `"production"` in the rsc project. Dev `NODE_ENV` crashes the bare worker (jsxDEV owner-stack machinery uninitialized) and emits volatile debug rows that defeat stable Flight snapshots.
|
|
111
111
|
- The forked rsc worker (`pool: "forks"`) must force the condition via `execArgv: ["--conditions=react-server"]`, or React throws "the react-server condition must be enabled".
|
|
112
112
|
- The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
|
package/src/__internal.ts
CHANGED
|
@@ -9,10 +9,6 @@
|
|
|
9
9
|
* adding them to the public API.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// Segment Resolution (Internal)
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
12
|
/**
|
|
17
13
|
* @internal
|
|
18
14
|
* Internal representation of a resolved route segment.
|
|
@@ -26,10 +22,6 @@ export type { ResolvedSegment, SegmentMetadata } from "./types.js";
|
|
|
26
22
|
*/
|
|
27
23
|
export type { MatchResult, SlotState } from "./types.js";
|
|
28
24
|
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// Intercept System (Internal)
|
|
31
|
-
// ============================================================================
|
|
32
|
-
|
|
33
25
|
/**
|
|
34
26
|
* @internal
|
|
35
27
|
* Context for intercept route selection.
|
|
@@ -40,10 +32,6 @@ export type {
|
|
|
40
32
|
InterceptWhenFn,
|
|
41
33
|
} from "./server/context.js";
|
|
42
34
|
|
|
43
|
-
// ============================================================================
|
|
44
|
-
// Browser State (Internal)
|
|
45
|
-
// ============================================================================
|
|
46
|
-
|
|
47
35
|
/**
|
|
48
36
|
* @internal
|
|
49
37
|
* RSC protocol payload structure.
|
|
@@ -68,10 +56,6 @@ export type {
|
|
|
68
56
|
NavigationUpdate,
|
|
69
57
|
} from "./browser/types.js";
|
|
70
58
|
|
|
71
|
-
// ============================================================================
|
|
72
|
-
// Handle System (Internal)
|
|
73
|
-
// ============================================================================
|
|
74
|
-
|
|
75
59
|
/**
|
|
76
60
|
* @internal
|
|
77
61
|
* Internal handle storage mechanism.
|
|
@@ -84,26 +68,17 @@ export type { HandleStore, HandleData } from "./server/handle-store.js";
|
|
|
84
68
|
*/
|
|
85
69
|
export type { SegmentHandleData } from "./cache/types.js";
|
|
86
70
|
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// Cache Internals
|
|
89
|
-
// ============================================================================
|
|
90
|
-
|
|
91
71
|
/**
|
|
92
72
|
* @internal
|
|
93
73
|
* Internal cache entry data structure.
|
|
94
74
|
*/
|
|
95
75
|
export type {
|
|
96
76
|
CachedEntryData,
|
|
97
|
-
CachedEntryResult,
|
|
98
77
|
CacheGetResult,
|
|
99
78
|
SerializedSegmentData,
|
|
100
79
|
CacheDefaults,
|
|
101
80
|
} from "./cache/types.js";
|
|
102
81
|
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// Router Context (Internal)
|
|
105
|
-
// ============================================================================
|
|
106
|
-
|
|
107
82
|
/**
|
|
108
83
|
* @internal
|
|
109
84
|
* Router context for AsyncLocalStorage.
|
|
@@ -114,10 +89,6 @@ export type {
|
|
|
114
89
|
InterceptResult,
|
|
115
90
|
} from "./router/router-context.js";
|
|
116
91
|
|
|
117
|
-
// ============================================================================
|
|
118
|
-
// Match Pipeline (Internal)
|
|
119
|
-
// ============================================================================
|
|
120
|
-
|
|
121
92
|
/**
|
|
122
93
|
* @internal
|
|
123
94
|
* Route match context during pipeline processing.
|
|
@@ -133,10 +104,6 @@ export type {
|
|
|
133
104
|
*/
|
|
134
105
|
export type { RouteMatchResult } from "./router/pattern-matching.js";
|
|
135
106
|
|
|
136
|
-
// ============================================================================
|
|
137
|
-
// Server Context (Internal)
|
|
138
|
-
// ============================================================================
|
|
139
|
-
|
|
140
107
|
/**
|
|
141
108
|
* @internal
|
|
142
109
|
* Entry data during route traversal.
|
|
@@ -153,10 +120,6 @@ export type {
|
|
|
153
120
|
EntryPropSegments,
|
|
154
121
|
} from "./server/context.js";
|
|
155
122
|
|
|
156
|
-
// ============================================================================
|
|
157
|
-
// Handler Context (Internal)
|
|
158
|
-
// ============================================================================
|
|
159
|
-
|
|
160
123
|
/**
|
|
161
124
|
* @internal
|
|
162
125
|
* Internal handler context with additional props for router internals.
|
|
@@ -164,30 +127,18 @@ export type {
|
|
|
164
127
|
*/
|
|
165
128
|
export type { InternalHandlerContext } from "./types.js";
|
|
166
129
|
|
|
167
|
-
// ============================================================================
|
|
168
|
-
// Rendering (Internal)
|
|
169
|
-
// ============================================================================
|
|
170
|
-
|
|
171
130
|
/**
|
|
172
131
|
* @internal
|
|
173
132
|
* Builds React element trees from route segments.
|
|
174
133
|
*/
|
|
175
134
|
export { renderSegments } from "./segment-system.js";
|
|
176
135
|
|
|
177
|
-
// ============================================================================
|
|
178
|
-
// Error Utilities (Internal)
|
|
179
|
-
// ============================================================================
|
|
180
|
-
|
|
181
136
|
/**
|
|
182
137
|
* @internal
|
|
183
138
|
* Error sanitization and network error utilities.
|
|
184
139
|
*/
|
|
185
140
|
export { sanitizeError, NetworkError, isNetworkError } from "./errors.js";
|
|
186
141
|
|
|
187
|
-
// ============================================================================
|
|
188
|
-
// Type Utilities (Internal)
|
|
189
|
-
// ============================================================================
|
|
190
|
-
|
|
191
142
|
/**
|
|
192
143
|
* @internal
|
|
193
144
|
* Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>.
|
|
@@ -217,10 +168,6 @@ export type {
|
|
|
217
168
|
RevalidationDecisionEvent,
|
|
218
169
|
} from "./router/telemetry.js";
|
|
219
170
|
|
|
220
|
-
// ============================================================================
|
|
221
|
-
// Pre-render / Static Handler Guards (Internal)
|
|
222
|
-
// ============================================================================
|
|
223
|
-
|
|
224
171
|
/**
|
|
225
172
|
* @internal
|
|
226
173
|
* Type guard for prerender handler definitions.
|
|
@@ -233,20 +180,12 @@ export { isPrerenderHandler, isPassthroughHandler } from "./prerender.js";
|
|
|
233
180
|
*/
|
|
234
181
|
export { isStaticHandler } from "./static-handler.js";
|
|
235
182
|
|
|
236
|
-
// ============================================================================
|
|
237
|
-
// URL Pattern Internals
|
|
238
|
-
// ============================================================================
|
|
239
|
-
|
|
240
183
|
/**
|
|
241
184
|
* @internal
|
|
242
185
|
* Sentinel used to tag response-type route entries.
|
|
243
186
|
*/
|
|
244
187
|
export { RESPONSE_TYPE } from "./urls.js";
|
|
245
188
|
|
|
246
|
-
// ============================================================================
|
|
247
|
-
// Route Match Debug (Internal)
|
|
248
|
-
// ============================================================================
|
|
249
|
-
|
|
250
189
|
/**
|
|
251
190
|
* @internal
|
|
252
191
|
* Debug utilities for route matching performance analysis.
|
|
@@ -256,10 +195,6 @@ export {
|
|
|
256
195
|
getMatchDebugStats,
|
|
257
196
|
} from "./router/pattern-matching.js";
|
|
258
197
|
|
|
259
|
-
// ============================================================================
|
|
260
|
-
// Debug Utilities (Internal)
|
|
261
|
-
// ============================================================================
|
|
262
|
-
|
|
263
198
|
/**
|
|
264
199
|
* @internal
|
|
265
200
|
* Debug utilities for manifest inspection and comparison.
|
|
@@ -5,7 +5,7 @@ import type { ActionEntry } from "./event-controller.js";
|
|
|
5
5
|
* full-update-unsupported cases are handled inline in the bridge before
|
|
6
6
|
* reconciliation; this only covers successfully-reconciled partial responses.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
type ActionScenario =
|
|
9
9
|
| {
|
|
10
10
|
type: "navigated-away";
|
|
11
11
|
historyKeyChanged: boolean;
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
* keeps a sibling tab from seeing a pre-commit signal. Refcounted so concurrent
|
|
16
16
|
* actions compose: each action raises and lowers its own reference, and the
|
|
17
17
|
* fence is down only when the count reaches zero.
|
|
18
|
+
*
|
|
19
|
+
* The refcount is a single module-level counter, not keyed by routerId.
|
|
20
|
+
* Consumers (navigation-client.ts, prefetch/fetch.ts, navigation-bridge.ts)
|
|
21
|
+
* read it unscoped, so an action in one router would suppress another router's
|
|
22
|
+
* navigation/prefetch caches. This is correct only because two routers cannot
|
|
23
|
+
* coexist in one live document: an SPA navigation crossing a host-router
|
|
24
|
+
* boundary forces a full document reload (src/router/request-classification.ts
|
|
25
|
+
* app-switch terminal), so there is always exactly one live router per
|
|
26
|
+
* document. A future multi-router-in-one-document feature must not silently
|
|
27
|
+
* inherit this global, cross-router cache suppression.
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
30
|
let fenceCount = 0;
|