@rangojs/router 0.0.0-experimental.c873df95 → 0.0.0-experimental.c9471f9b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -4
- package/README.md +303 -741
- package/dist/bin/rango.js +724 -183
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +4276 -1206
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +86 -15
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +251 -24
- package/skills/caching/SKILL.md +338 -13
- 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 +110 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +87 -56
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +73 -691
- 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 +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +94 -18
- package/skills/layout/SKILL.md +62 -19
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +302 -54
- package/skills/middleware/SKILL.md +59 -16
- package/skills/migrate-nextjs/SKILL.md +745 -0
- package/skills/migrate-react-router/SKILL.md +153 -0
- 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 +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +166 -10
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +178 -124
- package/skills/rango/SKILL.md +318 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +172 -9
- package/skills/router-setup/SKILL.md +131 -11
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- 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 +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -616
- 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 +74 -15
- package/skills/vercel/SKILL.md +128 -0
- 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 +1 -66
- 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/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +252 -158
- 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/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +131 -30
- package/src/browser/navigation-client.ts +204 -87
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +203 -80
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +198 -125
- package/src/browser/prefetch/cache.ts +246 -71
- package/src/browser/prefetch/fetch.ts +358 -40
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +112 -15
- package/src/browser/react/NavigationProvider.tsx +213 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +23 -69
- 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 +32 -7
- 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 +99 -8
- package/src/browser/rsc-router.tsx +168 -28
- package/src/browser/scroll-restoration.ts +44 -27
- package/src/browser/segment-reconciler.ts +44 -12
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +244 -71
- package/src/browser/types.ts +127 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +207 -158
- package/src/build/generate-route-types.ts +6 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +198 -41
- 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 +464 -63
- 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 +333 -117
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +13 -21
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +233 -101
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2206 -372
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +116 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -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 +104 -34
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +9 -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 +100 -13
- package/src/index.ts +143 -19
- 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 +37 -41
- package/src/prerender.ts +215 -86
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +153 -77
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +85 -62
- package/src/router/loader-resolution.ts +286 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +96 -53
- package/src/router/match-api.ts +178 -218
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +48 -21
- package/src/router/match-middleware/cache-lookup.ts +178 -277
- package/src/router/match-middleware/cache-store.ts +74 -45
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +124 -50
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +300 -177
- package/src/router/navigation-snapshot.ts +133 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +203 -58
- package/src/router/preview-match.ts +35 -103
- package/src/router/request-classification.ts +291 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +256 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +146 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +204 -88
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +354 -319
- 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 +75 -0
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +105 -20
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +205 -70
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +682 -508
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +138 -15
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +509 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +62 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +167 -0
- package/src/segment-system.tsx +386 -205
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +280 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +550 -171
- package/src/ssr/index.tsx +467 -175
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -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 +199 -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 +584 -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 +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +145 -79
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +40 -75
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +102 -23
- package/src/urls/path-helper.ts +62 -111
- package/src/urls/pattern-types.ts +84 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- 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 +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +15 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +261 -6
- 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-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 +48 -95
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- 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 +553 -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 +384 -0
- 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 +138 -27
- package/src/vite/rango.ts +247 -132
- package/src/vite/router-discovery.ts +1091 -159
- 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 +98 -38
- package/src/vite/utils/shared-utils.ts +144 -44
- 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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each layout/route pushes breadcrumb items via `ctx.use(Breadcrumbs)`.
|
|
5
5
|
* Items are collected in parent-to-child order with automatic deduplication
|
|
6
|
-
* by `href
|
|
6
|
+
* by `href`: each href keeps its FIRST position but takes the LAST value, so a
|
|
7
|
+
* child re-pushing a parent href refreshes the label without reordering the trail.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```tsx
|
|
@@ -38,19 +39,45 @@ export interface BreadcrumbItem {
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Collect function for Breadcrumbs handle.
|
|
41
|
-
* Flattens segments in parent-to-child order with deduplication by href
|
|
42
|
-
*
|
|
42
|
+
* Flattens segments in parent-to-child order with deduplication by href: each
|
|
43
|
+
* href keeps its FIRST position but takes the LAST value (re-pushing a parent
|
|
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.
|
|
43
49
|
*/
|
|
44
50
|
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
45
51
|
const all = segments.flat();
|
|
46
|
-
const seen = new Map<string, number>();
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
const hasHref = (item: unknown): item is BreadcrumbItem =>
|
|
54
|
+
item != null &&
|
|
55
|
+
typeof item === "object" &&
|
|
56
|
+
typeof (item as { href?: unknown }).href === "string";
|
|
57
|
+
|
|
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.
|
|
62
|
+
const valueByHref = new Map<string, BreadcrumbItem>();
|
|
63
|
+
for (const item of all) {
|
|
64
|
+
if (hasHref(item)) valueByHref.set(item.href, item);
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const result: BreadcrumbItem[] = [];
|
|
68
|
+
const emitted = new Set<string>();
|
|
69
|
+
for (const item of all) {
|
|
70
|
+
if (!hasHref(item)) {
|
|
71
|
+
result.push(item);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Emit each href once, at its first occurrence, with the final value.
|
|
75
|
+
if (!emitted.has(item.href)) {
|
|
76
|
+
emitted.add(item.href);
|
|
77
|
+
result.push(valueByHref.get(item.href)!);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* Requires a CALLABLE `then` (`typeof obj.then === "function"`), not merely a
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
13
|
+
return (
|
|
14
|
+
value !== null &&
|
|
15
|
+
typeof value === "object" &&
|
|
16
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/handles/meta.ts
CHANGED
|
@@ -31,13 +31,11 @@
|
|
|
31
31
|
import { createHandle, type Handle } from "../handle.js";
|
|
32
32
|
import type {
|
|
33
33
|
MetaDescriptor,
|
|
34
|
+
MetaDescriptorBase,
|
|
34
35
|
TitleDescriptor,
|
|
35
36
|
UnsetDescriptor,
|
|
36
37
|
} from "../router/types.js";
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
* Type guard for unset descriptor
|
|
40
|
-
*/
|
|
41
39
|
function isUnsetDescriptor(
|
|
42
40
|
descriptor: MetaDescriptor,
|
|
43
41
|
): descriptor is UnsetDescriptor {
|
|
@@ -49,9 +47,6 @@ function isUnsetDescriptor(
|
|
|
49
47
|
);
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
/**
|
|
53
|
-
* Type guard for title descriptor (any form)
|
|
54
|
-
*/
|
|
55
50
|
function isTitleDescriptor(
|
|
56
51
|
descriptor: MetaDescriptor,
|
|
57
52
|
): descriptor is { title: TitleDescriptor } {
|
|
@@ -62,9 +57,6 @@ function isTitleDescriptor(
|
|
|
62
57
|
);
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
/**
|
|
66
|
-
* Type guard for title template descriptor
|
|
67
|
-
*/
|
|
68
60
|
function isTitleTemplate(
|
|
69
61
|
title: TitleDescriptor,
|
|
70
62
|
): title is { template: string; default: string } {
|
|
@@ -76,21 +68,13 @@ function isTitleTemplate(
|
|
|
76
68
|
);
|
|
77
69
|
}
|
|
78
70
|
|
|
79
|
-
/**
|
|
80
|
-
* Type guard for absolute title descriptor
|
|
81
|
-
*/
|
|
82
71
|
function isAbsoluteTitle(
|
|
83
72
|
title: TitleDescriptor,
|
|
84
73
|
): title is { absolute: string } {
|
|
85
74
|
return typeof title === "object" && title !== null && "absolute" in title;
|
|
86
75
|
}
|
|
87
76
|
|
|
88
|
-
/**
|
|
89
|
-
* Get a unique key for a meta descriptor for deduplication.
|
|
90
|
-
* Returns undefined for descriptors that shouldn't be deduplicated.
|
|
91
|
-
*/
|
|
92
77
|
function getMetaKey(descriptor: MetaDescriptor): string | undefined {
|
|
93
|
-
// Skip unset descriptors - they are processed separately
|
|
94
78
|
if (isUnsetDescriptor(descriptor)) {
|
|
95
79
|
return undefined;
|
|
96
80
|
}
|
|
@@ -110,13 +94,10 @@ function getMetaKey(descriptor: MetaDescriptor): string | undefined {
|
|
|
110
94
|
return `httpEquiv:${descriptor.httpEquiv}`;
|
|
111
95
|
}
|
|
112
96
|
if ("script:ld+json" in descriptor) {
|
|
113
|
-
// JSON-LD scripts can have multiple, don't dedupe by default
|
|
114
97
|
return undefined;
|
|
115
98
|
}
|
|
116
99
|
if ("tagName" in descriptor) {
|
|
117
|
-
// For link tags, dedupe by rel if present
|
|
118
100
|
if (descriptor.tagName === "link" && "rel" in descriptor) {
|
|
119
|
-
// Some link rels should be unique (canonical), others not (stylesheet)
|
|
120
101
|
const uniqueRels = ["canonical", "icon", "apple-touch-icon"];
|
|
121
102
|
if (uniqueRels.includes(descriptor.rel as string)) {
|
|
122
103
|
return `link:${descriptor.rel}`;
|
|
@@ -136,9 +117,6 @@ const defaultMetaDescriptors: MetaDescriptor[] = [
|
|
|
136
117
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
137
118
|
];
|
|
138
119
|
|
|
139
|
-
/**
|
|
140
|
-
* Helper to add or replace a descriptor in the result array
|
|
141
|
-
*/
|
|
142
120
|
function addOrReplace(
|
|
143
121
|
result: MetaDescriptor[],
|
|
144
122
|
keyToIndex: Map<string, number>,
|
|
@@ -155,9 +133,6 @@ function addOrReplace(
|
|
|
155
133
|
}
|
|
156
134
|
}
|
|
157
135
|
|
|
158
|
-
/**
|
|
159
|
-
* Helper to update indices after removing an element
|
|
160
|
-
*/
|
|
161
136
|
function updateIndicesAfterRemoval(
|
|
162
137
|
keyToIndex: Map<string, number>,
|
|
163
138
|
removedIndex: number,
|
|
@@ -169,17 +144,11 @@ function updateIndicesAfterRemoval(
|
|
|
169
144
|
}
|
|
170
145
|
}
|
|
171
146
|
|
|
172
|
-
/**
|
|
173
|
-
* Collect function for Meta handle.
|
|
174
|
-
* Includes default meta descriptors, then deduplicates by key with later routes overriding earlier ones.
|
|
175
|
-
* Supports title templates, absolute titles, and unset descriptors.
|
|
176
|
-
*/
|
|
177
147
|
function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
178
148
|
const result: MetaDescriptor[] = [];
|
|
179
149
|
const keyToIndex = new Map<string, number>();
|
|
180
150
|
let titleTemplate: string | undefined;
|
|
181
151
|
|
|
182
|
-
// Add defaults first so they can be overridden
|
|
183
152
|
for (const descriptor of defaultMetaDescriptors) {
|
|
184
153
|
const key = getMetaKey(descriptor);
|
|
185
154
|
if (key !== undefined) {
|
|
@@ -190,7 +159,9 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
190
159
|
|
|
191
160
|
for (const descriptors of segments) {
|
|
192
161
|
for (const descriptor of descriptors) {
|
|
193
|
-
//
|
|
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.
|
|
194
165
|
if (isUnsetDescriptor(descriptor)) {
|
|
195
166
|
const keyToRemove = descriptor.unset;
|
|
196
167
|
if (keyToIndex.has(keyToRemove)) {
|
|
@@ -202,14 +173,11 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
202
173
|
continue;
|
|
203
174
|
}
|
|
204
175
|
|
|
205
|
-
// Handle title descriptors with template/absolute support
|
|
206
176
|
if (isTitleDescriptor(descriptor)) {
|
|
207
177
|
const titleValue = descriptor.title;
|
|
208
178
|
|
|
209
179
|
if (isTitleTemplate(titleValue)) {
|
|
210
|
-
// Store template for subsequent title descriptors in child segments
|
|
211
180
|
titleTemplate = titleValue.template;
|
|
212
|
-
// Set the default title
|
|
213
181
|
addOrReplace(
|
|
214
182
|
result,
|
|
215
183
|
keyToIndex,
|
|
@@ -220,7 +188,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
220
188
|
}
|
|
221
189
|
|
|
222
190
|
if (isAbsoluteTitle(titleValue)) {
|
|
223
|
-
// Absolute title bypasses any template
|
|
224
191
|
addOrReplace(
|
|
225
192
|
result,
|
|
226
193
|
keyToIndex,
|
|
@@ -230,9 +197,12 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
230
197
|
continue;
|
|
231
198
|
}
|
|
232
199
|
|
|
233
|
-
//
|
|
200
|
+
// Insert the title literally. String.prototype.replace treats the
|
|
201
|
+
// replacement string specially ($&, $`, $', $$, $n), so a title like
|
|
202
|
+
// "Save $5" or one containing "$&" would be mangled. split/join inserts
|
|
203
|
+
// the raw value with no special-character interpretation.
|
|
234
204
|
const finalTitle = titleTemplate
|
|
235
|
-
? titleTemplate.
|
|
205
|
+
? titleTemplate.split("%s").join(titleValue as string)
|
|
236
206
|
: titleValue;
|
|
237
207
|
addOrReplace(
|
|
238
208
|
result,
|
|
@@ -243,7 +213,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
243
213
|
continue;
|
|
244
214
|
}
|
|
245
215
|
|
|
246
|
-
// Handle all other descriptors
|
|
247
216
|
const key = getMetaKey(descriptor);
|
|
248
217
|
addOrReplace(result, keyToIndex, descriptor, key);
|
|
249
218
|
}
|
|
@@ -257,6 +226,11 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
257
226
|
*
|
|
258
227
|
* Use `ctx.use(Meta)` in route handlers to push meta descriptors.
|
|
259
228
|
* Use `<MetaTags />` component to render them in the document head.
|
|
229
|
+
*
|
|
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.
|
|
260
234
|
*/
|
|
261
235
|
export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
|
|
262
236
|
MetaDescriptor,
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Script handle for injecting <script> tags into the document from
|
|
3
|
+
* route/layout handlers.
|
|
4
|
+
*
|
|
5
|
+
* Push from a SERVER handler with `ctx.use(Script)(config)`; render with the
|
|
6
|
+
* `<Scripts />` component (from `@rangojs/router/client`) placed in the Document
|
|
7
|
+
* `<head>` (and optionally a second `<Scripts position="body" />` at the top of
|
|
8
|
+
* `<body>`). This mirrors the Meta / <MetaTags> pair.
|
|
9
|
+
*
|
|
10
|
+
* The request CSP nonce is applied AUTOMATICALLY by <Scripts> to document-rendered
|
|
11
|
+
* scripts; consumers never pass a nonce. (An async script first loaded on a soft
|
|
12
|
+
* navigation is injected client-side without a nonce — it relies on
|
|
13
|
+
* 'strict-dynamic' or a host allowance; see the EXECUTION CONTRACT below and the
|
|
14
|
+
* /scripts skill.) A ScriptConfig is fully serializable (it crosses the
|
|
15
|
+
* server -> client handle-collection boundary), so callbacks like onLoad are NOT
|
|
16
|
+
* supported — a consumer needing them renders their own "use client" script.
|
|
17
|
+
*
|
|
18
|
+
* EXECUTION CONTRACT (see the /scripts skill for the full story):
|
|
19
|
+
* - Inline (`children`) and ordered external (`src`, optional `defer`) scripts
|
|
20
|
+
* are DOCUMENT-LOAD scripts: they execute only when present in the initial HTML
|
|
21
|
+
* response. <Scripts> freezes them after hydration, so a later client (soft)
|
|
22
|
+
* navigation never inserts an inert copy — React creates client-mounted
|
|
23
|
+
* <script> elements via innerHTML, which the HTML spec makes non-executing.
|
|
24
|
+
* - Async external scripts (`src` + `async: true`) are React RESOURCES: they load
|
|
25
|
+
* once when first encountered, including after a soft navigation, deduped by
|
|
26
|
+
* `src`. Use this for a vendor that should load on first visit to a route.
|
|
27
|
+
* - Reusing an `id` shapes the INITIAL document output (last-push-wins); it does
|
|
28
|
+
* not re-run a script during navigation. Per-navigation behavior belongs in a
|
|
29
|
+
* "use client" component or hook (see the GtmPageViews pattern in the demo).
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* // External async loader (React resource — loads on first visit, even soft nav):
|
|
34
|
+
* ctx.use(Script)({ id: "stripe", src: "https://js.stripe.com/v3", async: true });
|
|
35
|
+
*
|
|
36
|
+
* // Inline bootstrap that self-injects its loader (GTM/GA4) — keep it inline so
|
|
37
|
+
* // React cannot hoist a declarative loader above the bootstrap:
|
|
38
|
+
* ctx.use(Script)({ id: "gtm", children: gtmBootstrap(containerId) });
|
|
39
|
+
*
|
|
40
|
+
* // External ordered (defer) with vendor attributes (document-load):
|
|
41
|
+
* ctx.use(Script)({
|
|
42
|
+
* id: "plausible",
|
|
43
|
+
* src: "https://plausible.io/js/script.js",
|
|
44
|
+
* defer: true,
|
|
45
|
+
* attributes: { "data-domain": "example.com" },
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import type { ScriptHTMLAttributes } from "react";
|
|
51
|
+
import { createHandle, type Handle } from "../handle.js";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extra attributes forwarded onto the emitted <script>. Typed by React, so the
|
|
55
|
+
* casing is React's (`crossOrigin`, not `crossorigin`) and value shapes are
|
|
56
|
+
* checked at compile time. `data-*` attributes are allowed. Two groups are
|
|
57
|
+
* excluded: the fields the Script handle manages itself (`id`, `src`, `async`,
|
|
58
|
+
* `defer`, `type`, `children`, `nonce`, `dangerouslySetInnerHTML` — set those via
|
|
59
|
+
* the ScriptConfig fields), and ALL `on*` event handlers (`onLoad`, `onError`,
|
|
60
|
+
* …): a ScriptConfig is serialized across the server -> client handle boundary, so
|
|
61
|
+
* a function cannot survive it — render your own "use client" script for callbacks.
|
|
62
|
+
*/
|
|
63
|
+
export type ScriptAttributes = Omit<
|
|
64
|
+
ScriptHTMLAttributes<HTMLScriptElement>,
|
|
65
|
+
| "id"
|
|
66
|
+
| "src"
|
|
67
|
+
| "async"
|
|
68
|
+
| "defer"
|
|
69
|
+
| "type"
|
|
70
|
+
| "children"
|
|
71
|
+
| "nonce"
|
|
72
|
+
| "dangerouslySetInnerHTML"
|
|
73
|
+
| `on${string}`
|
|
74
|
+
> & {
|
|
75
|
+
[dataAttr: `data-${string}`]: string | number | boolean | undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Fields shared by every script shape. */
|
|
79
|
+
interface ScriptConfigBase {
|
|
80
|
+
/**
|
|
81
|
+
* Where <Scripts> renders this script.
|
|
82
|
+
* - "head" (default): the `<head>` <Scripts> site.
|
|
83
|
+
* - "body": the `<Scripts position="body" />` site at the top of <body>.
|
|
84
|
+
* Note: an external `async` script is hoisted into <head> by React regardless.
|
|
85
|
+
*/
|
|
86
|
+
position?: "head" | "body";
|
|
87
|
+
/**
|
|
88
|
+
* The `type` attribute, as a free string: "module", "application/ld+json",
|
|
89
|
+
* "text/partytown", etc. Omitted means a classic script.
|
|
90
|
+
*/
|
|
91
|
+
type?: string;
|
|
92
|
+
/** Extra React-cased attributes (`data-*`, `crossOrigin`, `integrity`, ...). */
|
|
93
|
+
attributes?: ScriptAttributes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Inline script: a raw JS body rendered in place, escaped against `</script>`
|
|
98
|
+
* breakout. DOCUMENT-LOAD only (executes when present in the initial HTML;
|
|
99
|
+
* <Scripts> freezes it after hydration so navigation never inserts an inert
|
|
100
|
+
* copy). `id` is REQUIRED — inline scripts are never deduped by React, so a
|
|
101
|
+
* layout and a child pushing the same bootstrap would inject it twice. It is also
|
|
102
|
+
* rendered as the script's DOM `id`. Forbids `src`/`async`/`defer`. For analytics
|
|
103
|
+
* vendors (GTM/GA4/Segment) the body should
|
|
104
|
+
* create+append its own loader, so the loader is never a separate declarative tag
|
|
105
|
+
* React could hoist out of order.
|
|
106
|
+
*/
|
|
107
|
+
export interface InlineScriptConfig extends ScriptConfigBase {
|
|
108
|
+
id: string;
|
|
109
|
+
children: string;
|
|
110
|
+
src?: never;
|
|
111
|
+
async?: never;
|
|
112
|
+
defer?: never;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* External async script: a React-hoisted, `src`-deduped RESOURCE (the
|
|
117
|
+
* fire-and-forget loader case). Loads once when first encountered, including
|
|
118
|
+
* after a soft navigation. Deduped by `src` (matching React); `id` is optional
|
|
119
|
+
* and, when set, is rendered as the DOM `id` (not used as the dedup key here).
|
|
120
|
+
* Forbids `children`/`defer`.
|
|
121
|
+
*/
|
|
122
|
+
export interface AsyncScriptConfig extends ScriptConfigBase {
|
|
123
|
+
src: string;
|
|
124
|
+
async: true;
|
|
125
|
+
id?: string;
|
|
126
|
+
children?: never;
|
|
127
|
+
defer?: never;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* External ordered script: in-place, optionally `defer`. DOCUMENT-LOAD only
|
|
132
|
+
* (executes when present in the initial HTML; not re-run on navigation). `id` is
|
|
133
|
+
* optional (the dedup key falls back to `src`) and, when set, is rendered as the
|
|
134
|
+
* DOM `id`. Forbids `children`/`async`.
|
|
135
|
+
*/
|
|
136
|
+
export interface OrderedScriptConfig extends ScriptConfigBase {
|
|
137
|
+
src: string;
|
|
138
|
+
defer?: boolean;
|
|
139
|
+
id?: string;
|
|
140
|
+
children?: never;
|
|
141
|
+
async?: never;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A single script to inject, as a discriminated union — exactly one of:
|
|
146
|
+
* inline (`id` + `children`), external async (`src` + `async: true`), or external
|
|
147
|
+
* ordered (`src`, optional `defer`). Invalid combinations (both `src`+`children`,
|
|
148
|
+
* `async`+`defer`, inline without `id`) are compile errors. The CSP nonce is
|
|
149
|
+
* applied by <Scripts>, never here.
|
|
150
|
+
*/
|
|
151
|
+
export type ScriptConfig =
|
|
152
|
+
| InlineScriptConfig
|
|
153
|
+
| AsyncScriptConfig
|
|
154
|
+
| OrderedScriptConfig;
|
|
155
|
+
|
|
156
|
+
/** A config's runtime view, for validating untyped/serialized input. */
|
|
157
|
+
type LooseScriptConfig = {
|
|
158
|
+
id?: string;
|
|
159
|
+
src?: string;
|
|
160
|
+
children?: string;
|
|
161
|
+
async?: boolean;
|
|
162
|
+
defer?: boolean;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Dev-only validation. The discriminated union makes these states unrepresentable
|
|
167
|
+
* in TypeScript; the runtime checks exist only for untyped JavaScript callers and
|
|
168
|
+
* malformed serialized input, not as the primary contract.
|
|
169
|
+
*/
|
|
170
|
+
function validateConfigDev(config: ScriptConfig): void {
|
|
171
|
+
if (process.env.NODE_ENV === "production") return;
|
|
172
|
+
const c = config as LooseScriptConfig;
|
|
173
|
+
if (c.src != null && c.children != null) {
|
|
174
|
+
console.warn(
|
|
175
|
+
`[Script] A config has both "src" and "children"; they are mutually ` +
|
|
176
|
+
`exclusive — "src" wins and the inline body is ignored.`,
|
|
177
|
+
);
|
|
178
|
+
} else if (c.src == null && c.children == null) {
|
|
179
|
+
console.warn(
|
|
180
|
+
`[Script] A config has neither "src" nor "children"; it injects nothing.`,
|
|
181
|
+
);
|
|
182
|
+
} else if (c.src == null && c.id == null) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`[Script] An inline script was pushed without an "id" and cannot be ` +
|
|
185
|
+
`deduplicated. Pass an "id" so a layout + child pushing the same script ` +
|
|
186
|
+
`inject it only once.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (c.async && c.defer) {
|
|
190
|
+
console.warn(
|
|
191
|
+
`[Script] A config has both "async" and "defer"; they are mutually ` +
|
|
192
|
+
`exclusive.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Accumulate scripts across matched segments, parent -> child, preserving push
|
|
199
|
+
* order, last-push-wins per dedup key (mirroring the Meta handle).
|
|
200
|
+
*
|
|
201
|
+
* Dedup key:
|
|
202
|
+
* - async resources key by `src` ONLY — React itself dedups async scripts by src,
|
|
203
|
+
* so two async configs with different ids but the same src must collapse to one
|
|
204
|
+
* here (last wins) for a single, deterministic winner; otherwise React would
|
|
205
|
+
* silently pick one with undefined attribute precedence.
|
|
206
|
+
* - everything else keys by `id ?? src`.
|
|
207
|
+
*
|
|
208
|
+
* An (untyped) inline script with neither `id` nor `src` cannot be deduplicated;
|
|
209
|
+
* it is kept and validateConfigDev warns.
|
|
210
|
+
*/
|
|
211
|
+
function collectScripts(segments: ScriptConfig[][]): ScriptConfig[] {
|
|
212
|
+
const result: ScriptConfig[] = [];
|
|
213
|
+
const keyToIndex = new Map<string, number>();
|
|
214
|
+
|
|
215
|
+
for (const configs of segments) {
|
|
216
|
+
for (const config of configs) {
|
|
217
|
+
validateConfigDev(config);
|
|
218
|
+
const isAsyncResource = config.src != null && config.async === true;
|
|
219
|
+
const key = isAsyncResource ? config.src : (config.id ?? config.src);
|
|
220
|
+
if (key === undefined) {
|
|
221
|
+
result.push(config);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const existing = keyToIndex.get(key);
|
|
225
|
+
if (existing !== undefined) {
|
|
226
|
+
result[existing] = config;
|
|
227
|
+
} else {
|
|
228
|
+
keyToIndex.set(key, result.length);
|
|
229
|
+
result.push(config);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Built-in handle for injecting scripts. Uses an explicit stable id (built-ins
|
|
239
|
+
* do not rely on the Vite id-injection plugin, which only covers consumer code).
|
|
240
|
+
*/
|
|
241
|
+
export const Script: Handle<ScriptConfig, ScriptConfig[]> = createHandle<
|
|
242
|
+
ScriptConfig,
|
|
243
|
+
ScriptConfig[]
|
|
244
|
+
>(collectScripts, "__rsc_router_script__");
|