@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
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 +8 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- 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-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2170 -377
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- 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 +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- 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 +35 -15
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +357 -0
- package/src/testing/render-route.tsx +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- 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 +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- 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/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* runLoader — unit-test a
|
|
2
|
+
* runLoader — unit-test a loader function in isolation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* handle
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Pass the RAW async loader body `(ctx) => ...`, or a registered `createLoader()`
|
|
5
|
+
* handle (its fn is recovered from the fetchable registry by `$$id`). The raw
|
|
6
|
+
* body needs no build step; the handle works because `createLoader` assigns a
|
|
7
|
+
* runtime-fallback `$$id` and registers its fn even without the Vite plugin (when
|
|
8
|
+
* imported through the server build — the consumer's `@rangojs/router` under the
|
|
9
|
+
* `rangoTestConfig()` preset). Either way the function is invoked directly with a
|
|
10
|
+
* constructed LoaderContext.
|
|
8
11
|
*
|
|
9
12
|
* The LoaderContext mirrors the canonical shape the router builds at runtime
|
|
10
13
|
* (see createUseFunction in server/request-context.ts). The loader runs inside
|
|
@@ -35,16 +38,20 @@ import {
|
|
|
35
38
|
type RequestContext,
|
|
36
39
|
} from "../server/request-context.js";
|
|
37
40
|
import { createReverseFunction } from "../router/handler-context.js";
|
|
41
|
+
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
38
42
|
import type { LoaderContext, LoaderDefinition } from "../types.js";
|
|
39
43
|
import type { ContextVar } from "../context-var.js";
|
|
40
44
|
import { isHandle, type Handle } from "../handle.js";
|
|
45
|
+
import { collectHandle } from "./collect-handle.js";
|
|
41
46
|
import type { ThemeConfig } from "../theme/types.js";
|
|
42
47
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
43
48
|
import type { CacheProfile } from "../cache/profile-registry.js";
|
|
44
49
|
import {
|
|
45
50
|
createTestRequestContext,
|
|
51
|
+
buildRunSnapshot,
|
|
46
52
|
type CreateTestContextOptions,
|
|
47
53
|
type VarsInit,
|
|
54
|
+
type StateCookieSeed,
|
|
48
55
|
} from "./internal/context.js";
|
|
49
56
|
|
|
50
57
|
/**
|
|
@@ -99,7 +106,9 @@ export interface RunLoaderOptions<TEnv = any> {
|
|
|
99
106
|
basename?: string;
|
|
100
107
|
/**
|
|
101
108
|
* Theme config in the same shape `createRouter({ theme })` takes (e.g. `true`
|
|
102
|
-
* or `{ themes: [...] }`).
|
|
109
|
+
* or `{ themes: [...] }`). Seeds the request's theme config so nested handler
|
|
110
|
+
* or cache contexts created from this loader observe it. Loaders themselves do
|
|
111
|
+
* not expose `ctx.theme`/`ctx.setTheme` (those are handler/middleware-only).
|
|
103
112
|
*/
|
|
104
113
|
theme?: ThemeConfig | true;
|
|
105
114
|
/** Environment bindings surfaced as `ctx.env`. */
|
|
@@ -118,7 +127,15 @@ export interface RunLoaderOptions<TEnv = any> {
|
|
|
118
127
|
body?: unknown;
|
|
119
128
|
/** Form data surfaced as `ctx.formData`. */
|
|
120
129
|
formData?: FormData;
|
|
121
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Seed the data `ctx.use(OtherLoader)` returns, by loader REFERENCE — the same
|
|
132
|
+
* tuple form `renderHandler` / `renderRoute` use (`[[OtherLoader, data]]`).
|
|
133
|
+
* Matched by reference, so a real `createLoader()` handle resolves regardless
|
|
134
|
+
* of its build-injected `$$id`. For dynamic resolution (compute per dependency)
|
|
135
|
+
* use `use` instead; `loaders` is checked first.
|
|
136
|
+
*/
|
|
137
|
+
loaders?: ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>;
|
|
138
|
+
/** Resolver for `ctx.use(OtherLoader)` composition (dynamic; `loaders` wins if both match). */
|
|
122
139
|
use?: UseResolver;
|
|
123
140
|
/**
|
|
124
141
|
* Cache store backing `use cache` functions the loader invokes. Without it,
|
|
@@ -129,6 +146,13 @@ export interface RunLoaderOptions<TEnv = any> {
|
|
|
129
146
|
cacheStore?: SegmentCacheStore;
|
|
130
147
|
/** Cache profiles (the `createRouter({ cacheProfiles })` shape). */
|
|
131
148
|
cacheProfiles?: Record<string, CacheProfile>;
|
|
149
|
+
/**
|
|
150
|
+
* Customize the rango state cookie a loader that calls
|
|
151
|
+
* `invalidateClientCache()` rotates (the name is always seeded — default
|
|
152
|
+
* `rango-state_router_0` — so it rotates like production). Assert via the
|
|
153
|
+
* `Set-Cookie` on the request context's response.
|
|
154
|
+
*/
|
|
155
|
+
stateCookie?: StateCookieSeed;
|
|
132
156
|
/**
|
|
133
157
|
* Mock the `ctx.rendered()` render barrier so a loader that does
|
|
134
158
|
* `await ctx.rendered()` (to read handle data pushed during render) can be
|
|
@@ -155,10 +179,6 @@ export interface RunLoaderOptions<TEnv = any> {
|
|
|
155
179
|
handles?: ReadonlyArray<readonly [Handle<any, any>, unknown]>;
|
|
156
180
|
}
|
|
157
181
|
|
|
158
|
-
/**
|
|
159
|
-
* Merge `search` into a request's URL, returning a value `toRequest` can build.
|
|
160
|
-
* Keeps the original method/headers/body when a Request was passed.
|
|
161
|
-
*/
|
|
162
182
|
function withSearch(
|
|
163
183
|
request: Request | string | undefined,
|
|
164
184
|
search: Record<string, string> | undefined,
|
|
@@ -179,26 +199,37 @@ function withSearch(
|
|
|
179
199
|
return url.toString();
|
|
180
200
|
}
|
|
181
201
|
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
/** A raw loader body, or a registered `createLoader()` handle (its fn is recovered). */
|
|
203
|
+
export type RunnableLoader<T> =
|
|
204
|
+
| ((ctx: TestLoaderContext) => Promise<T> | T)
|
|
205
|
+
| LoaderDefinition<T, any>;
|
|
206
|
+
|
|
207
|
+
function resolveLoaderFn<T>(
|
|
208
|
+
loader: RunnableLoader<T>,
|
|
209
|
+
): (ctx: TestLoaderContext) => Promise<T> | T {
|
|
210
|
+
if (typeof loader === "function") {
|
|
211
|
+
return loader as (ctx: TestLoaderContext) => Promise<T> | T;
|
|
212
|
+
}
|
|
213
|
+
const def = loader as LoaderDefinition<T, any>;
|
|
214
|
+
const fn = def.fn ?? getFetchableLoader(def.$$id)?.fn;
|
|
215
|
+
if (!fn) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`runLoader() received a createLoader() handle whose function could not be ` +
|
|
218
|
+
`recovered (id "${def.$$id || "<empty>"}"). The loader was likely imported ` +
|
|
219
|
+
`through the CLIENT build, which drops the body. Either import it through ` +
|
|
220
|
+
`@rangojs/router with the rangoTestConfig() preset (resolves to the server ` +
|
|
221
|
+
`build that registers the fn), or pass the raw loader body directly: ` +
|
|
222
|
+
`runLoader((ctx) => ...).`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return fn as (ctx: TestLoaderContext) => Promise<T> | T;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildLoaderCtxOpts(
|
|
229
|
+
opts: RunLoaderOptions,
|
|
230
|
+
): CreateTestContextOptions<any> {
|
|
231
|
+
return {
|
|
198
232
|
env: opts.env,
|
|
199
|
-
// Bake opts.search into the request URL itself so ctx.request.url, ctx.url,
|
|
200
|
-
// and ctx.searchParams all agree (production carries the query string on the
|
|
201
|
-
// real request — a loader reading ctx.request.url must see it too).
|
|
202
233
|
request: withSearch(opts.request, opts.search),
|
|
203
234
|
requestInit: opts.method ? { method: opts.method } : undefined,
|
|
204
235
|
vars: opts.vars,
|
|
@@ -209,27 +240,23 @@ export async function runLoader<T>(
|
|
|
209
240
|
theme: opts.theme,
|
|
210
241
|
cacheStore: opts.cacheStore,
|
|
211
242
|
cacheProfiles: opts.cacheProfiles,
|
|
243
|
+
stateCookie: opts.stateCookie,
|
|
212
244
|
};
|
|
245
|
+
}
|
|
213
246
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// handle resolves regardless of its build-injected $$id).
|
|
247
|
+
function runWithLoaderContext<R>(
|
|
248
|
+
reqCtx: RequestContext<any>,
|
|
249
|
+
opts: RunLoaderOptions,
|
|
250
|
+
fn: (ctx: TestLoaderContext) => R,
|
|
251
|
+
): R {
|
|
220
252
|
const handleSeeds = new Map<unknown, unknown>(opts.handles ?? []);
|
|
221
|
-
|
|
222
|
-
// Tracks whether the mocked render barrier has settled. ctx.use(handle)
|
|
223
|
-
// reads are gated on this, matching production (loader-resolution.ts).
|
|
253
|
+
const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
|
|
224
254
|
let renderedResolved = false;
|
|
225
255
|
|
|
226
256
|
return runWithRequestContext(reqCtx, () => {
|
|
227
257
|
const reverse = opts.routeMap
|
|
228
258
|
? createReverseFunction(opts.routeMap, opts.routeName, opts.params ?? {})
|
|
229
259
|
: ((() => {
|
|
230
|
-
// Documented contract: reverse requires routeMap. Do NOT fall back to
|
|
231
|
-
// reqCtx.reverse (the global route map) — that leaks whichever routes
|
|
232
|
-
// another test registered and contradicts the documented behavior.
|
|
233
260
|
throw new Error(
|
|
234
261
|
"ctx.reverse() requires the `routeMap` option in runLoader(). " +
|
|
235
262
|
"Pass { routeMap: { name: pattern, ... } } to enable reverse().",
|
|
@@ -240,7 +267,7 @@ export async function runLoader<T>(
|
|
|
240
267
|
params: opts.params ?? {},
|
|
241
268
|
routeParams: (opts.params ?? {}) as Record<string, string>,
|
|
242
269
|
request: reqCtx.request,
|
|
243
|
-
searchParams:
|
|
270
|
+
searchParams: reqCtx.searchParams,
|
|
244
271
|
search: opts.searchData ?? {},
|
|
245
272
|
pathname: reqCtx.pathname,
|
|
246
273
|
url: reqCtx.url,
|
|
@@ -250,10 +277,6 @@ export async function runLoader<T>(
|
|
|
250
277
|
executionContext: reqCtx.executionContext,
|
|
251
278
|
get: reqCtx.get as TestLoaderContext["get"],
|
|
252
279
|
use: ((dep: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
253
|
-
// Match production (loader-resolution.ts): reading a handle in a loader
|
|
254
|
-
// requires the render barrier to have settled. Gate BEFORE returning a
|
|
255
|
-
// seed, so a loader that forgets `await ctx.rendered()` fails in the
|
|
256
|
-
// test exactly as it would at runtime.
|
|
257
280
|
if (isHandle(dep) && !renderedResolved) {
|
|
258
281
|
throw new Error(
|
|
259
282
|
`ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
|
|
@@ -261,9 +284,15 @@ export async function runLoader<T>(
|
|
|
261
284
|
`the render tree has settled.`,
|
|
262
285
|
);
|
|
263
286
|
}
|
|
264
|
-
// Handle reads (ctx.use(SomeHandle)) resolve from the seeded map first.
|
|
265
287
|
if (handleSeeds.has(dep)) return handleSeeds.get(dep);
|
|
266
|
-
if (
|
|
288
|
+
if (isHandle(dep)) return collectHandle(dep, []);
|
|
289
|
+
// Production ctx.use(Loader) ALWAYS returns a Promise (the cached loader
|
|
290
|
+
// promise). The seeded path must match, so a consumer composing on the
|
|
291
|
+
// result (ctx.use(Dep).then(...), Promise.race, etc.) works the same as
|
|
292
|
+
// production and the real-fn delegate path below.
|
|
293
|
+
if (loaderSeeds.has(dep)) return Promise.resolve(loaderSeeds.get(dep));
|
|
294
|
+
if (opts.use)
|
|
295
|
+
return Promise.resolve(opts.use(dep as LoaderDefinition<any, any>));
|
|
267
296
|
return reqCtx.use(dep as LoaderDefinition<any, any>);
|
|
268
297
|
}) as LoaderContext<any, any>["use"],
|
|
269
298
|
method: opts.method ?? "GET",
|
|
@@ -276,7 +305,6 @@ export async function runLoader<T>(
|
|
|
276
305
|
if (typeof opts.rendered === "function") {
|
|
277
306
|
await opts.rendered();
|
|
278
307
|
}
|
|
279
|
-
// Barrier has settled: subsequent ctx.use(handle) reads resolve.
|
|
280
308
|
renderedResolved = true;
|
|
281
309
|
}
|
|
282
310
|
: () => {
|
|
@@ -291,6 +319,67 @@ export async function runLoader<T>(
|
|
|
291
319
|
},
|
|
292
320
|
};
|
|
293
321
|
|
|
294
|
-
return
|
|
322
|
+
return fn(loaderCtx);
|
|
295
323
|
});
|
|
296
324
|
}
|
|
325
|
+
|
|
326
|
+
export async function runLoader<T>(
|
|
327
|
+
loader: RunnableLoader<T>,
|
|
328
|
+
opts: RunLoaderOptions = {},
|
|
329
|
+
): Promise<T> {
|
|
330
|
+
const loaderFn = resolveLoaderFn(loader);
|
|
331
|
+
const { ctx } = createTestRequestContext(buildLoaderCtxOpts(opts));
|
|
332
|
+
return runWithLoaderContext(ctx as RequestContext<any>, opts, (loaderCtx) =>
|
|
333
|
+
Promise.resolve(loaderFn(loaderCtx)),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface RunLoaderResult<T> {
|
|
338
|
+
/**
|
|
339
|
+
* The loader's resolved data (the value bare `runLoader` returns), or
|
|
340
|
+
* `undefined` if it threw (see {@link thrown}). Named `result` for parity with
|
|
341
|
+
* `runInRequestContext`'s envelope.
|
|
342
|
+
*/
|
|
343
|
+
result: T | undefined;
|
|
344
|
+
/**
|
|
345
|
+
* What the loader threw (commonly a `Response` from `throw redirect(...)` on a
|
|
346
|
+
* success path) — captured, NOT re-thrown; assert on it. `undefined` if the
|
|
347
|
+
* loader returned normally.
|
|
348
|
+
*/
|
|
349
|
+
thrown: unknown;
|
|
350
|
+
/**
|
|
351
|
+
* The merged `Response` (status + headers + Set-Cookie). On a thrown redirect,
|
|
352
|
+
* that redirect's `Location` merged with the accumulated cookies/headers — so a
|
|
353
|
+
* loader that sets a session cookie then `throw redirect("/")` exposes BOTH.
|
|
354
|
+
*/
|
|
355
|
+
response: Response;
|
|
356
|
+
/** Effective cookie view: request cookies + the loader's mutations, last-write-wins. */
|
|
357
|
+
cookies: Record<string, string>;
|
|
358
|
+
/** Response headers as `{ name: value }`, EXCLUDING set-cookie (use `cookies`). Lowercased. */
|
|
359
|
+
headers: Record<string, string>;
|
|
360
|
+
/** Location state the loader set (`ctx.setLocationState()` / `redirect({ state })`). */
|
|
361
|
+
locationState: Record<string, unknown>;
|
|
362
|
+
/** The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). */
|
|
363
|
+
stateCookieName: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function runLoaderResult<T>(
|
|
367
|
+
loader: RunnableLoader<T>,
|
|
368
|
+
opts: RunLoaderOptions = {},
|
|
369
|
+
): Promise<RunLoaderResult<T>> {
|
|
370
|
+
const loaderFn = resolveLoaderFn(loader);
|
|
371
|
+
const { ctx, stateCookieName } = createTestRequestContext(
|
|
372
|
+
buildLoaderCtxOpts(opts),
|
|
373
|
+
);
|
|
374
|
+
const reqCtx = ctx as RequestContext<any>;
|
|
375
|
+
let result: T | undefined;
|
|
376
|
+
let thrown: unknown;
|
|
377
|
+
try {
|
|
378
|
+
result = await runWithLoaderContext(reqCtx, opts, (loaderCtx) =>
|
|
379
|
+
Promise.resolve(loaderFn(loaderCtx)),
|
|
380
|
+
);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
thrown = error;
|
|
383
|
+
}
|
|
384
|
+
return { result, ...buildRunSnapshot(reqCtx, thrown, stateCookieName) };
|
|
385
|
+
}
|
|
@@ -24,9 +24,11 @@ import { createReverseFunction } from "../router/handler-context.js";
|
|
|
24
24
|
import type { MiddlewareFn } from "../router/middleware-types.js";
|
|
25
25
|
import {
|
|
26
26
|
createTestRequestContext,
|
|
27
|
+
headersToObject,
|
|
27
28
|
snapshotRunEffects,
|
|
28
29
|
type CreateTestContextOptions,
|
|
29
30
|
type VarsInit,
|
|
31
|
+
type StateCookieSeed,
|
|
30
32
|
} from "./internal/context.js";
|
|
31
33
|
import type { ThemeConfig } from "../theme/types.js";
|
|
32
34
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
@@ -36,6 +38,13 @@ import type { CacheProfile } from "../cache/profile-registry.js";
|
|
|
36
38
|
* Options for runMiddleware.
|
|
37
39
|
*/
|
|
38
40
|
export interface RunMiddlewareOptions<TEnv = any> {
|
|
41
|
+
/**
|
|
42
|
+
* The request the chain runs under: a `Request`, or a URL string (absolute or
|
|
43
|
+
* path). Optional for parity with `runLoader`/`runInRequestContext` — when
|
|
44
|
+
* omitted it defaults to `http://localhost/`. Pass it for path-, header-, or
|
|
45
|
+
* cookie-driven middleware.
|
|
46
|
+
*/
|
|
47
|
+
request?: Request | string;
|
|
39
48
|
/** Environment bindings surfaced as `ctx.env`. */
|
|
40
49
|
env?: TEnv;
|
|
41
50
|
/** Route params surfaced as `ctx.params`. */
|
|
@@ -44,7 +53,12 @@ export interface RunMiddlewareOptions<TEnv = any> {
|
|
|
44
53
|
vars?: VarsInit;
|
|
45
54
|
/** Route name -> pattern map enabling `ctx.reverse()`. */
|
|
46
55
|
routeMap?: Record<string, string>;
|
|
47
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Matched route name surfaced as `ctx.routeName`. Does NOT scope `.name`
|
|
58
|
+
* reverse: the chain receives a map-only `reverse` (built from `routeMap`
|
|
59
|
+
* alone), matching production app/response middleware — see the reverse
|
|
60
|
+
* construction below.
|
|
61
|
+
*/
|
|
48
62
|
routeName?: string;
|
|
49
63
|
/** Router basename surfaced on the context (drives redirect() prefixing). */
|
|
50
64
|
basename?: string;
|
|
@@ -64,6 +78,13 @@ export interface RunMiddlewareOptions<TEnv = any> {
|
|
|
64
78
|
cacheStore?: SegmentCacheStore;
|
|
65
79
|
/** Cache profiles (the `createRouter({ cacheProfiles })` shape). */
|
|
66
80
|
cacheProfiles?: Record<string, CacheProfile>;
|
|
81
|
+
/**
|
|
82
|
+
* Customize the rango state cookie a middleware that calls
|
|
83
|
+
* `invalidateClientCache()` rotates (the name is always seeded — default
|
|
84
|
+
* `rango-state_router_0` — so it rotates like production). Assert via the
|
|
85
|
+
* `Set-Cookie` on `result.response` / `result.cookies`.
|
|
86
|
+
*/
|
|
87
|
+
stateCookie?: StateCookieSeed;
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
/**
|
|
@@ -88,34 +109,39 @@ export interface RunMiddlewareResult<TEnv = any> {
|
|
|
88
109
|
* the `@internal` `ctx.cookies()`. Set-Cookie headers are also on `response`.
|
|
89
110
|
*/
|
|
90
111
|
cookies: Record<string, string>;
|
|
112
|
+
/**
|
|
113
|
+
* The final response's headers as a plain `{ name: value }` object (the same
|
|
114
|
+
* view as `response.headers`), EXCLUDING `set-cookie` (use `cookies`). The
|
|
115
|
+
* public way to assert a header a middleware set (e.g. a security header)
|
|
116
|
+
* without reading `ctx.res.headers`. Header names are lowercased.
|
|
117
|
+
*/
|
|
118
|
+
headers: Record<string, string>;
|
|
119
|
+
/**
|
|
120
|
+
* Location state the chain set via `ctx.setLocationState()` / `redirect({ state })`,
|
|
121
|
+
* resolved to the flat `{ key: value }` shape the client reads off
|
|
122
|
+
* `history.state` (empty object when none) — parity with `runInRequestContext`
|
|
123
|
+
* and `renderHandler`.
|
|
124
|
+
*/
|
|
125
|
+
locationState: Record<string, unknown>;
|
|
126
|
+
/**
|
|
127
|
+
* The resolved rango state cookie name seeded for the run (default
|
|
128
|
+
* `rango-state_router_0`, or composed from `opts.stateCookie`). Assert a
|
|
129
|
+
* middleware's `invalidateClientCache()` rotation against it without
|
|
130
|
+
* recomputing — parity with `runInRequestContext` / `runLoaderResult` /
|
|
131
|
+
* `renderHandler`.
|
|
132
|
+
*/
|
|
133
|
+
stateCookieName: string;
|
|
91
134
|
}
|
|
92
135
|
|
|
93
|
-
/**
|
|
94
|
-
* Run a middleware chain and return the response plus observable context.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* ```ts
|
|
98
|
-
* const { response, ctx, nextCalled } = await runMiddleware(
|
|
99
|
-
* async (ctx, next) => {
|
|
100
|
-
* if (!ctx.get("user")) return new Response(null, { status: 401 });
|
|
101
|
-
* return next();
|
|
102
|
-
* },
|
|
103
|
-
* "/dashboard",
|
|
104
|
-
* { vars: [["user", { id: 1 }]] },
|
|
105
|
-
* );
|
|
106
|
-
* // nextCalled === 1, response.status === 200
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
136
|
export async function runMiddleware<TEnv = any>(
|
|
110
137
|
mw: MiddlewareFn<TEnv> | MiddlewareFn<TEnv>[],
|
|
111
|
-
|
|
112
|
-
opts: RunMiddlewareOptions<TEnv> = {},
|
|
138
|
+
opts: RunMiddlewareOptions<TEnv>,
|
|
113
139
|
): Promise<RunMiddlewareResult<TEnv>> {
|
|
114
140
|
const mwArray = Array.isArray(mw) ? mw : [mw];
|
|
115
141
|
|
|
116
142
|
const ctxOpts: CreateTestContextOptions<TEnv> = {
|
|
117
143
|
env: opts.env,
|
|
118
|
-
request,
|
|
144
|
+
request: opts.request,
|
|
119
145
|
vars: opts.vars,
|
|
120
146
|
routeMap: opts.routeMap,
|
|
121
147
|
routeName: opts.routeName,
|
|
@@ -124,12 +150,14 @@ export async function runMiddleware<TEnv = any>(
|
|
|
124
150
|
theme: opts.theme,
|
|
125
151
|
cacheStore: opts.cacheStore,
|
|
126
152
|
cacheProfiles: opts.cacheProfiles,
|
|
153
|
+
stateCookie: opts.stateCookie,
|
|
127
154
|
};
|
|
128
155
|
|
|
129
156
|
const {
|
|
130
157
|
ctx,
|
|
131
158
|
request: builtRequest,
|
|
132
159
|
variables,
|
|
160
|
+
stateCookieName,
|
|
133
161
|
} = createTestRequestContext<TEnv>(ctxOpts);
|
|
134
162
|
|
|
135
163
|
let nextCalled = 0;
|
|
@@ -138,11 +166,6 @@ export async function runMiddleware<TEnv = any>(
|
|
|
138
166
|
return opts.next?.() ?? new Response(null, { status: 200 });
|
|
139
167
|
};
|
|
140
168
|
|
|
141
|
-
// Match production: app/response middleware receive ctx.reverse built from the
|
|
142
|
-
// route map ALONE (no matched route name or current params), so reversing a
|
|
143
|
-
// parameterized route without explicit params does NOT auto-fill from the
|
|
144
|
-
// current request. Passing routeName/params here would recreate the
|
|
145
|
-
// false-confidence class fixed in dispatch.
|
|
146
169
|
const reverse = opts.routeMap
|
|
147
170
|
? (createReverseFunction(opts.routeMap) as (
|
|
148
171
|
name: string,
|
|
@@ -151,12 +174,6 @@ export async function runMiddleware<TEnv = any>(
|
|
|
151
174
|
) => string)
|
|
152
175
|
: undefined;
|
|
153
176
|
|
|
154
|
-
// Keep the RETURNED ctx.reverse consistent with the map-only reverse the
|
|
155
|
-
// chain receives. createTestRequestContext installs an auto-fill reverse
|
|
156
|
-
// (correct for the loader phase) when routeName/params are passed, but
|
|
157
|
-
// production app/response middleware see a map-only reverse. Without this,
|
|
158
|
-
// a middleware reading getRequestContext().reverse — or a consumer asserting
|
|
159
|
-
// on result.ctx.reverse — would observe auto-fill that production never does.
|
|
160
177
|
if (reverse) {
|
|
161
178
|
(ctx as RequestContext<TEnv>).reverse =
|
|
162
179
|
reverse as RequestContext<TEnv>["reverse"];
|
|
@@ -174,6 +191,15 @@ export async function runMiddleware<TEnv = any>(
|
|
|
174
191
|
),
|
|
175
192
|
);
|
|
176
193
|
|
|
177
|
-
const { cookies } = snapshotRunEffects(ctx);
|
|
178
|
-
|
|
194
|
+
const { cookies, locationState } = snapshotRunEffects(ctx);
|
|
195
|
+
const headers = headersToObject(response.headers);
|
|
196
|
+
return {
|
|
197
|
+
response,
|
|
198
|
+
ctx,
|
|
199
|
+
nextCalled,
|
|
200
|
+
cookies,
|
|
201
|
+
headers,
|
|
202
|
+
locationState,
|
|
203
|
+
stateCookieName,
|
|
204
|
+
};
|
|
179
205
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runTransitionWhen — unit-test a transition({ when }) predicate in isolation.
|
|
3
|
+
*
|
|
4
|
+
* Runs the SAME two server functions the router uses — applyViewTransitionDefault
|
|
5
|
+
* (strips the `when` function from the serialized config and records the
|
|
6
|
+
* predicate on the request context) and gateTransitions (assembles the
|
|
7
|
+
* TransitionWhenContext and evaluates the predicate post-handler). So the
|
|
8
|
+
* predicate sees exactly the navigation/action metadata it would at runtime
|
|
9
|
+
* (currentUrl/currentParams/fromRouteName, nextUrl/nextParams/toRouteName,
|
|
10
|
+
* actionId/actionUrl/actionResult/formData/method, get/env), and `kept` reflects
|
|
11
|
+
* whether the transition would apply this request. The result also exposes the
|
|
12
|
+
* assembled `whenContext` so tests can assert the exact fields without reaching
|
|
13
|
+
* into private request-context state.
|
|
14
|
+
*
|
|
15
|
+
* This is the public way to exercise a transition gate: the full
|
|
16
|
+
* match -> render pipeline that wires these together only runs under real RSC
|
|
17
|
+
* rendering (which the Flight primitives do not drive), so without this primitive
|
|
18
|
+
* a consumer could not test their predicate through @rangojs/router/testing.
|
|
19
|
+
*
|
|
20
|
+
* Synchronous: a transition predicate returns a boolean and the gate has no I/O.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
runWithRequestContext,
|
|
25
|
+
type RequestContext,
|
|
26
|
+
} from "../server/request-context.js";
|
|
27
|
+
import { applyViewTransitionDefault } from "../router/segment-resolution/view-transition-default.js";
|
|
28
|
+
import { gateTransitions } from "../rsc/transition-gate.js";
|
|
29
|
+
import { createTestRequestContext, type VarsInit } from "./internal/context.js";
|
|
30
|
+
import type {
|
|
31
|
+
ResolvedSegment,
|
|
32
|
+
TransitionConfig,
|
|
33
|
+
TransitionWhenContext,
|
|
34
|
+
} from "../types/segments.js";
|
|
35
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
36
|
+
|
|
37
|
+
const toURL = (v: string | URL, base: URL): URL =>
|
|
38
|
+
typeof v === "string" ? new URL(v, base.origin) : v;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options for runTransitionWhen. All navigation/action fields are optional and
|
|
42
|
+
* default to "absent", matching what the gate sees for an initial full load with
|
|
43
|
+
* no action: omit `currentUrl`/`currentParams`/`fromRouteName` to model the
|
|
44
|
+
* navigation source being unavailable, and omit the `action*` fields to model a
|
|
45
|
+
* plain (non-action) navigation.
|
|
46
|
+
*/
|
|
47
|
+
export interface RunTransitionWhenOptions<TEnv = any> {
|
|
48
|
+
/** The navigation TARGET request (drives `nextUrl`): a Request or URL/path string. Defaults to `http://localhost/`. */
|
|
49
|
+
request?: Request | string;
|
|
50
|
+
/** Route params for the target (`nextParams`). */
|
|
51
|
+
params?: Record<string, string>;
|
|
52
|
+
/** Target route name (`toRouteName`). */
|
|
53
|
+
toRouteName?: string;
|
|
54
|
+
/** Environment bindings surfaced as `env` (and `ctx.env`). */
|
|
55
|
+
env?: TEnv;
|
|
56
|
+
/** Variables a handler/middleware would have set this request, readable via the predicate's `get()`. */
|
|
57
|
+
vars?: VarsInit;
|
|
58
|
+
/** Navigation SOURCE url (`currentUrl`): a URL or path string. */
|
|
59
|
+
currentUrl?: string | URL;
|
|
60
|
+
/** Source route params (`currentParams`). */
|
|
61
|
+
currentParams?: Record<string, string>;
|
|
62
|
+
/** Source route name (`fromRouteName`). */
|
|
63
|
+
fromRouteName?: string;
|
|
64
|
+
/** Id of the action that triggered a revalidation (`actionId`). */
|
|
65
|
+
actionId?: string;
|
|
66
|
+
/** Url the action was submitted from (`actionUrl`). */
|
|
67
|
+
actionUrl?: string | URL;
|
|
68
|
+
/** The action's return value (`actionResult`). */
|
|
69
|
+
actionResult?: unknown;
|
|
70
|
+
/** FormData from a form action (`formData`). */
|
|
71
|
+
formData?: FormData;
|
|
72
|
+
/** Receives an error thrown by the predicate (the gate reports to `router.onError`, phase `"rendering"`). */
|
|
73
|
+
onError?: OnErrorCallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Result of runTransitionWhen.
|
|
78
|
+
*/
|
|
79
|
+
export interface RunTransitionWhenResult<TEnv = any> {
|
|
80
|
+
/** True if the transition would apply this request (predicate returned non-false, or there is no `when`). */
|
|
81
|
+
kept: boolean;
|
|
82
|
+
/** Convenience inverse of `kept`. */
|
|
83
|
+
dropped: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The production-assembled predicate context. Undefined when the config has
|
|
86
|
+
* no `when` predicate.
|
|
87
|
+
*/
|
|
88
|
+
whenContext?: TransitionWhenContext<Record<string, string>, TEnv>;
|
|
89
|
+
/** The underlying RequestContext, for additional assertions (`ctx.get(...)`, etc.). */
|
|
90
|
+
ctx: RequestContext<TEnv>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function runTransitionWhen<TEnv = any>(
|
|
94
|
+
config: TransitionConfig,
|
|
95
|
+
opts: RunTransitionWhenOptions<TEnv> = {},
|
|
96
|
+
): RunTransitionWhenResult<TEnv> {
|
|
97
|
+
const { ctx } = createTestRequestContext<TEnv>({
|
|
98
|
+
env: opts.env,
|
|
99
|
+
request: opts.request,
|
|
100
|
+
vars: opts.vars,
|
|
101
|
+
params: opts.params,
|
|
102
|
+
});
|
|
103
|
+
const reqCtx = ctx as unknown as RequestContext<TEnv>;
|
|
104
|
+
|
|
105
|
+
// Target route name (the public field the gate reads for `toRouteName`).
|
|
106
|
+
if (opts.toRouteName !== undefined)
|
|
107
|
+
reqCtx.routeName = opts.toRouteName as RequestContext<TEnv>["routeName"];
|
|
108
|
+
// Source (match-time) data the gate reads for currentUrl/currentParams/fromRouteName.
|
|
109
|
+
if (opts.currentUrl !== undefined)
|
|
110
|
+
reqCtx._gateCurrentUrl = toURL(opts.currentUrl, reqCtx.url);
|
|
111
|
+
if (opts.currentParams !== undefined)
|
|
112
|
+
reqCtx._gateCurrentParams = opts.currentParams;
|
|
113
|
+
if (opts.fromRouteName !== undefined)
|
|
114
|
+
reqCtx._prevRouteKey = opts.fromRouteName;
|
|
115
|
+
// Action data the gate reads at the action-bearing call sites.
|
|
116
|
+
if (opts.actionId !== undefined) reqCtx._gateActionId = opts.actionId;
|
|
117
|
+
if (opts.actionUrl !== undefined)
|
|
118
|
+
reqCtx._gateActionUrl = toURL(opts.actionUrl, reqCtx.url);
|
|
119
|
+
if (opts.actionResult !== undefined)
|
|
120
|
+
reqCtx._gateActionResult = opts.actionResult;
|
|
121
|
+
if (opts.formData !== undefined) reqCtx._gateFormData = opts.formData;
|
|
122
|
+
|
|
123
|
+
let whenContext:
|
|
124
|
+
| TransitionWhenContext<Record<string, string>, TEnv>
|
|
125
|
+
| undefined;
|
|
126
|
+
const when = config.when;
|
|
127
|
+
const configForGate: TransitionConfig = when
|
|
128
|
+
? {
|
|
129
|
+
...config,
|
|
130
|
+
when: (c) => {
|
|
131
|
+
whenContext = c as TransitionWhenContext<
|
|
132
|
+
Record<string, string>,
|
|
133
|
+
TEnv
|
|
134
|
+
>;
|
|
135
|
+
return when(c);
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: config;
|
|
139
|
+
|
|
140
|
+
return runWithRequestContext(reqCtx, () => {
|
|
141
|
+
// The real resolution-time collection + post-handler gate, so the predicate
|
|
142
|
+
// sees the production-assembled TransitionWhenContext.
|
|
143
|
+
const serialized = applyViewTransitionDefault(
|
|
144
|
+
configForGate,
|
|
145
|
+
undefined,
|
|
146
|
+
"tx-when-seg",
|
|
147
|
+
);
|
|
148
|
+
const segment = {
|
|
149
|
+
id: "tx-when-seg",
|
|
150
|
+
namespace: "r",
|
|
151
|
+
type: "route",
|
|
152
|
+
index: 0,
|
|
153
|
+
component: null,
|
|
154
|
+
transition: serialized,
|
|
155
|
+
} as ResolvedSegment;
|
|
156
|
+
gateTransitions(
|
|
157
|
+
[segment],
|
|
158
|
+
reqCtx as Parameters<typeof gateTransitions>[1],
|
|
159
|
+
opts.onError,
|
|
160
|
+
);
|
|
161
|
+
const kept = segment.transition !== undefined;
|
|
162
|
+
return { kept, dropped: !kept, whenContext, ctx: reqCtx };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Stub for the `cloudflare:email` runtime virtual, shipped for Cloudflare
|
|
2
|
-
// consumers (enable via `rangoTestAliases({
|
|
2
|
+
// consumers (enable via `rangoTestAliases({ preset: "cloudflare" })`).
|
|
3
3
|
export class EmailMessage {
|
|
4
4
|
constructor(
|
|
5
5
|
public from: string,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Stub for the `cloudflare:workers` runtime virtual, shipped for Cloudflare
|
|
2
|
-
// consumers (enable via `rangoTestAliases({
|
|
2
|
+
// consumers (enable via `rangoTestAliases({ preset: "cloudflare" })`). A CF app's
|
|
3
3
|
// route tree commonly imports `cloudflare:workers` (e.g. `import { env } from
|
|
4
4
|
// "cloudflare:workers"`), which does not resolve in a bare Vitest process.
|
|
5
5
|
export const env: Record<string, unknown> = {};
|