@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- 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 +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- 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 +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -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,28 +241,78 @@ 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
|
|
|
239
|
-
|
|
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.
|
|
240
253
|
|
|
241
|
-
|
|
254
|
+
### Rango.PathResponse (global lookup by URL pattern or concrete path)
|
|
255
|
+
|
|
256
|
+
`Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
|
|
257
|
+
which carries response payload metadata. That surface is **not** auto-wired —
|
|
258
|
+
without the augmentation below, `Rango.PathResponse` falls back to the generated
|
|
259
|
+
path/search map, or to a permissive map when nothing is generated. Either way, it
|
|
260
|
+
has no response payload metadata, so response routes resolve to `never`:
|
|
242
261
|
|
|
243
262
|
```typescript
|
|
244
|
-
|
|
263
|
+
// router.tsx
|
|
264
|
+
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
245
265
|
|
|
266
|
+
declare global {
|
|
267
|
+
namespace Rango {
|
|
268
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
With that in place, look up the response type by URL pattern (ambient, no import):
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
246
276
|
// After include("/api", apiPatterns) in main urls
|
|
247
|
-
type Health = PathResponse<"/api/health">;
|
|
248
|
-
// =
|
|
277
|
+
type Health = Rango.PathResponse<"/api/health">;
|
|
278
|
+
// = { status: string; timestamp: number }
|
|
279
|
+
|
|
280
|
+
// RSC routes (no JSON payload) return never
|
|
281
|
+
type Home = Rango.PathResponse<"/">;
|
|
282
|
+
// = never
|
|
283
|
+
```
|
|
249
284
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
285
|
+
`Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
|
|
286
|
+
wrapper whose response is inferred from the path you pass:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { href } from "@rangojs/router/client";
|
|
290
|
+
|
|
291
|
+
async function get<T extends Rango.Path>(
|
|
292
|
+
path: T,
|
|
293
|
+
): Promise<Rango.PathResponse<T>> {
|
|
294
|
+
return fetch(href(path)).then((r) => r.json());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const product = await get("/api/products/42"); // Product (bare value)
|
|
253
298
|
```
|
|
254
299
|
|
|
300
|
+
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
301
|
+
route can match several patterns and union their responses.
|
|
302
|
+
|
|
303
|
+
`Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
|
|
304
|
+
return: `path.json()` serializes with `JSON.stringify`, so a handler returning
|
|
305
|
+
`{ createdAt: Date }` resolves to the bare `{ createdAt: string }`. This
|
|
306
|
+
runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
307
|
+
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
|
|
308
|
+
`RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
|
|
309
|
+
both response lookups report the identical wire shape.
|
|
310
|
+
|
|
311
|
+
For local/scoped response typing without global augmentation, prefer
|
|
312
|
+
`RouteResponse<typeof patterns, "routeName">` (see the section above) — it reads
|
|
313
|
+
the response payload straight from the `urls()` patterns and needs no
|
|
314
|
+
`RegisteredRoutes` wiring.
|
|
315
|
+
|
|
255
316
|
### ParamsFor with Response Routes
|
|
256
317
|
|
|
257
318
|
```typescript
|
|
@@ -361,15 +422,17 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
361
422
|
|
|
362
423
|
```typescript
|
|
363
424
|
import type { RouteResponse } from "@rangojs/router";
|
|
364
|
-
import type {
|
|
425
|
+
import type { ParamsFor } from "@rangojs/router/client";
|
|
365
426
|
|
|
366
|
-
// Scoped (before mount) -- use the module directly
|
|
427
|
+
// Scoped (before mount) -- use the module directly, no global wiring needed
|
|
367
428
|
type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
|
|
368
|
-
// =
|
|
429
|
+
// = { views: number; visitors: number }
|
|
369
430
|
|
|
370
|
-
// After mounting -- names get prefixed
|
|
371
|
-
|
|
372
|
-
//
|
|
431
|
+
// After mounting -- names get prefixed.
|
|
432
|
+
// Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
|
|
433
|
+
// otherwise it resolves to never.
|
|
434
|
+
type BlogStats = Rango.PathResponse<"/blog/api/stats">;
|
|
435
|
+
// = { views: number; visitors: number }
|
|
373
436
|
|
|
374
437
|
// Params work through nested includes
|
|
375
438
|
type LikesParams = ParamsFor<"blog.api.likes">;
|
|
@@ -413,7 +476,11 @@ best-effort basis.
|
|
|
413
476
|
1. `path.json()` tags the route at the trie level with a MIME type
|
|
414
477
|
2. `coreRequestHandler()` checks the tag before the RSC pipeline
|
|
415
478
|
3. Tagged routes short-circuit: handler runs, Response is returned directly
|
|
416
|
-
4. JSON routes
|
|
479
|
+
4. JSON routes serialize the return value verbatim (bare) on success; a thrown error becomes an RFC 9457 `problem+json` body (`application/problem+json`)
|
|
417
480
|
5. Client-side navigation to response routes gets `X-RSC-Reload` header, triggering hard navigation
|
|
418
481
|
6. Response types flow through `_responses` phantom type on `UrlPatterns`, propagated by `include()`
|
|
419
482
|
7. When multiple routes share a URL pattern, the trie merges them for content negotiation (see `/mime-routes`)
|
|
483
|
+
|
|
484
|
+
## Consuming response routes
|
|
485
|
+
|
|
486
|
+
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
|
@@ -33,6 +33,26 @@ urls(({ path }) => [
|
|
|
33
33
|
]);
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Optional URL params at runtime
|
|
37
|
+
|
|
38
|
+
Absent optional params are **omitted from `ctx.params`** — `ctx.params.<name>`
|
|
39
|
+
reads as `undefined`, matching the `RouteParams<"name">` type
|
|
40
|
+
(`{ query?: string }`). Use `??` to default and `=== undefined` to check
|
|
41
|
+
absence:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
path("/search/:query?", (ctx) => {
|
|
45
|
+
const query = ctx.params.query ?? ""; // works — undefined coalesces
|
|
46
|
+
if (ctx.params.query === undefined) return <EmptySearch />;
|
|
47
|
+
return <Results query={ctx.params.query} />;
|
|
48
|
+
}, { name: "search" });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For the common pattern of an optional locale prefix
|
|
52
|
+
(`include("/:locale?", routes)`) and the wider react-intl integration —
|
|
53
|
+
locale detection, fallback chains, URL generation with absent locale —
|
|
54
|
+
see `/i18n`.
|
|
55
|
+
|
|
36
56
|
## Route Handler Patterns
|
|
37
57
|
|
|
38
58
|
### Component Function
|
|
@@ -214,14 +234,24 @@ Cacheable vars (the default) can be read freely inside cache scopes.
|
|
|
214
234
|
|
|
215
235
|
### Revalidation Contracts for Handler Data
|
|
216
236
|
|
|
237
|
+
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
238
|
+
> It decides whether this segment re-runs and streams to the client on a
|
|
239
|
+
> navigation or action — never whether a cached value is stale. The cache
|
|
240
|
+
> decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
|
|
241
|
+
> `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
242
|
+
|
|
217
243
|
Handler-first guarantees apply within a single full render pass. For partial
|
|
218
244
|
action revalidation, define named revalidation contracts and reuse them on both
|
|
219
245
|
the producer route and the consumer child segments.
|
|
220
246
|
|
|
221
247
|
```typescript
|
|
222
248
|
// revalidation-contracts.ts
|
|
223
|
-
|
|
224
|
-
|
|
249
|
+
import * as CheckoutActions from "./actions/checkout";
|
|
250
|
+
|
|
251
|
+
// Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
|
|
252
|
+
// so when the same segment composes multiple contracts the later ones never run.
|
|
253
|
+
export const revalidateCheckoutData = (ctx) =>
|
|
254
|
+
ctx.isAction(CheckoutActions) || undefined;
|
|
225
255
|
|
|
226
256
|
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
227
257
|
revalidate(revalidateCheckoutData), // producer (route handler) reruns
|
|
@@ -250,9 +280,6 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
|
250
280
|
]);
|
|
251
281
|
```
|
|
252
282
|
|
|
253
|
-
For scope/revalidation guarantees and non-guarantees, see:
|
|
254
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
255
|
-
|
|
256
283
|
## Redirects
|
|
257
284
|
|
|
258
285
|
### Basic redirect
|
|
@@ -269,6 +296,12 @@ path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
|
|
|
269
296
|
path("/moved", () => redirect("/new-location", 301), { name: "moved" });
|
|
270
297
|
```
|
|
271
298
|
|
|
299
|
+
> **Redirecting from a route with `loading()`:** an `async` handler that returns
|
|
300
|
+
> a `Response`/`redirect()` on a route that also declares `loading()` is streamed,
|
|
301
|
+
> so the redirect is rendered into the RSC stream instead of becoming an HTTP
|
|
302
|
+
> redirect. Issue the redirect from `middleware`, a loader, or a **synchronous**
|
|
303
|
+
> handler return instead. (Dev logs a warning if this is hit.)
|
|
304
|
+
|
|
272
305
|
### Redirect with location state
|
|
273
306
|
|
|
274
307
|
Carry typed state through redirects (e.g. flash messages):
|
|
@@ -383,6 +416,10 @@ urls(({ path, layout }) => [
|
|
|
383
416
|
])
|
|
384
417
|
```
|
|
385
418
|
|
|
419
|
+
## View Transitions
|
|
420
|
+
|
|
421
|
+
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.
|
|
422
|
+
|
|
386
423
|
## Handler-attached `.use`
|
|
387
424
|
|
|
388
425
|
Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
|
|
@@ -71,7 +71,7 @@ urls(
|
|
|
71
71
|
## Router Options
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
interface
|
|
74
|
+
interface RangoOptions<TEnv> {
|
|
75
75
|
// URL patterns from urls() function
|
|
76
76
|
urls: UrlPatterns;
|
|
77
77
|
|
|
@@ -405,7 +405,7 @@ interface AppBindings {
|
|
|
405
405
|
KV: KVNamespace;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
// Variables declared via
|
|
408
|
+
// Variables declared via global namespace augmentation
|
|
409
409
|
interface AppVariables {
|
|
410
410
|
user?: { id: string; name: string };
|
|
411
411
|
}
|
|
@@ -417,7 +417,7 @@ const router = createRouter<AppBindings>({
|
|
|
417
417
|
|
|
418
418
|
// Register types globally for implicit typing
|
|
419
419
|
declare global {
|
|
420
|
-
namespace
|
|
420
|
+
namespace Rango {
|
|
421
421
|
interface Env extends AppBindings {}
|
|
422
422
|
interface Vars extends AppVariables {}
|
|
423
423
|
}
|
|
@@ -32,35 +32,37 @@ Actions mutate state; route handlers and loaders read the latest state. After
|
|
|
32
32
|
an action finishes, Rango performs a server-side revalidation render for the
|
|
33
33
|
matched route so the UI receives fresh segment output and loader data.
|
|
34
34
|
|
|
35
|
-
The main control point is `revalidate((
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The main control point is `revalidate((ctx) => ...)` on the segment that owns
|
|
36
|
+
the data. Match specific actions by imported reference with `ctx.isAction()`;
|
|
37
|
+
use raw `actionId` only when you intentionally need path or directory matching.
|
|
38
|
+
This applies to `path()` handlers, `layout()` handlers, `parallel()` slots,
|
|
39
|
+
`intercept()` routes, and loader registrations:
|
|
38
40
|
|
|
39
41
|
```typescript
|
|
40
42
|
// urls.tsx — path/layout/parallel/intercept/loader/revalidate are passed in by urls()
|
|
41
43
|
import { urls } from "@rangojs/router";
|
|
44
|
+
import * as CartActions from "./actions/cart";
|
|
42
45
|
|
|
43
46
|
export const urlpatterns = urls(({ path, loader, revalidate }) => [
|
|
44
47
|
// The loader belongs to the route that consumes its data — nest it inside
|
|
45
48
|
// the owning path() so the segment owns its data dependency.
|
|
46
49
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
47
|
-
revalidate(
|
|
48
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
49
|
-
),
|
|
50
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
50
51
|
loader(CartLoader, () => [
|
|
51
|
-
revalidate(
|
|
52
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
53
|
-
),
|
|
52
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
54
53
|
]),
|
|
55
54
|
]),
|
|
56
55
|
]);
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
`ctx.isAction()` resolves the imported action reference the same way the router
|
|
59
|
+
derives `actionId`, so it matches in both dev and production and survives action
|
|
60
|
+
renames/moves as type errors instead of silent substring drift.
|
|
61
|
+
|
|
62
|
+
For module-level `"use server"` files, the raw `actionId` passed to every
|
|
60
63
|
server-side `revalidate()` predicate is path-bearing in the server/RSC
|
|
61
|
-
environment in both dev and production: `src/actions/cart.ts#addToCart`. This
|
|
62
|
-
|
|
63
|
-
predicates can filter by action file, directory, or export name.
|
|
64
|
+
environment in both dev and production: `src/actions/cart.ts#addToCart`. This is
|
|
65
|
+
the escape hatch for broad filters by action file, directory, or export name.
|
|
64
66
|
|
|
65
67
|
Actions and the follow-up revalidation render share one request context.
|
|
66
68
|
Values written in the action with `ctx.set(MyVar, value)` or `ctx.set("key",
|
|
@@ -91,15 +93,18 @@ export async function switchTenant(tenantId: string) {
|
|
|
91
93
|
```typescript
|
|
92
94
|
// urls.tsx
|
|
93
95
|
import { urls } from "@rangojs/router";
|
|
96
|
+
import * as TenantActions from "./actions/tenant";
|
|
94
97
|
import { ChangedTenant } from "./context";
|
|
95
98
|
|
|
96
99
|
export const urlpatterns = urls(({ path, revalidate }) => [
|
|
97
100
|
path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
|
|
98
|
-
revalidate(
|
|
99
|
-
(
|
|
100
|
-
|
|
101
|
-
context.get(ChangedTenant) === context.params.tenantId
|
|
102
|
-
|
|
101
|
+
revalidate((ctx) => {
|
|
102
|
+
if (!ctx.isAction(TenantActions)) return undefined;
|
|
103
|
+
return (
|
|
104
|
+
ctx.context.get(ChangedTenant) === ctx.context.params.tenantId ||
|
|
105
|
+
undefined
|
|
106
|
+
);
|
|
107
|
+
}),
|
|
103
108
|
]),
|
|
104
109
|
]);
|
|
105
110
|
```
|
|
@@ -380,13 +385,18 @@ re-render so the UI updates. Rango runs the action, then evaluates
|
|
|
380
385
|
`revalidate()` on matched segments and loaders. Each path, layout, parallel,
|
|
381
386
|
intercept, or loader rule decides whether that piece re-renders/re-resolves.
|
|
382
387
|
|
|
383
|
-
|
|
384
|
-
|
|
388
|
+
Use `ctx.isAction()` for specific actions or modules. It accepts one action,
|
|
389
|
+
several actions, or a namespace import (`import * as CartActions`). Pair it with
|
|
390
|
+
`|| undefined` for "revalidate on match, otherwise defer to defaults/downstream
|
|
391
|
+
rules."
|
|
385
392
|
|
|
386
393
|
```typescript
|
|
387
394
|
// urls.tsx — inside the urls() callback. Nest each loader inside the path(),
|
|
388
395
|
// layout(), or parallel() that owns its data so the route tree mirrors the
|
|
389
396
|
// data dependencies.
|
|
397
|
+
import * as AccountActions from "./actions/account";
|
|
398
|
+
import * as CartActions from "./actions/cart";
|
|
399
|
+
|
|
390
400
|
urls(({ path, loader, revalidate }) => [
|
|
391
401
|
path("/", HomePage, { name: "home" }, () => [
|
|
392
402
|
// Loader data re-runs by default after any action. Opt out with revalidate(() => false).
|
|
@@ -395,36 +405,37 @@ urls(({ path, loader, revalidate }) => [
|
|
|
395
405
|
|
|
396
406
|
// Re-render the cart page handler AND re-resolve its loader after cart actions
|
|
397
407
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
398
|
-
revalidate(
|
|
399
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
400
|
-
),
|
|
408
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
401
409
|
loader(CartLoader, () => [
|
|
402
|
-
revalidate(
|
|
403
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
404
|
-
),
|
|
410
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
405
411
|
]),
|
|
406
412
|
]),
|
|
407
413
|
|
|
408
|
-
// Re-run after any action
|
|
414
|
+
// Re-run after any action exported by the account actions module
|
|
409
415
|
path("/account", AccountPage, { name: "account" }, () => [
|
|
410
416
|
loader(AccountLoader, () => [
|
|
411
|
-
revalidate(
|
|
412
|
-
({ actionId }) => actionId?.startsWith("src/actions/account/") ?? false,
|
|
413
|
-
),
|
|
417
|
+
revalidate((ctx) => ctx.isAction(AccountActions) || undefined),
|
|
414
418
|
]),
|
|
415
419
|
]),
|
|
416
420
|
]);
|
|
417
421
|
```
|
|
418
422
|
|
|
419
|
-
`actionId`
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
The raw `actionId` string stays available for broad path filters:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// Match any action under src/actions/account/, including modules not imported here.
|
|
427
|
+
revalidate(
|
|
428
|
+
({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
|
|
429
|
+
);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
For actions exported from a module-level `"use server"` file, the ID is prefixed
|
|
433
|
+
with the source file path (`src/actions/cart.ts#addToCart`). **Inline `"use
|
|
434
|
+
server"` actions** (declared inside an RSC component) intentionally keep their
|
|
435
|
+
hashed IDs — file paths are withheld from the client for security. If you need
|
|
436
|
+
file-path-based revalidation predicates, define the action in a module-level
|
|
437
|
+
`"use server"` file rather than inline. See `/loader` for the full revalidation
|
|
438
|
+
contract (deferred returns, soft suggestions).
|
|
428
439
|
|
|
429
440
|
### Cross-segment dependencies
|
|
430
441
|
|
|
@@ -434,8 +445,9 @@ stale context. Share the same `revalidate` predicate on both producer and
|
|
|
434
445
|
consumer:
|
|
435
446
|
|
|
436
447
|
```typescript
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
import * as CartActions from "./actions/cart";
|
|
449
|
+
|
|
450
|
+
const revalidateCart = (ctx) => ctx.isAction(CartActions) || undefined;
|
|
439
451
|
|
|
440
452
|
urls(({ path, layout, loader, revalidate }) => [
|
|
441
453
|
layout(CartLayout, () => [
|
|
@@ -453,7 +465,10 @@ See `/middleware` for the full cross-segment revalidation contract.
|
|
|
453
465
|
|
|
454
466
|
`redirect()` works inside actions. Both `return redirect(...)` and
|
|
455
467
|
`throw redirect(...)` are supported and behave the same way for the
|
|
456
|
-
client. Throwing is clearer when the redirect is conditional
|
|
468
|
+
client. Throwing is clearer when the redirect is conditional, and it keeps
|
|
469
|
+
the action's return type narrow (e.g. `Promise<void>`) — `redirect()` returns
|
|
470
|
+
a `Response`, so the `return` form needs `Promise<Response>` in the signature.
|
|
471
|
+
Prefer `throw redirect(...)`; never cast with `as any`.
|
|
457
472
|
|
|
458
473
|
```typescript
|
|
459
474
|
"use server";
|
|
@@ -478,6 +493,27 @@ Redirects from actions render the **target** route tree's matched segments
|
|
|
478
493
|
source page's — the target is what the user sees next. See `/hooks
|
|
479
494
|
useLocationState` for reading flash state on the target page.
|
|
480
495
|
|
|
496
|
+
### Same-origin by default (open-redirect protection)
|
|
497
|
+
|
|
498
|
+
`redirect()` is same-origin by default on every path — JS, no-JS PE, and
|
|
499
|
+
full-page. A cross-origin target (e.g. from unvalidated user input) is blocked
|
|
500
|
+
and the user is sent to the app root instead, so `redirect(userInput)` can never
|
|
501
|
+
become an open redirect. To intentionally redirect off-host (an OAuth provider,
|
|
502
|
+
say), opt in explicitly:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
throw redirect("https://accounts.google.com/o/oauth2/v2/auth?...", {
|
|
506
|
+
external: true,
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`{ external: true }` is the audit point: passing user input with it re-opens the
|
|
511
|
+
cross-origin risk and is your responsibility (the Rails `allow_other_host: true`
|
|
512
|
+
model). Omit it and off-host targets stay blocked. `external` only waives the
|
|
513
|
+
**same-origin** rule, not scheme safety: the target must be `http(s)` — a
|
|
514
|
+
`javascript:` or `data:` URL is still neutralized, so a forged or mistaken
|
|
515
|
+
`external` target can never become a scriptable navigation.
|
|
516
|
+
|
|
481
517
|
## Error Handling
|
|
482
518
|
|
|
483
519
|
### Validation errors — return them as state
|
package/skills/tailwind/SKILL.md
CHANGED
|
@@ -37,7 +37,11 @@ export default defineConfig({
|
|
|
37
37
|
|
|
38
38
|
## Document Component
|
|
39
39
|
|
|
40
|
-
Import the CSS file with `?url` to get a hashed URL, then preload and link it in
|
|
40
|
+
Import the CSS file with `?url` to get a hashed URL, then preload and link it in
|
|
41
|
+
`<head>`. Give the `<link rel="stylesheet">` a `precedence` prop so React 19
|
|
42
|
+
manages it as a resource — de-duped by `href`, ordered, and loaded before paint
|
|
43
|
+
(no flash of unstyled content). See
|
|
44
|
+
[Stylesheets and cross-app navigation](#stylesheets-and-cross-app-navigation):
|
|
41
45
|
|
|
42
46
|
```tsx
|
|
43
47
|
// src/document.tsx
|
|
@@ -51,8 +55,8 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
51
55
|
return (
|
|
52
56
|
<html lang="en">
|
|
53
57
|
<head>
|
|
54
|
-
<link rel="preload" href={styles} as="style" />
|
|
55
|
-
<link rel="stylesheet" href={styles} />
|
|
58
|
+
<link rel="preload" href={styles} as="style" precedence="default" />
|
|
59
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
56
60
|
<MetaTags />
|
|
57
61
|
</head>
|
|
58
62
|
<body className="font-sans antialiased text-slate-900 bg-slate-50">
|
|
@@ -63,6 +67,26 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
63
67
|
}
|
|
64
68
|
```
|
|
65
69
|
|
|
70
|
+
## Stylesheets and cross-app navigation
|
|
71
|
+
|
|
72
|
+
The `precedence` prop opts a `<link rel="stylesheet">` into React 19's managed
|
|
73
|
+
stylesheet model — React de-duplicates it by `href`, orders it by precedence, and
|
|
74
|
+
loads it before paint (avoiding a flash of unstyled content). It is the
|
|
75
|
+
recommended way to render a stylesheet link, which is why the example above uses
|
|
76
|
+
it. (A bare side-effect `import "./index.css"` also produces managed CSS via
|
|
77
|
+
`@vitejs/plugin-rsc`, but carries an SSR-streaming caveat — prefer the `?url` +
|
|
78
|
+
`<link precedence>` form for document CSS. See `/css`.)
|
|
79
|
+
|
|
80
|
+
For **host-router** apps (`/host-router`), a client-side navigation that crosses
|
|
81
|
+
an app boundary is a **full document load**, not a soft swap — the framework
|
|
82
|
+
redirects on an app switch. So each app's document (its stylesheets, theme, meta)
|
|
83
|
+
is always re-established cleanly by the target app's own load; you do not have to
|
|
84
|
+
coordinate stylesheet `href`s or `precedence` across apps. (This replaced an
|
|
85
|
+
earlier soft cross-app swap, under which a stylesheet shared across apps — every
|
|
86
|
+
app's `@import "tailwindcss"` compiles to one hashed asset — could be dropped by
|
|
87
|
+
React's by-`href` resource dedup if the apps disagreed on `precedence`. The full
|
|
88
|
+
reload removes that footgun entirely.)
|
|
89
|
+
|
|
66
90
|
The `?url` suffix tells Vite to return the processed CSS file's URL instead of injecting it as a side effect. This gives you a stable, hashed asset path that works in both development and production.
|
|
67
91
|
|
|
68
92
|
## Customizing the Theme
|