@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140
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 +17 -0
- package/README.md +432 -7
- package/dist/bin/rango.js +2073 -213
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +7258 -2714
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +140 -67
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +329 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +487 -0
- package/skills/caching/SKILL.md +357 -25
- 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 +246 -0
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +16 -10
- package/skills/document-cache/SKILL.md +87 -62
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +557 -79
- package/skills/host-router/SKILL.md +320 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +207 -15
- package/skills/layout/SKILL.md +146 -6
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +616 -54
- package/skills/middleware/SKILL.md +217 -37
- package/skills/migrate-nextjs/SKILL.md +611 -0
- package/skills/migrate-react-router/SKILL.md +927 -0
- package/skills/mime-routes/SKILL.md +42 -11
- package/skills/observability/SKILL.md +194 -0
- package/skills/parallel/SKILL.md +284 -3
- package/skills/ppr/SKILL.md +426 -0
- package/skills/prerender/SKILL.md +437 -52
- package/skills/rango/SKILL.md +369 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +263 -121
- package/skills/route/SKILL.md +350 -21
- package/skills/router-setup/SKILL.md +246 -33
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +126 -222
- 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 +9 -8
- package/skills/typesafety/SKILL.md +532 -103
- package/skills/use-cache/SKILL.md +367 -0
- 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 +77 -44
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +114 -0
- 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 +293 -202
- package/src/browser/history-state.ts +101 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +385 -576
- package/src/browser/navigation-client.ts +245 -75
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +184 -118
- package/src/browser/navigation-transaction.ts +247 -0
- package/src/browser/network-error-handler.ts +88 -0
- package/src/browser/partial-update.ts +412 -364
- package/src/browser/prefetch/cache.ts +359 -0
- package/src/browser/prefetch/fetch.ts +452 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +209 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +194 -0
- package/src/browser/react/Link.tsx +275 -68
- package/src/browser/react/NavigationProvider.tsx +265 -109
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +70 -0
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +272 -60
- package/src/browser/react/location-state.ts +90 -20
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +35 -66
- package/src/browser/react/use-handle.ts +39 -126
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +39 -13
- package/src/browser/react/use-navigation.ts +53 -69
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +98 -0
- package/src/browser/react/use-search-params.ts +51 -0
- package/src/browser/react/use-segments.ts +72 -99
- package/src/browser/response-adapter.ts +164 -0
- package/src/browser/rsc-router.tsx +300 -72
- package/src/browser/scroll-restoration.ts +138 -50
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +17 -1
- package/src/browser/server-action-bridge.ts +668 -613
- package/src/browser/types.ts +223 -51
- package/src/browser/validate-redirect-origin.ts +56 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +252 -161
- package/src/build/generate-route-types.ts +41 -1038
- package/src/build/index.ts +12 -7
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +225 -42
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +105 -0
- package/src/build/route-types/codegen.ts +113 -0
- package/src/build/route-types/include-resolution.ts +812 -0
- package/src/build/route-types/param-extraction.ts +51 -0
- package/src/build/route-types/per-module-writer.ts +144 -0
- package/src/build/route-types/router-processing.ts +695 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +223 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +60 -0
- package/src/cache/cache-policy.ts +199 -0
- package/src/cache/cache-runtime.ts +525 -0
- package/src/cache/cache-scope.ts +298 -332
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2500 -158
- 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 +17 -17
- package/src/cache/document-cache.ts +199 -92
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +111 -0
- package/src/cache/index.ts +29 -35
- package/src/cache/memory-segment-store.ts +363 -30
- package/src/cache/profile-registry.ts +88 -0
- package/src/cache/read-through-swr.ts +178 -0
- package/src/cache/segment-codec.ts +248 -0
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +156 -211
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1102 -0
- package/src/client.rsc.tsx +43 -21
- package/src/client.tsx +131 -347
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +23 -4
- package/src/components/DefaultDocument.tsx +13 -3
- package/src/context-var.ts +168 -0
- package/src/debug.ts +19 -9
- 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 +106 -10
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +110 -35
- package/src/handles/MetaTags.tsx +83 -59
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +93 -0
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +44 -53
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +20 -65
- package/src/host/errors.ts +21 -30
- package/src/host/index.ts +13 -9
- package/src/host/pattern-matcher.ts +50 -79
- package/src/host/router.ts +151 -121
- package/src/host/testing.ts +45 -32
- package/src/host/types.ts +52 -11
- package/src/host/utils.ts +2 -2
- package/src/href-client.ts +192 -57
- package/src/index.rsc.ts +177 -35
- package/src/index.ts +255 -71
- package/src/internal-debug.ts +9 -2
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +31 -99
- package/src/loader.ts +30 -12
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +41 -0
- package/src/prerender/param-hash.ts +16 -14
- package/src/prerender/store.ts +121 -21
- package/src/prerender.ts +460 -26
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +198 -128
- package/src/root-error-boundary.tsx +42 -48
- package/src/route-content-wrapper.tsx +22 -77
- package/src/route-definition/dsl-helpers.ts +1116 -0
- package/src/route-definition/helper-factories.ts +88 -0
- package/src/route-definition/helpers-types.ts +505 -0
- package/src/route-definition/index.ts +54 -0
- package/src/route-definition/redirect.ts +134 -0
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-definition.ts +1 -1481
- package/src/route-map-builder.ts +82 -144
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +71 -45
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +263 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +54 -27
- package/src/router/find-match.ts +245 -0
- package/src/router/handler-context.ts +377 -125
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +59 -28
- package/src/router/lazy-includes.ts +254 -0
- package/src/router/loader-resolution.ts +421 -157
- package/src/router/logging.ts +106 -6
- package/src/router/manifest.ts +131 -57
- package/src/router/match-api.ts +167 -246
- package/src/router/match-context.ts +4 -24
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +117 -93
- package/src/router/match-middleware/cache-lookup.ts +297 -150
- package/src/router/match-middleware/cache-store.ts +123 -51
- package/src/router/match-middleware/intercept-resolution.ts +44 -43
- package/src/router/match-middleware/segment-resolution.ts +64 -22
- package/src/router/match-pipelines.ts +11 -87
- package/src/router/match-result.ts +121 -50
- package/src/router/metrics.ts +219 -28
- package/src/router/middleware-types.ts +93 -0
- package/src/router/middleware.ts +505 -441
- 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 +311 -142
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +547 -0
- package/src/router/preview-match.ts +102 -0
- package/src/router/request-classification.ts +278 -0
- package/src/router/revalidation.ts +203 -62
- package/src/router/route-snapshot.ts +246 -0
- package/src/router/router-context.ts +45 -48
- package/src/router/router-interfaces.ts +554 -0
- package/src/router/router-options.ts +779 -0
- package/src/router/router-registry.ts +21 -0
- package/src/router/segment-resolution/fresh.ts +772 -0
- package/src/router/segment-resolution/helpers.ts +348 -0
- package/src/router/segment-resolution/loader-cache.ts +250 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/segment-resolution/revalidation.ts +1331 -0
- package/src/router/segment-resolution/static-store.ts +81 -0
- 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 +25 -1354
- package/src/router/segment-wrappers.ts +292 -0
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +261 -0
- package/src/router/telemetry.ts +377 -0
- package/src/router/timeout.ts +128 -0
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +240 -61
- package/src/router/types.ts +23 -70
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +781 -2378
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +46 -0
- package/src/rsc/handler.ts +905 -1142
- package/src/rsc/helpers.ts +275 -19
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +305 -0
- package/src/rsc/manifest-init.ts +77 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +155 -0
- package/src/rsc/progressive-enhancement.ts +502 -0
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +104 -0
- package/src/rsc/response-route-handler.ts +257 -0
- package/src/rsc/rsc-rendering.ts +337 -0
- package/src/rsc/runtime-warnings.ts +55 -0
- package/src/rsc/server-action.ts +522 -0
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +95 -12
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +99 -82
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +349 -134
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +452 -85
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +310 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +123 -42
- package/src/server/live.ts +130 -0
- package/src/server/loader-registry.ts +51 -100
- package/src/server/request-context.ts +842 -157
- package/src/server.ts +15 -8
- package/src/ssr/index.tsx +412 -136
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +45 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +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 +40 -72
- package/src/theme/ThemeScript.tsx +12 -14
- package/src/theme/constants.ts +57 -15
- package/src/theme/index.ts +3 -20
- package/src/theme/theme-context.ts +5 -35
- package/src/theme/theme-script.ts +43 -39
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +123 -0
- package/src/types/cache-types.ts +207 -0
- package/src/types/error-types.ts +132 -0
- package/src/types/global-namespace.ts +113 -0
- package/src/types/handler-context.ts +839 -0
- package/src/types/index.ts +81 -0
- package/src/types/loader-types.ts +212 -0
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +138 -0
- package/src/types/route-entry.ts +114 -0
- package/src/types/segments.ts +271 -0
- package/src/types.ts +1 -1795
- package/src/urls/include-helper.ts +162 -0
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +43 -0
- package/src/urls/path-helper-types.ts +413 -0
- package/src/urls/path-helper.ts +275 -0
- package/src/urls/pattern-types.ts +124 -0
- package/src/urls/response-types.ts +109 -0
- package/src/urls/type-extraction.ts +316 -0
- package/src/urls/urls-function.ts +80 -0
- package/src/urls.ts +1 -1341
- package/src/use-loader.tsx +406 -141
- 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 +182 -0
- package/src/vite/discovery/discover-routers.ts +389 -0
- 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 +467 -0
- package/src/vite/discovery/route-types-writer.ts +214 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +161 -0
- package/src/vite/discovery/virtual-module-codegen.ts +183 -0
- package/src/vite/index.ts +23 -2255
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +303 -0
- package/src/vite/plugins/cjs-to-esm.ts +90 -0
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +118 -0
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
- package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +805 -0
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +313 -0
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +94 -0
- package/src/vite/plugins/version-plugin.ts +263 -0
- package/src/vite/plugins/virtual-entries.ts +234 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +560 -0
- package/src/vite/router-discovery.ts +1638 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +132 -0
- 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 +15 -0
- package/src/vite/utils/package-resolution.ts +89 -0
- package/src/vite/utils/prerender-utils.ts +249 -0
- package/src/vite/utils/shared-utils.ts +269 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/react/use-client-cache.ts +0 -56
- package/src/browser/request-controller.ts +0 -164
- package/src/browser/shallow.ts +0 -35
- package/src/cache/memory-store.ts +0 -253
- package/src/handles/index.ts +0 -6
- package/src/href-context.ts +0 -33
- package/src/network-error-thrower.tsx +0 -21
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- package/src/vite/package-resolution.ts +0 -125
- package/src/vite/virtual-entries.ts +0 -114
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/handle.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { missingInjectedIdError } from "./missing-id-error.js";
|
|
2
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Handle definition for accumulating data across route segments.
|
|
3
6
|
*
|
|
@@ -7,8 +10,11 @@
|
|
|
7
10
|
*
|
|
8
11
|
* @example
|
|
9
12
|
* ```ts
|
|
10
|
-
* // Define a handle (name auto-generated from file + export)
|
|
11
|
-
*
|
|
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
|
+
* );
|
|
12
18
|
*
|
|
13
19
|
* // Use in handler
|
|
14
20
|
* const push = ctx.use(Breadcrumbs);
|
|
@@ -18,7 +24,7 @@
|
|
|
18
24
|
* const crumbs = useHandle(Breadcrumbs);
|
|
19
25
|
* ```
|
|
20
26
|
*/
|
|
21
|
-
export interface Handle<TData, TAccumulated = TData[]> {
|
|
27
|
+
export interface Handle<TData, TAccumulated = TData[][]> {
|
|
22
28
|
/**
|
|
23
29
|
* Brand to distinguish handles from loaders in ctx.use()
|
|
24
30
|
*/
|
|
@@ -29,14 +35,16 @@ export interface Handle<TData, TAccumulated = TData[]> {
|
|
|
29
35
|
* Format: "filePath#ExportName" in dev, "hash#ExportName" in production
|
|
30
36
|
*/
|
|
31
37
|
readonly $$id: string;
|
|
32
|
-
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
|
-
* 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())`.
|
|
37
45
|
*/
|
|
38
|
-
function defaultCollect<T>(segments: T[][]): T[] {
|
|
39
|
-
return segments
|
|
46
|
+
function defaultCollect<T>(segments: T[][]): T[][] {
|
|
47
|
+
return segments;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
// Module-level registry mapping $$id to collect functions.
|
|
@@ -44,11 +52,13 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
44
52
|
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
45
53
|
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export function getCollectFn(
|
|
55
|
+
// Monotonic counter for runtime fallback ids (see createHandle). Only used
|
|
56
|
+
// when no build id was injected (a bare unit test).
|
|
57
|
+
let runtimeHandleIdCounter = 0;
|
|
58
|
+
|
|
59
|
+
export function getCollectFn(
|
|
60
|
+
id: string,
|
|
61
|
+
): ((segments: unknown[][]) => unknown) | undefined {
|
|
52
62
|
return collectRegistry.get(id);
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -58,13 +68,22 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
|
|
|
58
68
|
* The $$id is auto-generated by the Vite exposeInternalIds plugin based on
|
|
59
69
|
* file path and export name. No manual naming required.
|
|
60
70
|
*
|
|
61
|
-
* @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()`.
|
|
62
75
|
* @param __injectedId - Auto-injected by Vite plugin, do not provide manually
|
|
63
76
|
*
|
|
64
77
|
* @example
|
|
65
78
|
* ```ts
|
|
66
|
-
* // Default:
|
|
67
|
-
* 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
|
+
* );
|
|
68
87
|
* // Result type: BreadcrumbItem[]
|
|
69
88
|
*
|
|
70
89
|
* // Custom: last value wins
|
|
@@ -79,8 +98,9 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
|
|
|
79
98
|
* );
|
|
80
99
|
* // Result type: MetaTags
|
|
81
100
|
*
|
|
82
|
-
* // Custom: dedupe by href
|
|
83
|
-
*
|
|
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[]>(
|
|
84
104
|
* (segments) => {
|
|
85
105
|
* const all = segments.flat();
|
|
86
106
|
* return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
|
|
@@ -88,28 +108,40 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
|
|
|
88
108
|
* );
|
|
89
109
|
* ```
|
|
90
110
|
*/
|
|
91
|
-
export function createHandle<TData, TAccumulated = TData[]>(
|
|
111
|
+
export function createHandle<TData, TAccumulated = TData[][]>(
|
|
92
112
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
93
|
-
__injectedId?: string
|
|
113
|
+
__injectedId?: string,
|
|
94
114
|
): Handle<TData, TAccumulated> {
|
|
95
|
-
|
|
115
|
+
let handleId = __injectedId ?? "";
|
|
96
116
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
// No build-injected id. Under a test runner: fall back to a synthetic id so the
|
|
118
|
+
// collect registers below and the handle is exercisable in tests (useHandle,
|
|
119
|
+
// collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
|
|
120
|
+
// or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
|
|
121
|
+
// fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
|
|
122
|
+
// so a production build folds it away and tree-shakes missing-id-error.ts out,
|
|
123
|
+
// shipping the small throw instead. isUnderTestRunner() is runtime-safe.
|
|
124
|
+
if (!handleId) {
|
|
125
|
+
if (isUnderTestRunner()) {
|
|
126
|
+
handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
|
|
127
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
128
|
+
throw missingInjectedIdError("Handle", "createHandle");
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"[rango] Handle is missing $$id — the build plugin did not inject one. " +
|
|
132
|
+
"Export it as `export const X = createHandle(...)`.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
103
135
|
}
|
|
104
136
|
|
|
105
|
-
const collectFn =
|
|
137
|
+
const collectFn =
|
|
138
|
+
collect ??
|
|
106
139
|
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
|
|
107
140
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
141
|
+
collectRegistry.set(
|
|
142
|
+
handleId,
|
|
143
|
+
collectFn as (segments: unknown[][]) => unknown,
|
|
144
|
+
);
|
|
113
145
|
|
|
114
146
|
return {
|
|
115
147
|
__brand: "handle" as const,
|
|
@@ -117,9 +149,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
117
149
|
};
|
|
118
150
|
}
|
|
119
151
|
|
|
120
|
-
/**
|
|
121
|
-
* Type guard to check if a value is a Handle.
|
|
122
|
-
*/
|
|
123
152
|
export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
124
153
|
return (
|
|
125
154
|
typeof value === "object" &&
|
|
@@ -128,3 +157,49 @@ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
|
128
157
|
(value as { __brand: unknown }).__brand === "handle"
|
|
129
158
|
);
|
|
130
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Collect handle data from a HandleData map, applying the handle's collect
|
|
163
|
+
* function over segments in order. Shared between server-side rendered()
|
|
164
|
+
* reads and client-side useHandle().
|
|
165
|
+
*
|
|
166
|
+
* @param handle - The handle to collect data for
|
|
167
|
+
* @param data - Full handle data map (handleName -> segmentId -> entries[])
|
|
168
|
+
* @param segmentOrder - Segment IDs in parent -> child resolution order
|
|
169
|
+
*/
|
|
170
|
+
export function collectHandleData<TData, TAccumulated>(
|
|
171
|
+
handle: Handle<TData, TAccumulated>,
|
|
172
|
+
data: Record<string, Record<string, unknown[]>>,
|
|
173
|
+
segmentOrder: string[],
|
|
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).
|
|
181
|
+
const collectFn = getCollectFn(handle.$$id);
|
|
182
|
+
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
183
|
+
console.warn(
|
|
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.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const collect = (collectFn ??
|
|
190
|
+
(defaultCollect as unknown as (segments: unknown[][]) => unknown)) as (
|
|
191
|
+
segments: TData[][],
|
|
192
|
+
) => TAccumulated;
|
|
193
|
+
|
|
194
|
+
const segmentData = data[handle.$$id];
|
|
195
|
+
if (!segmentData) return collect([]);
|
|
196
|
+
|
|
197
|
+
const segmentArrays: TData[][] = [];
|
|
198
|
+
for (const segmentId of segmentOrder) {
|
|
199
|
+
const entries = segmentData[segmentId];
|
|
200
|
+
if (entries && entries.length > 0) {
|
|
201
|
+
segmentArrays.push(entries as TData[]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return collect(segmentArrays);
|
|
205
|
+
}
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
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).
|
|
12
|
+
* This makes MetaTags the sole FOUC-script injector for apps that render it;
|
|
13
|
+
* the standalone `<ThemeScript />` is only needed when MetaTags is not used.
|
|
14
|
+
* Rendering both is safe (the inline script guards listener registration) but
|
|
15
|
+
* redundant.
|
|
11
16
|
*
|
|
12
17
|
* @example
|
|
13
18
|
* ```tsx
|
|
@@ -24,12 +29,13 @@
|
|
|
24
29
|
* ```
|
|
25
30
|
*/
|
|
26
31
|
|
|
27
|
-
import { use } from "react";
|
|
28
32
|
import { useHandle } from "../browser/react/use-handle.js";
|
|
29
33
|
import { Meta } from "./meta.js";
|
|
30
|
-
import type {
|
|
31
|
-
import {
|
|
34
|
+
import type { MetaDescriptorBase } from "../router/types.js";
|
|
35
|
+
import { useThemeContext } from "../theme/theme-context.js";
|
|
32
36
|
import { generateThemeScript } from "../theme/theme-script.js";
|
|
37
|
+
import { useNonce } from "../browser/react/nonce-context.js";
|
|
38
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
33
39
|
|
|
34
40
|
// Type guards for MetaDescriptorBase variants
|
|
35
41
|
function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
|
|
@@ -40,57 +46,67 @@ function hasTitle(d: MetaDescriptorBase): d is { title: string } {
|
|
|
40
46
|
return "title" in d && typeof (d as { title?: unknown }).title === "string";
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
function hasNameContent(
|
|
44
|
-
|
|
49
|
+
function hasNameContent(
|
|
50
|
+
d: MetaDescriptorBase,
|
|
51
|
+
): d is { name: string; content: string } {
|
|
52
|
+
return (
|
|
53
|
+
"name" in d &&
|
|
54
|
+
"content" in d &&
|
|
45
55
|
typeof (d as { name?: unknown }).name === "string" &&
|
|
46
|
-
typeof (d as { content?: unknown }).content === "string"
|
|
56
|
+
typeof (d as { content?: unknown }).content === "string"
|
|
57
|
+
);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
function hasPropertyContent(
|
|
50
|
-
|
|
60
|
+
function hasPropertyContent(
|
|
61
|
+
d: MetaDescriptorBase,
|
|
62
|
+
): d is { property: string; content: string } {
|
|
63
|
+
return (
|
|
64
|
+
"property" in d &&
|
|
65
|
+
"content" in d &&
|
|
51
66
|
typeof (d as { property?: unknown }).property === "string" &&
|
|
52
|
-
typeof (d as { content?: unknown }).content === "string"
|
|
67
|
+
typeof (d as { content?: unknown }).content === "string"
|
|
68
|
+
);
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
function hasHttpEquivContent(
|
|
56
|
-
|
|
71
|
+
function hasHttpEquivContent(
|
|
72
|
+
d: MetaDescriptorBase,
|
|
73
|
+
): d is { httpEquiv: string; content: string } {
|
|
74
|
+
return (
|
|
75
|
+
"httpEquiv" in d &&
|
|
76
|
+
"content" in d &&
|
|
57
77
|
typeof (d as { httpEquiv?: unknown }).httpEquiv === "string" &&
|
|
58
|
-
typeof (d as { content?: unknown }).content === "string"
|
|
78
|
+
typeof (d as { content?: unknown }).content === "string"
|
|
79
|
+
);
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
function hasScriptLdJson(
|
|
82
|
+
function hasScriptLdJson(
|
|
83
|
+
d: MetaDescriptorBase,
|
|
84
|
+
): d is { "script:ld+json": object } {
|
|
62
85
|
return "script:ld+json" in d;
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
function hasTagName(
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return value !== null && typeof value === "object" && "then" in value;
|
|
88
|
+
function hasTagName(
|
|
89
|
+
d: MetaDescriptorBase,
|
|
90
|
+
): d is { tagName: "meta" | "link"; [name: string]: string } {
|
|
91
|
+
return (
|
|
92
|
+
"tagName" in d &&
|
|
93
|
+
((d as { tagName?: unknown }).tagName === "meta" ||
|
|
94
|
+
(d as { tagName?: unknown }).tagName === "link")
|
|
95
|
+
);
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
|
|
77
|
-
* Render a single meta descriptor as a React element.
|
|
78
|
-
*/
|
|
79
|
-
function renderMetaDescriptor(
|
|
98
|
+
export function renderMetaDescriptor(
|
|
80
99
|
descriptor: MetaDescriptorBase,
|
|
81
|
-
index: number
|
|
100
|
+
index: number,
|
|
82
101
|
): React.ReactNode {
|
|
83
|
-
// charset
|
|
84
102
|
if (hasCharSet(descriptor)) {
|
|
85
103
|
return <meta key="charSet" charSet={descriptor.charSet} />;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
// title
|
|
89
106
|
if (hasTitle(descriptor)) {
|
|
90
107
|
return <title key="title">{descriptor.title}</title>;
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
// name + content (description, viewport, etc.)
|
|
94
110
|
if (hasNameContent(descriptor)) {
|
|
95
111
|
return (
|
|
96
112
|
<meta
|
|
@@ -101,7 +117,6 @@ function renderMetaDescriptor(
|
|
|
101
117
|
);
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
// property + content (Open Graph, etc.)
|
|
105
120
|
if (hasPropertyContent(descriptor)) {
|
|
106
121
|
return (
|
|
107
122
|
<meta
|
|
@@ -112,7 +127,6 @@ function renderMetaDescriptor(
|
|
|
112
127
|
);
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
// http-equiv + content
|
|
116
130
|
if (hasHttpEquivContent(descriptor)) {
|
|
117
131
|
return (
|
|
118
132
|
<meta
|
|
@@ -123,9 +137,10 @@ function renderMetaDescriptor(
|
|
|
123
137
|
);
|
|
124
138
|
}
|
|
125
139
|
|
|
126
|
-
// JSON-LD structured data
|
|
127
140
|
if (hasScriptLdJson(descriptor)) {
|
|
128
|
-
const json =
|
|
141
|
+
const json = escapeJsonForScript(
|
|
142
|
+
JSON.stringify(descriptor["script:ld+json"]),
|
|
143
|
+
);
|
|
129
144
|
return (
|
|
130
145
|
<script
|
|
131
146
|
key={`ld-json-${index}`}
|
|
@@ -135,27 +150,32 @@ function renderMetaDescriptor(
|
|
|
135
150
|
);
|
|
136
151
|
}
|
|
137
152
|
|
|
138
|
-
// Custom tagName (meta or link with arbitrary attributes)
|
|
139
153
|
if (hasTagName(descriptor)) {
|
|
140
154
|
const { tagName, ...rest } = descriptor;
|
|
141
155
|
if (tagName === "link") {
|
|
142
|
-
return
|
|
156
|
+
return (
|
|
157
|
+
<link
|
|
158
|
+
key={`link-${index}`}
|
|
159
|
+
{...(rest as React.LinkHTMLAttributes<HTMLLinkElement>)}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
143
162
|
}
|
|
144
163
|
if (tagName === "meta") {
|
|
145
|
-
return
|
|
164
|
+
return (
|
|
165
|
+
<meta
|
|
166
|
+
key={`meta-${index}`}
|
|
167
|
+
{...(rest as React.MetaHTMLAttributes<HTMLMetaElement>)}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
146
170
|
}
|
|
147
171
|
}
|
|
148
172
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
*/
|
|
156
|
-
function AsyncMetaTag({ promise, index }: { promise: Promise<MetaDescriptorBase>; index: number }): React.ReactNode {
|
|
157
|
-
const resolved = use(promise);
|
|
158
|
-
return renderMetaDescriptor(resolved, index);
|
|
173
|
+
return (
|
|
174
|
+
<meta
|
|
175
|
+
key={`meta-fallback-${index}`}
|
|
176
|
+
{...(descriptor as React.MetaHTMLAttributes<HTMLMetaElement>)}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
/**
|
|
@@ -167,27 +187,31 @@ function AsyncMetaTag({ promise, index }: { promise: Promise<MetaDescriptorBase>
|
|
|
167
187
|
* When theme is enabled in router config, also renders the theme initialization
|
|
168
188
|
* script to prevent FOUC (flash of unstyled content).
|
|
169
189
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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.
|
|
172
194
|
*/
|
|
173
195
|
export function MetaTags(): React.ReactNode {
|
|
174
|
-
|
|
175
|
-
|
|
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[];
|
|
200
|
+
const themeConfig = useThemeContext()?.config ?? null;
|
|
201
|
+
const nonce = useNonce();
|
|
176
202
|
|
|
177
203
|
return (
|
|
178
204
|
<>
|
|
179
205
|
{/* Theme script must be first to prevent FOUC */}
|
|
180
206
|
{themeConfig && (
|
|
181
207
|
<script
|
|
208
|
+
nonce={nonce}
|
|
182
209
|
dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
|
|
183
210
|
/>
|
|
184
211
|
)}
|
|
185
|
-
{descriptors.map((descriptor, index) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
return renderMetaDescriptor(descriptor, index);
|
|
190
|
-
})}
|
|
212
|
+
{descriptors.map((descriptor, index) =>
|
|
213
|
+
renderMetaDescriptor(descriptor, index),
|
|
214
|
+
)}
|
|
191
215
|
</>
|
|
192
216
|
);
|
|
193
217
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders the scripts collected by the Script handle into the document.
|
|
5
|
+
*
|
|
6
|
+
* Place `<Scripts />` inside `<head>` (default) and, if you push body scripts,
|
|
7
|
+
* `<Scripts position="body" />` at the top of `<body>`. Each site renders the
|
|
8
|
+
* configs whose `position` matches; the request CSP nonce is applied
|
|
9
|
+
* automatically to every DOCUMENT-RENDERED <script> (consumers never pass it). An
|
|
10
|
+
* async script first encountered on a soft navigation is injected client-side
|
|
11
|
+
* where the nonce is unavailable, so it carries no nonce and relies on
|
|
12
|
+
* 'strict-dynamic' (or a host allowance) — see the nonce caveat in the /scripts
|
|
13
|
+
* skill.
|
|
14
|
+
*
|
|
15
|
+
* EXECUTION CONTRACT — see the Script handle's docs. Inline + ordered (defer)
|
|
16
|
+
* scripts are document-load: they execute only when present in the initial HTML,
|
|
17
|
+
* so this component FREEZES that set after hydration (the initializer below runs
|
|
18
|
+
* once) — a later soft navigation never inserts an inert <script> (React creates
|
|
19
|
+
* client-mounted scripts via innerHTML, which the HTML spec makes non-executing).
|
|
20
|
+
* Async external scripts are React resources and stay reactive: React loads them
|
|
21
|
+
* on first encounter, including after navigation, deduped by src.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <html>
|
|
26
|
+
* <head>
|
|
27
|
+
* <MetaTags />
|
|
28
|
+
* <Scripts />
|
|
29
|
+
* </head>
|
|
30
|
+
* <body>
|
|
31
|
+
* <Scripts position="body" />
|
|
32
|
+
* {children}
|
|
33
|
+
* </body>
|
|
34
|
+
* </html>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { useState, type ReactNode } from "react";
|
|
39
|
+
import { useHandle } from "../browser/react/use-handle.js";
|
|
40
|
+
import { useNonce } from "../browser/react/nonce-context.js";
|
|
41
|
+
import { escapeScriptBody } from "../escape-script.js";
|
|
42
|
+
import { Script, type ScriptAttributes, type ScriptConfig } from "./script.js";
|
|
43
|
+
|
|
44
|
+
/** An external async script is a React-managed resource (reactive on nav). */
|
|
45
|
+
function isAsyncResource(config: ScriptConfig): boolean {
|
|
46
|
+
return config.src != null && config.async === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fields the Script handle owns (set via the ScriptConfig fields, applied as
|
|
50
|
+
// explicit props by renderScript) plus the inline-content props. Dropped from the
|
|
51
|
+
// attributes bag so untyped/serialized input cannot smuggle them in — e.g.
|
|
52
|
+
// `children`/`dangerouslySetInnerHTML` alongside an inline body makes React throw,
|
|
53
|
+
// or `src` on an inline script. The discriminated type already excludes these;
|
|
54
|
+
// this is the runtime guard.
|
|
55
|
+
const MANAGED_ATTRS = new Set([
|
|
56
|
+
"id",
|
|
57
|
+
"src",
|
|
58
|
+
"async",
|
|
59
|
+
"defer",
|
|
60
|
+
"type",
|
|
61
|
+
"children",
|
|
62
|
+
"nonce",
|
|
63
|
+
"dangerouslySetInnerHTML",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Drop managed fields + any `on*` event handlers (a config serializes across the
|
|
67
|
+
// server -> client boundary, so a function cannot survive it) from the passthrough
|
|
68
|
+
// attributes, warning in dev.
|
|
69
|
+
function passthroughAttributes(
|
|
70
|
+
attributes: ScriptAttributes | undefined,
|
|
71
|
+
): Record<string, unknown> {
|
|
72
|
+
if (!attributes) return {};
|
|
73
|
+
const out: Record<string, unknown> = {};
|
|
74
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
75
|
+
for (const [key, value] of Object.entries(
|
|
76
|
+
attributes as Record<string, unknown>,
|
|
77
|
+
)) {
|
|
78
|
+
const isHandler = key.startsWith("on");
|
|
79
|
+
if (isHandler || MANAGED_ATTRS.has(key)) {
|
|
80
|
+
if (dev) {
|
|
81
|
+
console.warn(
|
|
82
|
+
isHandler
|
|
83
|
+
? `[Scripts] event handler "${key}" in a script's attributes is ` +
|
|
84
|
+
`dropped; callbacks cannot cross the server -> client handle ` +
|
|
85
|
+
`boundary. Use a "use client" component for load/error handling.`
|
|
86
|
+
: `[Scripts] managed field "${key}" in a script's attributes is ` +
|
|
87
|
+
`dropped; set it via the ScriptConfig fields (the request nonce ` +
|
|
88
|
+
`is applied automatically).`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out[key] = value;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderScript(
|
|
99
|
+
config: ScriptConfig,
|
|
100
|
+
nonce: string | undefined,
|
|
101
|
+
index: number,
|
|
102
|
+
): ReactNode {
|
|
103
|
+
const { id, src, children, async, defer, type, attributes } = config;
|
|
104
|
+
const key = id ?? src ?? `rango-script-${index}`;
|
|
105
|
+
const attrs = passthroughAttributes(attributes);
|
|
106
|
+
|
|
107
|
+
// Inline: rendered in place (never hoisted), escaped against </script> breakout.
|
|
108
|
+
// The server-only nonce makes the attribute differ from the (undefined) client
|
|
109
|
+
// value, so suppressHydrationWarning is required — the same sanctioned pattern
|
|
110
|
+
// as the theme/Meta inline scripts.
|
|
111
|
+
if (src == null) {
|
|
112
|
+
if (children == null) return null;
|
|
113
|
+
return (
|
|
114
|
+
<script
|
|
115
|
+
key={key}
|
|
116
|
+
{...attrs}
|
|
117
|
+
id={id}
|
|
118
|
+
type={type}
|
|
119
|
+
nonce={nonce}
|
|
120
|
+
suppressHydrationWarning
|
|
121
|
+
dangerouslySetInnerHTML={{ __html: escapeScriptBody(children) }}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
process.env.NODE_ENV !== "production" &&
|
|
128
|
+
config.position === "body" &&
|
|
129
|
+
async
|
|
130
|
+
) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[Scripts] An async external script (src="${src}") is hoisted into ` +
|
|
133
|
+
`<head> by React; position: "body" is ignored for it.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// External: async => React-hoisted, src-deduped resource; otherwise in place
|
|
138
|
+
// (defer or plain), preserving authoring order.
|
|
139
|
+
return (
|
|
140
|
+
<script
|
|
141
|
+
key={key}
|
|
142
|
+
{...attrs}
|
|
143
|
+
id={id}
|
|
144
|
+
type={type}
|
|
145
|
+
src={src}
|
|
146
|
+
async={async}
|
|
147
|
+
defer={defer}
|
|
148
|
+
nonce={nonce}
|
|
149
|
+
suppressHydrationWarning
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function Scripts({
|
|
155
|
+
position = "head",
|
|
156
|
+
}: { position?: "head" | "body" } = {}): ReactNode {
|
|
157
|
+
const all = useHandle(Script) as ScriptConfig[];
|
|
158
|
+
const nonce = useNonce();
|
|
159
|
+
|
|
160
|
+
const forPosition = all.filter(
|
|
161
|
+
(config) => (config.position ?? "head") === position,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Document-load scripts (inline + ordered external) execute only from the
|
|
165
|
+
// initial HTML, so freeze them to the first-render set. The initializer runs
|
|
166
|
+
// during SSR and again at hydration with the same handle data, so the output
|
|
167
|
+
// matches; afterwards a navigation cannot add an inert <script>.
|
|
168
|
+
const [documentLoad] = useState(() =>
|
|
169
|
+
forPosition.filter((config) => !isAsyncResource(config)),
|
|
170
|
+
);
|
|
171
|
+
// Async external scripts are resources React loads on first encounter; keep
|
|
172
|
+
// them reactive so a script first reached via navigation still loads.
|
|
173
|
+
const asyncResources = forPosition.filter(isAsyncResource);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
{documentLoad.map((config, index) => renderScript(config, nonce, index))}
|
|
178
|
+
{asyncResources.map((config, index) =>
|
|
179
|
+
renderScript(config, nonce, index),
|
|
180
|
+
)}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
}
|