@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -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 +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -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/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- 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/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- 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 +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -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 +305 -0
- 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/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- 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 +192 -99
- 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 +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- 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 +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: streams-and-websockets
|
|
3
|
+
description: Long-lived Response handlers — Server-Sent Events (SSE) via path.stream and WebSocket upgrades via path.any on Cloudflare Workers, including middleware interaction and runtime caveats.
|
|
4
|
+
argument-hint: "[sse | websocket | agents]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Streams and WebSockets
|
|
8
|
+
|
|
9
|
+
Response routes can return long-lived responses — SSE streams and WebSocket
|
|
10
|
+
upgrades. Both require a `Response` that the router must forward through the
|
|
11
|
+
middleware chain without reconstruction.
|
|
12
|
+
|
|
13
|
+
## When each fits
|
|
14
|
+
|
|
15
|
+
| Shape | Tag | Status | Body | Runtime |
|
|
16
|
+
| ----------- | --------------- | ------ | ------------------------------- | -------------------------------- |
|
|
17
|
+
| Server-Sent | `path.stream()` | 200 | `ReadableStream` (event-stream) | any runtime (Node, workerd, bun) |
|
|
18
|
+
| WebSocket | `path.any()` | 101 | `null` + `webSocket` property | Cloudflare Workers (workerd) |
|
|
19
|
+
|
|
20
|
+
- **SSE** is a regular 200 response with `content-type: text/event-stream`
|
|
21
|
+
and a `ReadableStream` body. Works everywhere, flows through middleware
|
|
22
|
+
normally.
|
|
23
|
+
- **WebSocket upgrades** produce a status-101 response with a non-standard
|
|
24
|
+
`webSocket` property (Cloudflare). The router detects these and forwards
|
|
25
|
+
them without reconstruction; `Vary` and `Server-Timing` are skipped, and
|
|
26
|
+
stub headers are merged in place on a best-effort basis.
|
|
27
|
+
|
|
28
|
+
## Server-Sent Events (SSE)
|
|
29
|
+
|
|
30
|
+
Use `path.stream()` (or `path.any()` if you need full control) to return a
|
|
31
|
+
`ReadableStream`. Each chunk is an `event-stream` frame:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { urls } from "@rangojs/router";
|
|
35
|
+
|
|
36
|
+
export const urlpatterns = urls(({ path }) => [
|
|
37
|
+
path.stream(
|
|
38
|
+
"/events/ticks",
|
|
39
|
+
(ctx) => {
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
|
|
42
|
+
const stream = new ReadableStream({
|
|
43
|
+
async start(controller) {
|
|
44
|
+
let count = 0;
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
controller.enqueue(
|
|
47
|
+
encoder.encode(`event: tick\ndata: ${++count}\n\n`),
|
|
48
|
+
);
|
|
49
|
+
}, 1000);
|
|
50
|
+
|
|
51
|
+
// Honor client disconnect — signal comes from ctx.request.signal
|
|
52
|
+
ctx.request.signal.addEventListener("abort", () => {
|
|
53
|
+
clearInterval(interval);
|
|
54
|
+
controller.close();
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return new Response(stream, {
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "text/event-stream",
|
|
62
|
+
"cache-control": "no-store",
|
|
63
|
+
// Disable proxy buffering on Nginx/Traefik deployments
|
|
64
|
+
"x-accel-buffering": "no",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
{ name: "ticks" },
|
|
69
|
+
),
|
|
70
|
+
]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Client
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
"use client";
|
|
77
|
+
const source = new EventSource("/events/ticks");
|
|
78
|
+
source.addEventListener("tick", (e) => console.log("tick", e.data));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### SSE caveats
|
|
82
|
+
|
|
83
|
+
- **Never wrap SSE routes in `cache()`** — a cached `ReadableStream` is read
|
|
84
|
+
once and would replay an empty body on the next hit. `path.stream` is
|
|
85
|
+
already excluded from response-route caching, but don't layer a custom
|
|
86
|
+
cache() middleware on top.
|
|
87
|
+
- **Middleware is fine.** Global/route middleware rewraps the SSE `Response`
|
|
88
|
+
as `new Response(response.body, { status, headers })` to merge stub headers.
|
|
89
|
+
The `ReadableStream` body is passed by reference, not consumed, so the
|
|
90
|
+
client sees the stream unchanged. (WebSocket upgrades are the exception —
|
|
91
|
+
those bypass rewrap entirely; see below.)
|
|
92
|
+
- **Honor `ctx.request.signal`.** Without wiring abort to your source
|
|
93
|
+
(timer, DB cursor, upstream fetch), the stream leaks when the client
|
|
94
|
+
disconnects.
|
|
95
|
+
- **Disable Nginx/CDN buffering** via `x-accel-buffering: no` and ensure
|
|
96
|
+
no intermediate proxy rebuffers. On Cloudflare Workers this is a non-issue.
|
|
97
|
+
|
|
98
|
+
## WebSockets (Cloudflare Workers)
|
|
99
|
+
|
|
100
|
+
WebSocket upgrades on workerd produce a response with `status: 101` and a
|
|
101
|
+
non-standard `webSocket` property. The router detects this shape and forwards
|
|
102
|
+
the `Response` without reconstruction — the 101 status and the `webSocket`
|
|
103
|
+
property are preserved. `Vary` and `Server-Timing` writes are skipped, and
|
|
104
|
+
stub-header merging (cookies/custom headers set via `ctx.header()` or
|
|
105
|
+
`cookies().set()`) is best-effort: the router attempts to apply them in
|
|
106
|
+
place, but silently skips any write rejected by a runtime that exposes
|
|
107
|
+
immutable upgrade headers.
|
|
108
|
+
|
|
109
|
+
### Minimal upgrade handler
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { urls } from "@rangojs/router";
|
|
113
|
+
|
|
114
|
+
export const urlpatterns = urls(({ path }) => [
|
|
115
|
+
path.any(
|
|
116
|
+
"/ws",
|
|
117
|
+
(ctx) => {
|
|
118
|
+
// Manual WebSocketPair on workerd
|
|
119
|
+
const upgrade = ctx.request.headers.get("upgrade");
|
|
120
|
+
if (upgrade !== "websocket") {
|
|
121
|
+
return new Response("expected upgrade: websocket", { status: 426 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
125
|
+
server.accept();
|
|
126
|
+
server.addEventListener("message", (e) => {
|
|
127
|
+
server.send(`echo: ${e.data}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return new Response(null, {
|
|
131
|
+
status: 101,
|
|
132
|
+
webSocket: client,
|
|
133
|
+
} as ResponseInit);
|
|
134
|
+
},
|
|
135
|
+
{ name: "ws" },
|
|
136
|
+
),
|
|
137
|
+
]);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Durable Object pattern
|
|
141
|
+
|
|
142
|
+
Route into a Durable Object that owns the connection:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
export const urlpatterns = urls(({ path }) => [
|
|
146
|
+
path.any(
|
|
147
|
+
"/rooms/:roomId",
|
|
148
|
+
async (ctx) => {
|
|
149
|
+
const id = ctx.env.ROOMS.idFromName(ctx.params.roomId);
|
|
150
|
+
const stub = ctx.env.ROOMS.get(id);
|
|
151
|
+
// The DO's fetch handler calls handleWebSocketUpgrade(request)
|
|
152
|
+
// and returns the 101 Response. We forward it unchanged.
|
|
153
|
+
return stub.fetch(ctx.request);
|
|
154
|
+
},
|
|
155
|
+
{ name: "room" },
|
|
156
|
+
),
|
|
157
|
+
]);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Using the `agents` library
|
|
161
|
+
|
|
162
|
+
`routeAgentRequest` from `agents` returns a 101 `Response` targeted at a
|
|
163
|
+
Durable Object. Return it directly from `path.any()`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { routeAgentRequest } from "agents";
|
|
167
|
+
import { urls } from "@rangojs/router";
|
|
168
|
+
|
|
169
|
+
export const urlpatterns = urls(({ path }) => [
|
|
170
|
+
path.any("/agents/*", async (ctx) => {
|
|
171
|
+
const response = await routeAgentRequest(ctx.request, ctx.env);
|
|
172
|
+
if (!response) {
|
|
173
|
+
return new Response("not found", { status: 404 });
|
|
174
|
+
}
|
|
175
|
+
return response;
|
|
176
|
+
}),
|
|
177
|
+
]);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Middleware interaction
|
|
181
|
+
|
|
182
|
+
### Forwarded, not reconstructed
|
|
183
|
+
|
|
184
|
+
When a middleware is matched for the upgrade URL, the middleware still runs
|
|
185
|
+
**before** `next()` — but the Response from `next()` is forwarded as-is
|
|
186
|
+
rather than re-wrapped. This preserves:
|
|
187
|
+
|
|
188
|
+
- The 101 status (which would otherwise throw `RangeError: Responses may
|
|
189
|
+
only be constructed with status codes in the range 200 to 599, inclusive`
|
|
190
|
+
on standards-compliant runtimes).
|
|
191
|
+
- The Cloudflare `webSocket` property (which would otherwise be silently
|
|
192
|
+
dropped by `new Response(body, ...)` on workerd).
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// This works — logger runs, but the 101 flows through unchanged.
|
|
196
|
+
router.use(async (ctx, next) => {
|
|
197
|
+
console.log("ws request", ctx.url.pathname);
|
|
198
|
+
return next();
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Don't try to set cookies on an upgrade
|
|
203
|
+
|
|
204
|
+
Stub cookie/header writes made before `await next()` are applied to the
|
|
205
|
+
upgrade response on a best-effort basis — the router attempts an in-place
|
|
206
|
+
merge and skips any write rejected by runtimes that expose immutable 101
|
|
207
|
+
headers. Either way, a browser completing a WS handshake never reads them.
|
|
208
|
+
Do not rely on this for auth or state propagation: set cookies via a prior
|
|
209
|
+
HTTP request instead (e.g. during login), then read them at upgrade time
|
|
210
|
+
via `ctx.request.headers.get("cookie")`.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Avoid: this cookie may not land on the upgrade response, and the client
|
|
214
|
+
// never reads it during the handshake regardless.
|
|
215
|
+
router.use(async (ctx, next) => {
|
|
216
|
+
cookies().set("last-ws-at", Date.now().toString());
|
|
217
|
+
return next();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Prefer: authenticate by reading a cookie set on a prior HTTP request.
|
|
221
|
+
path.any("/ws", (ctx) => {
|
|
222
|
+
const session = parseCookie(ctx.request.headers.get("cookie"))?.session;
|
|
223
|
+
if (!verify(session)) return new Response("unauthorized", { status: 401 });
|
|
224
|
+
// ...upgrade
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Short-circuit before upgrade
|
|
229
|
+
|
|
230
|
+
Middleware can return a non-101 Response to deny the upgrade outright:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
router.use(async (ctx, next) => {
|
|
234
|
+
if (!isAllowed(ctx.request)) {
|
|
235
|
+
return new Response("forbidden", { status: 403 });
|
|
236
|
+
}
|
|
237
|
+
return next();
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Caching
|
|
242
|
+
|
|
243
|
+
- **SSE** — do not combine with `cache()` (streams can't be replayed).
|
|
244
|
+
- **WebSocket** — `cache()` is inert because only `status === 200` is cacheable.
|
|
245
|
+
|
|
246
|
+
## Runtime caveats
|
|
247
|
+
|
|
248
|
+
| Runtime | SSE | WebSocket upgrade (101) |
|
|
249
|
+
| -------------------------------------- | --- | ---------------------------------------------------- |
|
|
250
|
+
| Cloudflare Workers (workerd) | OK | OK (native `WebSocketPair`, DO, `agents`) |
|
|
251
|
+
| Node (undici fetch) | OK | N/A — Node's HTTP server must upgrade |
|
|
252
|
+
| Bun | OK | Bun's native `upgrade()` — not a Response-based path |
|
|
253
|
+
| Dev (Vite + `@cloudflare/vite-plugin`) | OK | OK via workerd emulation |
|
|
254
|
+
|
|
255
|
+
When running in pure Node without workerd, a `status: 101` Response cannot
|
|
256
|
+
even be constructed (`new Response(null, { status: 101 })` throws). For
|
|
257
|
+
tests, fabricate upgrade-style responses by overriding `.status` on a real
|
|
258
|
+
Response instance:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const upgrade = new Response(null, { status: 200 });
|
|
262
|
+
Object.defineProperty(upgrade, "status", { value: 101, configurable: true });
|
|
263
|
+
// optional: attach a webSocket stub
|
|
264
|
+
Object.defineProperty(upgrade, "webSocket", {
|
|
265
|
+
value: { stub: "ws" },
|
|
266
|
+
configurable: true,
|
|
267
|
+
enumerable: true,
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Testing
|
|
272
|
+
|
|
273
|
+
- Unit tests: `isWebSocketUpgradeResponse` and `executeMiddleware` passthrough
|
|
274
|
+
cases live in `src/rsc/__tests__/helpers.test.ts` and
|
|
275
|
+
`src/router/middleware.test.ts`.
|
|
276
|
+
- E2E: cover both dev and production modes against a workerd target. SSE
|
|
277
|
+
can be tested on any runtime; WS upgrades need workerd (use
|
|
278
|
+
`@cloudflare/vite-plugin` or `wrangler dev`).
|
|
279
|
+
|
|
280
|
+
## See also
|
|
281
|
+
|
|
282
|
+
- `response-routes` — the parent skill for `path.json/text/html/stream/any`.
|
|
283
|
+
- `middleware` — how global and route-level middleware compose with handlers.
|
package/skills/tailwind/SKILL.md
CHANGED
|
@@ -37,7 +37,11 @@ export default defineConfig({
|
|
|
37
37
|
|
|
38
38
|
## Document Component
|
|
39
39
|
|
|
40
|
-
Import the CSS file with `?url` to get a hashed URL, then preload and link it in
|
|
40
|
+
Import the CSS file with `?url` to get a hashed URL, then preload and link it in
|
|
41
|
+
`<head>`. Give the `<link rel="stylesheet">` a `precedence` prop so React 19
|
|
42
|
+
manages it as a resource — de-duped by `href`, ordered, and loaded before paint
|
|
43
|
+
(no flash of unstyled content). See
|
|
44
|
+
[Stylesheets and cross-app navigation](#stylesheets-and-cross-app-navigation):
|
|
41
45
|
|
|
42
46
|
```tsx
|
|
43
47
|
// src/document.tsx
|
|
@@ -51,8 +55,8 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
51
55
|
return (
|
|
52
56
|
<html lang="en">
|
|
53
57
|
<head>
|
|
54
|
-
<link rel="preload" href={styles} as="style" />
|
|
55
|
-
<link rel="stylesheet" href={styles} />
|
|
58
|
+
<link rel="preload" href={styles} as="style" precedence="default" />
|
|
59
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
56
60
|
<MetaTags />
|
|
57
61
|
</head>
|
|
58
62
|
<body className="font-sans antialiased text-slate-900 bg-slate-50">
|
|
@@ -63,6 +67,26 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
63
67
|
}
|
|
64
68
|
```
|
|
65
69
|
|
|
70
|
+
## Stylesheets and cross-app navigation
|
|
71
|
+
|
|
72
|
+
The `precedence` prop opts a `<link rel="stylesheet">` into React 19's managed
|
|
73
|
+
stylesheet model — React de-duplicates it by `href`, orders it by precedence, and
|
|
74
|
+
loads it before paint (avoiding a flash of unstyled content). It is the
|
|
75
|
+
recommended way to render a stylesheet link, which is why the example above uses
|
|
76
|
+
it. (A bare side-effect `import "./index.css"` also produces managed CSS via
|
|
77
|
+
`@vitejs/plugin-rsc`, but carries an SSR-streaming caveat — prefer the `?url` +
|
|
78
|
+
`<link precedence>` form for document CSS. See `/css`.)
|
|
79
|
+
|
|
80
|
+
For **host-router** apps (`/host-router`), a client-side navigation that crosses
|
|
81
|
+
an app boundary is a **full document load**, not a soft swap — the framework
|
|
82
|
+
redirects on an app switch. So each app's document (its stylesheets, theme, meta)
|
|
83
|
+
is always re-established cleanly by the target app's own load; you do not have to
|
|
84
|
+
coordinate stylesheet `href`s or `precedence` across apps. (This replaced an
|
|
85
|
+
earlier soft cross-app swap, under which a stylesheet shared across apps — every
|
|
86
|
+
app's `@import "tailwindcss"` compiles to one hashed asset — could be dropped by
|
|
87
|
+
React's by-`href` resource dedup if the apps disagreed on `precedence`. The full
|
|
88
|
+
reload removes that footgun entirely.)
|
|
89
|
+
|
|
66
90
|
The `?url` suffix tells Vite to return the processed CSS file's URL instead of injecting it as a side effect. This gives you a stable, hashed asset path that works in both development and production.
|
|
67
91
|
|
|
68
92
|
## Customizing the Theme
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: Test @rangojs/router apps — unit (loaders/middleware/reverse/components), integration (dispatch/Flight), and e2e (dev+prod parity, progressive enhancement)
|
|
4
|
+
argument-hint: [layer]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Testing @rangojs/router apps
|
|
8
|
+
|
|
9
|
+
Rango ships six consumer-facing testing entries, one per test runtime/dependency:
|
|
10
|
+
`@rangojs/router/testing` (unit + integration, under a Vite-driven Vitest
|
|
11
|
+
project), `@rangojs/router/testing/vitest` (the `rangoTestConfig`/`rangoTestAliases`
|
|
12
|
+
setup preset), `@rangojs/router/testing/dom` (`renderRoute`, needs RTL + a DOM
|
|
13
|
+
env), `@rangojs/router/testing/e2e` (the Playwright harness),
|
|
14
|
+
`@rangojs/router/testing/flight` (real Flight, react-server condition only), and
|
|
15
|
+
`@rangojs/router/testing/flight-matchers` (the Flight matchers).
|
|
16
|
+
|
|
17
|
+
The hard problem in an RSC app is that the layer you reach for is dictated by
|
|
18
|
+
**what the behavior touches** — a pure predicate is a one-line vitest test; a real
|
|
19
|
+
async Server Component cannot be a plain node test at all. Pick the layer
|
|
20
|
+
**first**, then the primitive. Reaching one layer too high (e2e for a reverse
|
|
21
|
+
function) is slow; one too low (a node test for Flight) fails to compile or
|
|
22
|
+
silently asserts nothing.
|
|
23
|
+
|
|
24
|
+
This page is the router. Each primitive's full API (options, the seeded context
|
|
25
|
+
your code receives, the return shape), a minimal recipe, and its caveats live in a
|
|
26
|
+
dedicated sub-file linked from the decision tree below. Read the one for your case.
|
|
27
|
+
|
|
28
|
+
> **Setup is the first wall.** The vitest projects, the `rangoTestConfig` vs
|
|
29
|
+
> `rangoTestAliases` choice (Node >= 23), and the react-server `@rangojs/router ->
|
|
30
|
+
index.rsc.ts` alias are all in [`./setup.md`](./setup.md). Read it before writing
|
|
31
|
+
> `vitest.config.ts`. Platform bindings (`env.DB`/DO/R2) are your own double —
|
|
32
|
+
> [`./bindings.md`](./bindings.md).
|
|
33
|
+
|
|
34
|
+
For the long-form prose guide (setup walkthrough + migration), see
|
|
35
|
+
[`docs/testing.md`](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md)
|
|
36
|
+
(the `docs/` directory is not shipped in the published package, so this is an
|
|
37
|
+
absolute link).
|
|
38
|
+
|
|
39
|
+
## When to use
|
|
40
|
+
|
|
41
|
+
Use this skill when adding or changing tests for a Rango app: a loader,
|
|
42
|
+
middleware, a server action, a route map, a client component, a response route,
|
|
43
|
+
cache/SWR behavior, prerender, or a navigation/PE flow.
|
|
44
|
+
|
|
45
|
+
Two non-negotiable mandates (from the repo's `CLAUDE.md`, and they apply to
|
|
46
|
+
consumer apps too):
|
|
47
|
+
|
|
48
|
+
- **Every e2e covers BOTH dev and production.** A dev-only e2e is not acceptable.
|
|
49
|
+
Use `parityDescribe` — it generates the dev and production describes from one
|
|
50
|
+
body, so you cannot forget the prod half. See [`./e2e-parity.md`](./e2e-parity.md).
|
|
51
|
+
- **Progressive-enhancement parity** is a first-class assertion. A form-driven
|
|
52
|
+
flow must produce the same observable result with JS on and JS off. Use
|
|
53
|
+
`expectParity`.
|
|
54
|
+
|
|
55
|
+
## The read-first shape
|
|
56
|
+
|
|
57
|
+
Four import roots, each matched to the dependency/runtime that can load it — this
|
|
58
|
+
split is forced by hard walls, not preference:
|
|
59
|
+
|
|
60
|
+
- `@rangojs/router/testing` — unit + integration primitives. Run these under a
|
|
61
|
+
**Vite-driven Vitest** project with the rango Vite plugin active (the router
|
|
62
|
+
internals import the `@rangojs/router:version` virtual; without the plugin, the
|
|
63
|
+
preset stubs it). References neither React, RTL, Playwright, nor the RSC runtime.
|
|
64
|
+
- `@rangojs/router/testing/dom` — `renderRoute` (the RTL component stub). Kept
|
|
65
|
+
separate so the unit barrel stays free of React/RTL; it lazy-loads
|
|
66
|
+
`@testing-library/react` and needs a DOM env (happy-dom/jsdom).
|
|
67
|
+
- `@rangojs/router/testing/e2e` — the Playwright harness. Kept separate so it
|
|
68
|
+
loads in a plain (non-Vite) Playwright runner; the helpers take your
|
|
69
|
+
`test`/`expect`, so this entry never imports `@playwright/test` at runtime.
|
|
70
|
+
- `@rangojs/router/testing/flight` — real Flight rendering. Its serializer loads
|
|
71
|
+
only under the `react-server` node condition; pulling it elsewhere throws.
|
|
72
|
+
|
|
73
|
+
The single rule that drives everything:
|
|
74
|
+
|
|
75
|
+
> **If the behavior needs a real Flight render, it cannot be a plain vitest node
|
|
76
|
+
> test.** It is either `renderToFlightString`/`renderServerTree`/`renderHandler`
|
|
77
|
+
> (under the react-server vitest project) or an e2e test. There is no middle
|
|
78
|
+
> ground in node.
|
|
79
|
+
|
|
80
|
+
## Decision tree: behavior -> layer -> primitive
|
|
81
|
+
|
|
82
|
+
Each primitive links to its sub-file (API + recipe + caveats).
|
|
83
|
+
|
|
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
|
+
|
|
102
|
+
Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
103
|
+
`/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
|
|
104
|
+
`/typesafety`.
|
|
105
|
+
|
|
106
|
+
## Sub-files
|
|
107
|
+
|
|
108
|
+
- Cross-cutting: [`setup.md`](./setup.md), [`bindings.md`](./bindings.md)
|
|
109
|
+
- Unit (node): [`loader.md`](./loader.md), [`middleware.md`](./middleware.md),
|
|
110
|
+
[`server-actions.md`](./server-actions.md), [`handles.md`](./handles.md),
|
|
111
|
+
[`reverse-and-types.md`](./reverse-and-types.md)
|
|
112
|
+
- Unit (DOM): [`client-components.md`](./client-components.md)
|
|
113
|
+
- RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
|
|
114
|
+
[`render-handler.md`](./render-handler.md)
|
|
115
|
+
- Integration: [`response-routes.md`](./response-routes.md)
|
|
116
|
+
- E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
|
|
117
|
+
|
|
118
|
+
## Pre-push checklist (mirror CLAUDE.md)
|
|
119
|
+
|
|
120
|
+
Before pushing, run all of these and fix any failure:
|
|
121
|
+
|
|
122
|
+
1. `pnpm run typecheck` (or `pnpm exec tsc --noEmit`)
|
|
123
|
+
2. `pnpm run test:unit` (node + DOM vitest)
|
|
124
|
+
3. `pnpm run test:unit:rsc` (the react-server Flight project)
|
|
125
|
+
4. `pnpm run lint`
|
|
126
|
+
5. `pnpm run format`
|
|
127
|
+
|
|
128
|
+
And: **every e2e has a production counterpart.** `parityDescribe` makes this
|
|
129
|
+
automatic — if you wrote a plain `test.describe` for a behavior, convert it.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Testing platform bindings — your double is the seam
|
|
2
|
+
|
|
3
|
+
**Layer:** cross-cutting (unit/integration) · **Seam:** the `env` option every primitive takes
|
|
4
|
+
|
|
5
|
+
The node primitives test the router's seams; the moment your loader/middleware/action calls a **platform binding** (`env.DB`, a Durable Object stub, `env.R2`), you have crossed out of rango and into your app's I/O. The router machinery is real — what you seed is the binding double behind it, injected through `env`.
|
|
6
|
+
|
|
7
|
+
## Where it plugs in
|
|
8
|
+
|
|
9
|
+
rango ships **no doubles** for platform bindings — they are app- and schema-specific. You build the double and inject it through the `env` option that every primitive already accepts:
|
|
10
|
+
|
|
11
|
+
- `runLoader(body, { env })`
|
|
12
|
+
- `runMiddleware(fn, { request, env })`
|
|
13
|
+
- `runInRequestContext(fn, { request, env })`
|
|
14
|
+
- `renderHandler(handler, { request, env })`
|
|
15
|
+
- `dispatch(router, { request, env })`
|
|
16
|
+
- `renderToFlightString(el, { env })`
|
|
17
|
+
|
|
18
|
+
Inside the run, `getRequestContext().env` (and anything that reads it — `cache()`, your loaders, your middleware) sees the object you passed.
|
|
19
|
+
|
|
20
|
+
## Driver contract
|
|
21
|
+
|
|
22
|
+
The work here is matching the binding's **driver contract**, not its public API. A double that satisfies the public surface but not the driver's wire shape mounts green and proves nothing.
|
|
23
|
+
|
|
24
|
+
- **Per-method shapes.** `drizzle-orm/d1` serves SELECTs through `.raw()` and writes (INSERT/UPDATE/DELETE) through `.run()`. The two return different shapes and hit different code paths in the decoder. Model **both**.
|
|
25
|
+
- **`.raw()` (reads).** Must serve **positional row arrays in schema-column order**, with the driver-level encodings so the decoder round-trips `Date`/JSON. NOT `{ column: value }` objects.
|
|
26
|
+
- **`.run()` (writes).** Returns `{ success, meta }` — no rows — and bypasses the row responder entirely.
|
|
27
|
+
|
|
28
|
+
## Recipe
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { describe, it, expect } from "vitest";
|
|
32
|
+
import {
|
|
33
|
+
runLoader,
|
|
34
|
+
runMiddleware,
|
|
35
|
+
runInRequestContext,
|
|
36
|
+
} from "@rangojs/router/testing";
|
|
37
|
+
import { bundleLoaderBody } from "../app/loaders";
|
|
38
|
+
import { requireMembership } from "../app/middleware";
|
|
39
|
+
import { authorizeAction } from "../app/actions";
|
|
40
|
+
|
|
41
|
+
// A D1Database double satisfying drizzle-orm/d1's driver contract.
|
|
42
|
+
const fakeD1 = makeFakeD1({
|
|
43
|
+
// .raw() serves positional rows in schema-column order, driver-encoded.
|
|
44
|
+
raw: () => [[1, "acme", "2026-01-01T00:00:00.000Z"]],
|
|
45
|
+
// .run() returns { success, meta }, no rows.
|
|
46
|
+
run: () => ({ success: true, meta: { changes: 1 } }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("bindings seam", () => {
|
|
50
|
+
it("loader reads through env.DB", async () => {
|
|
51
|
+
const result = await runLoader(bundleLoaderBody, { env: { DB: fakeD1 } });
|
|
52
|
+
expect(result).toMatchObject({ slug: "acme" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("middleware reads through env.DB", async () => {
|
|
56
|
+
const { nextCalled, response } = await runMiddleware(requireMembership, {
|
|
57
|
+
request: "/t/acme/edit",
|
|
58
|
+
env: { DB: fakeD1 },
|
|
59
|
+
});
|
|
60
|
+
expect(nextCalled).toBe(1); // membership passed, chain continued
|
|
61
|
+
expect(response.status).toBe(200);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("action reads through env.DB", async () => {
|
|
65
|
+
const { result } = await runInRequestContext(
|
|
66
|
+
() => authorizeAction({ id: 1 }),
|
|
67
|
+
{
|
|
68
|
+
env: { DB: fakeD1 },
|
|
69
|
+
request: "/t/acme/edit",
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
expect(result).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Caveats
|
|
78
|
+
|
|
79
|
+
- rango ships **no doubles** for platform bindings (`env.DB`, Durable Objects, `env.R2`) by design — they are app- and schema-specific. Inject your own double through the `env` option every primitive takes.
|
|
80
|
+
- This is usually the **single biggest effort** in a consumer unit suite, and the work is matching the **driver contract**, not the binding's public API.
|
|
81
|
+
- `drizzle-orm/d1`: a `D1Database` double must serve **positional row arrays in schema-column order** for drizzle's `.raw()` path (with driver-level encodings so the decoder round-trips `Date`/JSON), NOT `{ column: value }` objects — an object-shaped double returns silently-wrong or empty rows.
|
|
82
|
+
- The contract is **per-method**: SELECTs go through `.raw()` (positional rows); writes (INSERT/UPDATE/DELETE) go through `.run()`, which returns `{ success, meta }` (no rows) and bypasses the row responder entirely. Model **both** paths — a read-only `.raw()` double silently no-ops every write.
|
|
83
|
+
- Keep the double at the **binding boundary**; never mock a rango primitive to dodge building it.
|
|
84
|
+
|
|
85
|
+
## See also
|
|
86
|
+
|
|
87
|
+
- (cross-cutting)
|
|
88
|
+
- Siblings: `./loader.md`, `./middleware.md`, `./server-actions.md`
|
|
89
|
+
- Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "What these primitives deliberately don't cover (the platform-bindings paragraph)"
|