@timber-js/app 0.2.0-alpha.191 → 0.2.0-alpha.194
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/agent-skill.md +2 -0
- package/dist/_chunks/{actions-BjbbNRFN.js → actions-CWYtq6ii.js} +3 -3
- package/dist/_chunks/actions-CWYtq6ii.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -1
- package/dist/_chunks/{build-output-helper-C3DrfzZR.js → build-output-helper-9dqCv1pZ.js} +3 -2
- package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +1 -0
- package/dist/_chunks/{cache-api-DllJ-Lyw.js → cache-api-CQeYzA5g.js} +6 -8
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +1 -0
- package/dist/_chunks/chains-h7EO-u3n.js +412 -0
- package/dist/_chunks/chains-h7EO-u3n.js.map +1 -0
- package/dist/_chunks/{cli-check-DJZDc22E.js → cli-check-BVthpfLS.js} +43 -6
- package/dist/_chunks/cli-check-BVthpfLS.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js → cli-schema-sync-ZwM9u_ob.js} +6 -5
- package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js.map → cli-schema-sync-ZwM9u_ob.js.map} +1 -1
- package/dist/_chunks/{cloudflare-CGP6BZKO.js → cloudflare-BKJC3SC_.js} +28 -22
- package/dist/_chunks/{cloudflare-CGP6BZKO.js.map → cloudflare-BKJC3SC_.js.map} +1 -1
- package/dist/_chunks/codec-registry-oOUxugz3.js.map +1 -1
- package/dist/_chunks/{convention-lint-CpteIpTm.js → convention-lint-DO10_pVl.js} +93 -7
- package/dist/_chunks/convention-lint-DO10_pVl.js.map +1 -0
- package/dist/_chunks/dist-DtrJyBbw.js +386 -0
- package/dist/_chunks/dist-DtrJyBbw.js.map +1 -0
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -1
- package/dist/_chunks/{file-cache-DmX7OqZP.js → file-cache-Dw6BJPG7.js} +5 -1
- package/dist/_chunks/{file-cache-DmX7OqZP.js.map → file-cache-Dw6BJPG7.js.map} +1 -1
- package/dist/_chunks/{format-CfwjgPz9.js → format-BKclgVbk.js} +3 -3
- package/dist/_chunks/{format-CfwjgPz9.js.map → format-BKclgVbk.js.map} +1 -1
- package/dist/_chunks/graph-cache-CP4GEmf9.js +304 -0
- package/dist/_chunks/graph-cache-CP4GEmf9.js.map +1 -0
- package/dist/_chunks/href-validation-CMc5JRls.js.map +1 -1
- package/dist/_chunks/live-graph-Bx4HodF1.js +150 -0
- package/dist/_chunks/live-graph-Bx4HodF1.js.map +1 -0
- package/dist/_chunks/{logger-CH7IcMmg.js → logger-pumCm3Il.js} +25 -7
- package/dist/_chunks/{logger-CH7IcMmg.js.map → logger-pumCm3Il.js.map} +1 -1
- package/dist/_chunks/{mdx-file-C005ay-P.js → mdx-file-CXyHGUpS.js} +3 -3
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +1 -0
- package/dist/_chunks/{plugin-context-rCinWLiE.js → plugin-context-8idfR3UZ.js} +2 -2
- package/dist/_chunks/plugin-context-8idfR3UZ.js.map +1 -0
- package/dist/_chunks/poison-rules-DoEhbqaY.js +110 -0
- package/dist/_chunks/poison-rules-DoEhbqaY.js.map +1 -0
- package/dist/_chunks/poison-scan-BAxfTT5L.js +101 -0
- package/dist/_chunks/poison-scan-BAxfTT5L.js.map +1 -0
- package/dist/_chunks/resolve-schema-CBR6Lm4i.js.map +1 -1
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -1
- package/dist/_chunks/{rsc-media-type-DRqE_lD_.js → rsc-media-type-DDc7duTD.js} +17 -2
- package/dist/_chunks/{rsc-media-type-DRqE_lD_.js.map → rsc-media-type-DDc7duTD.js.map} +1 -1
- package/dist/_chunks/scan-BoP17W3w.js +140 -0
- package/dist/_chunks/scan-BoP17W3w.js.map +1 -0
- package/dist/_chunks/{scanner-CAietmj4.js → scanner-BRIOmHE2.js} +43 -4
- package/dist/_chunks/scanner-BRIOmHE2.js.map +1 -0
- package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -1
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{segment-keys-BawYuNFO.js → segment-keys-BhqoHiLc.js} +14 -2
- package/dist/_chunks/{segment-keys-BawYuNFO.js.map → segment-keys-BhqoHiLc.js.map} +1 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -1
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -1
- package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -1
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -1
- package/dist/_chunks/{walkers-BsVLmD1S.js → walkers-uCu3WW6_.js} +9 -6
- package/dist/_chunks/{walkers-BsVLmD1S.js.map → walkers-uCu3WW6_.js.map} +1 -1
- package/dist/adapters/build-output-helper.d.ts +1 -1
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +2 -1
- package/dist/adapters/cloudflare-dev.js.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +7 -8
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro-presets.d.ts +37 -0
- package/dist/adapters/nitro-presets.d.ts.map +1 -0
- package/dist/adapters/nitro-preview.d.ts +32 -0
- package/dist/adapters/nitro-preview.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts +28 -51
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +224 -171
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/chains.d.ts +124 -0
- package/dist/analyze/chains.d.ts.map +1 -0
- package/dist/analyze/classify.d.ts +150 -0
- package/dist/analyze/classify.d.ts.map +1 -0
- package/dist/analyze/crawl-entry.d.ts +17 -0
- package/dist/analyze/crawl-entry.d.ts.map +1 -0
- package/dist/analyze/crawl-entry.js +313 -0
- package/dist/analyze/crawl-entry.js.map +1 -0
- package/dist/analyze/crawl.d.ts +134 -0
- package/dist/analyze/crawl.d.ts.map +1 -0
- package/dist/analyze/graph-cache.d.ts +104 -0
- package/dist/analyze/graph-cache.d.ts.map +1 -0
- package/dist/analyze/graph-command.d.ts +43 -0
- package/dist/analyze/graph-command.d.ts.map +1 -0
- package/dist/analyze/graph-command.js +472 -0
- package/dist/analyze/graph-command.js.map +1 -0
- package/dist/analyze/live-graph.d.ts +81 -0
- package/dist/analyze/live-graph.d.ts.map +1 -0
- package/dist/analyze/poison-rules.d.ts +72 -0
- package/dist/analyze/poison-rules.d.ts.map +1 -0
- package/dist/analyze/poison-scan.d.ts +48 -0
- package/dist/analyze/poison-scan.d.ts.map +1 -0
- package/dist/analyze/scan.d.ts +73 -0
- package/dist/analyze/scan.d.ts.map +1 -0
- package/dist/cache/cache-api.d.ts +2 -2
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +8 -8
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/redis-handler.d.ts +1 -1
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.js.map +1 -1
- package/dist/cache/stores/memory.d.ts +1 -1
- package/dist/cache/stores/memory.d.ts.map +1 -1
- package/dist/cache/stores/memory.js.map +1 -1
- package/dist/cache/stores/redis.d.ts +1 -1
- package/dist/cache/stores/redis.d.ts.map +1 -1
- package/dist/cache/stores/redis.js.map +1 -1
- package/dist/cache/stores/vercel.d.ts +1 -1
- package/dist/cache/stores/vercel.d.ts.map +1 -1
- package/dist/cache/stores/vercel.js.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts +2 -2
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts +1 -1
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.js.map +1 -1
- package/dist/cdn/fastly-purge.d.ts +1 -1
- package/dist/cdn/fastly-purge.d.ts.map +1 -1
- package/dist/cdn/fastly-purge.js.map +1 -1
- package/dist/cdn/index.d.ts +4 -4
- package/dist/cdn/index.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.d.ts +5 -1
- package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.js.map +1 -1
- package/dist/cli-check.d.ts +10 -0
- package/dist/cli-check.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +30 -6
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +2 -2
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +2 -2
- package/dist/client/browser-entry/router-init.d.ts +2 -2
- package/dist/client/browser-entry/rsc-stream.d.ts +1 -1
- package/dist/client/form.d.ts +2 -2
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/history.d.ts +2 -2
- package/dist/client/index.d.ts +18 -18
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +19 -19
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +11 -15
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +3 -3
- package/dist/client/navigation-commit.d.ts +5 -5
- package/dist/client/navigation-root.d.ts +1 -1
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +3 -3
- package/dist/client/router-pipeline.d.ts +7 -7
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-types.d.ts +6 -6
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts +1 -1
- package/dist/client/rsc-fetch.d.ts +4 -4
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +2 -2
- package/dist/client/slot-provider.d.ts +1 -1
- package/dist/client/ssr-data.d.ts +2 -2
- package/dist/client/state.d.ts +4 -4
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +3 -3
- package/dist/codec.d.ts +2 -2
- package/dist/codec.js.map +1 -1
- package/dist/config-types.d.ts +24 -6
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts +1 -1
- package/dist/cookies/define-cookie.d.ts +3 -3
- package/dist/cookies/index.d.ts +3 -3
- package/dist/cookies/index.js +3 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/cookies/json-cookie.d.ts +1 -1
- package/dist/dev-tools/404-page.d.ts +2 -2
- package/dist/dev-tools/error-page.d.ts +2 -2
- package/dist/dev-tools/index.d.ts +12 -12
- package/dist/dev-tools/instrumentation.d.ts +1 -1
- package/dist/dev-tools/logs.d.ts +1 -1
- package/dist/dev-tools/overlay.d.ts +2 -2
- package/dist/dev-tools/terminal.d.ts +1 -1
- package/dist/fonts/ast.d.ts +4 -4
- package/dist/fonts/bundle.d.ts +3 -3
- package/dist/fonts/css.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/google.d.ts +1 -1
- package/dist/fonts/local.d.ts +1 -1
- package/dist/fonts/pipeline.d.ts +3 -3
- package/dist/fonts/transform.d.ts +2 -2
- package/dist/fonts/types.d.ts +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +816 -310
- package/dist/index.js.map +1 -1
- package/dist/params/codec-registry.d.ts +1 -1
- package/dist/params/define-schema.d.ts +2 -2
- package/dist/params/index.d.ts +4 -4
- package/dist/params/index.js.map +1 -1
- package/dist/params/resolve-schema.d.ts +1 -1
- package/dist/plugin-context.d.ts +9 -9
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts +2 -2
- package/dist/plugins/build-manifest.d.ts +2 -2
- package/dist/plugins/build-report.d.ts +5 -5
- package/dist/plugins/cache.d.ts +1 -1
- package/dist/plugins/callsite-jsx.d.ts +2 -2
- package/dist/plugins/chunks.d.ts +1 -1
- package/dist/plugins/chunks.d.ts.map +1 -1
- package/dist/plugins/cloudflare-wasm.d.ts +1 -1
- package/dist/plugins/content.d.ts +1 -1
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/dev-discovery.d.ts +123 -0
- package/dist/plugins/dev-discovery.d.ts.map +1 -0
- package/dist/plugins/dev-server.d.ts +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +1 -1
- package/dist/plugins/fonts.d.ts +3 -3
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/graph-endpoint.d.ts +112 -0
- package/dist/plugins/graph-endpoint.d.ts.map +1 -0
- package/dist/plugins/mdx.d.ts +18 -5
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/prebuilt-capture.d.ts +2 -2
- package/dist/plugins/prebuilt-options-analysis.d.ts +1 -1
- package/dist/plugins/prebuilt.d.ts +1 -1
- package/dist/plugins/prerender-sugar.d.ts +11 -14
- package/dist/plugins/prerender-sugar.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +1 -1
- package/dist/plugins/request-dep/index.d.ts +5 -5
- package/dist/plugins/request-dep/origin.d.ts +1 -1
- package/dist/plugins/routing.d.ts +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +5 -5
- package/dist/routing/codegen-shared.d.ts +1 -1
- package/dist/routing/codegen-write.d.ts +1 -1
- package/dist/routing/codegen.d.ts +1 -1
- package/dist/routing/collision-probe.d.ts +1 -1
- package/dist/routing/collision-spaces.d.ts +1 -1
- package/dist/routing/convention-lint.d.ts +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.d.ts +13 -13
- package/dist/routing/index.js +3 -3
- package/dist/routing/interception.d.ts +1 -1
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/link-codegen.d.ts +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -1
- package/dist/routing/scanner.d.ts +14 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/schema-validation.d.ts +1 -1
- package/dist/routing/segment-classify.d.ts +1 -1
- package/dist/routing/segment-codegen.d.ts +1 -1
- package/dist/routing/segment-keys.d.ts +1 -1
- package/dist/routing/slot-placement.d.ts +1 -1
- package/dist/routing/walkers.d.ts +1 -1
- package/dist/rsc-runtime/transforms.d.ts +13 -0
- package/dist/rsc-runtime/transforms.d.ts.map +1 -0
- package/dist/schema-bridge.d.ts +1 -1
- package/dist/search-params/define.d.ts +3 -3
- package/dist/search-params/index.d.ts +3 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +1 -1
- package/dist/server/access-gate.d.ts +1 -1
- package/dist/server/action-client.d.ts +1 -1
- package/dist/server/action-handler.d.ts +5 -5
- package/dist/server/actions.d.ts +2 -2
- package/dist/server/als-registry.d.ts +8 -8
- package/dist/server/asset-headers.d.ts +1 -1
- package/dist/server/chain-url-parts.d.ts +3 -3
- package/dist/server/children-interception.d.ts +2 -2
- package/dist/server/cookie-context.d.ts +1 -1
- package/dist/server/cookie-parsing.d.ts +2 -2
- package/dist/server/default-logger.d.ts +1 -1
- package/dist/server/deny-boundary.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts +5 -5
- package/dist/server/early-hints.d.ts +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +2 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +5 -5
- package/dist/server/flight-injection-state.d.ts +1 -1
- package/dist/server/form-flash.d.ts +1 -1
- package/dist/server/html-injector-core.d.ts +2 -2
- package/dist/server/html-injectors.d.ts +1 -1
- package/dist/server/index.d.ts +18 -18
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +33 -32
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +1 -1
- package/dist/server/internal.d.ts +50 -50
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +1023 -1025
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -2
- package/dist/server/metadata-platform.d.ts +2 -2
- package/dist/server/metadata-render.d.ts +2 -2
- package/dist/server/metadata-routes.d.ts +3 -3
- package/dist/server/metadata-social.d.ts +2 -2
- package/dist/server/metadata.d.ts +2 -2
- package/dist/server/middleware-runner.d.ts +1 -1
- package/dist/server/node-stream-transforms.d.ts +1 -1
- package/dist/server/param-coercion.d.ts +4 -4
- package/dist/server/pipeline-helpers.d.ts +3 -3
- package/dist/server/pipeline-interception.d.ts +1 -1
- package/dist/server/pipeline-metadata.d.ts +1 -1
- package/dist/server/pipeline-outcome.d.ts +2 -2
- package/dist/server/pipeline-phases.d.ts +3 -3
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +9 -9
- package/dist/server/prebuilt/cache-key.d.ts +1 -1
- package/dist/server/prebuilt/capture-state.d.ts +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +1 -1
- package/dist/server/prebuilt/manifest-tag-index.d.ts +1 -1
- package/dist/server/prebuilt/overlay.d.ts +2 -2
- package/dist/server/prebuilt/payload-source.d.ts +1 -1
- package/dist/server/prebuilt/slots.d.ts +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts +2 -2
- package/dist/server/prebuilt-builder.d.ts +2 -2
- package/dist/server/primitives.d.ts +2 -2
- package/dist/server/publish-params.d.ts +1 -1
- package/dist/server/request-context.d.ts +3 -3
- package/dist/server/route-element-builder.d.ts +16 -6
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts +1 -1
- package/dist/server/route-matcher.d.ts +4 -4
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +2 -2
- package/dist/server/rsc-entry/api-handler.d.ts +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts +3 -3
- package/dist/server/rsc-entry/error-renderer.d.ts +4 -4
- package/dist/server/rsc-entry/helpers.d.ts +6 -6
- package/dist/server/rsc-entry/index.d.ts +7 -6
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +4 -4
- package/dist/server/rsc-entry/revalidate-renderer.d.ts +4 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts +4 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -3
- package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +7 -7
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +5 -5
- package/dist/server/sitemap-generator.d.ts +2 -2
- package/dist/server/sitemap-handler.d.ts +1 -1
- package/dist/server/skippable-prefix.d.ts +3 -3
- package/dist/server/slot-interception.d.ts +2 -2
- package/dist/server/slot-resolver.d.ts +3 -7
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +2 -2
- package/dist/server/ssr-entry.d.ts +1 -1
- package/dist/server/static-generator.d.ts +1 -1
- package/dist/server/static-not-found.d.ts +1 -1
- package/dist/server/status-code-resolver.d.ts +1 -1
- package/dist/server/tracing.d.ts +3 -3
- package/dist/server/tree-builder.d.ts +3 -3
- package/dist/server/tree-match.d.ts +1 -1
- package/dist/server/types.d.ts +2 -2
- package/dist/server/utils/mdx-file.d.ts +2 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -1
- package/dist/server/workers-cache-bridge.d.ts +1 -1
- package/dist/shared/als-slots.d.ts +1 -1
- package/dist/shared/payload-root.d.ts +2 -2
- package/dist/shared/rsc-media-type.d.ts +12 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -1
- package/dist/shared/slot-params.d.ts +1 -1
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.d.ts +1 -1
- package/dist/shims/font-local.js.map +1 -1
- package/dist/shims/navigation-client.d.ts +4 -4
- package/dist/shims/navigation.d.ts +2 -2
- package/docs/api/34-api-config.mdx +9 -8
- package/docs/api/36-cli.mdx +23 -3
- package/docs/learn/11-error-handling.mdx +11 -0
- package/docs/more/01-advanced-routing.mdx +1 -0
- package/docs/more/04b-mdx.mdx +38 -18
- package/package.json +23 -18
- package/src/adapters/build-output-helper.ts +4 -4
- package/src/adapters/cloudflare-dev.ts +7 -1
- package/src/adapters/cloudflare-kv-cache.ts +11 -8
- package/src/adapters/cloudflare.ts +21 -15
- package/src/adapters/compress-module.ts +2 -2
- package/src/adapters/nitro-presets.ts +139 -0
- package/src/adapters/nitro-preview.ts +340 -0
- package/src/adapters/nitro.ts +100 -465
- package/src/adapters/shared.ts +1 -1
- package/src/analyze/chains.ts +411 -0
- package/src/analyze/classify.ts +258 -0
- package/src/analyze/crawl-entry.ts +68 -0
- package/src/analyze/crawl.ts +500 -0
- package/src/analyze/graph-cache.ts +332 -0
- package/src/analyze/graph-command.ts +762 -0
- package/src/analyze/live-graph.ts +232 -0
- package/src/analyze/poison-rules.ts +165 -0
- package/src/analyze/poison-scan.ts +162 -0
- package/src/analyze/scan.ts +149 -0
- package/src/cache/cache-api.ts +7 -7
- package/src/cache/index.ts +9 -9
- package/src/cache/redis-handler.ts +2 -2
- package/src/cache/stores/cloudflare-kv.ts +1 -1
- package/src/cache/stores/memory.ts +2 -2
- package/src/cache/stores/redis.ts +1 -1
- package/src/cache/stores/vercel.ts +1 -1
- package/src/cache/tag-aware-handler.ts +2 -2
- package/src/cache/timber-cache.ts +8 -8
- package/src/cdn/cloudflare-purge.ts +1 -1
- package/src/cdn/fastly-purge.ts +1 -1
- package/src/cdn/index.ts +10 -4
- package/src/cdn/workers-cache-purge.ts +6 -2
- package/src/cli-check.ts +53 -4
- package/src/cli-schema-sync.ts +3 -3
- package/src/cli.ts +50 -11
- package/src/client/browser-dev.ts +1 -1
- package/src/client/browser-entry/action-dispatch.ts +5 -5
- package/src/client/browser-entry/hmr.ts +1 -1
- package/src/client/browser-entry/hydrate.ts +9 -9
- package/src/client/browser-entry/index.ts +10 -10
- package/src/client/browser-entry/post-hydration.ts +5 -5
- package/src/client/browser-entry/router-init.ts +9 -9
- package/src/client/browser-entry/rsc-stream.ts +3 -3
- package/src/client/error-boundary.tsx +4 -4
- package/src/client/form.tsx +7 -2
- package/src/client/history.ts +2 -2
- package/src/client/index.ts +21 -18
- package/src/client/internal.ts +19 -19
- package/src/client/link.tsx +9 -9
- package/src/client/navigation-api.ts +1 -1
- package/src/client/navigation-commit.ts +5 -5
- package/src/client/navigation-context.ts +1 -1
- package/src/client/navigation-root.tsx +1 -1
- package/src/client/nuqs-adapter.tsx +2 -2
- package/src/client/params-context.ts +4 -4
- package/src/client/router-effects.ts +3 -3
- package/src/client/router-lifecycle.ts +2 -2
- package/src/client/router-pipeline.ts +10 -10
- package/src/client/router-ref.ts +2 -2
- package/src/client/router-skew.ts +2 -2
- package/src/client/router-types.ts +6 -6
- package/src/client/router.ts +13 -13
- package/src/client/rsc-fetch.ts +11 -16
- package/src/client/segment-cache.ts +2 -2
- package/src/client/segment-outlet.tsx +1 -1
- package/src/client/shallow-url.ts +1 -1
- package/src/client/slot-outlet.tsx +1 -1
- package/src/client/slot-provider.tsx +1 -1
- package/src/client/ssr-data.ts +3 -3
- package/src/client/stale-client.ts +1 -1
- package/src/client/state.ts +4 -4
- package/src/client/top-loader.tsx +1 -1
- package/src/client/unload-guard.ts +1 -1
- package/src/client/use-cookie.ts +2 -2
- package/src/client/use-pathname.ts +2 -2
- package/src/client/use-pending-navigation.ts +1 -1
- package/src/client/use-query-states.ts +3 -3
- package/src/client/use-router.ts +2 -2
- package/src/client/use-search-params.ts +3 -3
- package/src/client/use-segment-params.ts +6 -6
- package/src/client/use-selected-layout-segment.ts +2 -2
- package/src/codec.ts +2 -2
- package/src/config-types.ts +27 -7
- package/src/config-validation.ts +1 -1
- package/src/cookies/define-cookie.ts +6 -6
- package/src/cookies/index.ts +3 -3
- package/src/cookies/json-cookie.ts +1 -1
- package/src/dev-tools/404-page.ts +4 -4
- package/src/dev-tools/dev-page-shell.ts +1 -1
- package/src/dev-tools/error-page.ts +4 -4
- package/src/dev-tools/index.ts +12 -12
- package/src/dev-tools/instrumentation.ts +1 -1
- package/src/dev-tools/logs.ts +1 -1
- package/src/dev-tools/overlay.ts +3 -3
- package/src/dev-tools/terminal.ts +2 -2
- package/src/fonts/ast.ts +5 -5
- package/src/fonts/bundle.ts +5 -5
- package/src/fonts/css.ts +1 -1
- package/src/fonts/dev-middleware.ts +2 -2
- package/src/fonts/google.ts +2 -2
- package/src/fonts/local.ts +4 -4
- package/src/fonts/pipeline.ts +8 -8
- package/src/fonts/transform.ts +5 -5
- package/src/fonts/types.ts +1 -1
- package/src/fonts/virtual-modules.ts +1 -1
- package/src/index.ts +46 -37
- package/src/params/codec-registry.ts +1 -1
- package/src/params/define-schema.ts +2 -2
- package/src/params/index.ts +4 -4
- package/src/params/resolve-schema.ts +2 -2
- package/src/plugin-context.ts +10 -10
- package/src/plugins/adapter-build.ts +5 -5
- package/src/plugins/build-manifest.ts +4 -4
- package/src/plugins/build-report.ts +6 -6
- package/src/plugins/cache.ts +3 -3
- package/src/plugins/callsite-ast.ts +1 -1
- package/src/plugins/callsite-jsx.ts +2 -2
- package/src/plugins/chunks.ts +1 -1
- package/src/plugins/cloudflare-wasm.ts +1 -1
- package/src/plugins/content.ts +5 -1
- package/src/plugins/dev-discovery.ts +218 -0
- package/src/plugins/dev-server.ts +97 -12
- package/src/plugins/entries.ts +1 -1
- package/src/plugins/fonts.ts +17 -10
- package/src/plugins/graph-endpoint.ts +532 -0
- package/src/plugins/mdx.ts +73 -38
- package/src/plugins/prebuilt-capture.ts +3 -3
- package/src/plugins/prebuilt-options-analysis.ts +1 -1
- package/src/plugins/prebuilt.ts +7 -7
- package/src/plugins/prerender-sugar.ts +11 -19
- package/src/plugins/request-dep/analysis.ts +1 -1
- package/src/plugins/request-dep/index.ts +8 -8
- package/src/plugins/request-dep/origin.ts +1 -1
- package/src/plugins/routing.ts +12 -7
- package/src/plugins/server-bundle.ts +11 -3
- package/src/plugins/shims.ts +16 -31
- package/src/plugins/static-build.ts +10 -10
- package/src/routing/codegen-shared.ts +1 -1
- package/src/routing/codegen-write.ts +3 -3
- package/src/routing/codegen.ts +14 -5
- package/src/routing/collision-probe.ts +3 -3
- package/src/routing/collision-spaces.ts +3 -3
- package/src/routing/convention-lint.ts +136 -6
- package/src/routing/export-detect.ts +1 -1
- package/src/routing/index.ts +13 -13
- package/src/routing/interception.ts +41 -4
- package/src/routing/link-codegen.ts +3 -3
- package/src/routing/manifest-codegen.ts +2 -2
- package/src/routing/scanner.ts +33 -9
- package/src/routing/schema-validation.ts +4 -4
- package/src/routing/segment-classify.ts +2 -2
- package/src/routing/segment-codegen.ts +1 -1
- package/src/routing/segment-keys.ts +1 -1
- package/src/routing/slot-placement.ts +1 -1
- package/src/routing/walkers.ts +1 -1
- package/src/rsc-runtime/transforms.ts +13 -0
- package/src/rsc-runtime/vendor-types.d.ts +12 -0
- package/src/schema-bridge.ts +1 -1
- package/src/search-params/define.ts +8 -8
- package/src/search-params/index.ts +3 -3
- package/src/search-params/wrappers.ts +3 -3
- package/src/server/access-gate.tsx +6 -6
- package/src/server/action-client.ts +5 -5
- package/src/server/action-handler.ts +16 -16
- package/src/server/actions.ts +5 -5
- package/src/server/als-registry.ts +8 -8
- package/src/server/asset-headers.ts +2 -2
- package/src/server/chain-url-parts.ts +5 -5
- package/src/server/children-interception.ts +4 -4
- package/src/server/compress.ts +1 -1
- package/src/server/cookie-context.ts +4 -4
- package/src/server/cookie-parsing.ts +2 -2
- package/src/server/default-logger.ts +3 -3
- package/src/server/deny-boundary.ts +10 -10
- package/src/server/deny-renderer.ts +38 -25
- package/src/server/early-hints-sender.ts +2 -2
- package/src/server/early-hints.ts +2 -2
- package/src/server/error-boundary-wrapper.ts +4 -12
- package/src/server/fallback-error.ts +13 -13
- package/src/server/flight-injection-state.ts +1 -1
- package/src/server/form-flash.ts +2 -2
- package/src/server/html-injector-core.ts +4 -4
- package/src/server/html-injectors.ts +5 -5
- package/src/server/index.ts +18 -18
- package/src/server/instrumentation.ts +1 -1
- package/src/server/internal.ts +50 -50
- package/src/server/logger.ts +4 -4
- package/src/server/metadata-collector.ts +7 -7
- package/src/server/metadata-platform.ts +2 -2
- package/src/server/metadata-render.ts +4 -4
- package/src/server/metadata-routes.ts +3 -3
- package/src/server/metadata-social.ts +2 -2
- package/src/server/metadata.ts +2 -2
- package/src/server/middleware-runner.ts +1 -1
- package/src/server/node-stream-transforms.ts +4 -4
- package/src/server/param-coercion.ts +8 -8
- package/src/server/pipeline-helpers.ts +7 -7
- package/src/server/pipeline-interception.ts +2 -2
- package/src/server/pipeline-metadata.ts +1 -1
- package/src/server/pipeline-outcome.ts +8 -8
- package/src/server/pipeline-phases.ts +24 -28
- package/src/server/pipeline.ts +22 -22
- package/src/server/prebuilt/cache-key.ts +1 -1
- package/src/server/prebuilt/capture-state.ts +1 -1
- package/src/server/prebuilt/key-discipline.ts +1 -1
- package/src/server/prebuilt/manifest-tag-index.ts +1 -1
- package/src/server/prebuilt/overlay.ts +6 -6
- package/src/server/prebuilt/payload-source.ts +2 -2
- package/src/server/prebuilt/slots.ts +5 -5
- package/src/server/prebuilt/synthetic-store.ts +2 -2
- package/src/server/prebuilt-builder.ts +16 -16
- package/src/server/prebuilt-runtime.ts +12 -12
- package/src/server/primitives.ts +7 -7
- package/src/server/publish-params.ts +3 -3
- package/src/server/request-context.ts +7 -7
- package/src/server/route-element-builder.ts +108 -50
- package/src/server/route-handler.ts +4 -4
- package/src/server/route-matcher.ts +5 -5
- package/src/server/rsc-cache-key-guard.ts +3 -5
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -7
- package/src/server/rsc-entry/api-handler.ts +18 -9
- package/src/server/rsc-entry/deny-fallback.ts +10 -10
- package/src/server/rsc-entry/error-renderer.ts +27 -27
- package/src/server/rsc-entry/helpers.ts +10 -10
- package/src/server/rsc-entry/index.ts +51 -47
- package/src/server/rsc-entry/render-route.ts +20 -20
- package/src/server/rsc-entry/revalidate-renderer.ts +12 -12
- package/src/server/rsc-entry/rsc-payload.ts +19 -15
- package/src/server/rsc-entry/rsc-stream.ts +8 -8
- package/src/server/rsc-entry/ssr-bridge.ts +3 -3
- package/src/server/rsc-entry/ssr-renderer.ts +23 -23
- package/src/server/rsc-entry/wrap-action-dispatch.ts +9 -9
- package/src/server/rsc-prop-warnings.ts +1 -1
- package/src/server/sensitive-fields.ts +1 -1
- package/src/server/server-timing.ts +1 -1
- package/src/server/sitemap-generator.ts +3 -3
- package/src/server/sitemap-handler.ts +3 -3
- package/src/server/skippable-prefix.ts +7 -7
- package/src/server/slot-interception.ts +5 -5
- package/src/server/slot-resolver.ts +20 -28
- package/src/server/ssr-bridge-types.ts +2 -2
- package/src/server/ssr-entry.ts +18 -18
- package/src/server/ssr-render.ts +4 -4
- package/src/server/ssr-wrappers.tsx +1 -1
- package/src/server/state-tree-diff.ts +1 -1
- package/src/server/static-generator.ts +6 -6
- package/src/server/static-not-found.ts +3 -3
- package/src/server/status-code-resolver.ts +1 -1
- package/src/server/tracing.ts +5 -5
- package/src/server/tree-builder.ts +4 -4
- package/src/server/tree-match.ts +1 -1
- package/src/server/types.ts +2 -2
- package/src/server/utils/mdx-file.ts +2 -2
- package/src/server/waituntil-bridge.ts +1 -1
- package/src/server/workers-cache-bridge.ts +2 -2
- package/src/shared/als-slots.ts +1 -1
- package/src/shared/payload-root.ts +2 -2
- package/src/shared/rsc-media-type.ts +16 -0
- package/src/shared/slot-params.ts +1 -1
- package/src/shims/font-google.ts +1 -1
- package/src/shims/font-local.ts +1 -1
- package/src/shims/navigation-client.ts +5 -5
- package/src/shims/navigation.ts +2 -2
- package/LICENSE +0 -8
- package/dist/_chunks/actions-BjbbNRFN.js.map +0 -1
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +0 -1
- package/dist/_chunks/cache-api-DllJ-Lyw.js.map +0 -1
- package/dist/_chunks/cli-check-DJZDc22E.js.map +0 -1
- package/dist/_chunks/convention-lint-CpteIpTm.js.map +0 -1
- package/dist/_chunks/dist-BA3u1z3W.js +0 -423
- package/dist/_chunks/dist-BA3u1z3W.js.map +0 -1
- package/dist/_chunks/mdx-file-C005ay-P.js.map +0 -1
- package/dist/_chunks/plugin-context-rCinWLiE.js.map +0 -1
- package/dist/_chunks/scanner-CAietmj4.js.map +0 -1
- package/dist/content/index.d.ts +0 -2
- package/dist/content/index.d.ts.map +0 -1
- package/dist/content/index.js +0 -0
- package/src/content/index.ts +0 -5
- package/src/shims/server-only-noop.js +0 -6
package/dist/server/internal.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { n as isRscRequest } from "../_chunks/rsc-media-type-
|
|
2
|
-
import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-
|
|
1
|
+
import { n as isRscContentType, r as isRscRequest } from "../_chunks/rsc-media-type-DDc7duTD.js";
|
|
2
|
+
import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-BhqoHiLc.js";
|
|
3
|
+
import { n as isMdxFilePath } from "../_chunks/mdx-file-CXyHGUpS.js";
|
|
3
4
|
import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
|
|
4
5
|
import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
|
|
5
|
-
import { A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-
|
|
6
|
+
import { A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-pumCm3Il.js";
|
|
6
7
|
import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
|
|
7
|
-
import { t as isMdxFilePath } from "../_chunks/mdx-file-C005ay-P.js";
|
|
8
8
|
import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
|
|
9
|
-
import { i as coerce, t as executeAction } from "../_chunks/actions-
|
|
10
|
-
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-
|
|
9
|
+
import { i as coerce, t as executeAction } from "../_chunks/actions-CWYtq6ii.js";
|
|
10
|
+
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-CQeYzA5g.js";
|
|
11
11
|
import "../_chunks/error-boundary-D-ODYX41.js";
|
|
12
12
|
import { n as toBracketKey } from "../_chunks/resolve-schema-CBR6Lm4i.js";
|
|
13
13
|
import { r as normalizeParamValue } from "../_chunks/segment-context-CjOlyB8Y.js";
|
|
@@ -403,44 +403,46 @@ function renderOpenGraph(og, elements) {
|
|
|
403
403
|
content
|
|
404
404
|
}
|
|
405
405
|
});
|
|
406
|
-
if (og.images)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
406
|
+
if (og.images) {
|
|
407
|
+
if (typeof og.images === "string") elements.push({
|
|
408
|
+
tag: "meta",
|
|
409
|
+
attrs: {
|
|
410
|
+
property: "og:image",
|
|
411
|
+
content: og.images
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
else {
|
|
415
|
+
const imgList = Array.isArray(og.images) ? og.images : [og.images];
|
|
416
|
+
for (const img of imgList) {
|
|
417
|
+
elements.push({
|
|
418
|
+
tag: "meta",
|
|
419
|
+
attrs: {
|
|
420
|
+
property: "og:image",
|
|
421
|
+
content: img.url
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
if (img.width) elements.push({
|
|
425
|
+
tag: "meta",
|
|
426
|
+
attrs: {
|
|
427
|
+
property: "og:image:width",
|
|
428
|
+
content: String(img.width)
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
if (img.height) elements.push({
|
|
432
|
+
tag: "meta",
|
|
433
|
+
attrs: {
|
|
434
|
+
property: "og:image:height",
|
|
435
|
+
content: String(img.height)
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (img.alt) elements.push({
|
|
439
|
+
tag: "meta",
|
|
440
|
+
attrs: {
|
|
441
|
+
property: "og:image:alt",
|
|
442
|
+
content: img.alt
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
444
446
|
}
|
|
445
447
|
}
|
|
446
448
|
if (og.videos) for (const video of og.videos) elements.push({
|
|
@@ -488,24 +490,26 @@ function renderTwitter(tw, elements) {
|
|
|
488
490
|
content
|
|
489
491
|
}
|
|
490
492
|
});
|
|
491
|
-
if (tw.images)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
493
|
+
if (tw.images) {
|
|
494
|
+
if (typeof tw.images === "string") elements.push({
|
|
495
|
+
tag: "meta",
|
|
496
|
+
attrs: {
|
|
497
|
+
name: "twitter:image",
|
|
498
|
+
content: tw.images
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
else {
|
|
502
|
+
const imgList = Array.isArray(tw.images) ? tw.images : [tw.images];
|
|
503
|
+
for (const img of imgList) {
|
|
504
|
+
const url = typeof img === "string" ? img : img.url;
|
|
505
|
+
elements.push({
|
|
506
|
+
tag: "meta",
|
|
507
|
+
attrs: {
|
|
508
|
+
name: "twitter:image",
|
|
509
|
+
content: url
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
509
513
|
}
|
|
510
514
|
}
|
|
511
515
|
if (tw.players) for (const player of tw.players) {
|
|
@@ -1082,11 +1086,12 @@ function resolveMetadataUrls(metadata) {
|
|
|
1082
1086
|
* the user's error component with the proper props.
|
|
1083
1087
|
*/
|
|
1084
1088
|
function ErrorReconstituter({ error: serialized, digest, reset, component, status, dangerouslyPassData }) {
|
|
1089
|
+
const error = Object.assign(new Error(serialized.message), {
|
|
1090
|
+
name: serialized.name,
|
|
1091
|
+
...serialized.stack != null ? { stack: serialized.stack } : {}
|
|
1092
|
+
});
|
|
1085
1093
|
return createElement(component, {
|
|
1086
|
-
error
|
|
1087
|
-
name: serialized.name,
|
|
1088
|
-
...serialized.stack != null ? { stack: serialized.stack } : {}
|
|
1089
|
-
}),
|
|
1094
|
+
error,
|
|
1090
1095
|
digest,
|
|
1091
1096
|
reset,
|
|
1092
1097
|
...status != null ? {
|
|
@@ -1493,527 +1498,154 @@ function buildDeniedFallback(DeniedComponent, slotName, data, createElement) {
|
|
|
1493
1498
|
});
|
|
1494
1499
|
}
|
|
1495
1500
|
//#endregion
|
|
1496
|
-
//#region src/
|
|
1501
|
+
//#region src/server/error-boundary-wrapper.ts
|
|
1497
1502
|
/**
|
|
1498
|
-
*
|
|
1499
|
-
* cached component shells (TIM-1173).
|
|
1503
|
+
* Load a status/error file's component, or `null` if the boundary should be skipped.
|
|
1500
1504
|
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1505
|
+
* Two ways to get `null`, both deliberate:
|
|
1506
|
+
* - the module fails to load (syntax error in the error page) — building an
|
|
1507
|
+
* error boundary must not itself throw, so the boundary is skipped (TIM-584);
|
|
1508
|
+
* - the default export is not a valid React component type, which would
|
|
1509
|
+
* otherwise crash inside `createElement`.
|
|
1510
|
+
*/
|
|
1511
|
+
async function loadBoundaryComponent(file, loadModule) {
|
|
1512
|
+
let mod;
|
|
1513
|
+
try {
|
|
1514
|
+
mod = await loadModule(file);
|
|
1515
|
+
} catch {
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
return isValidElementType(mod?.default) ? mod.default : null;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Wrap `element` in one error boundary rendering `Component` on failure.
|
|
1506
1522
|
*
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
* independently. Layouts use a reserved slot name (LAYOUT_CHILDREN_SLOT)
|
|
1511
|
-
* that cannot collide with user-declared slot names (TIM-1191).
|
|
1523
|
+
* MDX status files are server components and cannot be passed as function
|
|
1524
|
+
* props to `TimberErrorBoundary` (a 'use client' component). They are
|
|
1525
|
+
* pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
|
|
1512
1526
|
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
|
|
1516
|
-
|
|
1527
|
+
* @param status boundary status filter — omitted entirely for `error.tsx`
|
|
1528
|
+
* @param fallbackProps props handed to an MDX fallback when it is pre-rendered
|
|
1529
|
+
*/
|
|
1530
|
+
function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
|
|
1531
|
+
const props = isMdxFilePath(file.filePath) ? {
|
|
1532
|
+
fallbackElement: config.createElement(Component, fallbackProps),
|
|
1533
|
+
children: element
|
|
1534
|
+
} : {
|
|
1535
|
+
fallbackComponent: Component,
|
|
1536
|
+
children: element
|
|
1537
|
+
};
|
|
1538
|
+
if (status !== void 0) props.status = status;
|
|
1539
|
+
if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
|
|
1540
|
+
return config.createElement(config.errorBoundaryComponent, props);
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Wrap an element in the error boundaries declared by a route segment.
|
|
1517
1544
|
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
* context instance.
|
|
1545
|
+
* Wrapping is applied inside-out — the last wrap produces the outermost
|
|
1546
|
+
* boundary — so the processing order below is innermost to outermost:
|
|
1521
1547
|
*
|
|
1522
|
-
*
|
|
1548
|
+
* 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
|
|
1549
|
+
* 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
|
|
1550
|
+
* 3. `error.tsx` — catches anything not matched by a status file
|
|
1551
|
+
*
|
|
1552
|
+
* The order is load-bearing: it decides which boundary catches a given throw.
|
|
1553
|
+
*
|
|
1554
|
+
* See design/10-error-handling.md §"Format-Aware Fallback Chains".
|
|
1523
1555
|
*/
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1556
|
+
async function wrapSegmentWithErrorBoundaries(segment, element, config) {
|
|
1557
|
+
const { loadModule } = config;
|
|
1558
|
+
if (segment.statusFiles) {
|
|
1559
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1560
|
+
if (key === "4xx" || key === "5xx") continue;
|
|
1561
|
+
const status = parseInt(key, 10);
|
|
1562
|
+
if (isNaN(status)) continue;
|
|
1563
|
+
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1564
|
+
if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
|
|
1565
|
+
}
|
|
1566
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1567
|
+
if (key !== "4xx" && key !== "5xx") continue;
|
|
1568
|
+
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1569
|
+
if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
|
|
1570
|
+
}
|
|
1532
1571
|
}
|
|
1572
|
+
if (segment.error) {
|
|
1573
|
+
const Component = await loadBoundaryComponent(segment.error, loadModule);
|
|
1574
|
+
if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
|
|
1575
|
+
}
|
|
1576
|
+
return element;
|
|
1533
1577
|
}
|
|
1534
|
-
var SlotContext = getOrCreateContext$1();
|
|
1535
1578
|
//#endregion
|
|
1536
|
-
//#region src/
|
|
1579
|
+
//#region src/server/param-coercion.ts
|
|
1580
|
+
/** Global schema codecs, set once at startup from virtual:timber-schema. */
|
|
1581
|
+
var _globalCodecs = null;
|
|
1537
1582
|
/**
|
|
1538
|
-
*
|
|
1583
|
+
* Run segment param coercion on the matched route's segments.
|
|
1539
1584
|
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
* content will fill the hole. At request time the revived shell sits under
|
|
1545
|
-
* a SlotsProvider and each outlet reads its live value from SlotContext.
|
|
1585
|
+
* Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
|
|
1586
|
+
* design/41-global-params.md migration step 9). A route with no schema, or
|
|
1587
|
+
* a param the schema does not declare, keeps its raw string — which is the
|
|
1588
|
+
* honest type for an uninterpreted URL segment.
|
|
1546
1589
|
*
|
|
1547
|
-
*
|
|
1590
|
+
* Throws ParamCoercionError if any codec fails (→ 404).
|
|
1591
|
+
*
|
|
1592
|
+
* This runs BEFORE middleware, so ctx.segmentParams is already typed.
|
|
1593
|
+
* See design/07-routing.md §"Where Coercion Runs"
|
|
1594
|
+
* See design/41-global-params.md §"Pipeline Integration"
|
|
1595
|
+
*
|
|
1596
|
+
* Stays `async` with nothing to await: it is injected as a
|
|
1597
|
+
* `CoerceSegmentParamsFn` into the revalidation renderer and the action
|
|
1598
|
+
* middleware runner, both of which await it. Narrowing the return type to
|
|
1599
|
+
* `void` would break those contracts and remove a microtask boundary the
|
|
1600
|
+
* pipeline's ordering has always had, for no gain.
|
|
1548
1601
|
*/
|
|
1549
|
-
function
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1602
|
+
async function coerceSegmentParams(match) {
|
|
1603
|
+
const globalCodecs = _globalCodecs;
|
|
1604
|
+
const mergeTarget = Object.create(null);
|
|
1605
|
+
for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
|
|
1606
|
+
match.segmentParams = mergeTarget;
|
|
1607
|
+
if (globalCodecs) for (const node of match.segments) {
|
|
1608
|
+
const segment = effectiveUrlSegment(node);
|
|
1609
|
+
if (!segment.paramName) continue;
|
|
1610
|
+
const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
|
|
1611
|
+
const codec = globalCodecs[bracketKey];
|
|
1612
|
+
if (!codec) continue;
|
|
1613
|
+
const key = segment.paramName;
|
|
1614
|
+
if (!(key in mergeTarget)) continue;
|
|
1615
|
+
let parsed;
|
|
1616
|
+
try {
|
|
1617
|
+
parsed = codec.parse(mergeTarget[key]);
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1620
|
+
if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
|
|
1621
|
+
throw new ParamCoercionError(message);
|
|
1622
|
+
}
|
|
1623
|
+
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
1624
|
+
}
|
|
1552
1625
|
}
|
|
1553
1626
|
//#endregion
|
|
1554
|
-
//#region src/
|
|
1627
|
+
//#region src/server/pipeline-interception.ts
|
|
1555
1628
|
/**
|
|
1556
|
-
*
|
|
1557
|
-
* to the SlotOutlet holes inside it (TIM-1173).
|
|
1629
|
+
* Interception route matching for the request pipeline.
|
|
1558
1630
|
*
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
* the layout's `children` slot while adding the component's own slots.
|
|
1562
|
-
* Inner values override outer ones with the same name (spread order).
|
|
1631
|
+
* Matches target URLs against interception rewrites to support the
|
|
1632
|
+
* modal route pattern (soft navigation intercepts).
|
|
1563
1633
|
*
|
|
1564
|
-
*
|
|
1565
|
-
* SlotsProvider(values={children: <Live />})
|
|
1566
|
-
* └── revived shell
|
|
1567
|
-
* └── SlotOutlet(slot="children") → reads context → <Live />
|
|
1634
|
+
* Extracted from pipeline.ts to keep files under 500 lines.
|
|
1568
1635
|
*
|
|
1569
|
-
* See design/
|
|
1636
|
+
* See design/07-routing.md §"Intercepting Routes"
|
|
1570
1637
|
*/
|
|
1571
|
-
function SlotsProvider({ values, children }) {
|
|
1572
|
-
const parent = useContext(SlotContext);
|
|
1573
|
-
const merged = parent ? {
|
|
1574
|
-
...parent,
|
|
1575
|
-
...values
|
|
1576
|
-
} : values;
|
|
1577
|
-
return createElement(SlotContext.Provider, { value: merged }, children);
|
|
1578
|
-
}
|
|
1579
|
-
//#endregion
|
|
1580
|
-
//#region src/server/prebuilt/cache-key.ts
|
|
1581
1638
|
/**
|
|
1582
|
-
*
|
|
1639
|
+
* Check if an intercepting route applies for this soft navigation.
|
|
1583
1640
|
*
|
|
1584
|
-
*
|
|
1585
|
-
* design/44-render-at-build-time.md §Cache Key. Both the request-time
|
|
1586
|
-
* lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
|
|
1587
|
-
* MUST use this exact function, or build artifacts become unaddressable.
|
|
1641
|
+
* Two conditions must hold:
|
|
1588
1642
|
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
* which hash as distinct sentinels because flight preserves them. Every
|
|
1596
|
-
* value outside that domain (functions, symbols, bigints, accessors,
|
|
1597
|
-
* hidden or symbol-keyed properties, non-`Object.prototype` prototypes
|
|
1598
|
-
* including null, aliased references, sparse arrays, cycles, non-finite
|
|
1599
|
-
* numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
|
|
1600
|
-
* null and the caller renders live — slower, never wrong.
|
|
1601
|
-
*
|
|
1602
|
-
* The line between rejected and declared-out-of-domain: values are
|
|
1603
|
-
* REJECTED when the JSON projection itself would be ambiguous or lossy
|
|
1604
|
-
* (hidden/symbol/non-enumerable properties, accessors, exotic
|
|
1605
|
-
* prototypes). Observable state that leaves the projection intact — key
|
|
1606
|
-
* order, reference identity, descriptor metadata (writable/configurable,
|
|
1607
|
-
* frozen/sealed/extensible), proxy behavior — is DECLARED outside
|
|
1608
|
-
* cache.component's supported input domain rather than rejected:
|
|
1609
|
-
* key-order insensitivity is required for determinism, and rejecting
|
|
1610
|
-
* frozen objects would unkey ubiquitous legitimate inputs
|
|
1611
|
-
* (Object.freeze'd config props) to defend against components that
|
|
1612
|
-
* render differently based on property attributes.
|
|
1613
|
-
*
|
|
1614
|
-
* Two hard rules for this walk itself: it must be TOTAL (never throw
|
|
1615
|
-
* anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
|
|
1616
|
-
* reads only; never invoke user code such as getters).
|
|
1617
|
-
*
|
|
1618
|
-
* Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
|
|
1619
|
-
* (design/13-security.md checklist #14 — cache key determinism; key order
|
|
1620
|
-
* is deliberately NOT part of identity).
|
|
1621
|
-
*/
|
|
1622
|
-
var UnkeyableValueError = class extends Error {};
|
|
1623
|
-
function canonicalize$1(value, seen) {
|
|
1624
|
-
if (value === null) return "null";
|
|
1625
|
-
switch (typeof value) {
|
|
1626
|
-
case "string": return JSON.stringify(value);
|
|
1627
|
-
case "boolean": return value ? "true" : "false";
|
|
1628
|
-
case "number":
|
|
1629
|
-
if (!Number.isFinite(value)) throw new UnkeyableValueError("non-finite number");
|
|
1630
|
-
return Object.is(value, -0) ? "-0" : String(value);
|
|
1631
|
-
case "object": break;
|
|
1632
|
-
case "undefined": return "undefined";
|
|
1633
|
-
default: throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
|
|
1634
|
-
}
|
|
1635
|
-
const obj = value;
|
|
1636
|
-
if (seen.has(obj)) throw new UnkeyableValueError("repeated object reference (cycle or alias)");
|
|
1637
|
-
seen.add(obj);
|
|
1638
|
-
if (Object.getOwnPropertySymbols(obj).length > 0) throw new UnkeyableValueError("symbol-keyed property");
|
|
1639
|
-
{
|
|
1640
|
-
if (Array.isArray(obj)) {
|
|
1641
|
-
if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) throw new UnkeyableValueError("non-index array property");
|
|
1642
|
-
const parts = [];
|
|
1643
|
-
for (let i = 0; i < obj.length; i++) {
|
|
1644
|
-
const desc = Object.getOwnPropertyDescriptor(obj, i);
|
|
1645
|
-
if (desc === void 0) throw new UnkeyableValueError("sparse array hole");
|
|
1646
|
-
if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
|
|
1647
|
-
if (!desc.enumerable) throw new UnkeyableValueError("non-enumerable array element");
|
|
1648
|
-
parts.push(canonicalize$1(desc.value, seen));
|
|
1649
|
-
}
|
|
1650
|
-
return "[" + parts.join(",") + "]";
|
|
1651
|
-
}
|
|
1652
|
-
if (Object.getPrototypeOf(obj) !== Object.prototype) throw new UnkeyableValueError("non-plain object");
|
|
1653
|
-
const keys = Object.keys(obj).sort();
|
|
1654
|
-
if (keys.length !== Object.getOwnPropertyNames(obj).length) throw new UnkeyableValueError("non-enumerable property");
|
|
1655
|
-
const parts = [];
|
|
1656
|
-
for (const key of keys) {
|
|
1657
|
-
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
1658
|
-
if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
|
|
1659
|
-
parts.push(JSON.stringify(key) + ":" + canonicalize$1(desc.value, seen));
|
|
1660
|
-
}
|
|
1661
|
-
return "{" + parts.join(",") + "}";
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Canonicalize a single value, or null if it is unkeyable. The ONE
|
|
1666
|
-
* definition of "faithfully comparable" — every prebuilt code path that
|
|
1667
|
-
* needs value equality (cache keys, slot-param divergence) goes through
|
|
1668
|
-
* this, so the unkeyable rules (accessors, hidden props, aliases,
|
|
1669
|
-
* bigints, …) live in exactly one place.
|
|
1670
|
-
*/
|
|
1671
|
-
function tryCanonicalize(value) {
|
|
1672
|
-
try {
|
|
1673
|
-
return canonicalize$1(value, /* @__PURE__ */ new Set());
|
|
1674
|
-
} catch (error) {
|
|
1675
|
-
return toUnkeyable(error);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
/**
|
|
1679
|
-
* Totality by construction: ANY throw from the walk means the input is
|
|
1680
|
-
* outside the keyable domain. The walk touches only user-controlled
|
|
1681
|
-
* values through reflection ops — and a Proxy trap can throw arbitrary
|
|
1682
|
-
* errors from any of them (codex review on PR #835) — so converting only
|
|
1683
|
-
* UnkeyableValueError would let hostile/exotic inputs abort a render
|
|
1684
|
-
* that plain live rendering would have served fine. Unexpected error
|
|
1685
|
-
* types are logged for visibility (they would indicate either a proxy
|
|
1686
|
-
* trap or a canonicalizer bug) but still degrade to unkeyable.
|
|
1687
|
-
*/
|
|
1688
|
-
function toUnkeyable(error) {
|
|
1689
|
-
if (!(error instanceof UnkeyableValueError)) console.error("[timber] cache key canonicalization threw — treating input as unkeyable:", error);
|
|
1690
|
-
return null;
|
|
1691
|
-
}
|
|
1692
|
-
/**
|
|
1693
|
-
* Split a props record into data props (participate in the cache key) and
|
|
1694
|
-
* slot values (React-element holes, excluded from the key) per the
|
|
1695
|
-
* component's declared `slots` option (TIM-1173).
|
|
1696
|
-
*
|
|
1697
|
-
* Every DECLARED slot lands in `slotValues` — present or not — because the
|
|
1698
|
-
* capture render always injects a SlotOutlet placeholder for each declared
|
|
1699
|
-
* slot: the cached shell is identical whether the caller passed the slot
|
|
1700
|
-
* or omitted it, so slot presence must not vary the key. `dataProps` is
|
|
1701
|
-
* everything else and is what gets hashed.
|
|
1702
|
-
*
|
|
1703
|
-
* Returns null when the props OBJECT itself is outside the identity
|
|
1704
|
-
* contract at the top level — accessors, symbol-keyed or non-enumerable
|
|
1705
|
-
* properties, or a non-`Object.prototype` prototype. Splitting such an
|
|
1706
|
-
* object would normalize away observable state (and reading an accessor
|
|
1707
|
-
* would invoke user code, which this walk must never do) BEFORE
|
|
1708
|
-
* `computeComponentCacheKey` gets the chance to reject it — a cached
|
|
1709
|
-
* shell could then be served for inputs a live render distinguishes.
|
|
1710
|
-
* Null means unkeyable: the caller renders live with the original props.
|
|
1711
|
-
* The walk is descriptor-reads only; getters are never invoked. Nested
|
|
1712
|
-
* data-prop values still face the full allowlist when hashed.
|
|
1713
|
-
*/
|
|
1714
|
-
/** Create an own enumerable data property — safe for keys like `__proto__`. */
|
|
1715
|
-
function defineOwn(target, key, value) {
|
|
1716
|
-
Object.defineProperty(target, key, {
|
|
1717
|
-
value,
|
|
1718
|
-
enumerable: true,
|
|
1719
|
-
writable: true,
|
|
1720
|
-
configurable: true
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
function splitSlotProps(props, slots) {
|
|
1724
|
-
try {
|
|
1725
|
-
if (Object.getPrototypeOf(props) !== Object.prototype) return null;
|
|
1726
|
-
if (Object.getOwnPropertySymbols(props).length > 0) return null;
|
|
1727
|
-
const slotSet = new Set(slots);
|
|
1728
|
-
const dataProps = {};
|
|
1729
|
-
const slotValues = {};
|
|
1730
|
-
for (const key of Object.getOwnPropertyNames(props)) {
|
|
1731
|
-
const desc = Object.getOwnPropertyDescriptor(props, key);
|
|
1732
|
-
if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
|
|
1733
|
-
defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
|
|
1734
|
-
}
|
|
1735
|
-
for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
|
|
1736
|
-
return {
|
|
1737
|
-
dataProps,
|
|
1738
|
-
slotValues
|
|
1739
|
-
};
|
|
1740
|
-
} catch (error) {
|
|
1741
|
-
return toUnkeyable(error);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
/**
|
|
1745
|
-
* Compute the cache-entry hash for a component render:
|
|
1746
|
-
* `sha256({props, params}).slice(0, 16)` over canonical JSON.
|
|
1747
|
-
*
|
|
1748
|
-
* Returns null when props (or params) contain unkeyable values — the
|
|
1749
|
-
* caller must fall back to dynamic rendering.
|
|
1750
|
-
*/
|
|
1751
|
-
function computeComponentCacheKey(props, params) {
|
|
1752
|
-
let canonical;
|
|
1753
|
-
try {
|
|
1754
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1755
|
-
canonical = "{\"params\":" + canonicalize$1({ ...params }, seen) + ",\"props\":" + canonicalize$1(props, seen) + "}";
|
|
1756
|
-
} catch (error) {
|
|
1757
|
-
return toUnkeyable(error);
|
|
1758
|
-
}
|
|
1759
|
-
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* Is this value the framework-injected layout children slot outlet
|
|
1763
|
-
* (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
|
|
1764
|
-
* receive this as their `children` prop — a stable client reference
|
|
1765
|
-
* whose cache key is stripped so layout `cache.component` hits work
|
|
1766
|
-
* (design/45 §Layout Propagation).
|
|
1767
|
-
*
|
|
1768
|
-
* Uses a framework-reserved slot name (LAYOUT_CHILDREN_SLOT) that
|
|
1769
|
-
* cannot collide with user-declared slot names, preventing context
|
|
1770
|
-
* shadowing when a cached component with its own SlotsProvider is
|
|
1771
|
-
* nested inside a layout. See TIM-1191.
|
|
1772
|
-
*
|
|
1773
|
-
* Total: the probes run against a user-controlled value, and a Proxy
|
|
1774
|
-
* trap can throw — that must classify as "not the outlet" (the
|
|
1775
|
-
* canonicalizer then rejects the value as unkeyable → live render), not
|
|
1776
|
-
* abort the request (codex P2 on PR #890).
|
|
1777
|
-
*/
|
|
1778
|
-
function isChildrenSlotOutletElement(value) {
|
|
1779
|
-
try {
|
|
1780
|
-
if (value == null || typeof value !== "object") return false;
|
|
1781
|
-
if (!("type" in value)) return false;
|
|
1782
|
-
const el = value;
|
|
1783
|
-
return el.type === SlotOutlet && el.props?.slot === "\0timber:children";
|
|
1784
|
-
} catch {
|
|
1785
|
-
return false;
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
/**
|
|
1789
|
-
* Dense array of non-empty strings — no holes, no non-string entries.
|
|
1790
|
-
* Total: the reads run against a user-controlled value at module load,
|
|
1791
|
-
* and a throwing Proxy trap must classify as invalid, not crash startup.
|
|
1792
|
-
*/
|
|
1793
|
-
function isDenseStringArray(value) {
|
|
1794
|
-
try {
|
|
1795
|
-
if (!Array.isArray(value)) return false;
|
|
1796
|
-
for (let i = 0; i < value.length; i++) {
|
|
1797
|
-
if (!Object.hasOwn(value, i)) return false;
|
|
1798
|
-
const entry = value[i];
|
|
1799
|
-
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
1800
|
-
}
|
|
1801
|
-
return true;
|
|
1802
|
-
} catch {
|
|
1803
|
-
return false;
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
/**
|
|
1807
|
-
* Validate the `slots` option. Both the production and dev wrappers gate
|
|
1808
|
-
* on this — slot substitution is active in dev exactly when it would be
|
|
1809
|
-
* active in production, so the tree shape a component sees never
|
|
1810
|
-
* diverges between the two.
|
|
1811
|
-
*/
|
|
1812
|
-
function resolveSlotNames(id, options) {
|
|
1813
|
-
const slots = options.slots;
|
|
1814
|
-
if (slots === void 0) return { kind: "none" };
|
|
1815
|
-
if (!isDenseStringArray(slots)) {
|
|
1816
|
-
console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
|
|
1817
|
-
return { kind: "disabled" };
|
|
1818
|
-
}
|
|
1819
|
-
if (slots.length === 0) return { kind: "none" };
|
|
1820
|
-
if (!(options.ttl !== void 0 || options.tags != null)) {
|
|
1821
|
-
console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
|
|
1822
|
-
return { kind: "disabled" };
|
|
1823
|
-
}
|
|
1824
|
-
return {
|
|
1825
|
-
kind: "active",
|
|
1826
|
-
slots
|
|
1827
|
-
};
|
|
1828
|
-
}
|
|
1829
|
-
/**
|
|
1830
|
-
* Split the live props into the cacheable shell render (data props plus a
|
|
1831
|
-
* deterministic SlotOutlet placeholder per declared slot — identical
|
|
1832
|
-
* whether or not the caller passed the slot) and the per-instance live
|
|
1833
|
-
* slot values.
|
|
1834
|
-
*
|
|
1835
|
-
* Null when the props object itself is unkeyable at the top level
|
|
1836
|
-
* (accessors, hidden properties, exotic prototype — see splitSlotProps):
|
|
1837
|
-
* the caller must render live with the original props.
|
|
1838
|
-
*/
|
|
1839
|
-
function prepareSlotRender(props, slots) {
|
|
1840
|
-
const split = splitSlotProps(props ?? {}, slots);
|
|
1841
|
-
if (split === null) return null;
|
|
1842
|
-
const { dataProps, slotValues } = split;
|
|
1843
|
-
const shellProps = { ...dataProps };
|
|
1844
|
-
for (const slot of slots) Object.defineProperty(shellProps, slot, {
|
|
1845
|
-
value: createElement(SlotOutlet, { slot }),
|
|
1846
|
-
enumerable: true,
|
|
1847
|
-
writable: true,
|
|
1848
|
-
configurable: true
|
|
1849
|
-
});
|
|
1850
|
-
return {
|
|
1851
|
-
shellProps,
|
|
1852
|
-
dataProps,
|
|
1853
|
-
slotValues
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
/**
|
|
1857
|
-
* Wrap a rendered shell (revived payload or live element) in a
|
|
1858
|
-
* per-instance SlotsProvider feeding the live values to its outlets.
|
|
1859
|
-
*/
|
|
1860
|
-
function wrapInSlotsProvider(shell, slotValues) {
|
|
1861
|
-
return createElement(SlotsProvider, {
|
|
1862
|
-
values: slotValues,
|
|
1863
|
-
children: shell
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
//#endregion
|
|
1867
|
-
//#region src/server/error-boundary-wrapper.ts
|
|
1868
|
-
/**
|
|
1869
|
-
* Load a status/error file's component, or `null` if the boundary should be skipped.
|
|
1870
|
-
*
|
|
1871
|
-
* Two ways to get `null`, both deliberate:
|
|
1872
|
-
* - the module fails to load (syntax error in the error page) — building an
|
|
1873
|
-
* error boundary must not itself throw, so the boundary is skipped (TIM-584);
|
|
1874
|
-
* - the default export is not a valid React component type, which would
|
|
1875
|
-
* otherwise crash inside `createElement`.
|
|
1876
|
-
*/
|
|
1877
|
-
async function loadBoundaryComponent(file, loadModule) {
|
|
1878
|
-
let mod;
|
|
1879
|
-
try {
|
|
1880
|
-
mod = await loadModule(file);
|
|
1881
|
-
} catch {
|
|
1882
|
-
return null;
|
|
1883
|
-
}
|
|
1884
|
-
return isValidElementType(mod?.default) ? mod.default : null;
|
|
1885
|
-
}
|
|
1886
|
-
/**
|
|
1887
|
-
* Wrap `element` in one error boundary rendering `Component` on failure.
|
|
1888
|
-
*
|
|
1889
|
-
* MDX status files are server components and cannot be passed as function
|
|
1890
|
-
* props to `TimberErrorBoundary` (a 'use client' component). They are
|
|
1891
|
-
* pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
|
|
1892
|
-
*
|
|
1893
|
-
* @param status boundary status filter — omitted entirely for `error.tsx`
|
|
1894
|
-
* @param fallbackProps props handed to an MDX fallback when it is pre-rendered
|
|
1895
|
-
*/
|
|
1896
|
-
function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
|
|
1897
|
-
const props = isMdxFilePath(file.filePath) ? {
|
|
1898
|
-
fallbackElement: config.createElement(Component, fallbackProps),
|
|
1899
|
-
children: element
|
|
1900
|
-
} : {
|
|
1901
|
-
fallbackComponent: Component,
|
|
1902
|
-
children: element
|
|
1903
|
-
};
|
|
1904
|
-
if (status !== void 0) props.status = status;
|
|
1905
|
-
if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
|
|
1906
|
-
return config.createElement(config.errorBoundaryComponent, props);
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Wrap an element in the error boundaries declared by a route segment.
|
|
1910
|
-
*
|
|
1911
|
-
* Wrapping is applied inside-out — the last wrap produces the outermost
|
|
1912
|
-
* boundary — so the processing order below is innermost to outermost:
|
|
1913
|
-
*
|
|
1914
|
-
* 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
|
|
1915
|
-
* 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
|
|
1916
|
-
* 3. `error.tsx` — catches anything not matched by a status file
|
|
1917
|
-
*
|
|
1918
|
-
* The order is load-bearing: it decides which boundary catches a given throw.
|
|
1919
|
-
*
|
|
1920
|
-
* See design/10-error-handling.md §"Format-Aware Fallback Chains".
|
|
1921
|
-
*/
|
|
1922
|
-
async function wrapSegmentWithErrorBoundaries(segment, element, config) {
|
|
1923
|
-
const { loadModule, skipDenyBoundaries = false } = config;
|
|
1924
|
-
if (segment.statusFiles) {
|
|
1925
|
-
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1926
|
-
if (key === "4xx" || key === "5xx") continue;
|
|
1927
|
-
const status = parseInt(key, 10);
|
|
1928
|
-
if (isNaN(status)) continue;
|
|
1929
|
-
if (skipDenyBoundaries && status >= 400 && status <= 499) continue;
|
|
1930
|
-
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1931
|
-
if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
|
|
1932
|
-
}
|
|
1933
|
-
for (const [key, file] of Object.entries(segment.statusFiles)) {
|
|
1934
|
-
if (key !== "4xx" && key !== "5xx") continue;
|
|
1935
|
-
if (skipDenyBoundaries && key === "4xx") continue;
|
|
1936
|
-
const Component = await loadBoundaryComponent(file, loadModule);
|
|
1937
|
-
if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
if (segment.error) {
|
|
1941
|
-
const Component = await loadBoundaryComponent(segment.error, loadModule);
|
|
1942
|
-
if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
|
|
1943
|
-
}
|
|
1944
|
-
return element;
|
|
1945
|
-
}
|
|
1946
|
-
//#endregion
|
|
1947
|
-
//#region src/server/param-coercion.ts
|
|
1948
|
-
/** Global schema codecs, set once at startup from virtual:timber-schema. */
|
|
1949
|
-
var _globalCodecs = null;
|
|
1950
|
-
/**
|
|
1951
|
-
* Run segment param coercion on the matched route's segments.
|
|
1952
|
-
*
|
|
1953
|
-
* Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
|
|
1954
|
-
* design/41-global-params.md migration step 9). A route with no schema, or
|
|
1955
|
-
* a param the schema does not declare, keeps its raw string — which is the
|
|
1956
|
-
* honest type for an uninterpreted URL segment.
|
|
1957
|
-
*
|
|
1958
|
-
* Throws ParamCoercionError if any codec fails (→ 404).
|
|
1959
|
-
*
|
|
1960
|
-
* This runs BEFORE middleware, so ctx.segmentParams is already typed.
|
|
1961
|
-
* See design/07-routing.md §"Where Coercion Runs"
|
|
1962
|
-
* See design/41-global-params.md §"Pipeline Integration"
|
|
1963
|
-
*
|
|
1964
|
-
* Stays `async` with nothing to await: it is injected as a
|
|
1965
|
-
* `CoerceSegmentParamsFn` into the revalidation renderer and the action
|
|
1966
|
-
* middleware runner, both of which await it. Narrowing the return type to
|
|
1967
|
-
* `void` would break those contracts and remove a microtask boundary the
|
|
1968
|
-
* pipeline's ordering has always had, for no gain.
|
|
1969
|
-
*/
|
|
1970
|
-
async function coerceSegmentParams(match) {
|
|
1971
|
-
const globalCodecs = _globalCodecs;
|
|
1972
|
-
const mergeTarget = Object.create(null);
|
|
1973
|
-
for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
|
|
1974
|
-
match.segmentParams = mergeTarget;
|
|
1975
|
-
if (globalCodecs) for (const node of match.segments) {
|
|
1976
|
-
const segment = effectiveUrlSegment(node);
|
|
1977
|
-
if (!segment.paramName) continue;
|
|
1978
|
-
const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
|
|
1979
|
-
const codec = globalCodecs[bracketKey];
|
|
1980
|
-
if (!codec) continue;
|
|
1981
|
-
const key = segment.paramName;
|
|
1982
|
-
if (!(key in mergeTarget)) continue;
|
|
1983
|
-
let parsed;
|
|
1984
|
-
try {
|
|
1985
|
-
parsed = codec.parse(mergeTarget[key]);
|
|
1986
|
-
} catch (err) {
|
|
1987
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1988
|
-
if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
|
|
1989
|
-
throw new ParamCoercionError(message);
|
|
1990
|
-
}
|
|
1991
|
-
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
//#endregion
|
|
1995
|
-
//#region src/server/pipeline-interception.ts
|
|
1996
|
-
/**
|
|
1997
|
-
* Interception route matching for the request pipeline.
|
|
1998
|
-
*
|
|
1999
|
-
* Matches target URLs against interception rewrites to support the
|
|
2000
|
-
* modal route pattern (soft navigation intercepts).
|
|
2001
|
-
*
|
|
2002
|
-
* Extracted from pipeline.ts to keep files under 500 lines.
|
|
2003
|
-
*
|
|
2004
|
-
* See design/07-routing.md §"Intercepting Routes"
|
|
2005
|
-
*/
|
|
2006
|
-
/**
|
|
2007
|
-
* Check if an intercepting route applies for this soft navigation.
|
|
2008
|
-
*
|
|
2009
|
-
* Two conditions must hold:
|
|
2010
|
-
*
|
|
2011
|
-
* 1. The **target** pathname matches the rewrite's intercepted pattern.
|
|
2012
|
-
* 2. The **source** route — the page the user is navigating FROM, per the
|
|
2013
|
-
* validated X-Timber-URL header — renders the segment that owns the slot
|
|
2014
|
-
* holding the intercepting page (`interceptingScope`). Only then does that
|
|
2015
|
-
* slot exist in the rendered tree, so only then can the intercepting page
|
|
2016
|
-
* render.
|
|
1643
|
+
* 1. The **target** pathname matches the rewrite's intercepted pattern.
|
|
1644
|
+
* 2. The **source** route — the page the user is navigating FROM, per the
|
|
1645
|
+
* validated X-Timber-URL header — renders the segment that owns the slot
|
|
1646
|
+
* holding the intercepting page (`interceptingScope`). Only then does that
|
|
1647
|
+
* slot exist in the rendered tree, so only then can the intercepting page
|
|
1648
|
+
* render.
|
|
2017
1649
|
*
|
|
2018
1650
|
* Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
|
|
2019
1651
|
* distinguish a route-group-scoped interception from a genuinely root-level
|
|
@@ -2127,17 +1759,17 @@ function pathnameMatchesPattern(pathname, pattern) {
|
|
|
2127
1759
|
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
2128
1760
|
*/
|
|
2129
1761
|
var EMPTY_SEGMENT_UPDATES = /* @__PURE__ */ new Map();
|
|
2130
|
-
var CTX_KEY = Symbol.for("__timber_segment_update_ctx");
|
|
2131
|
-
function getOrCreateContext() {
|
|
2132
|
-
const existing = globalThis[CTX_KEY];
|
|
1762
|
+
var CTX_KEY$1 = Symbol.for("__timber_segment_update_ctx");
|
|
1763
|
+
function getOrCreateContext$1() {
|
|
1764
|
+
const existing = globalThis[CTX_KEY$1];
|
|
2133
1765
|
if (existing !== void 0) return existing;
|
|
2134
1766
|
if (typeof React.createContext === "function") {
|
|
2135
1767
|
const ctx = React.createContext(EMPTY_SEGMENT_UPDATES);
|
|
2136
|
-
globalThis[CTX_KEY] = ctx;
|
|
1768
|
+
globalThis[CTX_KEY$1] = ctx;
|
|
2137
1769
|
return ctx;
|
|
2138
1770
|
}
|
|
2139
1771
|
}
|
|
2140
|
-
getOrCreateContext();
|
|
1772
|
+
getOrCreateContext$1();
|
|
2141
1773
|
//#endregion
|
|
2142
1774
|
//#region src/client/segment-outlet.tsx
|
|
2143
1775
|
/**
|
|
@@ -2596,11 +2228,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2596
2228
|
return new Response("Metadata route must export a default function", { status: 500 });
|
|
2597
2229
|
}
|
|
2598
2230
|
const handlerResult = await mod.default();
|
|
2599
|
-
if (handlerResult instanceof Response)
|
|
2600
|
-
const res = cloneWithMutableHeaders(handlerResult);
|
|
2601
|
-
if (res.ok && !res.headers.has("Cache-Control")) res.headers.set("Cache-Control", "public, max-age=14400, must-revalidate");
|
|
2602
|
-
return res;
|
|
2603
|
-
}
|
|
2231
|
+
if (handlerResult instanceof Response) return cloneWithMutableHeaders(handlerResult);
|
|
2604
2232
|
const contentType = metaMatch.contentType;
|
|
2605
2233
|
let body;
|
|
2606
2234
|
if (typeof handlerResult === "string") body = handlerResult;
|
|
@@ -2829,9 +2457,7 @@ async function hasValidRscCacheKey(req) {
|
|
|
2829
2457
|
* #36c.
|
|
2830
2458
|
*/
|
|
2831
2459
|
function hardenRscPayloadResponse(response) {
|
|
2832
|
-
|
|
2833
|
-
if (!contentType) return;
|
|
2834
|
-
if (contentType.split(";")[0].trim().toLowerCase() !== "text/x-component") return;
|
|
2460
|
+
if (!isRscContentType(response.headers.get("content-type"))) return;
|
|
2835
2461
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
2836
2462
|
}
|
|
2837
2463
|
/**
|
|
@@ -2933,7 +2559,8 @@ function createPipeline(config) {
|
|
|
2933
2559
|
const path = url.pathname;
|
|
2934
2560
|
const startTime = performance.now();
|
|
2935
2561
|
activeRequests++;
|
|
2936
|
-
|
|
2562
|
+
const traceIdValue = generateTraceId();
|
|
2563
|
+
return runWithTraceId(traceIdValue, async () => {
|
|
2937
2564
|
return runWithRequestContext(req, async () => {
|
|
2938
2565
|
const runRequest = async () => {
|
|
2939
2566
|
logRequestReceived({
|
|
@@ -3109,547 +2736,918 @@ function collectEarlyHintHeaders(segments, manifest, options) {
|
|
|
3109
2736
|
//#endregion
|
|
3110
2737
|
//#region src/server/early-hints-sender.ts
|
|
3111
2738
|
/**
|
|
3112
|
-
* Per-request 103 Early Hints sender — ALS bridge for platform adapters.
|
|
3113
|
-
*
|
|
3114
|
-
* The pipeline collects Link headers for CSS, fonts, and JS chunks at
|
|
3115
|
-
* route-match time. On platforms that support it (Node.js v18.11+, Bun),
|
|
3116
|
-
* the adapter can send these as a 103 Early Hints interim response before
|
|
3117
|
-
* the final response is ready.
|
|
2739
|
+
* Per-request 103 Early Hints sender — ALS bridge for platform adapters.
|
|
2740
|
+
*
|
|
2741
|
+
* The pipeline collects Link headers for CSS, fonts, and JS chunks at
|
|
2742
|
+
* route-match time. On platforms that support it (Node.js v18.11+, Bun),
|
|
2743
|
+
* the adapter can send these as a 103 Early Hints interim response before
|
|
2744
|
+
* the final response is ready.
|
|
2745
|
+
*
|
|
2746
|
+
* This module provides an ALS-based bridge: the generated entry point
|
|
2747
|
+
* (e.g., the Nitro entry) wraps the handler with `runWithEarlyHintsSender`,
|
|
2748
|
+
* binding a per-request sender function. The pipeline calls
|
|
2749
|
+
* `sendEarlyHints103()` to fire the 103 if a sender is available.
|
|
2750
|
+
*
|
|
2751
|
+
* On platforms where 103 is handled at the CDN level (e.g., Cloudflare
|
|
2752
|
+
* converts Link headers into 103 automatically), no sender is installed
|
|
2753
|
+
* and `sendEarlyHints103()` is a no-op.
|
|
2754
|
+
*
|
|
2755
|
+
* Design doc: 02-rendering-pipeline.md §"Early Hints (103)"
|
|
2756
|
+
*/
|
|
2757
|
+
/**
|
|
2758
|
+
* Run a function with a per-request early hints sender installed.
|
|
2759
|
+
*
|
|
2760
|
+
* Called by generated entry points (e.g., Nitro node-server/bun) to
|
|
2761
|
+
* bind the platform's writeEarlyHints capability for the request duration.
|
|
2762
|
+
*/
|
|
2763
|
+
function runWithEarlyHintsSender(sender, fn) {
|
|
2764
|
+
return earlyHintsSenderAls.run(sender, fn);
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Send collected Link headers as a 103 Early Hints response.
|
|
2768
|
+
*
|
|
2769
|
+
* No-op if no sender is installed for the current request (e.g., on
|
|
2770
|
+
* Cloudflare where the CDN handles 103 automatically, or in dev mode).
|
|
2771
|
+
*
|
|
2772
|
+
* Non-fatal: errors from the sender are caught and silently ignored.
|
|
2773
|
+
*/
|
|
2774
|
+
function sendEarlyHints103(links) {
|
|
2775
|
+
if (!links.length) return;
|
|
2776
|
+
const sender = earlyHintsSenderAls.getStore();
|
|
2777
|
+
if (!sender) return;
|
|
2778
|
+
try {
|
|
2779
|
+
sender(links);
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
swallow(err, "early hints 103 send failed");
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
//#endregion
|
|
2785
|
+
//#region src/server/tree-builder.ts
|
|
2786
|
+
/**
|
|
2787
|
+
* Build the unified element tree from a matched segment chain.
|
|
2788
|
+
*
|
|
2789
|
+
* Construction is bottom-up:
|
|
2790
|
+
* 1. Start with the page component (leaf segment)
|
|
2791
|
+
* 2. Wrap in status-code error boundaries (fallback chain)
|
|
2792
|
+
* 3. Wrap in AccessGate (if segment has access.ts)
|
|
2793
|
+
* 4. Pass as children to the segment's layout
|
|
2794
|
+
* 5. Repeat up the segment chain to root
|
|
2795
|
+
*
|
|
2796
|
+
* Parallel slots are resolved at each layout level and composed as named props.
|
|
2797
|
+
*/
|
|
2798
|
+
async function buildElementTree(config) {
|
|
2799
|
+
const { segments, loadModule, createElement, errorBoundaryComponent } = config;
|
|
2800
|
+
if (segments.length === 0) throw new Error("[timber] buildElementTree: empty segment chain");
|
|
2801
|
+
const leaf = segments[segments.length - 1];
|
|
2802
|
+
if (leaf.route && !leaf.page) return {
|
|
2803
|
+
tree: null,
|
|
2804
|
+
isApiRoute: true
|
|
2805
|
+
};
|
|
2806
|
+
const PageComponent = (leaf.page ? await loadModule(leaf.page) : null)?.default;
|
|
2807
|
+
if (!PageComponent) throw new Error(`[timber] No page component found for route at ${leaf.urlPath}. Each route must have a page.tsx or route.ts.`);
|
|
2808
|
+
let element = createElement(PageComponent, {});
|
|
2809
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2810
|
+
const segment = segments[i];
|
|
2811
|
+
element = await wrapSegmentWithErrorBoundaries(segment, element, {
|
|
2812
|
+
loadModule,
|
|
2813
|
+
createElement,
|
|
2814
|
+
errorBoundaryComponent
|
|
2815
|
+
});
|
|
2816
|
+
if (segment.access) {
|
|
2817
|
+
const accessFn = (await loadModule(segment.access)).default;
|
|
2818
|
+
element = createElement("timber:access-gate", {
|
|
2819
|
+
accessFn,
|
|
2820
|
+
segmentName: segment.segmentName,
|
|
2821
|
+
children: element
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
if (segment.layout) {
|
|
2825
|
+
const LayoutComponent = (await loadModule(segment.layout)).default;
|
|
2826
|
+
if (LayoutComponent) {
|
|
2827
|
+
const slotProps = {};
|
|
2828
|
+
const slotNames = Object.keys(segment.slots);
|
|
2829
|
+
if (slotNames.length > 0) for (const slotName of slotNames) {
|
|
2830
|
+
const slotNode = segment.slots[slotName];
|
|
2831
|
+
slotProps[slotName] = await buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent);
|
|
2832
|
+
}
|
|
2833
|
+
element = createElement(LayoutComponent, {
|
|
2834
|
+
...slotProps,
|
|
2835
|
+
children: element
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return {
|
|
2841
|
+
tree: element,
|
|
2842
|
+
isApiRoute: false
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Build the element tree for a parallel slot.
|
|
2847
|
+
*
|
|
2848
|
+
* Slots have their own access.ts (SlotAccessGate) and error boundaries.
|
|
2849
|
+
* On access denial: denied.tsx → default.tsx → null (graceful degradation).
|
|
2850
|
+
*/
|
|
2851
|
+
async function buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent) {
|
|
2852
|
+
const PageComponent = (slotNode.page ? await loadModule(slotNode.page) : null)?.default;
|
|
2853
|
+
const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
|
|
2854
|
+
if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
|
|
2855
|
+
let element = createElement(PageComponent, {});
|
|
2856
|
+
element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
|
|
2857
|
+
loadModule,
|
|
2858
|
+
createElement,
|
|
2859
|
+
errorBoundaryComponent
|
|
2860
|
+
});
|
|
2861
|
+
if (slotNode.access) {
|
|
2862
|
+
const accessFn = (await loadModule(slotNode.access)).default;
|
|
2863
|
+
const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
|
|
2864
|
+
const defaultFallback = DefaultComponent ? createElement(DefaultComponent, {}) : null;
|
|
2865
|
+
element = createElement("timber:slot-access-gate", {
|
|
2866
|
+
accessFn,
|
|
2867
|
+
DeniedComponent,
|
|
2868
|
+
slotName: slotNode.segmentName.replace(/^@/, ""),
|
|
2869
|
+
createElement,
|
|
2870
|
+
defaultFallback,
|
|
2871
|
+
children: element
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
return element;
|
|
2875
|
+
}
|
|
2876
|
+
//#endregion
|
|
2877
|
+
//#region src/server/status-code-resolver.ts
|
|
2878
|
+
/**
|
|
2879
|
+
* Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
|
|
2880
|
+
* status-file group on a single segment. Shared by all three fallback
|
|
2881
|
+
* chains — the only structural difference between component 4xx,
|
|
2882
|
+
* component 5xx, and JSON resolution is *which* group is searched and
|
|
2883
|
+
* how the per-segment loop is layered around it.
|
|
2884
|
+
*/
|
|
2885
|
+
function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
|
|
2886
|
+
if (!group) return null;
|
|
2887
|
+
const exact = group[statusStr];
|
|
2888
|
+
if (exact) return {
|
|
2889
|
+
file: exact,
|
|
2890
|
+
status,
|
|
2891
|
+
kind: "exact",
|
|
2892
|
+
segmentIndex
|
|
2893
|
+
};
|
|
2894
|
+
const category = group[categoryKey];
|
|
2895
|
+
if (category) return {
|
|
2896
|
+
file: category,
|
|
2897
|
+
status,
|
|
2898
|
+
kind: "category",
|
|
2899
|
+
segmentIndex
|
|
2900
|
+
};
|
|
2901
|
+
return null;
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Resolve the status-code file to render for a given HTTP status code.
|
|
2905
|
+
*
|
|
2906
|
+
* Walks the segment chain from leaf to root following the fallback chain
|
|
2907
|
+
* defined in design/10-error-handling.md. Returns null if no file is found
|
|
2908
|
+
* (caller should render the framework default).
|
|
2909
|
+
*
|
|
2910
|
+
* @param status - The HTTP status code (4xx or 5xx).
|
|
2911
|
+
* @param segments - The matched segment chain from root (index 0) to leaf (last).
|
|
2912
|
+
* @param format - The response format family ('component' or 'json'). Defaults to 'component'.
|
|
2913
|
+
* @param maxIndex - Optional upper bound on the segment index to search.
|
|
2914
|
+
* When set, the resolver only considers segments `0..maxIndex` (inclusive),
|
|
2915
|
+
* skipping any segments deeper in the tree. Used by the deny renderers to
|
|
2916
|
+
* prevent resolving a status file below a denied segment — matching
|
|
2917
|
+
* `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
|
|
2918
|
+
* this parameter, so the full chain is searched — a 500 from a leaf page
|
|
2919
|
+
* should use the leaf's own `5xx.tsx`. See TIM-1358.
|
|
2920
|
+
*/
|
|
2921
|
+
function resolveStatusFile(status, segments, format = "component", maxIndex) {
|
|
2922
|
+
if (status < 400 || status > 599) return null;
|
|
2923
|
+
if (format === "json") return resolveJson(status, segments, maxIndex);
|
|
2924
|
+
if (status <= 499) return resolve4xx(status, segments, maxIndex);
|
|
2925
|
+
return resolve5xx(status, segments, maxIndex);
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* 4xx component fallback chain — two separate full passes leaf→root.
|
|
2929
|
+
*
|
|
2930
|
+
* The passes must be separate (not interleaved per-segment) so that a
|
|
2931
|
+
* root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
|
|
2932
|
+
* inverts this and is per-segment: a leaf's `error.tsx` beats a root's
|
|
2933
|
+
* `5xx.tsx`. This asymmetry is the only reason these two functions exist
|
|
2934
|
+
* separately.
|
|
2935
|
+
*
|
|
2936
|
+
* Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
|
|
2937
|
+
* Pass 2 — error.tsx (error)
|
|
2938
|
+
*/
|
|
2939
|
+
function resolve4xx(status, segments, maxIndex) {
|
|
2940
|
+
const statusStr = String(status);
|
|
2941
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
2942
|
+
for (let i = start; i >= 0; i--) {
|
|
2943
|
+
const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
|
|
2944
|
+
if (r) return r;
|
|
2945
|
+
}
|
|
2946
|
+
for (let i = start; i >= 0; i--) {
|
|
2947
|
+
const errorFile = segments[i].error;
|
|
2948
|
+
if (errorFile) return {
|
|
2949
|
+
file: errorFile,
|
|
2950
|
+
status,
|
|
2951
|
+
kind: "error",
|
|
2952
|
+
segmentIndex: i
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
return null;
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* 5xx component fallback chain — single pass, per-segment leaf→root.
|
|
2959
|
+
*
|
|
2960
|
+
* At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
|
|
2961
|
+
* `error.tsx` therefore beats a root's `5xx.tsx`, which is the
|
|
2962
|
+
* intentional inverse of the 4xx chain.
|
|
2963
|
+
*/
|
|
2964
|
+
function resolve5xx(status, segments, maxIndex) {
|
|
2965
|
+
const statusStr = String(status);
|
|
2966
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
2967
|
+
for (let i = start; i >= 0; i--) {
|
|
2968
|
+
const segment = segments[i];
|
|
2969
|
+
const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
|
|
2970
|
+
if (r) return r;
|
|
2971
|
+
if (segment.error) return {
|
|
2972
|
+
file: segment.error,
|
|
2973
|
+
status,
|
|
2974
|
+
kind: "error",
|
|
2975
|
+
segmentIndex: i
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
|
|
3118
2982
|
*
|
|
3119
|
-
*
|
|
3120
|
-
*
|
|
3121
|
-
*
|
|
3122
|
-
|
|
2983
|
+
* At each segment: {status}.json → {category}.json. No legacy compat,
|
|
2984
|
+
* no error.tsx — the JSON chain terminates at the category catch-all
|
|
2985
|
+
* and the caller falls back to a bare-JSON framework default.
|
|
2986
|
+
*/
|
|
2987
|
+
function resolveJson(status, segments, maxIndex) {
|
|
2988
|
+
const statusStr = String(status);
|
|
2989
|
+
const categoryKey = status >= 500 ? "5xx" : "4xx";
|
|
2990
|
+
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
2991
|
+
for (let i = start; i >= 0; i--) {
|
|
2992
|
+
const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
|
|
2993
|
+
if (r) return r;
|
|
2994
|
+
}
|
|
2995
|
+
return null;
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Resolve the denial file for a parallel route slot.
|
|
3123
2999
|
*
|
|
3124
|
-
*
|
|
3125
|
-
*
|
|
3126
|
-
* and `sendEarlyHints103()` is a no-op.
|
|
3000
|
+
* Slot denial is graceful degradation — no HTTP status on the wire.
|
|
3001
|
+
* Fallback chain: denied.tsx → default.tsx → null.
|
|
3127
3002
|
*
|
|
3128
|
-
*
|
|
3003
|
+
* @param slotNode - The segment node for the slot (segmentType === 'slot').
|
|
3129
3004
|
*/
|
|
3005
|
+
function resolveSlotDenied(slotNode) {
|
|
3006
|
+
const slotName = slotNode.segmentName.replace(/^@/, "");
|
|
3007
|
+
if (slotNode.denied) return {
|
|
3008
|
+
file: slotNode.denied,
|
|
3009
|
+
slotName,
|
|
3010
|
+
kind: "denied"
|
|
3011
|
+
};
|
|
3012
|
+
if (slotNode.default) return {
|
|
3013
|
+
file: slotNode.default,
|
|
3014
|
+
slotName,
|
|
3015
|
+
kind: "default"
|
|
3016
|
+
};
|
|
3017
|
+
return null;
|
|
3018
|
+
}
|
|
3019
|
+
//#endregion
|
|
3020
|
+
//#region src/server/csrf.ts
|
|
3021
|
+
/** HTTP methods that are considered safe (no mutation). */
|
|
3022
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set([
|
|
3023
|
+
"GET",
|
|
3024
|
+
"HEAD",
|
|
3025
|
+
"OPTIONS"
|
|
3026
|
+
]);
|
|
3130
3027
|
/**
|
|
3131
|
-
*
|
|
3028
|
+
* Derive the request's scheme from trusted sources.
|
|
3132
3029
|
*
|
|
3133
|
-
*
|
|
3134
|
-
*
|
|
3030
|
+
* Priority:
|
|
3031
|
+
* 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
|
|
3032
|
+
* 2. Request URL protocol
|
|
3033
|
+
*
|
|
3034
|
+
* Falls back to 'https' (fail closed) if neither is available.
|
|
3135
3035
|
*/
|
|
3136
|
-
function
|
|
3137
|
-
|
|
3036
|
+
function deriveRequestScheme(req) {
|
|
3037
|
+
const forwarded = req.headers.get("X-Forwarded-Proto");
|
|
3038
|
+
if (forwarded) {
|
|
3039
|
+
const proto = forwarded.split(",")[0].trim().toLowerCase();
|
|
3040
|
+
if (proto === "http" || proto === "https") return proto;
|
|
3041
|
+
}
|
|
3042
|
+
try {
|
|
3043
|
+
return new URL(req.url).protocol.replace(":", "");
|
|
3044
|
+
} catch {
|
|
3045
|
+
return "https";
|
|
3046
|
+
}
|
|
3138
3047
|
}
|
|
3139
3048
|
/**
|
|
3140
|
-
*
|
|
3049
|
+
* Validate the Origin header against the request's full origin
|
|
3050
|
+
* (scheme + host + port).
|
|
3141
3051
|
*
|
|
3142
|
-
*
|
|
3143
|
-
*
|
|
3052
|
+
* For mutation methods (POST, PUT, PATCH, DELETE):
|
|
3053
|
+
* - If `csrf: false`, skip validation.
|
|
3054
|
+
* - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
|
|
3055
|
+
* - Otherwise, Origin must match the derived request origin.
|
|
3144
3056
|
*
|
|
3145
|
-
*
|
|
3057
|
+
* Safe methods (GET, HEAD, OPTIONS) always pass.
|
|
3146
3058
|
*/
|
|
3147
|
-
function
|
|
3148
|
-
if (
|
|
3149
|
-
|
|
3150
|
-
|
|
3059
|
+
function validateCsrf(req, config) {
|
|
3060
|
+
if (SAFE_METHODS.has(req.method)) return { ok: true };
|
|
3061
|
+
if (config.csrf === false) return { ok: true };
|
|
3062
|
+
const origin = req.headers.get("Origin");
|
|
3063
|
+
if (!origin) return {
|
|
3064
|
+
ok: false,
|
|
3065
|
+
status: 403
|
|
3066
|
+
};
|
|
3067
|
+
if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
|
|
3068
|
+
ok: false,
|
|
3069
|
+
status: 403
|
|
3070
|
+
};
|
|
3071
|
+
const host = req.headers.get("Host");
|
|
3072
|
+
if (!host) return {
|
|
3073
|
+
ok: false,
|
|
3074
|
+
status: 403
|
|
3075
|
+
};
|
|
3076
|
+
let originOrigin;
|
|
3151
3077
|
try {
|
|
3152
|
-
|
|
3153
|
-
} catch
|
|
3154
|
-
|
|
3078
|
+
originOrigin = new URL(origin).origin;
|
|
3079
|
+
} catch {
|
|
3080
|
+
return {
|
|
3081
|
+
ok: false,
|
|
3082
|
+
status: 403
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
const scheme = deriveRequestScheme(req);
|
|
3086
|
+
let expectedOrigin;
|
|
3087
|
+
try {
|
|
3088
|
+
expectedOrigin = new URL(`${scheme}://${host}`).origin;
|
|
3089
|
+
} catch {
|
|
3090
|
+
return {
|
|
3091
|
+
ok: false,
|
|
3092
|
+
status: 403
|
|
3093
|
+
};
|
|
3155
3094
|
}
|
|
3095
|
+
return originOrigin === expectedOrigin ? { ok: true } : {
|
|
3096
|
+
ok: false,
|
|
3097
|
+
status: 403
|
|
3098
|
+
};
|
|
3156
3099
|
}
|
|
3157
3100
|
//#endregion
|
|
3158
|
-
//#region src/server/
|
|
3101
|
+
//#region src/server/body-limits.ts
|
|
3102
|
+
var KB = 1024;
|
|
3103
|
+
var MB = 1024 * KB;
|
|
3104
|
+
var GB = 1024 * MB;
|
|
3105
|
+
var DEFAULT_LIMITS = {
|
|
3106
|
+
actionBodySize: 1 * MB,
|
|
3107
|
+
uploadBodySize: 10 * MB,
|
|
3108
|
+
maxFields: 100
|
|
3109
|
+
};
|
|
3110
|
+
var SIZE_PATTERN = /^(\d+(?:\.\d+)?)\s*(kb|mb|gb)?$/i;
|
|
3111
|
+
/** Parse a human-readable size string ("1mb", "512kb", "1024") into bytes. */
|
|
3112
|
+
function parseBodySize(size) {
|
|
3113
|
+
const match = SIZE_PATTERN.exec(size.trim());
|
|
3114
|
+
if (!match) throw new Error(`Invalid body size format: "${size}". Expected format like "1mb", "512kb", or "1024".`);
|
|
3115
|
+
const value = Number.parseFloat(match[1]);
|
|
3116
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
3117
|
+
switch (unit) {
|
|
3118
|
+
case "kb": return Math.floor(value * KB);
|
|
3119
|
+
case "mb": return Math.floor(value * MB);
|
|
3120
|
+
case "gb": return Math.floor(value * GB);
|
|
3121
|
+
case "": return Math.floor(value);
|
|
3122
|
+
default: throw new Error(`Unknown size unit: "${unit}"`);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
/** Strict integer pattern: one or more digits, nothing else. */
|
|
3126
|
+
var STRICT_INTEGER_RE = /^\d+$/;
|
|
3127
|
+
/** Check whether a request body exceeds the configured size limit (stateless, no ALS). */
|
|
3128
|
+
function enforceBodyLimits(req, kind, config) {
|
|
3129
|
+
const contentLength = req.headers.get("Content-Length");
|
|
3130
|
+
if (!contentLength) return {
|
|
3131
|
+
ok: false,
|
|
3132
|
+
status: 411
|
|
3133
|
+
};
|
|
3134
|
+
const trimmed = contentLength.trim();
|
|
3135
|
+
if (!STRICT_INTEGER_RE.test(trimmed)) return {
|
|
3136
|
+
ok: false,
|
|
3137
|
+
status: 411
|
|
3138
|
+
};
|
|
3139
|
+
return Number.parseInt(trimmed, 10) <= resolveLimit(kind, config) ? { ok: true } : {
|
|
3140
|
+
ok: false,
|
|
3141
|
+
status: 413
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3159
3144
|
/**
|
|
3160
|
-
*
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3145
|
+
* Resolve the byte limit for a given body kind, using config overrides or defaults.
|
|
3146
|
+
*/
|
|
3147
|
+
function resolveLimit(kind, config) {
|
|
3148
|
+
const userLimits = config.limits;
|
|
3149
|
+
if (kind === "action") return userLimits?.actionBodySize ? parseBodySize(userLimits.actionBodySize) : DEFAULT_LIMITS.actionBodySize;
|
|
3150
|
+
return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
|
|
3151
|
+
}
|
|
3152
|
+
//#endregion
|
|
3153
|
+
//#region src/server/route-handler.ts
|
|
3154
|
+
/** All recognized HTTP method export names. */
|
|
3155
|
+
var HTTP_METHODS = [
|
|
3156
|
+
"GET",
|
|
3157
|
+
"POST",
|
|
3158
|
+
"PUT",
|
|
3159
|
+
"PATCH",
|
|
3160
|
+
"DELETE",
|
|
3161
|
+
"HEAD",
|
|
3162
|
+
"OPTIONS"
|
|
3163
|
+
];
|
|
3164
|
+
/**
|
|
3165
|
+
* Resolve the full list of allowed methods for a route module.
|
|
3168
3166
|
*
|
|
3169
|
-
*
|
|
3167
|
+
* Includes:
|
|
3168
|
+
* - All explicitly exported methods
|
|
3169
|
+
* - HEAD (implicit when GET is exported)
|
|
3170
|
+
* - OPTIONS (always implicit)
|
|
3170
3171
|
*/
|
|
3171
|
-
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
tree: null,
|
|
3177
|
-
isApiRoute: true
|
|
3178
|
-
};
|
|
3179
|
-
const PageComponent = (leaf.page ? await loadModule(leaf.page) : null)?.default;
|
|
3180
|
-
if (!PageComponent) throw new Error(`[timber] No page component found for route at ${leaf.urlPath}. Each route must have a page.tsx or route.ts.`);
|
|
3181
|
-
let element = createElement(PageComponent, {});
|
|
3182
|
-
for (let i = segments.length - 1; i >= 0; i--) {
|
|
3183
|
-
const segment = segments[i];
|
|
3184
|
-
element = await wrapSegmentWithErrorBoundaries(segment, element, {
|
|
3185
|
-
loadModule,
|
|
3186
|
-
createElement,
|
|
3187
|
-
errorBoundaryComponent
|
|
3188
|
-
});
|
|
3189
|
-
if (segment.access) {
|
|
3190
|
-
const accessFn = (await loadModule(segment.access)).default;
|
|
3191
|
-
element = createElement("timber:access-gate", {
|
|
3192
|
-
accessFn,
|
|
3193
|
-
segmentName: segment.segmentName,
|
|
3194
|
-
children: element
|
|
3195
|
-
});
|
|
3196
|
-
}
|
|
3197
|
-
if (segment.layout) {
|
|
3198
|
-
const LayoutComponent = (await loadModule(segment.layout)).default;
|
|
3199
|
-
if (LayoutComponent) {
|
|
3200
|
-
const slotProps = {};
|
|
3201
|
-
const slotNames = Object.keys(segment.slots);
|
|
3202
|
-
if (slotNames.length > 0) for (const slotName of slotNames) {
|
|
3203
|
-
const slotNode = segment.slots[slotName];
|
|
3204
|
-
slotProps[slotName] = await buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent);
|
|
3205
|
-
}
|
|
3206
|
-
element = createElement(LayoutComponent, {
|
|
3207
|
-
...slotProps,
|
|
3208
|
-
children: element
|
|
3209
|
-
});
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3172
|
+
function resolveAllowedMethods(mod) {
|
|
3173
|
+
const methods = [];
|
|
3174
|
+
for (const method of HTTP_METHODS) {
|
|
3175
|
+
if (method === "HEAD" || method === "OPTIONS") continue;
|
|
3176
|
+
if (mod[method]) methods.push(method);
|
|
3212
3177
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3178
|
+
if (mod.GET && !mod.HEAD) methods.push("HEAD");
|
|
3179
|
+
else if (mod.HEAD) methods.push("HEAD");
|
|
3180
|
+
if (!mod.OPTIONS) methods.push("OPTIONS");
|
|
3181
|
+
else methods.push("OPTIONS");
|
|
3182
|
+
return methods;
|
|
3217
3183
|
}
|
|
3218
3184
|
/**
|
|
3219
|
-
*
|
|
3185
|
+
* Handle an incoming request against a route.ts module.
|
|
3220
3186
|
*
|
|
3221
|
-
*
|
|
3222
|
-
*
|
|
3187
|
+
* Dispatches to the named method handler, auto-generates 405/OPTIONS,
|
|
3188
|
+
* and merges response headers from ctx.headers.
|
|
3223
3189
|
*/
|
|
3224
|
-
async function
|
|
3225
|
-
const
|
|
3226
|
-
const
|
|
3227
|
-
if (
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3190
|
+
async function handleRouteRequest(mod, ctx) {
|
|
3191
|
+
const method = ctx.req.method.toUpperCase();
|
|
3192
|
+
const allowHeader = resolveAllowedMethods(mod).join(", ");
|
|
3193
|
+
if (method === "OPTIONS") {
|
|
3194
|
+
if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
|
|
3195
|
+
return new Response(null, {
|
|
3196
|
+
status: 204,
|
|
3197
|
+
headers: { Allow: allowHeader }
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
if (method === "HEAD") {
|
|
3201
|
+
if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
|
|
3202
|
+
if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
|
|
3203
|
+
}
|
|
3204
|
+
const handler = mod[method];
|
|
3205
|
+
if (!handler) return new Response(null, {
|
|
3206
|
+
status: 405,
|
|
3207
|
+
headers: { Allow: allowHeader }
|
|
3233
3208
|
});
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3209
|
+
return runHandler(handler, ctx);
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Run a handler, merge ctx.headers into the response, and catch errors.
|
|
3213
|
+
*/
|
|
3214
|
+
async function runHandler(handler, ctx) {
|
|
3215
|
+
try {
|
|
3216
|
+
return mergeResponseHeaders(await handler(ctx), ctx.headers);
|
|
3217
|
+
} catch (error) {
|
|
3218
|
+
if (error instanceof DenySignal || error instanceof RedirectSignal) throw error;
|
|
3219
|
+
logRouteError({
|
|
3220
|
+
method: ctx.req.method,
|
|
3221
|
+
path: new URL(ctx.req.url).pathname,
|
|
3222
|
+
error
|
|
3245
3223
|
});
|
|
3224
|
+
return new Response(null, { status: 500 });
|
|
3246
3225
|
}
|
|
3247
|
-
return element;
|
|
3248
3226
|
}
|
|
3249
|
-
//#endregion
|
|
3250
|
-
//#region src/server/status-code-resolver.ts
|
|
3251
3227
|
/**
|
|
3252
|
-
*
|
|
3253
|
-
*
|
|
3254
|
-
*
|
|
3255
|
-
* component 5xx, and JSON resolution is *which* group is searched and
|
|
3256
|
-
* how the per-segment loop is layered around it.
|
|
3228
|
+
* Merge response headers from ctx.headers into the handler's response.
|
|
3229
|
+
* ctx.headers (set by middleware or the handler) are applied to the final response.
|
|
3230
|
+
* Handler-set headers take precedence over ctx.headers.
|
|
3257
3231
|
*/
|
|
3258
|
-
function
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
};
|
|
3274
|
-
return
|
|
3232
|
+
function mergeResponseHeaders(res, ctxHeaders) {
|
|
3233
|
+
let hasCtxHeaders = false;
|
|
3234
|
+
ctxHeaders.forEach(() => {
|
|
3235
|
+
hasCtxHeaders = true;
|
|
3236
|
+
});
|
|
3237
|
+
if (!hasCtxHeaders) return res;
|
|
3238
|
+
const merged = new Headers();
|
|
3239
|
+
ctxHeaders.forEach((value, key) => {
|
|
3240
|
+
if (key.toLowerCase() === "set-cookie") merged.append(key, value);
|
|
3241
|
+
else merged.set(key, value);
|
|
3242
|
+
});
|
|
3243
|
+
const resCookies = res.headers.getSetCookie();
|
|
3244
|
+
for (const cookie of resCookies) merged.append("Set-Cookie", cookie);
|
|
3245
|
+
res.headers.forEach((value, key) => {
|
|
3246
|
+
if (key.toLowerCase() !== "set-cookie") merged.set(key, value);
|
|
3247
|
+
});
|
|
3248
|
+
return new Response(res.body, {
|
|
3249
|
+
status: res.status,
|
|
3250
|
+
statusText: res.statusText,
|
|
3251
|
+
headers: merged
|
|
3252
|
+
});
|
|
3275
3253
|
}
|
|
3254
|
+
//#endregion
|
|
3255
|
+
//#region src/server/render-timeout.ts
|
|
3276
3256
|
/**
|
|
3277
|
-
*
|
|
3257
|
+
* Render timeout utilities for SSR streaming pipeline.
|
|
3278
3258
|
*
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3259
|
+
* Provides a RenderTimeoutError class and a helper to create
|
|
3260
|
+
* timeout-guarded AbortSignals. Used to defend against hung RSC
|
|
3261
|
+
* streams and infinite SSR renders.
|
|
3282
3262
|
*
|
|
3283
|
-
*
|
|
3284
|
-
* @param segments - The matched segment chain from root (index 0) to leaf (last).
|
|
3285
|
-
* @param format - The response format family ('component' or 'json'). Defaults to 'component'.
|
|
3286
|
-
* @param maxIndex - Optional upper bound on the segment index to search.
|
|
3287
|
-
* When set, the resolver only considers segments `0..maxIndex` (inclusive),
|
|
3288
|
-
* skipping any segments deeper in the tree. Used by the deny renderers to
|
|
3289
|
-
* prevent resolving a status file below a denied segment — matching
|
|
3290
|
-
* `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
|
|
3291
|
-
* this parameter, so the full chain is searched — a 500 from a leaf page
|
|
3292
|
-
* should use the leaf's own `5xx.tsx`. See TIM-1358.
|
|
3263
|
+
* Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
|
|
3293
3264
|
*/
|
|
3294
|
-
function resolveStatusFile(status, segments, format = "component", maxIndex) {
|
|
3295
|
-
if (status < 400 || status > 599) return null;
|
|
3296
|
-
if (format === "json") return resolveJson(status, segments, maxIndex);
|
|
3297
|
-
if (status <= 499) return resolve4xx(status, segments, maxIndex);
|
|
3298
|
-
return resolve5xx(status, segments, maxIndex);
|
|
3299
|
-
}
|
|
3300
3265
|
/**
|
|
3301
|
-
*
|
|
3266
|
+
* Error thrown when an SSR render or RSC stream read exceeds the
|
|
3267
|
+
* configured timeout. Callers can check `instanceof RenderTimeoutError`
|
|
3268
|
+
* to distinguish timeout from other errors and return a 504 or close
|
|
3269
|
+
* the connection cleanly.
|
|
3270
|
+
*/
|
|
3271
|
+
var RenderTimeoutError = class extends Error {
|
|
3272
|
+
timeoutMs;
|
|
3273
|
+
constructor(timeoutMs, context) {
|
|
3274
|
+
const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
|
|
3275
|
+
super(message);
|
|
3276
|
+
this.name = "RenderTimeoutError";
|
|
3277
|
+
this.timeoutMs = timeoutMs;
|
|
3278
|
+
}
|
|
3279
|
+
};
|
|
3280
|
+
//#endregion
|
|
3281
|
+
//#region src/server/prebuilt/cache-key.ts
|
|
3282
|
+
/**
|
|
3283
|
+
* Component cache-key hashing for prebuilt (cache.component) entries.
|
|
3302
3284
|
*
|
|
3303
|
-
*
|
|
3304
|
-
*
|
|
3305
|
-
*
|
|
3306
|
-
*
|
|
3307
|
-
* separately.
|
|
3285
|
+
* Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
|
|
3286
|
+
* design/44-render-at-build-time.md §Cache Key. Both the request-time
|
|
3287
|
+
* lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
|
|
3288
|
+
* MUST use this exact function, or build artifacts become unaddressable.
|
|
3308
3289
|
*
|
|
3309
|
-
*
|
|
3310
|
-
*
|
|
3290
|
+
* THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
|
|
3291
|
+
* value semantics** — two inputs are the same entry iff they are the same
|
|
3292
|
+
* JSON value. This is an ALLOWLIST, not a denylist: a value participates
|
|
3293
|
+
* in caching iff it is plain JSON data — null, boolean, finite number,
|
|
3294
|
+
* string, dense array, or a plain `Object.prototype` object with only
|
|
3295
|
+
* enumerable string-keyed data properties — plus `undefined` and `-0`,
|
|
3296
|
+
* which hash as distinct sentinels because flight preserves them. Every
|
|
3297
|
+
* value outside that domain (functions, symbols, bigints, accessors,
|
|
3298
|
+
* hidden or symbol-keyed properties, non-`Object.prototype` prototypes
|
|
3299
|
+
* including null, aliased references, sparse arrays, cycles, non-finite
|
|
3300
|
+
* numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
|
|
3301
|
+
* null and the caller renders live — slower, never wrong.
|
|
3302
|
+
*
|
|
3303
|
+
* The line between rejected and declared-out-of-domain: values are
|
|
3304
|
+
* REJECTED when the JSON projection itself would be ambiguous or lossy
|
|
3305
|
+
* (hidden/symbol/non-enumerable properties, accessors, exotic
|
|
3306
|
+
* prototypes). Observable state that leaves the projection intact — key
|
|
3307
|
+
* order, reference identity, descriptor metadata (writable/configurable,
|
|
3308
|
+
* frozen/sealed/extensible), proxy behavior — is DECLARED outside
|
|
3309
|
+
* cache.component's supported input domain rather than rejected:
|
|
3310
|
+
* key-order insensitivity is required for determinism, and rejecting
|
|
3311
|
+
* frozen objects would unkey ubiquitous legitimate inputs
|
|
3312
|
+
* (Object.freeze'd config props) to defend against components that
|
|
3313
|
+
* render differently based on property attributes.
|
|
3314
|
+
*
|
|
3315
|
+
* Two hard rules for this walk itself: it must be TOTAL (never throw
|
|
3316
|
+
* anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
|
|
3317
|
+
* reads only; never invoke user code such as getters).
|
|
3318
|
+
*
|
|
3319
|
+
* Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
|
|
3320
|
+
* (design/13-security.md checklist #14 — cache key determinism; key order
|
|
3321
|
+
* is deliberately NOT part of identity).
|
|
3311
3322
|
*/
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3323
|
+
var UnkeyableValueError = class extends Error {};
|
|
3324
|
+
function canonicalize$1(value, seen) {
|
|
3325
|
+
if (value === null) return "null";
|
|
3326
|
+
switch (typeof value) {
|
|
3327
|
+
case "string": return JSON.stringify(value);
|
|
3328
|
+
case "boolean": return value ? "true" : "false";
|
|
3329
|
+
case "number":
|
|
3330
|
+
if (!Number.isFinite(value)) throw new UnkeyableValueError("non-finite number");
|
|
3331
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
3332
|
+
case "object": break;
|
|
3333
|
+
case "undefined": return "undefined";
|
|
3334
|
+
default: throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
|
|
3318
3335
|
}
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3336
|
+
const obj = value;
|
|
3337
|
+
if (seen.has(obj)) throw new UnkeyableValueError("repeated object reference (cycle or alias)");
|
|
3338
|
+
seen.add(obj);
|
|
3339
|
+
if (Object.getOwnPropertySymbols(obj).length > 0) throw new UnkeyableValueError("symbol-keyed property");
|
|
3340
|
+
{
|
|
3341
|
+
if (Array.isArray(obj)) {
|
|
3342
|
+
if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) throw new UnkeyableValueError("non-index array property");
|
|
3343
|
+
const parts = [];
|
|
3344
|
+
for (let i = 0; i < obj.length; i++) {
|
|
3345
|
+
const desc = Object.getOwnPropertyDescriptor(obj, i);
|
|
3346
|
+
if (desc === void 0) throw new UnkeyableValueError("sparse array hole");
|
|
3347
|
+
if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
|
|
3348
|
+
if (!desc.enumerable) throw new UnkeyableValueError("non-enumerable array element");
|
|
3349
|
+
parts.push(canonicalize$1(desc.value, seen));
|
|
3350
|
+
}
|
|
3351
|
+
return "[" + parts.join(",") + "]";
|
|
3352
|
+
}
|
|
3353
|
+
if (Object.getPrototypeOf(obj) !== Object.prototype) throw new UnkeyableValueError("non-plain object");
|
|
3354
|
+
const keys = Object.keys(obj).sort();
|
|
3355
|
+
if (keys.length !== Object.getOwnPropertyNames(obj).length) throw new UnkeyableValueError("non-enumerable property");
|
|
3356
|
+
const parts = [];
|
|
3357
|
+
for (const key of keys) {
|
|
3358
|
+
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
3359
|
+
if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
|
|
3360
|
+
parts.push(JSON.stringify(key) + ":" + canonicalize$1(desc.value, seen));
|
|
3361
|
+
}
|
|
3362
|
+
return "{" + parts.join(",") + "}";
|
|
3327
3363
|
}
|
|
3328
|
-
return null;
|
|
3329
3364
|
}
|
|
3330
3365
|
/**
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
3333
|
-
*
|
|
3334
|
-
*
|
|
3335
|
-
*
|
|
3366
|
+
* Canonicalize a single value, or null if it is unkeyable. The ONE
|
|
3367
|
+
* definition of "faithfully comparable" — every prebuilt code path that
|
|
3368
|
+
* needs value equality (cache keys, slot-param divergence) goes through
|
|
3369
|
+
* this, so the unkeyable rules (accessors, hidden props, aliases,
|
|
3370
|
+
* bigints, …) live in exactly one place.
|
|
3336
3371
|
*/
|
|
3337
|
-
function
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
|
|
3343
|
-
if (r) return r;
|
|
3344
|
-
if (segment.error) return {
|
|
3345
|
-
file: segment.error,
|
|
3346
|
-
status,
|
|
3347
|
-
kind: "error",
|
|
3348
|
-
segmentIndex: i
|
|
3349
|
-
};
|
|
3372
|
+
function tryCanonicalize(value) {
|
|
3373
|
+
try {
|
|
3374
|
+
return canonicalize$1(value, /* @__PURE__ */ new Set());
|
|
3375
|
+
} catch (error) {
|
|
3376
|
+
return toUnkeyable(error);
|
|
3350
3377
|
}
|
|
3351
|
-
return null;
|
|
3352
3378
|
}
|
|
3353
3379
|
/**
|
|
3354
|
-
*
|
|
3355
|
-
*
|
|
3356
|
-
*
|
|
3357
|
-
*
|
|
3358
|
-
*
|
|
3380
|
+
* Totality by construction: ANY throw from the walk means the input is
|
|
3381
|
+
* outside the keyable domain. The walk touches only user-controlled
|
|
3382
|
+
* values through reflection ops — and a Proxy trap can throw arbitrary
|
|
3383
|
+
* errors from any of them (codex review on PR #835) — so converting only
|
|
3384
|
+
* UnkeyableValueError would let hostile/exotic inputs abort a render
|
|
3385
|
+
* that plain live rendering would have served fine. Unexpected error
|
|
3386
|
+
* types are logged for visibility (they would indicate either a proxy
|
|
3387
|
+
* trap or a canonicalizer bug) but still degrade to unkeyable.
|
|
3359
3388
|
*/
|
|
3360
|
-
function
|
|
3361
|
-
|
|
3362
|
-
const categoryKey = status >= 500 ? "5xx" : "4xx";
|
|
3363
|
-
const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
|
|
3364
|
-
for (let i = start; i >= 0; i--) {
|
|
3365
|
-
const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
|
|
3366
|
-
if (r) return r;
|
|
3367
|
-
}
|
|
3389
|
+
function toUnkeyable(error) {
|
|
3390
|
+
if (!(error instanceof UnkeyableValueError)) console.error("[timber] cache key canonicalization threw — treating input as unkeyable:", error);
|
|
3368
3391
|
return null;
|
|
3369
3392
|
}
|
|
3370
3393
|
/**
|
|
3371
|
-
*
|
|
3394
|
+
* Split a props record into data props (participate in the cache key) and
|
|
3395
|
+
* slot values (React-element holes, excluded from the key) per the
|
|
3396
|
+
* component's declared `slots` option (TIM-1173).
|
|
3372
3397
|
*
|
|
3373
|
-
*
|
|
3374
|
-
*
|
|
3398
|
+
* Every DECLARED slot lands in `slotValues` — present or not — because the
|
|
3399
|
+
* capture render always injects a SlotOutlet placeholder for each declared
|
|
3400
|
+
* slot: the cached shell is identical whether the caller passed the slot
|
|
3401
|
+
* or omitted it, so slot presence must not vary the key. `dataProps` is
|
|
3402
|
+
* everything else and is what gets hashed.
|
|
3375
3403
|
*
|
|
3376
|
-
*
|
|
3404
|
+
* Returns null when the props OBJECT itself is outside the identity
|
|
3405
|
+
* contract at the top level — accessors, symbol-keyed or non-enumerable
|
|
3406
|
+
* properties, or a non-`Object.prototype` prototype. Splitting such an
|
|
3407
|
+
* object would normalize away observable state (and reading an accessor
|
|
3408
|
+
* would invoke user code, which this walk must never do) BEFORE
|
|
3409
|
+
* `computeComponentCacheKey` gets the chance to reject it — a cached
|
|
3410
|
+
* shell could then be served for inputs a live render distinguishes.
|
|
3411
|
+
* Null means unkeyable: the caller renders live with the original props.
|
|
3412
|
+
* The walk is descriptor-reads only; getters are never invoked. Nested
|
|
3413
|
+
* data-prop values still face the full allowlist when hashed.
|
|
3377
3414
|
*/
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
file: slotNode.default,
|
|
3387
|
-
slotName,
|
|
3388
|
-
kind: "default"
|
|
3389
|
-
};
|
|
3390
|
-
return null;
|
|
3415
|
+
/** Create an own enumerable data property — safe for keys like `__proto__`. */
|
|
3416
|
+
function defineOwn(target, key, value) {
|
|
3417
|
+
Object.defineProperty(target, key, {
|
|
3418
|
+
value,
|
|
3419
|
+
enumerable: true,
|
|
3420
|
+
writable: true,
|
|
3421
|
+
configurable: true
|
|
3422
|
+
});
|
|
3391
3423
|
}
|
|
3392
|
-
|
|
3393
|
-
//#region src/server/csrf.ts
|
|
3394
|
-
/** HTTP methods that are considered safe (no mutation). */
|
|
3395
|
-
var SAFE_METHODS = /* @__PURE__ */ new Set([
|
|
3396
|
-
"GET",
|
|
3397
|
-
"HEAD",
|
|
3398
|
-
"OPTIONS"
|
|
3399
|
-
]);
|
|
3400
|
-
/**
|
|
3401
|
-
* Derive the request's scheme from trusted sources.
|
|
3402
|
-
*
|
|
3403
|
-
* Priority:
|
|
3404
|
-
* 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
|
|
3405
|
-
* 2. Request URL protocol
|
|
3406
|
-
*
|
|
3407
|
-
* Falls back to 'https' (fail closed) if neither is available.
|
|
3408
|
-
*/
|
|
3409
|
-
function deriveRequestScheme(req) {
|
|
3410
|
-
const forwarded = req.headers.get("X-Forwarded-Proto");
|
|
3411
|
-
if (forwarded) {
|
|
3412
|
-
const proto = forwarded.split(",")[0].trim().toLowerCase();
|
|
3413
|
-
if (proto === "http" || proto === "https") return proto;
|
|
3414
|
-
}
|
|
3424
|
+
function splitSlotProps(props, slots) {
|
|
3415
3425
|
try {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3426
|
+
if (Object.getPrototypeOf(props) !== Object.prototype) return null;
|
|
3427
|
+
if (Object.getOwnPropertySymbols(props).length > 0) return null;
|
|
3428
|
+
const slotSet = new Set(slots);
|
|
3429
|
+
const dataProps = {};
|
|
3430
|
+
const slotValues = {};
|
|
3431
|
+
for (const key of Object.getOwnPropertyNames(props)) {
|
|
3432
|
+
const desc = Object.getOwnPropertyDescriptor(props, key);
|
|
3433
|
+
if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
|
|
3434
|
+
defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
|
|
3435
|
+
}
|
|
3436
|
+
for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
|
|
3437
|
+
return {
|
|
3438
|
+
dataProps,
|
|
3439
|
+
slotValues
|
|
3440
|
+
};
|
|
3441
|
+
} catch (error) {
|
|
3442
|
+
return toUnkeyable(error);
|
|
3419
3443
|
}
|
|
3420
3444
|
}
|
|
3421
3445
|
/**
|
|
3422
|
-
*
|
|
3423
|
-
* (
|
|
3424
|
-
*
|
|
3425
|
-
* For mutation methods (POST, PUT, PATCH, DELETE):
|
|
3426
|
-
* - If `csrf: false`, skip validation.
|
|
3427
|
-
* - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
|
|
3428
|
-
* - Otherwise, Origin must match the derived request origin.
|
|
3446
|
+
* Compute the cache-entry hash for a component render:
|
|
3447
|
+
* `sha256({props, params}).slice(0, 16)` over canonical JSON.
|
|
3429
3448
|
*
|
|
3430
|
-
*
|
|
3449
|
+
* Returns null when props (or params) contain unkeyable values — the
|
|
3450
|
+
* caller must fall back to dynamic rendering.
|
|
3431
3451
|
*/
|
|
3432
|
-
function
|
|
3433
|
-
|
|
3434
|
-
if (config.csrf === false) return { ok: true };
|
|
3435
|
-
const origin = req.headers.get("Origin");
|
|
3436
|
-
if (!origin) return {
|
|
3437
|
-
ok: false,
|
|
3438
|
-
status: 403
|
|
3439
|
-
};
|
|
3440
|
-
if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
|
|
3441
|
-
ok: false,
|
|
3442
|
-
status: 403
|
|
3443
|
-
};
|
|
3444
|
-
const host = req.headers.get("Host");
|
|
3445
|
-
if (!host) return {
|
|
3446
|
-
ok: false,
|
|
3447
|
-
status: 403
|
|
3448
|
-
};
|
|
3449
|
-
let originOrigin;
|
|
3450
|
-
try {
|
|
3451
|
-
originOrigin = new URL(origin).origin;
|
|
3452
|
-
} catch {
|
|
3453
|
-
return {
|
|
3454
|
-
ok: false,
|
|
3455
|
-
status: 403
|
|
3456
|
-
};
|
|
3457
|
-
}
|
|
3458
|
-
const scheme = deriveRequestScheme(req);
|
|
3459
|
-
let expectedOrigin;
|
|
3452
|
+
function computeComponentCacheKey(props, params) {
|
|
3453
|
+
let canonical;
|
|
3460
3454
|
try {
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
status: 403
|
|
3466
|
-
};
|
|
3455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3456
|
+
canonical = "{\"params\":" + canonicalize$1({ ...params }, seen) + ",\"props\":" + canonicalize$1(props, seen) + "}";
|
|
3457
|
+
} catch (error) {
|
|
3458
|
+
return toUnkeyable(error);
|
|
3467
3459
|
}
|
|
3468
|
-
return
|
|
3469
|
-
ok: false,
|
|
3470
|
-
status: 403
|
|
3471
|
-
};
|
|
3460
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
3472
3461
|
}
|
|
3473
3462
|
//#endregion
|
|
3474
|
-
//#region src/
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3463
|
+
//#region src/client/slot-context.ts
|
|
3464
|
+
/**
|
|
3465
|
+
* SlotContext — delivers live slot content to SlotOutlet holes inside
|
|
3466
|
+
* cached component shells (TIM-1173).
|
|
3467
|
+
*
|
|
3468
|
+
* A `cache.component(Fn, { slots: [...] })` shell is captured with a
|
|
3469
|
+
* stable <SlotOutlet slot={name} /> client reference in place of each
|
|
3470
|
+
* declared slot prop. At request time the revived shell is wrapped in a
|
|
3471
|
+
* SlotsProvider carrying the live slot values; each outlet reads its
|
|
3472
|
+
* value from this context by name.
|
|
3473
|
+
*
|
|
3474
|
+
* Providers MERGE per-key with their parent: an inner SlotsProvider
|
|
3475
|
+
* inherits the outer's values and overrides only the keys it declares.
|
|
3476
|
+
* Two sibling instances each have their own provider and resolve
|
|
3477
|
+
* independently. Layouts use a reserved slot name (LAYOUT_CHILDREN_SLOT)
|
|
3478
|
+
* that cannot collide with user-declared slot names (TIM-1191).
|
|
3479
|
+
*
|
|
3480
|
+
* Generalizes the original layout children pattern (TIM-1181) from a
|
|
3481
|
+
* single implicit `children` hole on layouts to explicitly declared,
|
|
3482
|
+
* named slot props on any cached component. Layouts use this same
|
|
3483
|
+
* context with the reserved LAYOUT_CHILDREN_SLOT key (TIM-1191).
|
|
3484
|
+
*
|
|
3485
|
+
* SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
|
|
3486
|
+
* can duplicate this module across chunks; globalThis guarantees a single
|
|
3487
|
+
* context instance.
|
|
3488
|
+
*
|
|
3489
|
+
* See design/45-cache-lifetimes.md §Slot Components.
|
|
3490
|
+
*/
|
|
3491
|
+
var CTX_KEY = Symbol.for("__timber_slot_ctx");
|
|
3492
|
+
function getOrCreateContext() {
|
|
3493
|
+
const existing = globalThis[CTX_KEY];
|
|
3494
|
+
if (existing !== void 0) return existing;
|
|
3495
|
+
if (typeof React.createContext === "function") {
|
|
3496
|
+
const ctx = React.createContext(null);
|
|
3497
|
+
globalThis[CTX_KEY] = ctx;
|
|
3498
|
+
return ctx;
|
|
3496
3499
|
}
|
|
3497
3500
|
}
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
function enforceBodyLimits(req, kind, config) {
|
|
3502
|
-
const contentLength = req.headers.get("Content-Length");
|
|
3503
|
-
if (!contentLength) return {
|
|
3504
|
-
ok: false,
|
|
3505
|
-
status: 411
|
|
3506
|
-
};
|
|
3507
|
-
const trimmed = contentLength.trim();
|
|
3508
|
-
if (!STRICT_INTEGER_RE.test(trimmed)) return {
|
|
3509
|
-
ok: false,
|
|
3510
|
-
status: 411
|
|
3511
|
-
};
|
|
3512
|
-
return Number.parseInt(trimmed, 10) <= resolveLimit(kind, config) ? { ok: true } : {
|
|
3513
|
-
ok: false,
|
|
3514
|
-
status: 413
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3501
|
+
var SlotContext = getOrCreateContext();
|
|
3502
|
+
//#endregion
|
|
3503
|
+
//#region src/client/slot-outlet.tsx
|
|
3517
3504
|
/**
|
|
3518
|
-
*
|
|
3505
|
+
* SlotOutlet — the hole in a cached component shell (TIM-1173).
|
|
3506
|
+
*
|
|
3507
|
+
* At capture time the prebuilt runtime renders the wrapped component with
|
|
3508
|
+
* <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
|
|
3509
|
+
* client component, it serializes into the flight payload as a stable,
|
|
3510
|
+
* deterministic client reference — the same bytes regardless of what live
|
|
3511
|
+
* content will fill the hole. At request time the revived shell sits under
|
|
3512
|
+
* a SlotsProvider and each outlet reads its live value from SlotContext.
|
|
3513
|
+
*
|
|
3514
|
+
* See design/45-cache-lifetimes.md §Slot Components.
|
|
3519
3515
|
*/
|
|
3520
|
-
function
|
|
3521
|
-
const
|
|
3522
|
-
|
|
3523
|
-
return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
|
|
3516
|
+
function SlotOutlet({ slot }) {
|
|
3517
|
+
const values = useContext(SlotContext);
|
|
3518
|
+
return values ? values[slot] ?? null : null;
|
|
3524
3519
|
}
|
|
3525
3520
|
//#endregion
|
|
3526
|
-
//#region src/
|
|
3527
|
-
/** All recognized HTTP method export names. */
|
|
3528
|
-
var HTTP_METHODS = [
|
|
3529
|
-
"GET",
|
|
3530
|
-
"POST",
|
|
3531
|
-
"PUT",
|
|
3532
|
-
"PATCH",
|
|
3533
|
-
"DELETE",
|
|
3534
|
-
"HEAD",
|
|
3535
|
-
"OPTIONS"
|
|
3536
|
-
];
|
|
3521
|
+
//#region src/client/slot-provider.tsx
|
|
3537
3522
|
/**
|
|
3538
|
-
*
|
|
3523
|
+
* SlotsProvider — wraps a revived cached shell to supply live slot content
|
|
3524
|
+
* to the SlotOutlet holes inside it (TIM-1173).
|
|
3539
3525
|
*
|
|
3540
|
-
*
|
|
3541
|
-
*
|
|
3542
|
-
*
|
|
3543
|
-
*
|
|
3526
|
+
* Values MERGE with the nearest parent provider so that nesting a
|
|
3527
|
+
* cache.component SlotsProvider inside a layout SlotsProvider preserves
|
|
3528
|
+
* the layout's `children` slot while adding the component's own slots.
|
|
3529
|
+
* Inner values override outer ones with the same name (spread order).
|
|
3530
|
+
*
|
|
3531
|
+
* Tree structure per cached-component instance:
|
|
3532
|
+
* SlotsProvider(values={children: <Live />})
|
|
3533
|
+
* └── revived shell
|
|
3534
|
+
* └── SlotOutlet(slot="children") → reads context → <Live />
|
|
3535
|
+
*
|
|
3536
|
+
* See design/45-cache-lifetimes.md §Slot Components.
|
|
3544
3537
|
*/
|
|
3545
|
-
function
|
|
3546
|
-
const
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
else if (mod.HEAD) methods.push("HEAD");
|
|
3553
|
-
if (!mod.OPTIONS) methods.push("OPTIONS");
|
|
3554
|
-
else methods.push("OPTIONS");
|
|
3555
|
-
return methods;
|
|
3538
|
+
function SlotsProvider({ values, children }) {
|
|
3539
|
+
const parent = useContext(SlotContext);
|
|
3540
|
+
const merged = parent ? {
|
|
3541
|
+
...parent,
|
|
3542
|
+
...values
|
|
3543
|
+
} : values;
|
|
3544
|
+
return createElement(SlotContext.Provider, { value: merged }, children);
|
|
3556
3545
|
}
|
|
3557
3546
|
/**
|
|
3558
|
-
*
|
|
3547
|
+
* Is this value the framework-injected layout children slot outlet
|
|
3548
|
+
* (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
|
|
3549
|
+
* receive this as their `children` prop — a stable client reference
|
|
3550
|
+
* whose cache key is stripped so layout `cache.component` hits work
|
|
3551
|
+
* (design/45 §Layout Propagation).
|
|
3559
3552
|
*
|
|
3560
|
-
*
|
|
3561
|
-
*
|
|
3553
|
+
* Uses a framework-reserved slot name (LAYOUT_CHILDREN_SLOT) that
|
|
3554
|
+
* cannot collide with user-declared slot names, preventing context
|
|
3555
|
+
* shadowing when a cached component with its own SlotsProvider is
|
|
3556
|
+
* nested inside a layout. See TIM-1191.
|
|
3557
|
+
*
|
|
3558
|
+
* Total: the probes run against a user-controlled value, and a Proxy
|
|
3559
|
+
* trap can throw — that must classify as "not the outlet" (the
|
|
3560
|
+
* canonicalizer then rejects the value as unkeyable → live render), not
|
|
3561
|
+
* abort the request (codex P2 on PR #890).
|
|
3562
3562
|
*/
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
return
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
});
|
|
3572
|
-
}
|
|
3573
|
-
if (method === "HEAD") {
|
|
3574
|
-
if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
|
|
3575
|
-
if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
|
|
3563
|
+
function isChildrenSlotOutletElement(value) {
|
|
3564
|
+
try {
|
|
3565
|
+
if (value == null || typeof value !== "object") return false;
|
|
3566
|
+
if (!("type" in value)) return false;
|
|
3567
|
+
const el = value;
|
|
3568
|
+
return el.type === SlotOutlet && el.props?.slot === "\0timber:children";
|
|
3569
|
+
} catch {
|
|
3570
|
+
return false;
|
|
3576
3571
|
}
|
|
3577
|
-
const handler = mod[method];
|
|
3578
|
-
if (!handler) return new Response(null, {
|
|
3579
|
-
status: 405,
|
|
3580
|
-
headers: { Allow: allowHeader }
|
|
3581
|
-
});
|
|
3582
|
-
return runHandler(handler, ctx);
|
|
3583
3572
|
}
|
|
3584
3573
|
/**
|
|
3585
|
-
*
|
|
3574
|
+
* Dense array of non-empty strings — no holes, no non-string entries.
|
|
3575
|
+
* Total: the reads run against a user-controlled value at module load,
|
|
3576
|
+
* and a throwing Proxy trap must classify as invalid, not crash startup.
|
|
3586
3577
|
*/
|
|
3587
|
-
|
|
3578
|
+
function isDenseStringArray(value) {
|
|
3588
3579
|
try {
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
return
|
|
3580
|
+
if (!Array.isArray(value)) return false;
|
|
3581
|
+
for (let i = 0; i < value.length; i++) {
|
|
3582
|
+
if (!Object.hasOwn(value, i)) return false;
|
|
3583
|
+
const entry = value[i];
|
|
3584
|
+
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
3585
|
+
}
|
|
3586
|
+
return true;
|
|
3587
|
+
} catch {
|
|
3588
|
+
return false;
|
|
3598
3589
|
}
|
|
3599
3590
|
}
|
|
3600
3591
|
/**
|
|
3601
|
-
*
|
|
3602
|
-
*
|
|
3603
|
-
*
|
|
3592
|
+
* Validate the `slots` option. Both the production and dev wrappers gate
|
|
3593
|
+
* on this — slot substitution is active in dev exactly when it would be
|
|
3594
|
+
* active in production, so the tree shape a component sees never
|
|
3595
|
+
* diverges between the two.
|
|
3604
3596
|
*/
|
|
3605
|
-
function
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
}
|
|
3621
|
-
return new Response(res.body, {
|
|
3622
|
-
status: res.status,
|
|
3623
|
-
statusText: res.statusText,
|
|
3624
|
-
headers: merged
|
|
3625
|
-
});
|
|
3597
|
+
function resolveSlotNames(id, options) {
|
|
3598
|
+
const slots = options.slots;
|
|
3599
|
+
if (slots === void 0) return { kind: "none" };
|
|
3600
|
+
if (!isDenseStringArray(slots)) {
|
|
3601
|
+
console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
|
|
3602
|
+
return { kind: "disabled" };
|
|
3603
|
+
}
|
|
3604
|
+
if (slots.length === 0) return { kind: "none" };
|
|
3605
|
+
if (!(options.ttl !== void 0 || options.tags != null)) {
|
|
3606
|
+
console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
|
|
3607
|
+
return { kind: "disabled" };
|
|
3608
|
+
}
|
|
3609
|
+
return {
|
|
3610
|
+
kind: "active",
|
|
3611
|
+
slots
|
|
3612
|
+
};
|
|
3626
3613
|
}
|
|
3627
|
-
//#endregion
|
|
3628
|
-
//#region src/server/render-timeout.ts
|
|
3629
3614
|
/**
|
|
3630
|
-
*
|
|
3631
|
-
*
|
|
3632
|
-
*
|
|
3633
|
-
*
|
|
3634
|
-
* streams and infinite SSR renders.
|
|
3615
|
+
* Split the live props into the cacheable shell render (data props plus a
|
|
3616
|
+
* deterministic SlotOutlet placeholder per declared slot — identical
|
|
3617
|
+
* whether or not the caller passed the slot) and the per-instance live
|
|
3618
|
+
* slot values.
|
|
3635
3619
|
*
|
|
3636
|
-
*
|
|
3620
|
+
* Null when the props object itself is unkeyable at the top level
|
|
3621
|
+
* (accessors, hidden properties, exotic prototype — see splitSlotProps):
|
|
3622
|
+
* the caller must render live with the original props.
|
|
3637
3623
|
*/
|
|
3624
|
+
function prepareSlotRender(props, slots) {
|
|
3625
|
+
const split = splitSlotProps(props ?? {}, slots);
|
|
3626
|
+
if (split === null) return null;
|
|
3627
|
+
const { dataProps, slotValues } = split;
|
|
3628
|
+
const shellProps = { ...dataProps };
|
|
3629
|
+
for (const slot of slots) Object.defineProperty(shellProps, slot, {
|
|
3630
|
+
value: createElement(SlotOutlet, { slot }),
|
|
3631
|
+
enumerable: true,
|
|
3632
|
+
writable: true,
|
|
3633
|
+
configurable: true
|
|
3634
|
+
});
|
|
3635
|
+
return {
|
|
3636
|
+
shellProps,
|
|
3637
|
+
dataProps,
|
|
3638
|
+
slotValues
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3638
3641
|
/**
|
|
3639
|
-
*
|
|
3640
|
-
*
|
|
3641
|
-
* to distinguish timeout from other errors and return a 504 or close
|
|
3642
|
-
* the connection cleanly.
|
|
3642
|
+
* Wrap a rendered shell (revived payload or live element) in a
|
|
3643
|
+
* per-instance SlotsProvider feeding the live values to its outlets.
|
|
3643
3644
|
*/
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
this.timeoutMs = timeoutMs;
|
|
3651
|
-
}
|
|
3652
|
-
};
|
|
3645
|
+
function wrapInSlotsProvider(shell, slotValues) {
|
|
3646
|
+
return createElement(SlotsProvider, {
|
|
3647
|
+
values: slotValues,
|
|
3648
|
+
children: shell
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3653
3651
|
//#endregion
|
|
3654
3652
|
//#region src/server/prebuilt/key-discipline.ts
|
|
3655
3653
|
/** Components already warned — exported so tests can reset between cases. */
|