@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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: response-routes
|
|
3
|
-
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses
|
|
3
|
+
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages, or asking how to return raw JSON instead of RSC from a route.
|
|
4
4
|
argument-hint: [json|text|html|xml|md|image|stream]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -68,16 +68,16 @@ export const urlpatterns = urls(({ path, layout, include }) => [
|
|
|
68
68
|
|
|
69
69
|
## Available Tags
|
|
70
70
|
|
|
71
|
-
| Tag | Usage | Handler returns | Auto-wrap
|
|
72
|
-
| -------- | --------------- | ------------------ |
|
|
73
|
-
| `json` | `path.json()` | plain object/array |
|
|
74
|
-
| `text` | `path.text()` | string | text/plain Response
|
|
75
|
-
| `html` | `path.html()` | string | text/html Response
|
|
76
|
-
| `xml` | `path.xml()` | string | application/xml Response
|
|
77
|
-
| `md` | `path.md()` | string | text/markdown Response
|
|
78
|
-
| `image` | `path.image()` | Response | pass-through
|
|
79
|
-
| `stream` | `path.stream()` | Response | pass-through
|
|
80
|
-
| `any` | `path.any()` | Response | pass-through
|
|
71
|
+
| Tag | Usage | Handler returns | Auto-wrap |
|
|
72
|
+
| -------- | --------------- | ------------------ | ----------------------------- |
|
|
73
|
+
| `json` | `path.json()` | plain object/array | bare JSON value (no envelope) |
|
|
74
|
+
| `text` | `path.text()` | string | text/plain Response |
|
|
75
|
+
| `html` | `path.html()` | string | text/html Response |
|
|
76
|
+
| `xml` | `path.xml()` | string | application/xml Response |
|
|
77
|
+
| `md` | `path.md()` | string | text/markdown Response |
|
|
78
|
+
| `image` | `path.image()` | Response | pass-through |
|
|
79
|
+
| `stream` | `path.stream()` | Response | pass-through |
|
|
80
|
+
| `any` | `path.any()` | Response | pass-through |
|
|
81
81
|
|
|
82
82
|
## ResponseHandlerContext
|
|
83
83
|
|
|
@@ -139,22 +139,31 @@ path.json(
|
|
|
139
139
|
);
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
## JSON
|
|
142
|
+
## JSON Wire Shape
|
|
143
143
|
|
|
144
|
-
`path.json()` handlers return plain data. The framework
|
|
145
|
-
|
|
144
|
+
`path.json()` handlers return plain data. The framework serializes the handler's
|
|
145
|
+
return value **verbatim** (no envelope) on success, and an RFC 9457 `problem+json`
|
|
146
|
+
body on error. Discriminate with `res.ok` / the HTTP status — there is no in-body
|
|
147
|
+
`data`/`error` union:
|
|
146
148
|
|
|
147
149
|
```typescript
|
|
148
|
-
// Success: HTTP 200
|
|
149
|
-
{ "
|
|
150
|
-
|
|
151
|
-
// Error: HTTP 404 (or whatever status RouterError specifies)
|
|
152
|
-
|
|
150
|
+
// Success: HTTP 200, content-type application/json
|
|
151
|
+
{ "status": "ok", "timestamp": 1700000000 }
|
|
152
|
+
|
|
153
|
+
// Error: HTTP 404 (or whatever status RouterError specifies),
|
|
154
|
+
// content-type application/problem+json
|
|
155
|
+
{
|
|
156
|
+
"title": "Not Found",
|
|
157
|
+
"status": 404,
|
|
158
|
+
"detail": "Product 999 not found",
|
|
159
|
+
"code": "NOT_FOUND"
|
|
160
|
+
// "stack": included in development only
|
|
161
|
+
}
|
|
153
162
|
```
|
|
154
163
|
|
|
155
164
|
### Error Handling with RouterError
|
|
156
165
|
|
|
157
|
-
Throw `RouterError` to return structured
|
|
166
|
+
Throw `RouterError` to return a structured `problem+json` body:
|
|
158
167
|
|
|
159
168
|
```typescript
|
|
160
169
|
import { RouterError } from "@rangojs/router";
|
|
@@ -199,25 +208,27 @@ path.json(
|
|
|
199
208
|
|
|
200
209
|
## Client-Side Type Safety
|
|
201
210
|
|
|
202
|
-
###
|
|
211
|
+
### Discriminating success vs. error with res.ok
|
|
212
|
+
|
|
213
|
+
Success bodies are the bare value; error bodies are RFC 9457 `ProblemDetails`.
|
|
214
|
+
Branch on `res.ok` (or the HTTP status) — not an in-body union:
|
|
203
215
|
|
|
204
216
|
```typescript
|
|
205
217
|
"use client";
|
|
206
|
-
import type {
|
|
207
|
-
import { isResponseError } from "@rangojs/router/client";
|
|
218
|
+
import type { ProblemDetails } from "@rangojs/router";
|
|
208
219
|
|
|
209
220
|
// Fetch a typed response
|
|
210
221
|
const res = await fetch("/api/products/1");
|
|
211
|
-
const result: ResponseEnvelope<Product> = await res.json();
|
|
212
222
|
|
|
213
|
-
if (
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
// Error body: application/problem+json
|
|
225
|
+
const problem: ProblemDetails = await res.json();
|
|
226
|
+
// problem.detail: string, problem.code: string, problem.status: number
|
|
227
|
+
console.error(problem.code, problem.detail);
|
|
217
228
|
} else {
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
console.log(
|
|
229
|
+
// Success body: the bare value (no envelope)
|
|
230
|
+
const product: Product = await res.json();
|
|
231
|
+
console.log(product.name);
|
|
221
232
|
}
|
|
222
233
|
```
|
|
223
234
|
|
|
@@ -230,20 +241,23 @@ import type { RouteResponse } from "@rangojs/router";
|
|
|
230
241
|
|
|
231
242
|
// From the apiPatterns module (before include)
|
|
232
243
|
type HealthData = RouteResponse<typeof apiPatterns, "health">;
|
|
233
|
-
// =
|
|
244
|
+
// = { status: string; timestamp: number }
|
|
234
245
|
|
|
235
246
|
type ProductsData = RouteResponse<typeof apiPatterns, "products">;
|
|
236
|
-
// =
|
|
247
|
+
// = { id: string; name: string; price: number }[]
|
|
237
248
|
```
|
|
238
249
|
|
|
250
|
+
`RouteResponse` is the bare success payload (the JSON wire shape) — the same value
|
|
251
|
+
a `fetch().then(r => r.json())` yields on a 2xx. Error bodies are `ProblemDetails`,
|
|
252
|
+
keyed off `res.ok` at runtime, not part of this type.
|
|
253
|
+
|
|
239
254
|
### Rango.PathResponse (global lookup by URL pattern or concrete path)
|
|
240
255
|
|
|
241
256
|
`Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
|
|
242
257
|
which carries response payload metadata. That surface is **not** auto-wired —
|
|
243
258
|
without the augmentation below, `Rango.PathResponse` falls back to the generated
|
|
244
259
|
path/search map, or to a permissive map when nothing is generated. Either way, it
|
|
245
|
-
has no response payload metadata, so response routes resolve to
|
|
246
|
-
`ResponseEnvelope<never>`:
|
|
260
|
+
has no response payload metadata, so response routes resolve to `never`:
|
|
247
261
|
|
|
248
262
|
```typescript
|
|
249
263
|
// router.tsx
|
|
@@ -261,11 +275,11 @@ With that in place, look up the response type by URL pattern (ambient, no import
|
|
|
261
275
|
```typescript
|
|
262
276
|
// After include("/api", apiPatterns) in main urls
|
|
263
277
|
type Health = Rango.PathResponse<"/api/health">;
|
|
264
|
-
// =
|
|
278
|
+
// = { status: string; timestamp: number }
|
|
265
279
|
|
|
266
|
-
// RSC routes return
|
|
280
|
+
// RSC routes (no JSON payload) return never
|
|
267
281
|
type Home = Rango.PathResponse<"/">;
|
|
268
|
-
// =
|
|
282
|
+
// = never
|
|
269
283
|
```
|
|
270
284
|
|
|
271
285
|
`Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
|
|
@@ -280,7 +294,7 @@ async function get<T extends Rango.Path>(
|
|
|
280
294
|
return fetch(href(path)).then((r) => r.json());
|
|
281
295
|
}
|
|
282
296
|
|
|
283
|
-
const product = await get("/api/products/42"); //
|
|
297
|
+
const product = await get("/api/products/42"); // Product (bare value)
|
|
284
298
|
```
|
|
285
299
|
|
|
286
300
|
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
@@ -288,7 +302,7 @@ route can match several patterns and union their responses.
|
|
|
288
302
|
|
|
289
303
|
`Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
|
|
290
304
|
return: `path.json()` serializes with `JSON.stringify`, so a handler returning
|
|
291
|
-
`{ createdAt: Date }` resolves to `
|
|
305
|
+
`{ createdAt: Date }` resolves to the bare `{ createdAt: string }`. This
|
|
292
306
|
runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
293
307
|
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
|
|
294
308
|
`RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
|
|
@@ -302,7 +316,7 @@ the response payload straight from the `urls()` patterns and needs no
|
|
|
302
316
|
### ParamsFor with Response Routes
|
|
303
317
|
|
|
304
318
|
```typescript
|
|
305
|
-
import type { ParamsFor } from "@rangojs/router
|
|
319
|
+
import type { ParamsFor } from "@rangojs/router";
|
|
306
320
|
|
|
307
321
|
// Works for both RSC and response routes
|
|
308
322
|
type ProductParams = ParamsFor<"api.productDetail">;
|
|
@@ -404,21 +418,35 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
404
418
|
]);
|
|
405
419
|
```
|
|
406
420
|
|
|
421
|
+
A heavy module like this is a good code-split candidate. Pass an async provider
|
|
422
|
+
and the module — its handlers, response serializers, and any nested
|
|
423
|
+
`include()`s — loads on the first request under the prefix instead of at startup:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// blog/urls.tsx: `export default blogPatterns`
|
|
427
|
+
include("/blog", () => import("./blog/urls"), { name: "blog" }),
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Named routes and response types still resolve through the split: `TRoutes` and
|
|
431
|
+
the `_responses` phantom are inferred from the resolved `urls()` value, so
|
|
432
|
+
`Rango.PathResponse<"/blog/api/stats">` and `ctx.reverse` are unchanged. See
|
|
433
|
+
`/composability`.
|
|
434
|
+
|
|
407
435
|
### Type safety after mounting
|
|
408
436
|
|
|
409
437
|
```typescript
|
|
410
438
|
import type { RouteResponse } from "@rangojs/router";
|
|
411
|
-
import type { ParamsFor } from "@rangojs/router
|
|
439
|
+
import type { ParamsFor } from "@rangojs/router";
|
|
412
440
|
|
|
413
441
|
// Scoped (before mount) -- use the module directly, no global wiring needed
|
|
414
442
|
type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
|
|
415
|
-
// =
|
|
443
|
+
// = { views: number; visitors: number }
|
|
416
444
|
|
|
417
445
|
// After mounting -- names get prefixed.
|
|
418
446
|
// Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
|
|
419
|
-
// otherwise it resolves to
|
|
447
|
+
// otherwise it resolves to never.
|
|
420
448
|
type BlogStats = Rango.PathResponse<"/blog/api/stats">;
|
|
421
|
-
// =
|
|
449
|
+
// = { views: number; visitors: number }
|
|
422
450
|
|
|
423
451
|
// Params work through nested includes
|
|
424
452
|
type LikesParams = ParamsFor<"blog.api.likes">;
|
|
@@ -462,7 +490,11 @@ best-effort basis.
|
|
|
462
490
|
1. `path.json()` tags the route at the trie level with a MIME type
|
|
463
491
|
2. `coreRequestHandler()` checks the tag before the RSC pipeline
|
|
464
492
|
3. Tagged routes short-circuit: handler runs, Response is returned directly
|
|
465
|
-
4. JSON routes
|
|
493
|
+
4. JSON routes serialize the return value verbatim (bare) on success; a thrown error becomes an RFC 9457 `problem+json` body (`application/problem+json`)
|
|
466
494
|
5. Client-side navigation to response routes gets `X-RSC-Reload` header, triggering hard navigation
|
|
467
495
|
6. Response types flow through `_responses` phantom type on `UrlPatterns`, propagated by `include()`
|
|
468
496
|
7. When multiple routes share a URL pattern, the trie merges them for content negotiation (see `/mime-routes`)
|
|
497
|
+
|
|
498
|
+
## Consuming response routes
|
|
499
|
+
|
|
500
|
+
To call your own response-route JSON APIs from first-party TypeScript with a typed client (typed params, typed payloads inferred from the handler, no `.data`, typed `ProblemDetails` errors), see `/api-client` — a copy-paste recipe over `RouteResponse` + `ExtractParams` + a client-safe path builder. External/third-party consumers use the plain wire directly: bare JSON on success, `application/problem+json` on error.
|
package/skills/route/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: route
|
|
3
|
-
description: Define routes with path() in @rangojs/router
|
|
3
|
+
description: Define routes with path() in @rangojs/router. Use when creating a new page or route, or asking how to define a URL path and its handler.
|
|
4
4
|
argument-hint: [pattern]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -53,6 +53,49 @@ For the common pattern of an optional locale prefix
|
|
|
53
53
|
locale detection, fallback chains, URL generation with absent locale —
|
|
54
54
|
see `/i18n`.
|
|
55
55
|
|
|
56
|
+
### Named catch-all params (`:name+` / `:name*`)
|
|
57
|
+
|
|
58
|
+
A catch-all consumes the **rest of the path** and exposes it as a single
|
|
59
|
+
decoded string at `ctx.params.<name>`, with the internal `/` separators kept.
|
|
60
|
+
It must be the **last** segment of the pattern.
|
|
61
|
+
|
|
62
|
+
- `:name+` — **one-or-more** segments (Next `[...name]`, React-Router splat).
|
|
63
|
+
`/docs/:slug+` matches `/docs/a` and `/docs/a/b/c`, but **not** the bare
|
|
64
|
+
`/docs`.
|
|
65
|
+
- `:name*` — **zero-or-more** segments (Next `[[...name]]`). `/docs/:slug*`
|
|
66
|
+
additionally matches the bare `/docs`, binding `ctx.params.slug` to `""`.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
urls(({ path }) => [
|
|
70
|
+
// /shop/electronics/phones -> ctx.params.path === "electronics/phones"
|
|
71
|
+
path("/shop/:path+", ShopCatchAll, { name: "shopCatchAll" }),
|
|
72
|
+
|
|
73
|
+
// /docs -> ctx.params.slug === ""
|
|
74
|
+
// /docs/intro -> ctx.params.slug === "intro"
|
|
75
|
+
// /docs/a/b -> ctx.params.slug === "a/b"
|
|
76
|
+
path("/docs/:slug*", (ctx) => {
|
|
77
|
+
const parts = ctx.params.slug === "" ? [] : ctx.params.slug.split("/");
|
|
78
|
+
return <Docs segments={parts} />;
|
|
79
|
+
}, { name: "docs" }),
|
|
80
|
+
]);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`ctx.params.<name>` is always a `string` for a catch-all (never `undefined`) —
|
|
84
|
+
`:name*` binds `""` for the empty case, so read it directly. `reverse()` /
|
|
85
|
+
`ctx.reverse()` rebuild the URL with separators preserved:
|
|
86
|
+
`reverse("docs", { slug: "a/b" })` -> `/docs/a/b`.
|
|
87
|
+
|
|
88
|
+
The value is the URL-decoded remainder. `split("/")` recovers the segments in the
|
|
89
|
+
common case, but note that a segment containing an encoded slash (`%2F`) decodes
|
|
90
|
+
to a literal `/` and is therefore indistinguishable from a separator — the same
|
|
91
|
+
trade-off the bare `*` splat has. If you need to distinguish those, match on the
|
|
92
|
+
raw pathname instead.
|
|
93
|
+
|
|
94
|
+
The bare unnamed wildcard `path("/files/*", …)` still works and is read at
|
|
95
|
+
`ctx.params["*"]`; prefer a named catch-all when you want a typed param key.
|
|
96
|
+
Combining a modifier with `?`, a literal suffix, or a constraint
|
|
97
|
+
(`:slug*?`, `:slug*.html`, `:slug(a|b)+`) is rejected at build time.
|
|
98
|
+
|
|
56
99
|
## Route Handler Patterns
|
|
57
100
|
|
|
58
101
|
### Component Function
|
|
@@ -154,6 +197,13 @@ first. Use `ctx.set(key, value)` to share data with children, who read it
|
|
|
154
197
|
via `ctx.get(key)`. Caching wraps all segments together, so either all run
|
|
155
198
|
or none do.
|
|
156
199
|
|
|
200
|
+
This pattern is also safe under partial action revalidation: on an action,
|
|
201
|
+
the route entry re-runs as a unit by default — route segment, loaders, and
|
|
202
|
+
`belongsToRoute` children (orphan layouts, entry parallels) all seed
|
|
203
|
+
revalidate-true, with handler-first ordering preserved. Handler-set data
|
|
204
|
+
stays consistent with no configuration. See `/rango` → "Passing data down
|
|
205
|
+
the tree" for the safest-first ladder.
|
|
206
|
+
|
|
157
207
|
### Typed context variables with createVar
|
|
158
208
|
|
|
159
209
|
Use `createVar<T>()` to create a typed token for `ctx.set()`/`ctx.get()`.
|
|
@@ -240,16 +290,27 @@ Cacheable vars (the default) can be read freely inside cache scopes.
|
|
|
240
290
|
> decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
|
|
241
291
|
> `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
242
292
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
With no `revalidate()` configured, an entry needs no contract: on an action
|
|
294
|
+
the route handler and its children re-run together by default, so handler
|
|
295
|
+
data stays consistent on its own. Contracts matter in two cases:
|
|
296
|
+
|
|
297
|
+
1. **You narrow the entry's revalidation** with a predicate that can return a
|
|
298
|
+
hard `false` (e.g. bare `ctx.isAction(X)`). A hard `false` on one side of a
|
|
299
|
+
producer/consumer pair desyncs it — the child re-runs by default and reads
|
|
300
|
+
`undefined`, or vice versa. Put the same named contract on the route and
|
|
301
|
+
its dependent children so they narrow together.
|
|
302
|
+
2. **The producer is an outer entry** (a standalone `layout()` above this
|
|
303
|
+
route). Outer entries skip action revalidation by default, so the shared
|
|
304
|
+
contract is mandatory — see `/layout` → "Revalidation Contracts".
|
|
246
305
|
|
|
247
306
|
```typescript
|
|
248
307
|
// revalidation-contracts.ts
|
|
308
|
+
import * as CheckoutActions from "./actions/checkout";
|
|
309
|
+
|
|
249
310
|
// Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
|
|
250
311
|
// so when the same segment composes multiple contracts the later ones never run.
|
|
251
|
-
export const revalidateCheckoutData = (
|
|
252
|
-
|
|
312
|
+
export const revalidateCheckoutData = (ctx) =>
|
|
313
|
+
ctx.isAction(CheckoutActions) || undefined;
|
|
253
314
|
|
|
254
315
|
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
255
316
|
revalidate(revalidateCheckoutData), // producer (route handler) reruns
|
|
@@ -294,6 +355,12 @@ path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
|
|
|
294
355
|
path("/moved", () => redirect("/new-location", 301), { name: "moved" });
|
|
295
356
|
```
|
|
296
357
|
|
|
358
|
+
> **Redirecting from a route with `loading()`:** an `async` handler that returns
|
|
359
|
+
> a `Response`/`redirect()` on a route that also declares `loading()` is streamed,
|
|
360
|
+
> so the redirect is rendered into the RSC stream instead of becoming an HTTP
|
|
361
|
+
> redirect. Issue the redirect from `middleware`, a loader, or a **synchronous**
|
|
362
|
+
> handler return instead. (Dev logs a warning if this is hit.)
|
|
363
|
+
|
|
297
364
|
### Redirect with location state
|
|
298
365
|
|
|
299
366
|
Carry typed state through redirects (e.g. flash messages):
|
|
@@ -338,6 +405,10 @@ state persists on back/forward. See `/hooks` for details.
|
|
|
338
405
|
Attach location state to any server response (not just redirects):
|
|
339
406
|
|
|
340
407
|
```typescript
|
|
408
|
+
import { createLocationState } from "@rangojs/router";
|
|
409
|
+
|
|
410
|
+
const ServerInfo = createLocationState<{ data: string }>();
|
|
411
|
+
|
|
341
412
|
path("/dashboard", (ctx) => {
|
|
342
413
|
ctx.setLocationState(ServerInfo({ data: "welcome" }));
|
|
343
414
|
return <Dashboard />;
|
|
@@ -408,6 +479,14 @@ urls(({ path, layout }) => [
|
|
|
408
479
|
])
|
|
409
480
|
```
|
|
410
481
|
|
|
482
|
+
For composing whole route MODULES, reach for `include()` — and prefer the
|
|
483
|
+
code-split form `include("/shop", () => import("./shop-patterns"))` for any
|
|
484
|
+
group that is a natural unit: it keeps the group off the cold-start path, and
|
|
485
|
+
measured first-hit cost scales with routes-per-chunk, so many small groups
|
|
486
|
+
beat one giant one. Sizing rules and the numbers behind them:
|
|
487
|
+
[skills/composability](../composability/SKILL.md) → "Sizing async include
|
|
488
|
+
groups (measured)".
|
|
489
|
+
|
|
411
490
|
## View Transitions
|
|
412
491
|
|
|
413
492
|
A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: router-setup
|
|
3
|
-
description: Create and configure the RSC router with createRouter
|
|
3
|
+
description: Create and configure the RSC router with createRouter. Use when bootstrapping a new Rango app, or configuring top-level router options like base path, cache store, or environment.
|
|
4
4
|
argument-hint: [option]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -60,8 +60,10 @@ urls(
|
|
|
60
60
|
cache, // Configure caching
|
|
61
61
|
middleware, // Add middleware
|
|
62
62
|
revalidate, // Control revalidation
|
|
63
|
-
intercept, // Intercept routes for modals
|
|
64
|
-
|
|
63
|
+
intercept, // Intercept routes for modals (conditional via intercept(..., { when }))
|
|
64
|
+
errorBoundary, // Add an error boundary
|
|
65
|
+
notFoundBoundary, // Add a not-found boundary
|
|
66
|
+
transition, // Configure view transitions
|
|
65
67
|
}) => [
|
|
66
68
|
// Route definitions here
|
|
67
69
|
],
|
|
@@ -396,6 +398,18 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
396
398
|
]);
|
|
397
399
|
```
|
|
398
400
|
|
|
401
|
+
`include()` also accepts an async provider to code-split that group into its own
|
|
402
|
+
chunk, imported on the first request reaching the prefix instead of at startup:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// urls/shop.tsx: `export default shopPatterns`
|
|
406
|
+
include("/shop", () => import("./urls/shop"), { name: "shop" }),
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Build-time discovery still `await`s the provider, so route types, `href()`, and
|
|
410
|
+
prerender see every route in the split group. Reach for it when a group is a
|
|
411
|
+
large, independently-loadable unit — see `/composability`.
|
|
412
|
+
|
|
399
413
|
## Environment Types
|
|
400
414
|
|
|
401
415
|
```typescript
|
|
@@ -469,14 +483,35 @@ const router = createRouter({
|
|
|
469
483
|
```
|
|
470
484
|
|
|
471
485
|
```typescript
|
|
472
|
-
// OpenTelemetry for production
|
|
473
|
-
|
|
486
|
+
// OpenTelemetry for production: phase spans via the tracing slot,
|
|
487
|
+
// discrete-fact spans via the telemetry sink.
|
|
488
|
+
import {
|
|
489
|
+
createRouter,
|
|
490
|
+
createOTelTracing,
|
|
491
|
+
createOTelSink,
|
|
492
|
+
} from "@rangojs/router";
|
|
474
493
|
import { trace } from "@opentelemetry/api";
|
|
475
494
|
|
|
495
|
+
const tracer = trace.getTracer("my-app");
|
|
496
|
+
|
|
497
|
+
const router = createRouter({
|
|
498
|
+
document: Document,
|
|
499
|
+
urls: urlpatterns,
|
|
500
|
+
tracing: createOTelTracing(tracer),
|
|
501
|
+
telemetry: createOTelSink(tracer),
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// On Cloudflare Workers, swap the tracing factory for native custom spans
|
|
507
|
+
// (no @opentelemetry/api dependency); the telemetry slot is unchanged.
|
|
508
|
+
// On Vercel (Node runtime) use createVercelTracing() from @rangojs/router/vercel.
|
|
509
|
+
import { createCloudflareTracing } from "@rangojs/router/cloudflare";
|
|
510
|
+
|
|
476
511
|
const router = createRouter({
|
|
477
512
|
document: Document,
|
|
478
513
|
urls: urlpatterns,
|
|
479
|
-
|
|
514
|
+
tracing: createCloudflareTracing(), // { spans: { ssr: false } } to toggle phases
|
|
480
515
|
});
|
|
481
516
|
```
|
|
482
517
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scripts
|
|
3
|
+
description: Inject third-party scripts (GTM, analytics, widgets) into the document head/body via the Script handle. Use when adding Google Tag Manager, an analytics snippet, or a third-party widget script to the page.
|
|
4
|
+
argument-hint: "[vendor]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Scripts
|
|
8
|
+
|
|
9
|
+
Inject `<script>` tags into the document the idiomatic Rango way: push a config
|
|
10
|
+
from a **server** route/layout handler with `ctx.use(Script)(config)`, and render
|
|
11
|
+
them with the built-in **`<Scripts />`** component (the `Meta` / `<MetaTags>`
|
|
12
|
+
pair, but for scripts). The request CSP **nonce is applied automatically to
|
|
13
|
+
document-rendered scripts** — you never read or pass it. (The one exception is an
|
|
14
|
+
async script first encountered on a soft navigation; see the nonce caveat under
|
|
15
|
+
"Execution contract".)
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
`<Scripts />` is a client component; place it in your Document (which is
|
|
20
|
+
`"use client"`). The default Document already includes both sites; a custom one
|
|
21
|
+
adds them next to `<MetaTags />`:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// document.tsx ("use client")
|
|
25
|
+
import { MetaTags, Scripts } from "@rangojs/router/client";
|
|
26
|
+
|
|
27
|
+
export function Document({ children }) {
|
|
28
|
+
return (
|
|
29
|
+
<html lang="en" suppressHydrationWarning>
|
|
30
|
+
<head>
|
|
31
|
+
<MetaTags />
|
|
32
|
+
<Scripts /> {/* renders position: "head" scripts (the default) */}
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<Scripts position="body" /> {/* renders position: "body" scripts */}
|
|
36
|
+
{children}
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Push from a handler
|
|
44
|
+
|
|
45
|
+
`ScriptConfig` is a discriminated union — exactly one of three shapes, so invalid
|
|
46
|
+
combinations are compile errors:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Script } from "@rangojs/router";
|
|
50
|
+
|
|
51
|
+
// 1. External ASYNC — a React resource. Loads once when first encountered,
|
|
52
|
+
// including after a soft navigation, deduped by src. The fire-and-forget case.
|
|
53
|
+
ctx.use(Script)({ id: "stripe", src: "https://js.stripe.com/v3", async: true });
|
|
54
|
+
|
|
55
|
+
// 2. External ORDERED — in-place, optional `defer`. Document-load (see below).
|
|
56
|
+
ctx.use(Script)({
|
|
57
|
+
id: "plausible",
|
|
58
|
+
src: "https://plausible.io/js/script.js",
|
|
59
|
+
defer: true,
|
|
60
|
+
attributes: { "data-domain": "example.com" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 3. INLINE — `id` REQUIRED, raw JS body (escaped against </script> by <Scripts>).
|
|
64
|
+
// For GTM/GA4/Segment let the body self-inject its loader (see below).
|
|
65
|
+
ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Shape | Required | Optional | Forbidden |
|
|
69
|
+
| ---------------- | -------------------- | ----------------------------------------------- | ----------------------- |
|
|
70
|
+
| Inline | `id`, `children` | `position`, `type`, `attributes` | `src`, `async`, `defer` |
|
|
71
|
+
| External async | `src`, `async: true` | `id`, `position`, `type`, `attributes` | `children`, `defer` |
|
|
72
|
+
| External ordered | `src` | `defer`, `id`, `position`, `type`, `attributes` | `children`, `async` |
|
|
73
|
+
|
|
74
|
+
- `id` — dedup key (last-push-wins), and rendered as the script's DOM `id` (for
|
|
75
|
+
vendors that target `<script id="…">`). Required for inline (React never dedups
|
|
76
|
+
inline scripts); for ordered external it falls back to `src`. Async externals
|
|
77
|
+
dedup by `src` (matching React), so there `id` is the DOM id only.
|
|
78
|
+
- `position` — `"head"` (default) or `"body"`. An async script is hoisted to
|
|
79
|
+
`<head>` by React regardless.
|
|
80
|
+
- `type` — free string: `"module"`, `"application/ld+json"`, `"text/partytown"`, …
|
|
81
|
+
- `attributes` — React-cased (`crossOrigin`, not `crossorigin`) and React-typed
|
|
82
|
+
(`data-*`, `integrity`, `referrerPolicy`, …). Excluded: the fields the handle
|
|
83
|
+
manages (`id`/`src`/`async`/`defer`/`type`/`children`/`nonce`) and all `on*`
|
|
84
|
+
handlers (`onLoad`/`onError`/… — a config is serialized to the client, so a
|
|
85
|
+
function can't survive; use a `"use client"` component for callbacks).
|
|
86
|
+
|
|
87
|
+
## Execution contract (read this)
|
|
88
|
+
|
|
89
|
+
React makes a `<script>` it mounts on the client INERT (it creates the element via
|
|
90
|
+
innerHTML, which the HTML spec never executes). So:
|
|
91
|
+
|
|
92
|
+
| Script | Runs on hard load | Runs on soft (`<Link>`) navigation |
|
|
93
|
+
| -------------------------------- | ------------------------------ | ----------------------------------------------------- |
|
|
94
|
+
| Inline (`children`) | Yes (it's in the initial HTML) | **No** — it is document-load only |
|
|
95
|
+
| External ordered (`defer`/plain) | Yes | **No** — document-load only |
|
|
96
|
+
| External `async` | Yes | **Yes** — React loads the resource on first encounter |
|
|
97
|
+
|
|
98
|
+
`<Scripts>` enforces this honestly: after hydration it **freezes** the inline +
|
|
99
|
+
ordered set to what was in the initial HTML, so a navigation never inserts an
|
|
100
|
+
inert (silently dead) `<script>`. Async configs stay reactive. Reusing an `id`
|
|
101
|
+
shapes the INITIAL document output (last-push-wins) — it does not re-run a script
|
|
102
|
+
during navigation.
|
|
103
|
+
|
|
104
|
+
**Nonce caveat for soft-nav async.** The "nonce is applied automatically" claim
|
|
105
|
+
holds for DOCUMENT-RENDERED scripts (they carry the nonce in the SSR HTML). An
|
|
106
|
+
async script first encountered on a soft navigation is injected by React on the
|
|
107
|
+
client, where `useNonce()` is `undefined` by design (the router does not serialize
|
|
108
|
+
the nonce to the client — that would weaken CSP), so it has no nonce attribute. It
|
|
109
|
+
still loads under `'strict-dynamic'` (React's nonced runtime injects it, so the
|
|
110
|
+
trust propagates) — which is the recommended policy — or if your `script-src`
|
|
111
|
+
allows the host. A nonce-only policy without `'strict-dynamic'` would block it.
|
|
112
|
+
|
|
113
|
+
**Per-navigation behavior belongs in a client component or hook**, not in a
|
|
114
|
+
re-pushed inline script. The GTM demo does exactly this: a root-layout `Script`
|
|
115
|
+
bootstrap fires the first page_view on document load, and a `"use client"`
|
|
116
|
+
`<GtmPageViews>` component fires a page_view on every subsequent soft navigation.
|
|
117
|
+
|
|
118
|
+
## The inline-self-inject rule (GTM/GA4/Segment)
|
|
119
|
+
|
|
120
|
+
If an inline bootstrap must run **before** an external loader, do NOT push the
|
|
121
|
+
loader as a separate `{ src, async }` config: React 19 hoists a declarative
|
|
122
|
+
`<script async src>` to the **top** of `<head>`, above your inline bootstrap, so
|
|
123
|
+
the loader could run before the bootstrap. Instead let the bootstrap inject its
|
|
124
|
+
own loader (Google's snippet does exactly this):
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
function gtmBootstrap(id: string): string {
|
|
128
|
+
return [
|
|
129
|
+
"window.dataLayer=window.dataLayer||[];",
|
|
130
|
+
'window.dataLayer.push({"gtm.start":new Date().getTime(),event:"gtm.js"});',
|
|
131
|
+
`(function(d,s,i){var j=d.createElement(s);j.async=true;j.src="https://www.googletagmanager.com/gtm.js?id="+encodeURIComponent(i);var f=d.getElementsByTagName(s)[0];f.parentNode.insertBefore(j,f);})(document,"script",${JSON.stringify(id)});`,
|
|
132
|
+
].join("");
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Under a `'strict-dynamic'` CSP the nonced inline script vouches for the loader it
|
|
137
|
+
creates, so the injected loader needs no nonce of its own.
|
|
138
|
+
|
|
139
|
+
### Per-route tagging on the first render
|
|
140
|
+
|
|
141
|
+
A route can **override** a layout's bootstrap by reusing the `id`, baking
|
|
142
|
+
per-route data into the FIRST (hard-load) page_view server-side — the Script
|
|
143
|
+
handle is collected after handlers run (parent → child, last-wins):
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// root layout: generic bootstrap
|
|
147
|
+
ctx.use(Script)({ id: "gtm", children: gtmBootstrap("GTM-XXXX") });
|
|
148
|
+
// a route: same id, with content_group baked in
|
|
149
|
+
ctx.use(Script)({
|
|
150
|
+
id: "gtm",
|
|
151
|
+
children: gtmBootstrapWith({ content_group: "blog" }),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## CSP
|
|
156
|
+
|
|
157
|
+
The nonce is automatic for document-rendered scripts. Include `'strict-dynamic'`
|
|
158
|
+
in `script-src` (recommended): besides letting a nonced loader vouch for the
|
|
159
|
+
scripts it injects, it also covers the one nonce-less case — an async script first
|
|
160
|
+
loaded on a soft navigation is injected client-side without a nonce (see the
|
|
161
|
+
caveat above), and `'strict-dynamic'` trusts it via React's nonced runtime.
|
|
162
|
+
Otherwise allow the vendor hosts. For GTM/GA4 (Google's wildcards): `script-src
|
|
163
|
+
'self' 'nonce-…' 'strict-dynamic' https://*.googletagmanager.com`, plus `img-src`
|
|
164
|
+
/ `connect-src` for `*.google-analytics.com` / `*.analytics.google.com`, and
|
|
165
|
+
`frame-src https://*.googletagmanager.com` for the GTM `<noscript>` iframe. See
|
|
166
|
+
[Google's CSP guide](https://developers.google.com/tag-platform/security/guides/csp).
|
|
167
|
+
|
|
168
|
+
## Not covered (do it yourself)
|
|
169
|
+
|
|
170
|
+
- **`onLoad` / `onReady` / `onError`** — callbacks can't cross the server handle
|
|
171
|
+
boundary. Render your own `"use client"` component with a load listener keyed
|
|
172
|
+
off the script id.
|
|
173
|
+
- **`<noscript>` fallbacks** (e.g. the GTM body iframe) — not a `<script>`;
|
|
174
|
+
render it directly in your Document `<body>`.
|
|
175
|
+
- **Partytown / web-worker offloading** — push the worker config with
|
|
176
|
+
`type: "text/partytown"` and wire Partytown's own nonce config manually.
|
|
177
|
+
|
|
178
|
+
A full GTM + GA4-style integration (page_view on first render + soft nav, nonce,
|
|
179
|
+
ecommerce events) lives in `tests/vite-rsc-demo`.
|