@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +63 -24
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +338 -126
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/src/context-var.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* interface PaginationData { current: number; total: number }
|
|
13
13
|
* export const Pagination = createVar<PaginationData>();
|
|
14
14
|
*
|
|
15
|
+
* // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
|
|
16
|
+
* export const User = createVar<UserData>({ cache: false });
|
|
17
|
+
*
|
|
15
18
|
* // handler
|
|
16
19
|
* ctx.set(Pagination, { current: 1, total: 4 });
|
|
17
20
|
*
|
|
@@ -23,18 +26,36 @@
|
|
|
23
26
|
export interface ContextVar<T> {
|
|
24
27
|
readonly __brand: "context-var";
|
|
25
28
|
readonly key: symbol;
|
|
29
|
+
/** When false, ctx.get(var) throws inside a cache() boundary. */
|
|
30
|
+
readonly cache: boolean;
|
|
26
31
|
/** Phantom field to carry the type parameter. Never set at runtime. */
|
|
27
32
|
readonly __type?: T;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
export interface ContextVarOptions {
|
|
36
|
+
/**
|
|
37
|
+
* When false, marks this variable as non-cacheable.
|
|
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
|
+
*
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
cache?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Create a typed context variable token.
|
|
32
49
|
*
|
|
33
50
|
* The returned object is used with ctx.set(token, value) and ctx.get(token)
|
|
34
51
|
* for compile-time-checked data flow between handlers, layouts, and middleware.
|
|
35
52
|
*/
|
|
36
|
-
export function createVar<T>(): ContextVar<T> {
|
|
37
|
-
return {
|
|
53
|
+
export function createVar<T>(options?: ContextVarOptions): ContextVar<T> {
|
|
54
|
+
return {
|
|
55
|
+
__brand: "context-var" as const,
|
|
56
|
+
key: Symbol(),
|
|
57
|
+
cache: options?.cache !== false,
|
|
58
|
+
};
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
/**
|
|
@@ -49,6 +70,48 @@ export function isContextVar(value: unknown): value is ContextVar<unknown> {
|
|
|
49
70
|
);
|
|
50
71
|
}
|
|
51
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
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Symbol used as a Set stored on the variables object to track
|
|
87
|
+
* which keys hold non-cacheable values (from write-level { cache: false }).
|
|
88
|
+
*/
|
|
89
|
+
const NON_CACHEABLE_KEYS: unique symbol = Symbol.for(
|
|
90
|
+
"rango:non-cacheable-keys",
|
|
91
|
+
) as any;
|
|
92
|
+
|
|
93
|
+
function getNonCacheableKeys(variables: any): Set<string | symbol> {
|
|
94
|
+
if (!variables[NON_CACHEABLE_KEYS]) {
|
|
95
|
+
variables[NON_CACHEABLE_KEYS] = new Set();
|
|
96
|
+
}
|
|
97
|
+
return variables[NON_CACHEABLE_KEYS];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a variable value is non-cacheable (either var-level or write-level).
|
|
102
|
+
*/
|
|
103
|
+
export function isNonCacheable(
|
|
104
|
+
variables: any,
|
|
105
|
+
keyOrVar: string | ContextVar<any>,
|
|
106
|
+
): boolean {
|
|
107
|
+
if (typeof keyOrVar !== "string" && !keyOrVar.cache) {
|
|
108
|
+
return true; // var-level policy
|
|
109
|
+
}
|
|
110
|
+
const key = typeof keyOrVar === "string" ? keyOrVar : keyOrVar.key;
|
|
111
|
+
const set = variables[NON_CACHEABLE_KEYS] as Set<string | symbol> | undefined;
|
|
112
|
+
return set?.has(key) ?? false; // write-level policy
|
|
113
|
+
}
|
|
114
|
+
|
|
52
115
|
/**
|
|
53
116
|
* Read a variable from the variables store.
|
|
54
117
|
* Accepts either a string key (legacy) or a ContextVar token (typed).
|
|
@@ -64,6 +127,17 @@ export function contextGet(
|
|
|
64
127
|
/** Keys that must never be used as string variable names */
|
|
65
128
|
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
66
129
|
|
|
130
|
+
export interface ContextSetOptions {
|
|
131
|
+
/**
|
|
132
|
+
* When false, marks this specific write as non-cacheable.
|
|
133
|
+
* "Least cacheable wins" — if either the var definition or this option
|
|
134
|
+
* says cache: false, the value is non-cacheable.
|
|
135
|
+
*
|
|
136
|
+
* @default true (inherits from createVar)
|
|
137
|
+
*/
|
|
138
|
+
cache?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
67
141
|
/**
|
|
68
142
|
* Write a variable to the variables store.
|
|
69
143
|
* Accepts either a string key (legacy) or a ContextVar token (typed).
|
|
@@ -72,6 +146,7 @@ export function contextSet(
|
|
|
72
146
|
variables: any,
|
|
73
147
|
keyOrVar: string | ContextVar<any>,
|
|
74
148
|
value: any,
|
|
149
|
+
options?: ContextSetOptions,
|
|
75
150
|
): void {
|
|
76
151
|
if (typeof keyOrVar === "string") {
|
|
77
152
|
if (FORBIDDEN_KEYS.has(keyOrVar)) {
|
|
@@ -80,7 +155,14 @@ export function contextSet(
|
|
|
80
155
|
);
|
|
81
156
|
}
|
|
82
157
|
variables[keyOrVar] = value;
|
|
158
|
+
if (options?.cache === false) {
|
|
159
|
+
getNonCacheableKeys(variables).add(keyOrVar);
|
|
160
|
+
}
|
|
83
161
|
} else {
|
|
84
162
|
variables[keyOrVar.key] = value;
|
|
163
|
+
// Track write-level non-cacheable (var-level is checked via keyOrVar.cache)
|
|
164
|
+
if (options?.cache === false) {
|
|
165
|
+
getNonCacheableKeys(variables).add(keyOrVar.key);
|
|
166
|
+
}
|
|
85
167
|
}
|
|
86
168
|
}
|
package/src/debug.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Debug utilities for manifest inspection and comparison
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type
|
|
5
|
+
import { getParallelSlotCount, type EntryData } from "./server/context";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Serialized entry for debug output
|
|
@@ -64,7 +64,7 @@ export function serializeManifest(
|
|
|
64
64
|
hasLoader: entry.loader?.length > 0,
|
|
65
65
|
hasMiddleware: entry.middleware?.length > 0,
|
|
66
66
|
hasErrorBoundary: entry.errorBoundary?.length > 0,
|
|
67
|
-
parallelCount: entry.parallel
|
|
67
|
+
parallelCount: getParallelSlotCount(entry.parallel),
|
|
68
68
|
interceptCount: entry.intercept?.length ?? 0,
|
|
69
69
|
};
|
|
70
70
|
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
if (result.fallback) {
|
|
29
|
+
errorFallback = result.fallback;
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(result.error.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { loaderData, errorFallback };
|
|
36
|
+
}
|
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/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
|
}
|
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" &&
|
|
@@ -133,3 +141,43 @@ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
|
133
141
|
(value as { __brand: unknown }).__brand === "handle"
|
|
134
142
|
);
|
|
135
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Collect handle data from a HandleData map, applying the handle's collect
|
|
147
|
+
* function over segments in order. Shared between server-side rendered()
|
|
148
|
+
* reads and client-side useHandle().
|
|
149
|
+
*
|
|
150
|
+
* @param handle - The handle to collect data for
|
|
151
|
+
* @param data - Full handle data map (handleName -> segmentId -> entries[])
|
|
152
|
+
* @param segmentOrder - Segment IDs in parent -> child resolution order
|
|
153
|
+
*/
|
|
154
|
+
export function collectHandleData<TData, TAccumulated>(
|
|
155
|
+
handle: Handle<TData, TAccumulated>,
|
|
156
|
+
data: Record<string, Record<string, unknown[]>>,
|
|
157
|
+
segmentOrder: string[],
|
|
158
|
+
): TAccumulated {
|
|
159
|
+
const collectFn = getCollectFn(handle.$$id);
|
|
160
|
+
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
161
|
+
console.warn(
|
|
162
|
+
`[rango] Handle "${handle.$$id}" has no registered collect function. ` +
|
|
163
|
+
`Falling back to flat array. Ensure the handle module is imported so ` +
|
|
164
|
+
`createHandle() runs and registers the collect function.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const collect = (collectFn ??
|
|
168
|
+
(defaultCollect as unknown as (segments: unknown[][]) => unknown)) as (
|
|
169
|
+
segments: TData[][],
|
|
170
|
+
) => TAccumulated;
|
|
171
|
+
|
|
172
|
+
const segmentData = data[handle.$$id];
|
|
173
|
+
if (!segmentData) return collect([]);
|
|
174
|
+
|
|
175
|
+
const segmentArrays: TData[][] = [];
|
|
176
|
+
for (const segmentId of segmentOrder) {
|
|
177
|
+
const entries = segmentData[segmentId];
|
|
178
|
+
if (entries && entries.length > 0) {
|
|
179
|
+
segmentArrays.push(entries as TData[]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return collect(segmentArrays);
|
|
183
|
+
}
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -97,24 +97,18 @@ function isPromise(value: unknown): value is Promise<unknown> {
|
|
|
97
97
|
return value !== null && typeof value === "object" && "then" in value;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* Render a single meta descriptor as a React element.
|
|
102
|
-
*/
|
|
103
100
|
function renderMetaDescriptor(
|
|
104
101
|
descriptor: MetaDescriptorBase,
|
|
105
102
|
index: number,
|
|
106
103
|
): React.ReactNode {
|
|
107
|
-
// charset
|
|
108
104
|
if (hasCharSet(descriptor)) {
|
|
109
105
|
return <meta key="charSet" charSet={descriptor.charSet} />;
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
// title
|
|
113
108
|
if (hasTitle(descriptor)) {
|
|
114
109
|
return <title key="title">{descriptor.title}</title>;
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
// name + content (description, viewport, etc.)
|
|
118
112
|
if (hasNameContent(descriptor)) {
|
|
119
113
|
return (
|
|
120
114
|
<meta
|
|
@@ -125,7 +119,6 @@ function renderMetaDescriptor(
|
|
|
125
119
|
);
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
// property + content (Open Graph, etc.)
|
|
129
122
|
if (hasPropertyContent(descriptor)) {
|
|
130
123
|
return (
|
|
131
124
|
<meta
|
|
@@ -136,7 +129,6 @@ function renderMetaDescriptor(
|
|
|
136
129
|
);
|
|
137
130
|
}
|
|
138
131
|
|
|
139
|
-
// http-equiv + content
|
|
140
132
|
if (hasHttpEquivContent(descriptor)) {
|
|
141
133
|
return (
|
|
142
134
|
<meta
|
|
@@ -147,7 +139,6 @@ function renderMetaDescriptor(
|
|
|
147
139
|
);
|
|
148
140
|
}
|
|
149
141
|
|
|
150
|
-
// JSON-LD structured data
|
|
151
142
|
if (hasScriptLdJson(descriptor)) {
|
|
152
143
|
const json = JSON.stringify(descriptor["script:ld+json"]);
|
|
153
144
|
return (
|
|
@@ -159,7 +150,6 @@ function renderMetaDescriptor(
|
|
|
159
150
|
);
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
// Custom tagName (meta or link with arbitrary attributes)
|
|
163
153
|
if (hasTagName(descriptor)) {
|
|
164
154
|
const { tagName, ...rest } = descriptor;
|
|
165
155
|
if (tagName === "link") {
|
|
@@ -180,7 +170,6 @@ function renderMetaDescriptor(
|
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
|
|
183
|
-
// Fallback: treat as meta attributes
|
|
184
173
|
return (
|
|
185
174
|
<meta
|
|
186
175
|
key={`meta-fallback-${index}`}
|
|
@@ -189,9 +178,6 @@ function renderMetaDescriptor(
|
|
|
189
178
|
);
|
|
190
179
|
}
|
|
191
180
|
|
|
192
|
-
/**
|
|
193
|
-
* Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
|
|
194
|
-
*/
|
|
195
181
|
function AsyncMetaTag({
|
|
196
182
|
promise,
|
|
197
183
|
index,
|