@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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 +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
package/src/client.rsc.tsx
CHANGED
|
@@ -101,8 +101,5 @@ export { useHref } from "./browser/react/use-href.js";
|
|
|
101
101
|
export { useReverse } from "./browser/react/use-reverse.js";
|
|
102
102
|
|
|
103
103
|
export { useHandle } from "./browser/react/use-handle.js";
|
|
104
|
-
// Type a deferred-aware consumer narrows: an accumulated entry may be a Promise
|
|
105
|
-
// (a `ctx.use(Handle).defer()` slot) until it resolves.
|
|
106
|
-
export type { DeferredHandleEntry } from "./defer.js";
|
|
107
104
|
|
|
108
105
|
export { useLocationState } from "./browser/react/location-state.js";
|
package/src/client.tsx
CHANGED
|
@@ -389,9 +389,6 @@ export {
|
|
|
389
389
|
|
|
390
390
|
export { type Handle } from "./handle.js";
|
|
391
391
|
export { useHandle } from "./browser/react/use-handle.js";
|
|
392
|
-
// Type a deferred-aware consumer narrows: an accumulated entry may be a Promise
|
|
393
|
-
// (a `ctx.use(Handle).defer()` slot) until it resolves.
|
|
394
|
-
export type { DeferredHandleEntry } from "./defer.js";
|
|
395
392
|
|
|
396
393
|
export { Meta } from "./handles/meta.js";
|
|
397
394
|
export { MetaTags } from "./handles/MetaTags.js";
|
|
@@ -39,7 +39,7 @@ import { _getRequestContext } from "../server/request-context.js";
|
|
|
39
39
|
import {
|
|
40
40
|
type RouterTracingConfig,
|
|
41
41
|
type SpanRunner,
|
|
42
|
-
type
|
|
42
|
+
type TracingToggleOptions,
|
|
43
43
|
NOOP_TRACE_SPAN,
|
|
44
44
|
} from "../router/tracing.js";
|
|
45
45
|
|
|
@@ -57,13 +57,12 @@ interface CloudflareTracing {
|
|
|
57
57
|
enterSpan<T>(name: string, callback: (span: CloudflareSpan) => T): T;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
60
|
+
/**
|
|
61
|
+
* Options for createCloudflareTracing. Alias of the shared
|
|
62
|
+
* {@link TracingToggleOptions} (`enabled` master switch + per-phase `spans`
|
|
63
|
+
* toggles); the name is public API.
|
|
64
|
+
*/
|
|
65
|
+
export type CloudflareTracingOptions = TracingToggleOptions;
|
|
67
66
|
|
|
68
67
|
/**
|
|
69
68
|
* Resolve the per-request Cloudflare tracer from the active execution context.
|
package/src/defer.ts
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
* // deep async component, far from ctx:
|
|
15
15
|
* resolve({ label, href, content }); // identical call, just deferred
|
|
16
16
|
*
|
|
17
|
-
* Under the hood the reserved slot is a Promise
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* slot whose resolver is never called would keep the
|
|
22
|
-
* response —
|
|
23
|
-
* (default {@link DEFAULT_DEFER_TIMEOUT_MS}) if the resolver is
|
|
24
|
-
* degrading gracefully (and warning in dev) instead of hanging
|
|
17
|
+
* Under the hood the reserved slot is a Promise. Handle values are resolved
|
|
18
|
+
* before any consumer sees them (resolve-by-default: the full render resolves
|
|
19
|
+
* server-side, navigation resolves client-side before apply), so `useHandle`
|
|
20
|
+
* receives the resolved value, never the Promise. The hazard that guards against
|
|
21
|
+
* bugs: a deferred slot whose resolver is never called would keep the render —
|
|
22
|
+
* and the HTTP response — waiting forever. So a deferred auto-resolves to `else`
|
|
23
|
+
* after `timeoutMs` (default {@link DEFAULT_DEFER_TIMEOUT_MS}) if the resolver is
|
|
24
|
+
* never called, degrading gracefully (and warning in dev) instead of hanging.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
/** Default auto-resolve window. Long enough for genuine deep async work, short
|
|
@@ -74,24 +74,13 @@ export type HandlePush<TData> = HandlePushFn<TData> & {
|
|
|
74
74
|
* re-enter the deadlock-guard push-callback scope a direct push thunk gets,
|
|
75
75
|
* because a deferred resolver fires after the handler phase has closed.
|
|
76
76
|
*
|
|
77
|
-
* The reserved slot
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
80
|
-
* check) before dereferencing.
|
|
77
|
+
* The reserved slot is resolved before any consumer reads it
|
|
78
|
+
* (resolve-by-default), so `useHandle` receives the resolved value (or the
|
|
79
|
+
* `else` fallback on timeout), never a Promise.
|
|
81
80
|
*/
|
|
82
81
|
defer(options?: DeferOptions<TData>): HandlePushFn<TData>;
|
|
83
82
|
};
|
|
84
83
|
|
|
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
84
|
// Internal: a timeout-bounded { promise, resolve }. Not part of the public API
|
|
96
85
|
// (the public surface is `ctx.use(Handle).defer()`); exported for `withDefer`
|
|
97
86
|
// and unit tests only. Resolves to `T`, the `else` fallback, or `undefined`.
|
package/src/handle.ts
CHANGED
|
@@ -10,8 +10,11 @@ import { isUnderTestRunner } from "./runtime-env.js";
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
|
-
* // Define a handle (name auto-generated from file + export)
|
|
14
|
-
*
|
|
13
|
+
* // Define a handle (name auto-generated from file + export).
|
|
14
|
+
* // Default collect is the identity: one array per segment that pushed.
|
|
15
|
+
* export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
16
|
+
* (segments) => segments.flat(), // opt into a single flat list
|
|
17
|
+
* );
|
|
15
18
|
*
|
|
16
19
|
* // Use in handler
|
|
17
20
|
* const push = ctx.use(Breadcrumbs);
|
|
@@ -21,7 +24,7 @@ import { isUnderTestRunner } from "./runtime-env.js";
|
|
|
21
24
|
* const crumbs = useHandle(Breadcrumbs);
|
|
22
25
|
* ```
|
|
23
26
|
*/
|
|
24
|
-
export interface Handle<TData, TAccumulated = TData[]> {
|
|
27
|
+
export interface Handle<TData, TAccumulated = TData[][]> {
|
|
25
28
|
/**
|
|
26
29
|
* Brand to distinguish handles from loaders in ctx.use()
|
|
27
30
|
*/
|
|
@@ -35,10 +38,13 @@ export interface Handle<TData, TAccumulated = TData[]> {
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
|
-
* Default collect
|
|
41
|
+
* Default collect: pass the per-segment data through as-is — one array per segment
|
|
42
|
+
* that pushed, in segment order. Lossless, so a consumer can tell which/how-many
|
|
43
|
+
* segments contributed. Callers that want a single flat list opt in with
|
|
44
|
+
* `createHandle((segments) => segments.flat())`.
|
|
39
45
|
*/
|
|
40
|
-
function defaultCollect<T>(segments: T[][]): T[] {
|
|
41
|
-
return segments
|
|
46
|
+
function defaultCollect<T>(segments: T[][]): T[][] {
|
|
47
|
+
return segments;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
// Module-level registry mapping $$id to collect functions.
|
|
@@ -62,13 +68,22 @@ export function getCollectFn(
|
|
|
62
68
|
* The $$id is auto-generated by the Vite exposeInternalIds plugin based on
|
|
63
69
|
* file path and export name. No manual naming required.
|
|
64
70
|
*
|
|
65
|
-
* @param collect - Optional collect function
|
|
71
|
+
* @param collect - Optional collect function. Default: pass the per-segment data
|
|
72
|
+
* through as-is (one array per segment that pushed, in segment order). Lossless,
|
|
73
|
+
* so a consumer can tell which/how-many segments contributed. Opt into a single
|
|
74
|
+
* flat list with `(segments) => segments.flat()`.
|
|
66
75
|
* @param __injectedId - Auto-injected by Vite plugin, do not provide manually
|
|
67
76
|
*
|
|
68
77
|
* @example
|
|
69
78
|
* ```ts
|
|
70
|
-
* // Default:
|
|
71
|
-
* export const
|
|
79
|
+
* // Default: per-segment grouping, as-is
|
|
80
|
+
* export const Pushed = createHandle<string>();
|
|
81
|
+
* // Result type: string[][] (e.g. [["a"], ["b", "c"]])
|
|
82
|
+
*
|
|
83
|
+
* // Opt into a single flat list
|
|
84
|
+
* export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
85
|
+
* (segments) => segments.flat()
|
|
86
|
+
* );
|
|
72
87
|
* // Result type: BreadcrumbItem[]
|
|
73
88
|
*
|
|
74
89
|
* // Custom: last value wins
|
|
@@ -83,8 +98,9 @@ export function getCollectFn(
|
|
|
83
98
|
* );
|
|
84
99
|
* // Result type: MetaTags
|
|
85
100
|
*
|
|
86
|
-
* // Custom: dedupe by href
|
|
87
|
-
*
|
|
101
|
+
* // Custom: dedupe by href (TAccumulated must be given — a custom collect that
|
|
102
|
+
* // returns a flat array no longer matches the default TData[][])
|
|
103
|
+
* export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
88
104
|
* (segments) => {
|
|
89
105
|
* const all = segments.flat();
|
|
90
106
|
* return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
|
|
@@ -92,7 +108,7 @@ export function getCollectFn(
|
|
|
92
108
|
* );
|
|
93
109
|
* ```
|
|
94
110
|
*/
|
|
95
|
-
export function createHandle<TData, TAccumulated = TData[]>(
|
|
111
|
+
export function createHandle<TData, TAccumulated = TData[][]>(
|
|
96
112
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
97
113
|
__injectedId?: string,
|
|
98
114
|
): Handle<TData, TAccumulated> {
|
|
@@ -156,12 +172,18 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
156
172
|
data: Record<string, Record<string, unknown[]>>,
|
|
157
173
|
segmentOrder: string[],
|
|
158
174
|
): TAccumulated {
|
|
175
|
+
// Fall back to the default (identity, pass-through) collect when none is
|
|
176
|
+
// registered — e.g. the handle's module was not imported so createHandle() never
|
|
177
|
+
// ran. This is harmless for a handle that wanted the default, but a handle with a
|
|
178
|
+
// CUSTOM collect that failed to register silently gets the wrong shape (identity
|
|
179
|
+
// TData[][]) cast as its declared TAccumulated. The runtime can't tell the two
|
|
180
|
+
// apart (a Handle only carries $$id), so warn in dev (folded out of production).
|
|
159
181
|
const collectFn = getCollectFn(handle.$$id);
|
|
160
182
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
161
183
|
console.warn(
|
|
162
|
-
`[rango] Handle "${handle.$$id}" has no registered collect
|
|
163
|
-
`
|
|
164
|
-
`createHandle() runs
|
|
184
|
+
`[rango] Handle "${handle.$$id}" has no registered collect — falling back ` +
|
|
185
|
+
`to the identity (per-segment data as-is). If this handle declares a ` +
|
|
186
|
+
`CUSTOM collect, import its module so createHandle() runs where it is read.`,
|
|
165
187
|
);
|
|
166
188
|
}
|
|
167
189
|
const collect = (collectFn ??
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Component to render collected meta descriptors in the document head.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
6
|
+
* Deferred (Promise) meta descriptors are resolved before MetaTags renders
|
|
7
|
+
* (server-side on the full render, client-side before apply on navigation), so
|
|
8
|
+
* it only ever receives resolved descriptors and never suspends.
|
|
8
9
|
*
|
|
9
10
|
* When theme is enabled in the router config, MetaTags also renders
|
|
10
11
|
* the theme initialization script to prevent FOUC (flash of unstyled content).
|
|
@@ -28,11 +29,9 @@
|
|
|
28
29
|
* ```
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
|
-
import { use } from "react";
|
|
32
32
|
import { useHandle } from "../browser/react/use-handle.js";
|
|
33
33
|
import { Meta } from "./meta.js";
|
|
34
|
-
import {
|
|
35
|
-
import type { MetaDescriptor, MetaDescriptorBase } from "../router/types.js";
|
|
34
|
+
import type { MetaDescriptorBase } from "../router/types.js";
|
|
36
35
|
import { useThemeContext } from "../theme/theme-context.js";
|
|
37
36
|
import { generateThemeScript } from "../theme/theme-script.js";
|
|
38
37
|
import { useNonce } from "../browser/react/nonce-context.js";
|
|
@@ -96,17 +95,7 @@ function hasTagName(
|
|
|
96
95
|
);
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
* Check if a value is a Promise. Uses the shared thenable predicate (callable
|
|
101
|
-
* `then`) so collect (meta.ts) and render never disagree: an object carrying a
|
|
102
|
-
* non-callable `then` (e.g. `{ then: 5 }`) is a SYNC descriptor on both sides,
|
|
103
|
-
* not a Promise that would crash React's `use()`.
|
|
104
|
-
*/
|
|
105
|
-
function isPromise(value: unknown): value is Promise<unknown> {
|
|
106
|
-
return isThenable(value);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function renderMetaDescriptor(
|
|
98
|
+
export function renderMetaDescriptor(
|
|
110
99
|
descriptor: MetaDescriptorBase,
|
|
111
100
|
index: number,
|
|
112
101
|
): React.ReactNode {
|
|
@@ -189,57 +178,6 @@ function renderMetaDescriptor(
|
|
|
189
178
|
);
|
|
190
179
|
}
|
|
191
180
|
|
|
192
|
-
// Sentinel a rejected async descriptor resolves to: renderMetaDescriptor sees
|
|
193
|
-
// no recognized fields and returns nothing renderable (see renderRejected).
|
|
194
|
-
const REJECTED_META: unique symbol = Symbol("rango.rejectedMeta");
|
|
195
|
-
|
|
196
|
-
// Cache the rejection-swallowing wrapper per source promise so use() gets a
|
|
197
|
-
// stable reference across re-renders (a fresh .then() each render would make
|
|
198
|
-
// React treat it as a new pending promise and never settle). WeakMap keys on
|
|
199
|
-
// the original promise so entries are collected with it.
|
|
200
|
-
const safeMetaPromises = new WeakMap<
|
|
201
|
-
Promise<MetaDescriptorBase>,
|
|
202
|
-
Promise<MetaDescriptorBase | typeof REJECTED_META>
|
|
203
|
-
>();
|
|
204
|
-
|
|
205
|
-
function toSafeMetaPromise(
|
|
206
|
-
promise: Promise<MetaDescriptorBase>,
|
|
207
|
-
): Promise<MetaDescriptorBase | typeof REJECTED_META> {
|
|
208
|
-
let safe = safeMetaPromises.get(promise);
|
|
209
|
-
if (!safe) {
|
|
210
|
-
// Swallow the rejection at the promise boundary, not via an error boundary:
|
|
211
|
-
// an error boundary above a suspended use() makes React abandon the whole
|
|
212
|
-
// Suspense subtree (and on the server switch it to client rendering). A
|
|
213
|
-
// settled-to-sentinel promise degrades the single bad descriptor to nothing
|
|
214
|
-
// while every sibling descriptor still renders.
|
|
215
|
-
//
|
|
216
|
-
// Normalize via Promise.resolve first: a collected async descriptor may be a
|
|
217
|
-
// non-native thenable (a React wakeable in SSR/RSC) whose .then() returns
|
|
218
|
-
// void rather than a Promise. Calling .then directly would leave `safe`
|
|
219
|
-
// undefined and use(undefined) would throw ("unsupported type passed to
|
|
220
|
-
// use()"), 500-ing the page. Promise.resolve adopts the thenable into a
|
|
221
|
-
// native Promise whose .then always returns one.
|
|
222
|
-
safe = Promise.resolve(promise).then(
|
|
223
|
-
(value) => value,
|
|
224
|
-
() => REJECTED_META,
|
|
225
|
-
);
|
|
226
|
-
safeMetaPromises.set(promise, safe);
|
|
227
|
-
}
|
|
228
|
-
return safe;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function AsyncMetaTag({
|
|
232
|
-
promise,
|
|
233
|
-
index,
|
|
234
|
-
}: {
|
|
235
|
-
promise: Promise<MetaDescriptorBase>;
|
|
236
|
-
index: number;
|
|
237
|
-
}): React.ReactNode {
|
|
238
|
-
const resolved = use(toSafeMetaPromise(promise));
|
|
239
|
-
if (resolved === REJECTED_META) return null;
|
|
240
|
-
return renderMetaDescriptor(resolved, index);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
181
|
/**
|
|
244
182
|
* Renders all collected meta descriptors from route handlers.
|
|
245
183
|
*
|
|
@@ -249,11 +187,16 @@ export function AsyncMetaTag({
|
|
|
249
187
|
* When theme is enabled in router config, also renders the theme initialization
|
|
250
188
|
* script to prevent FOUC (flash of unstyled content).
|
|
251
189
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
190
|
+
* Deferred (Promise) meta descriptors are resolved BEFORE MetaTags renders —
|
|
191
|
+
* server-side on the full/SSR render, client-side before apply on navigation
|
|
192
|
+
* (resolve-by-default) — so MetaTags only ever receives resolved descriptors and
|
|
193
|
+
* never suspends.
|
|
254
194
|
*/
|
|
255
195
|
export function MetaTags(): React.ReactNode {
|
|
256
|
-
|
|
196
|
+
// Deferred descriptors are resolved BEFORE collect runs (resolve-by-default),
|
|
197
|
+
// and collectMeta strips unset markers, so the collected output is always
|
|
198
|
+
// resolved base descriptors (never a Promise).
|
|
199
|
+
const descriptors = useHandle(Meta) as MetaDescriptorBase[];
|
|
257
200
|
const themeConfig = useThemeContext()?.config ?? null;
|
|
258
201
|
const nonce = useNonce();
|
|
259
202
|
|
|
@@ -266,18 +209,9 @@ export function MetaTags(): React.ReactNode {
|
|
|
266
209
|
dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
|
|
267
210
|
/>
|
|
268
211
|
)}
|
|
269
|
-
{descriptors.map((descriptor, index) =>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<AsyncMetaTag
|
|
273
|
-
key={`async-${index}`}
|
|
274
|
-
promise={descriptor}
|
|
275
|
-
index={index}
|
|
276
|
-
/>
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
return renderMetaDescriptor(descriptor, index);
|
|
280
|
-
})}
|
|
212
|
+
{descriptors.map((descriptor, index) =>
|
|
213
|
+
renderMetaDescriptor(descriptor, index),
|
|
214
|
+
)}
|
|
281
215
|
</>
|
|
282
216
|
);
|
|
283
217
|
}
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
import type { ReactNode } from "react";
|
|
25
25
|
import { createHandle, type Handle } from "../handle.js";
|
|
26
|
-
import { isThenable } from "./is-thenable.js";
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* A single breadcrumb item.
|
|
@@ -42,34 +41,33 @@ export interface BreadcrumbItem {
|
|
|
42
41
|
* Collect function for Breadcrumbs handle.
|
|
43
42
|
* Flattens segments in parent-to-child order with deduplication by href: each
|
|
44
43
|
* href keeps its FIRST position but takes the LAST value (re-pushing a parent
|
|
45
|
-
* href refreshes the label in place without reordering the trail).
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* href refreshes the label in place without reordering the trail). Deferred
|
|
45
|
+
* crumbs (a pushed Promise or `ctx.use(Breadcrumbs).defer()`) are resolved
|
|
46
|
+
* BEFORE collect runs (resolve-by-default), so collect only ever sees resolved
|
|
47
|
+
* items. An item without a string `href` is passed through by identity and
|
|
48
|
+
* excluded from the href dedup.
|
|
50
49
|
*/
|
|
51
50
|
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
52
51
|
const all = segments.flat();
|
|
53
52
|
|
|
54
|
-
const
|
|
53
|
+
const hasHref = (item: unknown): item is BreadcrumbItem =>
|
|
55
54
|
item != null &&
|
|
56
55
|
typeof item === "object" &&
|
|
57
|
-
!isThenable(item) &&
|
|
58
56
|
typeof (item as { href?: unknown }).href === "string";
|
|
59
57
|
|
|
60
|
-
// Dedup
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
58
|
+
// Dedup crumbs by href: keep the FIRST position (preserving parent->child
|
|
59
|
+
// order) but the LAST value (a child re-pushing a parent's href can refresh
|
|
60
|
+
// its label). Items without an href pass through by identity at their original
|
|
61
|
+
// position.
|
|
64
62
|
const valueByHref = new Map<string, BreadcrumbItem>();
|
|
65
63
|
for (const item of all) {
|
|
66
|
-
if (
|
|
64
|
+
if (hasHref(item)) valueByHref.set(item.href, item);
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
const result: BreadcrumbItem[] = [];
|
|
70
68
|
const emitted = new Set<string>();
|
|
71
69
|
for (const item of all) {
|
|
72
|
-
if (!
|
|
70
|
+
if (!hasHref(item)) {
|
|
73
71
|
result.push(item);
|
|
74
72
|
continue;
|
|
75
73
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { HandleData, HandleStore } from "../server/handle-store.js";
|
|
2
|
+
import { isThenable } from "./is-thenable.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime-neutral resolution of deferred (Promise) handle values. Shared by the
|
|
6
|
+
* server full-render path (resolve before the payload is finalized so SSR sees
|
|
7
|
+
* resolved values) and the client soft-nav path (NavigationProvider resolves
|
|
8
|
+
* each yield before applying it). Pure — imports only `isThenable` — so it is
|
|
9
|
+
* safe in both the RSC server environment AND the browser.
|
|
10
|
+
*
|
|
11
|
+
* Resolution is SHALLOW: only an entry that is ITSELF a thenable is awaited. A
|
|
12
|
+
* value that merely contains a promise (e.g. `{ data: somePromise }`) is a
|
|
13
|
+
* non-thenable, so the `isThenable` check skips it and it passes through verbatim
|
|
14
|
+
* — the nested promise is the consumer's responsibility. Resolution runs each
|
|
15
|
+
* entry through `Promise.all` with a per-entry `try/catch` (NOT `allSettled`), so
|
|
16
|
+
* one rejecting entry is dropped without rejecting the batch or aborting siblings.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle names ($$id) whose snapshot carries a deferred (top-level Promise)
|
|
21
|
+
* value. Used by the client to decide which buckets to hold-and-resolve, and
|
|
22
|
+
* `.size > 0` answers "does this snapshot have any deferred value".
|
|
23
|
+
*/
|
|
24
|
+
export function deferredHandleNames(data: HandleData): Set<string> {
|
|
25
|
+
const names = new Set<string>();
|
|
26
|
+
for (const [handleName, segments] of Object.entries(data)) {
|
|
27
|
+
for (const values of Object.values(segments)) {
|
|
28
|
+
if (values.some(isThenable)) {
|
|
29
|
+
names.add(handleName);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot with deferred (Promise) values awaited (shallow — only top-level
|
|
39
|
+
* thenables). A rejected deferred is dropped, and a deferred that resolves to
|
|
40
|
+
* `null`/`undefined` is dropped (the `.defer({ else })` skip semantics), so a
|
|
41
|
+
* nullish deferred slot never reaches a collector. Sync (non-thenable) values —
|
|
42
|
+
* including a legitimate sync `null` push — pass through unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveDeferredHandleValues(
|
|
45
|
+
data: HandleData,
|
|
46
|
+
): Promise<HandleData> {
|
|
47
|
+
const out: HandleData = {};
|
|
48
|
+
await Promise.all(
|
|
49
|
+
Object.entries(data).flatMap(([handleName, segments]) => {
|
|
50
|
+
out[handleName] = {};
|
|
51
|
+
return Object.entries(segments).map(async ([segmentId, values]) => {
|
|
52
|
+
out[handleName][segmentId] = await resolveValues(values);
|
|
53
|
+
});
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Resolve each entry IN ORDER. A non-thenable passes through verbatim (including
|
|
60
|
+
// a legitimate sync `null`/`undefined` push — the "await iff thenable" contract).
|
|
61
|
+
// A thenable is awaited and dropped when it rejects OR resolves to null/undefined
|
|
62
|
+
// (the `.defer({ else })` skip semantics: a forgotten resolver times out to
|
|
63
|
+
// `else ?? undefined`), so a nullish deferred slot never reaches a collector.
|
|
64
|
+
async function resolveValues(values: unknown[]): Promise<unknown[]> {
|
|
65
|
+
const resolved = await Promise.all(
|
|
66
|
+
values.map(async (v) => {
|
|
67
|
+
if (!isThenable(v)) return { keep: true, value: v };
|
|
68
|
+
try {
|
|
69
|
+
const value = await v;
|
|
70
|
+
return { keep: value != null, value };
|
|
71
|
+
} catch {
|
|
72
|
+
return { keep: false, value: undefined };
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
return resolved.filter((r) => r.keep).map((r) => r.value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Full-render handle stream: a one-shot async generator that yields the FINAL
|
|
81
|
+
* handle snapshot with every top-level deferred value resolved. Drop-in for
|
|
82
|
+
* `handleStore.stream()` on full / HTML renders (initial load, 404, progressive
|
|
83
|
+
* enhancement), where the consumer drains the generator to completion before
|
|
84
|
+
* rendering anyway — so the single final yield is equivalent to the streamed
|
|
85
|
+
* yields' converged state, and the SSR markup plus the first sync `useHandle`
|
|
86
|
+
* read see resolved values. `getData()` seals the store and awaits the handler
|
|
87
|
+
* barrier; `resolveDeferredHandleValues` then awaits the pushed-value promises.
|
|
88
|
+
* A hung handle promise blocks the full render like any unresolved await (no
|
|
89
|
+
* handle-specific timeout). Partial / action payloads keep streaming via
|
|
90
|
+
* `handleStore.stream()`.
|
|
91
|
+
*/
|
|
92
|
+
export async function* resolvedHandleStream(
|
|
93
|
+
handleStore: HandleStore,
|
|
94
|
+
): AsyncGenerator<HandleData, void, unknown> {
|
|
95
|
+
// Drain stream() (NOT getData()) for the converged snapshot: stream() sets the
|
|
96
|
+
// store's `completed` flag on seal+settle, and that flag is what makes a LATE
|
|
97
|
+
// push throw LateHandlePushError — an async JSX subtree that suspended and later
|
|
98
|
+
// calls ctx.use(Handle)(...) after collection. getData() never sets `completed`,
|
|
99
|
+
// so the late push would silently land. Both wait for the same settle barrier, so
|
|
100
|
+
// the final yielded value is identical; we just keep the late-push guard.
|
|
101
|
+
let snapshot: HandleData = {};
|
|
102
|
+
for await (const data of handleStore.stream()) {
|
|
103
|
+
snapshot = data;
|
|
104
|
+
}
|
|
105
|
+
yield await resolveDeferredHandleValues(snapshot);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the deferred (Promise) values in ONE segment's handle bucket
|
|
110
|
+
* (`handleName -> entries[]`). Same shallow + drop-rejections rule as
|
|
111
|
+
* {@link resolveDeferredHandleValues}, but for the segment-keyed shape the
|
|
112
|
+
* prerender/static collection paths use (`getDataForSegment`). Prerender is a
|
|
113
|
+
* build-time cache, so the baked artifact must hold resolved values — a hit
|
|
114
|
+
* replays them into the segment system, where collect/useHandle expect resolved
|
|
115
|
+
* data.
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveSegmentHandleValues(
|
|
118
|
+
segHandles: Record<string, unknown[]>,
|
|
119
|
+
): Promise<Record<string, unknown[]>> {
|
|
120
|
+
const out: Record<string, unknown[]> = {};
|
|
121
|
+
await Promise.all(
|
|
122
|
+
Object.entries(segHandles).map(async ([handleName, values]) => {
|
|
123
|
+
out[handleName] = await resolveValues(values);
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Single thenable predicate
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Single thenable predicate for the resolve-by-default handle machinery
|
|
3
|
+
* (`handles/deferred-resolution.ts`, its sole owner) — it decides which top-level
|
|
4
|
+
* handle entries are deferred (`Promise`) values to await before any consumer
|
|
5
|
+
* sees them, and which pass through verbatim.
|
|
5
6
|
*
|
|
6
7
|
* Requires a CALLABLE `then` (`typeof obj.then === "function"`), not merely a
|
|
7
|
-
* `"then" in obj` membership check
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* React's `use()` on a non-thenable and throw. One owner keeps the collect and
|
|
11
|
-
* render sides from ever disagreeing.
|
|
8
|
+
* `"then" in obj` membership check, so a non-promise carrying a `then` field
|
|
9
|
+
* (e.g. a serialized shape `{ then: 5 }`) is treated as a plain sync value rather
|
|
10
|
+
* than awaited. A single predicate keeps that classification consistent.
|
|
12
11
|
*/
|
|
13
12
|
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
14
13
|
return (
|
package/src/handles/meta.ts
CHANGED
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { createHandle, type Handle } from "../handle.js";
|
|
32
|
-
import { isThenable } from "./is-thenable.js";
|
|
33
32
|
import type {
|
|
34
33
|
MetaDescriptor,
|
|
35
34
|
MetaDescriptorBase,
|
|
@@ -37,12 +36,6 @@ import type {
|
|
|
37
36
|
UnsetDescriptor,
|
|
38
37
|
} from "../router/types.js";
|
|
39
38
|
|
|
40
|
-
function isPromiseDescriptor(
|
|
41
|
-
descriptor: MetaDescriptor,
|
|
42
|
-
): descriptor is Promise<MetaDescriptorBase> {
|
|
43
|
-
return isThenable(descriptor);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
39
|
function isUnsetDescriptor(
|
|
47
40
|
descriptor: MetaDescriptor,
|
|
48
41
|
): descriptor is UnsetDescriptor {
|
|
@@ -166,37 +159,9 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
166
159
|
|
|
167
160
|
for (const descriptors of segments) {
|
|
168
161
|
for (const descriptor of descriptors) {
|
|
169
|
-
// Promise descriptors
|
|
170
|
-
//
|
|
171
|
-
// key-based dedup
|
|
172
|
-
// dev when a title template is active so the author knows an async
|
|
173
|
-
// descriptor will NOT participate in the template/dedup.
|
|
174
|
-
//
|
|
175
|
-
// The warning is deliberately a GENERAL note, not a duplicate-<title>
|
|
176
|
-
// prediction: collectMeta cannot tell whether this Promise resolves to a
|
|
177
|
-
// title (which would indeed yield a 2nd <title>) or to an ordinary
|
|
178
|
-
// descriptor like an async og:image (which would not). Asserting a
|
|
179
|
-
// duplicate <title> here is a false positive for the common og:image case,
|
|
180
|
-
// so the message states only that async descriptors bypass templating —
|
|
181
|
-
// not that a duplicate <title> WILL occur.
|
|
182
|
-
if (isPromiseDescriptor(descriptor)) {
|
|
183
|
-
if (
|
|
184
|
-
titleTemplate !== undefined &&
|
|
185
|
-
process.env.NODE_ENV !== "production"
|
|
186
|
-
) {
|
|
187
|
-
console.warn(
|
|
188
|
-
`[Meta] A Promise meta descriptor was pushed while a title template is active. ` +
|
|
189
|
-
`Async descriptors bypass deduplication and title-templating: the template is ` +
|
|
190
|
-
`not applied to them. If this Promise resolves to a title, resolve the value ` +
|
|
191
|
-
`before pushing (or push a synchronous descriptor) so it participates in the ` +
|
|
192
|
-
`template; if it resolves to a non-title descriptor (e.g. og:image), this ` +
|
|
193
|
-
`note does not apply.`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
result.push(descriptor);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
162
|
+
// Deferred (Promise) descriptors are resolved BEFORE collectMeta runs
|
|
163
|
+
// (resolve-by-default), so every descriptor here is synchronous and
|
|
164
|
+
// participates in key-based dedup + title-templating like any other.
|
|
200
165
|
if (isUnsetDescriptor(descriptor)) {
|
|
201
166
|
const keyToRemove = descriptor.unset;
|
|
202
167
|
if (keyToIndex.has(keyToRemove)) {
|
|
@@ -262,12 +227,10 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
262
227
|
* Use `ctx.use(Meta)` in route handlers to push meta descriptors.
|
|
263
228
|
* Use `<MetaTags />` component to render them in the document head.
|
|
264
229
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* to participate in a layout's `%s` template, push the resolved string title
|
|
270
|
-
* synchronously rather than a `Promise<{ title }>`.
|
|
230
|
+
* Deferred (Promise) descriptors are resolved before collectMeta runs
|
|
231
|
+
* (resolve-by-default), so they participate in deduplication and `%s`
|
|
232
|
+
* title-templating identically to synchronous descriptors. A descriptor that
|
|
233
|
+
* resolves to `null`/`undefined` (or rejects) is dropped.
|
|
271
234
|
*/
|
|
272
235
|
export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
|
|
273
236
|
MetaDescriptor,
|