@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- 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 +129 -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 +84 -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/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -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 +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- 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 +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -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-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 +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- 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 +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -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 +1989 -378
- 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 +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- 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 +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- 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/outlet-context.ts +1 -1
- 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 +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- 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 +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- 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 +56 -0
- 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 +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -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 +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- 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 +701 -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 +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -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 +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +20 -58
- 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 +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/component-utils.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ComponentType } from "react";
|
|
8
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Symbol used by React to mark client component references.
|
|
@@ -48,11 +49,21 @@ export function isClientComponent(
|
|
|
48
49
|
*
|
|
49
50
|
* @param component - The component to check
|
|
50
51
|
* @param name - Name to use in error message (e.g., "document")
|
|
52
|
+
* @param opts.allowServerInTest - When true AND running under a test runner
|
|
53
|
+
* (`isUnderTestRunner()`), relax ONLY the "use client" requirement: a server
|
|
54
|
+
* component is accepted. The plugin's "use client" transform does not run in a
|
|
55
|
+
* bare unit test, so a real exported `document` (almost every app sets one) has
|
|
56
|
+
* no client marker and would otherwise throw at `createRouter`, blocking
|
|
57
|
+
* `dispatch`/`assertGeneratedRoutesMatch` against the real router. The document
|
|
58
|
+
* reference is irrelevant to those (no Flight render). The "not a JSX element"
|
|
59
|
+
* guard still fires, and a real dev/build still throws (mirrors the runtime
|
|
60
|
+
* fallback-id gating in handle.ts/loader.ts).
|
|
51
61
|
* @throws Error if the component is not a client component
|
|
52
62
|
*/
|
|
53
63
|
export function assertClientComponent(
|
|
54
64
|
component: ComponentType<unknown> | unknown,
|
|
55
65
|
name: string,
|
|
66
|
+
opts?: { allowServerInTest?: boolean },
|
|
56
67
|
): asserts component is ComponentType<unknown> {
|
|
57
68
|
if (typeof component !== "function") {
|
|
58
69
|
throw new Error(
|
|
@@ -62,6 +73,14 @@ export function assertClientComponent(
|
|
|
62
73
|
);
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
// Under a test runner the "use client" transform did not run, so a real
|
|
77
|
+
// server-rendered `document` has no client marker; accept it (the reference is
|
|
78
|
+
// never serialized in dispatch/route-map checks). Outside a test runner this
|
|
79
|
+
// still throws — the build-time safety net is preserved.
|
|
80
|
+
if (opts?.allowServerInTest && isUnderTestRunner()) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
if (!isClientComponent(component)) {
|
|
66
85
|
throw new Error(
|
|
67
86
|
`${name} must be a client component with "use client" directive at the top of the file. ` +
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ReactNode, ReactElement } from "react";
|
|
4
4
|
import { MetaTags } from "../handles/MetaTags.js";
|
|
5
|
+
import { Scripts } from "../handles/Scripts.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Default document component that provides a basic HTML structure.
|
|
8
9
|
* Used when no custom document is provided to createRouter.
|
|
9
|
-
* Includes MetaTags for automatic charset, viewport, and route meta support
|
|
10
|
+
* Includes MetaTags for automatic charset, viewport, and route meta support,
|
|
11
|
+
* and Scripts (head + body sites) so the Script handle works out of the box.
|
|
10
12
|
*
|
|
11
13
|
* Uses suppressHydrationWarning on <html> because the theme script
|
|
12
14
|
* may modify class/style attributes before React hydrates.
|
|
@@ -20,8 +22,12 @@ export function DefaultDocument({
|
|
|
20
22
|
<html lang="en" suppressHydrationWarning>
|
|
21
23
|
<head>
|
|
22
24
|
<MetaTags />
|
|
25
|
+
<Scripts />
|
|
23
26
|
</head>
|
|
24
|
-
<body>
|
|
27
|
+
<body>
|
|
28
|
+
<Scripts position="body" />
|
|
29
|
+
{children}
|
|
30
|
+
</body>
|
|
25
31
|
</html>
|
|
26
32
|
);
|
|
27
33
|
}
|
package/src/context-var.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* interface PaginationData { current: number; total: number }
|
|
13
13
|
* export const Pagination = createVar<PaginationData>();
|
|
14
14
|
*
|
|
15
|
-
* // Non-cacheable var — throws
|
|
15
|
+
* // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
|
|
16
16
|
* export const User = createVar<UserData>({ cache: false });
|
|
17
17
|
*
|
|
18
18
|
* // handler
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export interface ContextVar<T> {
|
|
27
27
|
readonly __brand: "context-var";
|
|
28
28
|
readonly key: symbol;
|
|
29
|
-
/** When false,
|
|
29
|
+
/** When false, ctx.get(var) throws inside a cache() boundary. */
|
|
30
30
|
readonly cache: boolean;
|
|
31
31
|
/** Phantom field to carry the type parameter. Never set at runtime. */
|
|
32
32
|
readonly __type?: T;
|
|
@@ -35,9 +35,9 @@ export interface ContextVar<T> {
|
|
|
35
35
|
export interface ContextVarOptions {
|
|
36
36
|
/**
|
|
37
37
|
* When false, marks this variable as non-cacheable.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Reading this var with ctx.get() inside a cache() boundary throws. Use for
|
|
39
|
+
* inherently request-specific data (user sessions, auth tokens, etc.) that
|
|
40
|
+
* must never be baked into cached segments.
|
|
41
41
|
*
|
|
42
42
|
* @default true
|
|
43
43
|
*/
|
|
@@ -70,13 +70,25 @@ export function isContextVar(value: unknown): value is ContextVar<unknown> {
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Does a variables object hold any entries? Counts both string keys and the
|
|
75
|
+
* symbol-keyed entries (context vars are stored under symbols), so an object
|
|
76
|
+
* carrying only symbol-keyed vars is still reported as non-empty.
|
|
77
|
+
*/
|
|
78
|
+
export function hasContextVars(variables: object): boolean {
|
|
79
|
+
return (
|
|
80
|
+
Object.keys(variables).length > 0 ||
|
|
81
|
+
Object.getOwnPropertySymbols(variables).length > 0
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
/**
|
|
74
86
|
* Symbol used as a Set stored on the variables object to track
|
|
75
87
|
* which keys hold non-cacheable values (from write-level { cache: false }).
|
|
76
88
|
*/
|
|
77
89
|
const NON_CACHEABLE_KEYS: unique symbol = Symbol.for(
|
|
78
90
|
"rango:non-cacheable-keys",
|
|
79
|
-
)
|
|
91
|
+
);
|
|
80
92
|
|
|
81
93
|
function getNonCacheableKeys(variables: any): Set<string | symbol> {
|
|
82
94
|
if (!variables[NON_CACHEABLE_KEYS]) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { isLoaderDataResult } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Shared by segment-system (server) and LoaderResolver (client) so the
|
|
5
|
+
// legacy/ok/error-fallback/throw decode of resolved loader values lives once.
|
|
6
|
+
// Last failing loader wins errorFallback; an error without a fallback throws.
|
|
7
|
+
export function decodeLoaderResults(
|
|
8
|
+
resolvedData: any[],
|
|
9
|
+
loaderIds: string[],
|
|
10
|
+
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
11
|
+
const loaderData: Record<string, any> = {};
|
|
12
|
+
let errorFallback: ReactNode = null;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < loaderIds.length; i++) {
|
|
15
|
+
const id = loaderIds[i];
|
|
16
|
+
const result = resolvedData[i];
|
|
17
|
+
|
|
18
|
+
if (!isLoaderDataResult(result)) {
|
|
19
|
+
loaderData[id] = result;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
loaderData[id] = result.data;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// null/undefined is the producer's ONLY "no boundary found" sentinel
|
|
29
|
+
// (loader-resolution.ts sets fallback: null for the no-boundary and the
|
|
30
|
+
// fallback-render-threw cases). A matched boundary's rendered ReactNode can
|
|
31
|
+
// legitimately be falsy (0, "", false), so test for null explicitly rather
|
|
32
|
+
// than truthiness, otherwise a valid falsy fallback is discarded and the
|
|
33
|
+
// original loader error is rethrown.
|
|
34
|
+
if (result.fallback != null) {
|
|
35
|
+
errorFallback = result.fallback;
|
|
36
|
+
} else {
|
|
37
|
+
// No boundary: rethrow preserving the ErrorInfo identity (name/stack/
|
|
38
|
+
// code/cause) instead of a stripped generic Error.
|
|
39
|
+
const info = result.error;
|
|
40
|
+
const err = new Error(
|
|
41
|
+
info.message,
|
|
42
|
+
info.cause !== undefined ? { cause: info.cause } : undefined,
|
|
43
|
+
);
|
|
44
|
+
if (info.name) err.name = info.name;
|
|
45
|
+
if (info.stack) err.stack = info.stack;
|
|
46
|
+
if (info.code !== undefined) (err as { code?: string }).code = info.code;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { loaderData, errorFallback };
|
|
52
|
+
}
|
package/src/defer.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred handle values — "decide synchronously, resolve late".
|
|
3
|
+
*
|
|
4
|
+
* A handle is pushed from code that holds `ctx` (a route/layout handler), so the
|
|
5
|
+
* decision to push lands before the handles stream seals. But the value often
|
|
6
|
+
* isn't known there — it may come from a deep async component far from the
|
|
7
|
+
* handler. `ctx.use(Handle).defer()` reserves the handle's slot now (synchronous,
|
|
8
|
+
* so ordering and the pre-seal timing hold) and returns a resolver with the SAME
|
|
9
|
+
* signature as the push: you call it later, anywhere in the render, with the same
|
|
10
|
+
* value you would have passed to the push.
|
|
11
|
+
*
|
|
12
|
+
* const breadcrumb = ctx.use(Breadcrumbs); // (item) => void & .defer()
|
|
13
|
+
* const resolve = breadcrumb.defer({ timeoutMs: 5000, else: null });
|
|
14
|
+
* // deep async component, far from ctx:
|
|
15
|
+
* resolve({ label, href, content }); // identical call, just deferred
|
|
16
|
+
*
|
|
17
|
+
* Under the hood the reserved slot is a Promise the renderer `use()`s; RSC Flight
|
|
18
|
+
* streams it as a late row, so a deferred-aware consumer reading the handle
|
|
19
|
+
* (`useHandle`) sees that entry as a `Promise` until it resolves (see
|
|
20
|
+
* {@link DeferredHandleEntry}). The hazard that guards against bugs: a deferred
|
|
21
|
+
* slot whose resolver is never called would keep the Flight stream — and the HTTP
|
|
22
|
+
* response — open forever. So a deferred auto-resolves to `else` after `timeoutMs`
|
|
23
|
+
* (default {@link DEFAULT_DEFER_TIMEOUT_MS}) if the resolver is never called,
|
|
24
|
+
* degrading gracefully (and warning in dev) instead of hanging the request.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Default auto-resolve window. Long enough for genuine deep async work, short
|
|
28
|
+
* enough that a forgotten resolve does not hang the response indefinitely. */
|
|
29
|
+
export const DEFAULT_DEFER_TIMEOUT_MS = 10_000;
|
|
30
|
+
|
|
31
|
+
/** Options for `ctx.use(Handle).defer()`. */
|
|
32
|
+
export interface DeferOptions<TData> {
|
|
33
|
+
/**
|
|
34
|
+
* Auto-resolve to `else` after this many ms if the resolver is never called,
|
|
35
|
+
* so a forgotten resolve cannot hold the Flight stream — and thus the HTTP
|
|
36
|
+
* response — open. Defaults to {@link DEFAULT_DEFER_TIMEOUT_MS}. `0` or
|
|
37
|
+
* `Infinity` disable the timeout intentionally (not recommended on a request
|
|
38
|
+
* path). Any other non-finite or negative value is treated as a mistake and
|
|
39
|
+
* falls back to the default rather than silently disabling the safety net.
|
|
40
|
+
* Named `timeoutMs` to match the router's `*Ms` duration convention.
|
|
41
|
+
*/
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Value the slot resolves to if the timeout fires before the resolver is
|
|
45
|
+
* called. Defaults to `undefined` (the deferred item is skipped/empty). For
|
|
46
|
+
* renderable handle content, `null` is the usual graceful fallback, so the
|
|
47
|
+
* type admits `null` even when `TData` does not.
|
|
48
|
+
*/
|
|
49
|
+
else?: TData | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The call signature shared by a handle push and the resolver returned by
|
|
54
|
+
* `.defer()`: a concrete value, a `Promise` of the value (Flight streams it as a
|
|
55
|
+
* late row), or a thunk returning a `Promise` (called immediately).
|
|
56
|
+
*/
|
|
57
|
+
export type HandlePushFn<TData> = (
|
|
58
|
+
data: TData | Promise<TData> | (() => Promise<TData>),
|
|
59
|
+
) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The push function returned by `ctx.use(Handle)`. Call it to push a value now,
|
|
63
|
+
* or call `.defer()` to reserve the slot now and resolve the value later (e.g.
|
|
64
|
+
* from a deep async component) with a timeout safety net.
|
|
65
|
+
*/
|
|
66
|
+
export type HandlePush<TData> = HandlePushFn<TData> & {
|
|
67
|
+
/**
|
|
68
|
+
* Reserve this handle's slot synchronously and return a resolver that is
|
|
69
|
+
* push-equal: it takes the same argument shapes as the push (value, Promise, or
|
|
70
|
+
* thunk) and behaves identically. Two things the resolver adds over a direct
|
|
71
|
+
* push: a timeout (if the resolver is never called, the slot auto-resolves to
|
|
72
|
+
* `options.else` after `options.timeoutMs`; calling the resolver cancels it),
|
|
73
|
+
* and — on the action/revalidation path only — a thunk it runs does NOT
|
|
74
|
+
* re-enter the deadlock-guard push-callback scope a direct push thunk gets,
|
|
75
|
+
* because a deferred resolver fires after the handler phase has closed.
|
|
76
|
+
*
|
|
77
|
+
* The reserved slot appears in the accumulated handle data as a pending
|
|
78
|
+
* `Promise` until it resolves (see {@link DeferredHandleEntry}); a
|
|
79
|
+
* deferred-aware consumer narrows thenable entries (`use()`/`await` + null
|
|
80
|
+
* check) before dereferencing.
|
|
81
|
+
*/
|
|
82
|
+
defer(options?: DeferOptions<TData>): HandlePushFn<TData>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A handle entry a deferred-aware consumer may read from `useHandle`: either a
|
|
87
|
+
* resolved value, or a pending `Promise` that resolves to the value, to `else`,
|
|
88
|
+
* or (when no `else` was given) `undefined` on timeout. Reading code should treat
|
|
89
|
+
* thenable entries as such and narrow before dereferencing.
|
|
90
|
+
*/
|
|
91
|
+
export type DeferredHandleEntry<TData> =
|
|
92
|
+
| TData
|
|
93
|
+
| Promise<TData | null | undefined>;
|
|
94
|
+
|
|
95
|
+
// Internal: a timeout-bounded { promise, resolve }. Not part of the public API
|
|
96
|
+
// (the public surface is `ctx.use(Handle).defer()`); exported for `withDefer`
|
|
97
|
+
// and unit tests only. Resolves to `T`, the `else` fallback, or `undefined`.
|
|
98
|
+
export function createDeferred<T>(options?: {
|
|
99
|
+
timeoutMs?: number;
|
|
100
|
+
fallback?: T | null;
|
|
101
|
+
}): {
|
|
102
|
+
promise: Promise<T | null | undefined>;
|
|
103
|
+
resolve: (value: T | null | undefined) => void;
|
|
104
|
+
} {
|
|
105
|
+
let resolveInner!: (value: T | null | undefined) => void;
|
|
106
|
+
let settled = false;
|
|
107
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
108
|
+
|
|
109
|
+
const promise = new Promise<T | null | undefined>((resolve) => {
|
|
110
|
+
resolveInner = resolve;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const finish = (value: T | null | undefined): void => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
settled = true;
|
|
116
|
+
if (timer !== undefined) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
timer = undefined;
|
|
119
|
+
}
|
|
120
|
+
resolveInner(value);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// 0 and Infinity are documented intentional disables. Any other non-finite or
|
|
124
|
+
// negative value (NaN, -1, a bad parsed config/env) is a mistake — fall back to
|
|
125
|
+
// the default rather than SILENTLY disabling the safety net, which would let a
|
|
126
|
+
// forgotten resolve hang the Flight stream and the response forever.
|
|
127
|
+
const requested = options?.timeoutMs ?? DEFAULT_DEFER_TIMEOUT_MS;
|
|
128
|
+
let ms: number;
|
|
129
|
+
if (requested === 0 || requested === Infinity) {
|
|
130
|
+
ms = requested;
|
|
131
|
+
} else if (Number.isFinite(requested) && requested > 0) {
|
|
132
|
+
ms = requested;
|
|
133
|
+
} else {
|
|
134
|
+
if (process.env.NODE_ENV !== "production") {
|
|
135
|
+
console.warn(
|
|
136
|
+
`[rango] defer(): invalid timeout ${String(requested)}; using the ` +
|
|
137
|
+
`${DEFAULT_DEFER_TIMEOUT_MS}ms default so the safety net stays on. ` +
|
|
138
|
+
`Use 0 or Infinity to disable the timeout intentionally.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
ms = DEFAULT_DEFER_TIMEOUT_MS;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (ms > 0 && ms !== Infinity) {
|
|
145
|
+
timer = setTimeout(() => {
|
|
146
|
+
if (process.env.NODE_ENV !== "production") {
|
|
147
|
+
console.warn(
|
|
148
|
+
`[rango] A deferred handle value was not resolved within ${ms}ms; ` +
|
|
149
|
+
`resolving to the fallback so the response can flush. Call the ` +
|
|
150
|
+
`resolver from the component that produces the value, or raise timeoutMs.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
finish(options?.fallback);
|
|
154
|
+
}, ms);
|
|
155
|
+
// Don't let a pending timer alone keep a Node process alive (no-op on workerd).
|
|
156
|
+
(timer as { unref?: () => void }).unref?.();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { promise, resolve: finish };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Attach `.defer()` to a handle push function. The deferred slot is reserved by
|
|
164
|
+
* pushing the deferred promise through the same push (so ordering, sealing, and
|
|
165
|
+
* Flight streaming all reuse the existing path); the returned resolver settles it.
|
|
166
|
+
*/
|
|
167
|
+
export function withDefer<TData>(push: HandlePushFn<TData>): HandlePush<TData> {
|
|
168
|
+
const handlePush = push as HandlePush<TData>;
|
|
169
|
+
// Safe to mutate push in place: each ctx.use(Handle) call (request-context.ts,
|
|
170
|
+
// loader-resolution.ts) builds a fresh closure, so .defer never leaks across
|
|
171
|
+
// handles or requests.
|
|
172
|
+
handlePush.defer = (options) => {
|
|
173
|
+
const deferred = createDeferred<TData>({
|
|
174
|
+
timeoutMs: options?.timeoutMs,
|
|
175
|
+
fallback: options?.else,
|
|
176
|
+
});
|
|
177
|
+
// Reserve the slot now by pushing the pending promise (the renderer use()s it).
|
|
178
|
+
push(deferred.promise as Promise<TData>);
|
|
179
|
+
// The resolver is push-equal: a thunk is invoked immediately (as push does)
|
|
180
|
+
// and a Promise is adopted by the reserved slot. Calling it settles the slot
|
|
181
|
+
// and cancels the timeout — the timeout only fires if it is never called.
|
|
182
|
+
const resolveSlot = deferred.resolve as (
|
|
183
|
+
value: TData | Promise<TData>,
|
|
184
|
+
) => void;
|
|
185
|
+
return (data) => {
|
|
186
|
+
// The thunk runs without re-entering the push-callback scope a direct push
|
|
187
|
+
// thunk gets on the action/revalidation path (loader-resolution.ts): a
|
|
188
|
+
// deferred resolver fires from a deep component after the handler phase has
|
|
189
|
+
// closed, so there is no live deadlock-guard window to exempt.
|
|
190
|
+
resolveSlot(
|
|
191
|
+
typeof data === "function" ? (data as () => Promise<TData>)() : data,
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
return handlePush;
|
|
196
|
+
}
|
package/src/deps/ssr.ts
CHANGED
package/src/encode-kv.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared key=value serialization core.
|
|
3
|
+
*
|
|
4
|
+
* One encode+join rule shared by every deterministic query-string serializer
|
|
5
|
+
* in the codebase (cache keys, route-param strings, search-param strings,
|
|
6
|
+
* prerender param hashes). Each call site differs only in how it pre-filters
|
|
7
|
+
* and orders the pairs it hands in, plus whether it sorts; the encoding itself
|
|
8
|
+
* (`encodeURIComponent(k)=encodeURIComponent(v)` joined by `&`) is identical.
|
|
9
|
+
*
|
|
10
|
+
* Dependency-free leaf module so any layer (build-time prerender hashing,
|
|
11
|
+
* runtime cache keys, client search-param serialization) can import it without
|
|
12
|
+
* pulling in cache/router internals.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface EncodeKVOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Sort pairs by key in codepoint (byte) order before joining. Byte order via
|
|
18
|
+
* the `<` operator, NOT localeCompare, so the result is identical across
|
|
19
|
+
* Node, Workers, and browsers regardless of runtime locale.
|
|
20
|
+
*
|
|
21
|
+
* Defaults to false (insertion order preserved).
|
|
22
|
+
*/
|
|
23
|
+
sort?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Serialize an array of [key, value] pairs to a deterministic query string
|
|
28
|
+
* (no leading `?`). Both key and value are passed through encodeURIComponent
|
|
29
|
+
* so a key/value containing `&` or `=` cannot collide with a structurally
|
|
30
|
+
* different pair set.
|
|
31
|
+
*
|
|
32
|
+
* Callers are responsible for any filtering (e.g. dropping reserved params) and
|
|
33
|
+
* any value coercion (e.g. String(v), skipping null/undefined) BEFORE calling.
|
|
34
|
+
* This function never inspects or skips values; it encodes exactly what it is
|
|
35
|
+
* given so each call site keeps its own existing output.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeKV(
|
|
38
|
+
pairs: Iterable<readonly [string, string]>,
|
|
39
|
+
options: EncodeKVOptions = {},
|
|
40
|
+
): string {
|
|
41
|
+
const list = [...pairs];
|
|
42
|
+
if (list.length === 0) return "";
|
|
43
|
+
if (options.sort) {
|
|
44
|
+
list.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
45
|
+
}
|
|
46
|
+
return list
|
|
47
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
48
|
+
.join("&");
|
|
49
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom error classes for
|
|
2
|
+
* Custom error classes for Rango
|
|
3
3
|
*
|
|
4
4
|
* All errors include:
|
|
5
5
|
* - Descriptive names for easy identification
|
|
@@ -27,6 +27,17 @@ export class RouteNotFoundError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// name fallback covers cross-realm errors (Vite dev dupes, RSC serialization)
|
|
31
|
+
// where instanceof fails.
|
|
32
|
+
export function isRouteNotFoundError(
|
|
33
|
+
error: unknown,
|
|
34
|
+
): error is RouteNotFoundError {
|
|
35
|
+
return (
|
|
36
|
+
error instanceof RouteNotFoundError ||
|
|
37
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Thrown when data is not found (e.g., product with ID doesn't exist)
|
|
32
43
|
* Use this in handlers/loaders to trigger the nearest notFoundBoundary
|
|
@@ -109,6 +120,24 @@ export class BuildError extends Error {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Thrown when a route-definition DSL helper (route/layout/loader/cache/…) is
|
|
125
|
+
* called outside an active urls()/map() builder, so there is no
|
|
126
|
+
* AsyncLocalStorage build context to attach to. The message names the specific
|
|
127
|
+
* helper and how to fix it; the `cause` records the mechanical reason so the
|
|
128
|
+
* failure mode is identifiable (not conflated with an unrelated throw).
|
|
129
|
+
*/
|
|
130
|
+
export class DslContextError extends Error {
|
|
131
|
+
name = "DslContextError" as const;
|
|
132
|
+
cause?: unknown;
|
|
133
|
+
|
|
134
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
135
|
+
super(message);
|
|
136
|
+
Object.setPrototypeOf(this, DslContextError.prototype);
|
|
137
|
+
this.cause = options?.cause;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
/**
|
|
113
142
|
* Thrown when a network request fails (server unreachable, no internet, etc.)
|
|
114
143
|
* This error triggers the root error boundary with retry capability.
|
|
@@ -196,7 +225,6 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
196
225
|
export class RouterError extends Error {
|
|
197
226
|
name = "RouterError" as const;
|
|
198
227
|
code: string;
|
|
199
|
-
type?: string;
|
|
200
228
|
status: number;
|
|
201
229
|
cause?: unknown;
|
|
202
230
|
|
|
@@ -205,7 +233,6 @@ export class RouterError extends Error {
|
|
|
205
233
|
message: string,
|
|
206
234
|
options?: {
|
|
207
235
|
status?: number;
|
|
208
|
-
type?: string;
|
|
209
236
|
cause?: unknown;
|
|
210
237
|
},
|
|
211
238
|
) {
|
|
@@ -213,7 +240,6 @@ export class RouterError extends Error {
|
|
|
213
240
|
Object.setPrototypeOf(this, RouterError.prototype);
|
|
214
241
|
this.code = code;
|
|
215
242
|
this.status = options?.status ?? 500;
|
|
216
|
-
this.type = options?.type;
|
|
217
243
|
this.cause = options?.cause;
|
|
218
244
|
}
|
|
219
245
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape a JSON (or JSON-derived) string for safe embedding inside an HTML
|
|
3
|
+
* <script> element via dangerouslySetInnerHTML. Without this a value containing
|
|
4
|
+
* "</script>" closes the tag early — the rest of the page leaks as raw HTML, and
|
|
5
|
+
* in an executable script the trailing content runs. Escaping "<" defeats the
|
|
6
|
+
* early close; ">" and "&" are escaped for completeness so the serialized payload
|
|
7
|
+
* can never form HTML syntax. The result is still valid JSON and a valid JS
|
|
8
|
+
* string literal (\uXXXX escapes are legal in both) and re-parses identically.
|
|
9
|
+
*
|
|
10
|
+
* Used by every site that interpolates JSON.stringify(...) into inline <script>
|
|
11
|
+
* content: the JSON-LD meta descriptors (handles/MetaTags) and the FOUC theme
|
|
12
|
+
* init script (theme/theme-script).
|
|
13
|
+
*/
|
|
14
|
+
export function escapeJsonForScript(json: string): string {
|
|
15
|
+
return json
|
|
16
|
+
.replace(/</g, "\\u003c")
|
|
17
|
+
.replace(/>/g, "\\u003e")
|
|
18
|
+
.replace(/&/g, "\\u0026");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Escape an inline <script> body so it cannot terminate or corrupt the document.
|
|
23
|
+
* Two sequences are rewritten, each via a JS escape that is valid in string,
|
|
24
|
+
* template, regex (including the `u`/`v` flags), and JSON contexts — so the body
|
|
25
|
+
* still parses identically as code AND as JSON (application/json, ld+json):
|
|
26
|
+
* - "</script" -> "<\/script": stops a literal close tag inside the body from
|
|
27
|
+
* ending the element early. `\/` is a valid JSON escape and a valid regex escape.
|
|
28
|
+
* - "<!--": the "!" (U+0021) is emitted as a unicode escape (see the replacement
|
|
29
|
+
* string below), so the literal "<!--" token never reaches the HTML parser. A
|
|
30
|
+
* literal "<!--" puts the parser into the "script data escaped" state and a
|
|
31
|
+
* following "<script" into "script data DOUBLE escaped", where the real
|
|
32
|
+
* "</script>" no longer closes the element — `var x = "<!--<script>"` would
|
|
33
|
+
* swallow the rest of the document. The unicode-escape form decodes back to "!"
|
|
34
|
+
* in string/template/JSON/regex contexts, unlike "\!" (invalid JSON, invalid
|
|
35
|
+
* /u-regex escape).
|
|
36
|
+
* Real operators such as `a < b` and `a && b` are untouched (unlike
|
|
37
|
+
* escapeJsonForScript, which \u-escapes every "<", "&", ">").
|
|
38
|
+
*
|
|
39
|
+
* GUARANTEE / LIMITATION: value-preserving for the contexts where these sequences
|
|
40
|
+
* legitimately appear — string/template literals, regexes (incl. `u`/`v`), and
|
|
41
|
+
* JSON. It is NOT source-text-preserving (e.g. String.raw`</script>` sees the
|
|
42
|
+
* extra backslash), and it cannot rewrite "</script"/"<!--" that appear as bare
|
|
43
|
+
* code (a legacy `<!--` line comment, or `</script` outside any literal) — neither
|
|
44
|
+
* occurs in valid script payloads. Not a general sanitizer for arbitrary UNTRUSTED
|
|
45
|
+
* source; for untrusted dynamic data, JSON-encode it and read it back, rather than
|
|
46
|
+
* inlining it as code.
|
|
47
|
+
*
|
|
48
|
+
* Used by the Script handle's <Scripts> renderer for inline `children`.
|
|
49
|
+
*/
|
|
50
|
+
export function escapeScriptBody(js: string): string {
|
|
51
|
+
return js.replace(/<!--/g, "<\\u0021--").replace(/<\/(script)/gi, "<\\/$1");
|
|
52
|
+
}
|
package/src/handle.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { missingInjectedIdError } from "./missing-id-error.js";
|
|
2
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Handle definition for accumulating data across route segments.
|
|
3
6
|
*
|
|
@@ -43,10 +46,10 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
43
46
|
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
44
47
|
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Monotonic counter for runtime fallback ids (see createHandle). Only used
|
|
50
|
+
// when no build id was injected (a bare unit test).
|
|
51
|
+
let runtimeHandleIdCounter = 0;
|
|
52
|
+
|
|
50
53
|
export function getCollectFn(
|
|
51
54
|
id: string,
|
|
52
55
|
): ((segments: unknown[][]) => unknown) | undefined {
|
|
@@ -93,28 +96,36 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
93
96
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
94
97
|
__injectedId?: string,
|
|
95
98
|
): Handle<TData, TAccumulated> {
|
|
96
|
-
|
|
99
|
+
let handleId = __injectedId ?? "";
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// No build-injected id. Under a test runner: fall back to a synthetic id so the
|
|
102
|
+
// collect registers below and the handle is exercisable in tests (useHandle,
|
|
103
|
+
// collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
|
|
104
|
+
// or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
|
|
105
|
+
// fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
|
|
106
|
+
// so a production build folds it away and tree-shakes missing-id-error.ts out,
|
|
107
|
+
// shipping the small throw instead. isUnderTestRunner() is runtime-safe.
|
|
108
|
+
if (!handleId) {
|
|
109
|
+
if (isUnderTestRunner()) {
|
|
110
|
+
handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
|
|
111
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
112
|
+
throw missingInjectedIdError("Handle", "createHandle");
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"[rango] Handle is missing $$id — the build plugin did not inject one. " +
|
|
116
|
+
"Export it as `export const X = createHandle(...)`.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
const collectFn =
|
|
107
122
|
collect ??
|
|
108
123
|
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
handleId,
|
|
115
|
-
collectFn as (segments: unknown[][]) => unknown,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
125
|
+
collectRegistry.set(
|
|
126
|
+
handleId,
|
|
127
|
+
collectFn as (segments: unknown[][]) => unknown,
|
|
128
|
+
);
|
|
118
129
|
|
|
119
130
|
return {
|
|
120
131
|
__brand: "handle" as const,
|
|
@@ -122,9 +133,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
122
133
|
};
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
/**
|
|
126
|
-
* Type guard to check if a value is a Handle.
|
|
127
|
-
*/
|
|
128
136
|
export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
129
137
|
return (
|
|
130
138
|
typeof value === "object" &&
|
|
@@ -151,7 +159,7 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
151
159
|
const collectFn = getCollectFn(handle.$$id);
|
|
152
160
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
153
161
|
console.warn(
|
|
154
|
-
`[
|
|
162
|
+
`[rango] Handle "${handle.$$id}" has no registered collect function. ` +
|
|
155
163
|
`Falling back to flat array. Ensure the handle module is imported so ` +
|
|
156
164
|
`createHandle() runs and registers the collect function.`,
|
|
157
165
|
);
|