@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.99
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/dist/_chunks/actions-CQ8Z8VGL.js +1061 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
- package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
- package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
- package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
- package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
- package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
- package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
- package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
- package/dist/_chunks/logger-0m8MsKdc.js +291 -0
- package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
- package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
- package/dist/_chunks/registry-I2ss-lvy.js +20 -0
- package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
- package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
- package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
- package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
- package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
- package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
- package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
- package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
- package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
- package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
- package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
- package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
- package/dist/_chunks/{interception-BbqMCVXa.js → walkers-Tg0Alwcg.js} +66 -87
- package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
- package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
- package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +28 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +8 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +63 -31
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +16 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/cache/index.js +9 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/form.d.ts +10 -24
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +41 -91
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +2 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +81 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/state.d.ts +1 -1
- package/dist/client/use-cookie.d.ts +8 -0
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
- package/dist/client/use-segment-params.d.ts.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -2
- package/dist/config-types.d.ts +28 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts +87 -35
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +2 -1
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +48 -2
- package/dist/cookies/index.js.map +1 -0
- package/dist/cookies/json-cookie.d.ts +64 -0
- package/dist/cookies/json-cookie.d.ts.map +1 -0
- package/dist/cookies/validation.d.ts +46 -0
- package/dist/cookies/validation.d.ts.map +1 -0
- package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +9 -19
- package/dist/dev-tools/404-page.d.ts.map +1 -0
- package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
- package/dist/dev-tools/browser-logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
- package/dist/dev-tools/error-page.d.ts.map +1 -0
- package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +5 -3
- package/dist/dev-tools/holding-server.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +31 -0
- package/dist/dev-tools/index.d.ts.map +1 -0
- package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
- package/dist/dev-tools/instrumentation.d.ts.map +1 -0
- package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -0
- package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
- package/dist/dev-tools/overlay.d.ts.map +1 -0
- package/dist/dev-tools/stack-classifier.d.ts +34 -0
- package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
- package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
- package/dist/dev-tools/terminal.d.ts.map +1 -0
- package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
- package/dist/dev-tools/warnings.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +285 -133
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +6 -4
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.d.ts +5 -3
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/scanner.d.ts +1 -10
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +37 -8
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +63 -23
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +51 -0
- package/dist/routing/walkers.d.ts.map +1 -0
- package/dist/search-params/define.d.ts +25 -7
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +23 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts +4 -3
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +15 -6
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +5 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +1 -15
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +170 -0
- package/dist/server/cookie-context.d.ts.map +1 -0
- package/dist/server/cookie-parsing.d.ts +51 -0
- package/dist/server/cookie-parsing.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +90 -0
- package/dist/server/deny-boundary.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +212 -0
- package/dist/server/html-injector-core.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +59 -59
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -149
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +6 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +852 -852
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +14 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +17 -0
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +46 -49
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +26 -0
- package/dist/server/param-coercion.d.ts.map +1 -0
- package/dist/server/pipeline-helpers.d.ts +95 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -0
- package/dist/server/pipeline-outcome.d.ts +49 -0
- package/dist/server/pipeline-outcome.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +52 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +51 -32
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +117 -0
- package/dist/server/port-resolution.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +22 -159
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +20 -47
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +50 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +119 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +16 -11
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +45 -16
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/escape-html.d.ts +14 -0
- package/dist/server/utils/escape-html.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +2 -2
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +3 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +9 -4
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +0 -45
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +77 -0
- package/src/adapters/cloudflare.ts +10 -50
- package/src/adapters/nitro.ts +66 -50
- package/src/adapters/shared.ts +40 -0
- package/src/cache/timber-cache.ts +3 -2
- package/src/client/form.tsx +17 -25
- package/src/client/index.ts +16 -9
- package/src/client/internal.ts +3 -2
- package/src/client/router.ts +1 -1
- package/src/client/rsc-fetch.ts +15 -0
- package/src/client/state.ts +2 -2
- package/src/client/use-cookie.ts +29 -0
- package/src/codec.ts +3 -7
- package/src/config-types.ts +28 -0
- package/src/cookies/define-cookie.ts +271 -78
- package/src/cookies/index.ts +11 -8
- package/src/cookies/json-cookie.ts +105 -0
- package/src/cookies/validation.ts +134 -0
- package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +17 -48
- package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
- package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +4 -2
- package/src/dev-tools/index.ts +90 -0
- package/src/dev-tools/instrumentation.ts +176 -0
- package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
- package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
- package/src/dev-tools/stack-classifier.ts +75 -0
- package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
- package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
- package/src/index.ts +95 -34
- package/src/plugin-context.ts +1 -1
- package/src/plugins/adapter-build.ts +3 -1
- package/src/plugins/build-report.ts +13 -22
- package/src/plugins/dev-server.ts +3 -3
- package/src/plugins/routing.ts +14 -12
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +1 -1
- package/src/routing/codegen.ts +1 -1
- package/src/routing/convention-lint.ts +9 -8
- package/src/routing/index.ts +5 -3
- package/src/routing/interception.ts +1 -1
- package/src/routing/scanner.ts +22 -95
- package/src/routing/segment-classify.ts +107 -8
- package/src/routing/status-file-lint.ts +7 -5
- package/src/routing/types.ts +63 -23
- package/src/routing/walkers.ts +90 -0
- package/src/search-params/define.ts +71 -15
- package/src/search-params/wrappers.ts +9 -2
- package/src/segment-params/define.ts +66 -13
- package/src/server/access-gate.tsx +9 -8
- package/src/server/action-handler.ts +34 -38
- package/src/server/als-registry.ts +5 -5
- package/src/server/asset-headers.ts +8 -34
- package/src/server/cookie-context.ts +468 -0
- package/src/server/cookie-parsing.ts +135 -0
- package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
- package/src/server/deny-renderer.ts +7 -12
- package/src/server/early-hints-sender.ts +3 -2
- package/src/server/fallback-error.ts +2 -2
- package/src/server/html-injector-core.ts +403 -0
- package/src/server/html-injectors.ts +158 -297
- package/src/server/index.ts +13 -14
- package/src/server/internal.ts +10 -3
- package/src/server/logger.ts +23 -0
- package/src/server/middleware-runner.ts +44 -0
- package/src/server/node-stream-transforms.ts +108 -248
- package/src/server/param-coercion.ts +76 -0
- package/src/server/pipeline-helpers.ts +204 -0
- package/src/server/pipeline-outcome.ts +167 -0
- package/src/server/pipeline-phases.ts +409 -0
- package/src/server/pipeline.ts +70 -540
- package/src/server/port-resolution.ts +215 -0
- package/src/server/request-context.ts +46 -451
- package/src/server/route-element-builder.ts +8 -4
- package/src/server/route-matcher.ts +28 -60
- package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
- package/src/server/rsc-entry/api-handler.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +2 -2
- package/src/server/rsc-entry/helpers.ts +2 -7
- package/src/server/rsc-entry/index.ts +81 -366
- package/src/server/rsc-entry/render-route.ts +304 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/rsc-entry/wrap-action-dispatch.ts +449 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/state-tree-diff.ts +4 -1
- package/src/server/status-code-resolver.ts +112 -128
- package/src/server/tracing.ts +3 -3
- package/src/server/tree-builder.ts +134 -56
- package/src/server/types.ts +52 -0
- package/src/server/utils/escape-html.ts +20 -0
- package/src/shims/headers.ts +3 -3
- package/src/shims/navigation-client.ts +4 -3
- package/src/shims/navigation.ts +9 -7
- package/src/utils/directive-parser.ts +0 -392
- package/dist/_chunks/actions-DLnUaR65.js +0 -421
- package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
- package/dist/_chunks/debug-ECi_61pb.js +0 -108
- package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
- package/dist/_chunks/define-C77ScO0m.js.map +0 -1
- package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
- package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
- package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
- package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
- package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
- package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
- package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
- package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
- package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
- package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
- package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
- package/dist/client/use-params.d.ts.map +0 -1
- package/dist/plugins/dev-404-page.d.ts.map +0 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
- package/dist/plugins/dev-error-page.d.ts.map +0 -1
- package/dist/plugins/dev-logs.d.ts.map +0 -1
- package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
- package/dist/server/deny-page-resolver.d.ts +0 -52
- package/dist/server/deny-page-resolver.d.ts.map +0 -1
- package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
- package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
- package/dist/server/dev-holding-server.d.ts.map +0 -1
- package/dist/server/dev-logger.d.ts.map +0 -1
- package/dist/server/dev-span-processor.d.ts.map +0 -1
- package/dist/server/dev-warnings.d.ts.map +0 -1
- package/dist/server/manifest-status-resolver.d.ts +0 -58
- package/dist/server/manifest-status-resolver.d.ts.map +0 -1
- package/dist/server/page-deny-boundary.d.ts +0 -31
- package/dist/server/page-deny-boundary.d.ts.map +0 -1
- package/src/server/dev-fetch-instrumentation.ts +0 -96
- package/src/server/dev-span-processor.ts +0 -78
- package/src/server/manifest-status-resolver.ts +0 -215
- package/src/server/page-deny-boundary.tsx +0 -56
- /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
- /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
- /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
package/dist/server/internal.js
CHANGED
|
@@ -1,129 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as warnRedirectInSuspense, c as warnSuspenseWrappingChildren, i as warnRedirectInAccess, n as setViteServer, o as warnSlowSlotWithoutSuspense, r as warnDenyInSuspense, s as warnStaticRequestApi, t as WarningId } from "../_chunks/
|
|
3
|
-
import {
|
|
4
|
-
import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
7
|
-
import { l as RenderError, n as executeAction, o as DenySignal, r as isRscActionRequest, s as RedirectSignal, t as buildNoJsResponse } from "../_chunks/actions-DLnUaR65.js";
|
|
8
|
-
import { c as replaceTraceId, d as withSpan, i as getOtelTraceId, l as runWithTraceId, o as getTraceId, r as generateTraceId, s as getTraceStore, u as setSpanAttribute } from "../_chunks/tracing-CCYbKn5n.js";
|
|
1
|
+
import { c as replaceTraceId, d as withSpan, f as earlyHintsSenderAls, g as timingAls, i as getOtelTraceId, l as runWithTraceId, m as requestContextAls, o as getTraceId, r as generateTraceId, u as setSpanAttribute, v as isDebug } from "../_chunks/tracing-C8V-YGsP.js";
|
|
2
|
+
import { a as warnRedirectInSuspense, c as warnSuspenseWrappingChildren, i as warnRedirectInAccess, n as setViteServer, o as warnSlowSlotWithoutSuspense, r as warnDenyInSuspense, s as warnStaticRequestApi, t as WarningId } from "../_chunks/warnings-Cg47l5sk.js";
|
|
3
|
+
import { n as classifyUrlSegment } from "../_chunks/segment-classify-BjfuctV2.js";
|
|
4
|
+
import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-BU684ls2.js";
|
|
5
|
+
import { a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, h as swallow, i as logMiddlewareShortCircuit, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest } from "../_chunks/logger-0m8MsKdc.js";
|
|
6
|
+
import { C as setMutableCookieContext, D as getSetCookieHeaders, S as runWithRequestContext, T as getCookie, _ as getHeader, b as getSegmentParams, c as DenySignal, d as RenderError, g as applyRequestHeaderOverlay, l as RedirectSignal, n as executeAction, o as coerce, r as isRscActionRequest, t as buildNoJsResponse, w as setSegmentParams, x as markResponseFlushed, y as getSearchParams } from "../_chunks/actions-CQ8Z8VGL.js";
|
|
9
7
|
import "../client/error-boundary.js";
|
|
10
|
-
import "../_chunks/segment-context-
|
|
8
|
+
import "../_chunks/segment-context-Dx_OizxD.js";
|
|
11
9
|
import { readFile } from "node:fs/promises";
|
|
12
10
|
import { createElement } from "react";
|
|
13
|
-
//#region src/server/canonicalize.ts
|
|
14
|
-
/**
|
|
15
|
-
* Encoded separators that produce a 400 rejection.
|
|
16
|
-
* %2f (/) and %5c (\) cause path-confusion attacks.
|
|
17
|
-
*/
|
|
18
|
-
var ENCODED_SEPARATOR_RE = /%2f|%5c/i;
|
|
19
|
-
/** Null byte — rejected. */
|
|
20
|
-
var NULL_BYTE_RE = /%00/i;
|
|
21
|
-
/**
|
|
22
|
-
* Canonicalize a URL pathname.
|
|
23
|
-
*
|
|
24
|
-
* 1. Reject encoded separators (%2f, %5c) and null bytes (%00)
|
|
25
|
-
* 2. Single percent-decode
|
|
26
|
-
* 3. Collapse // → /
|
|
27
|
-
* 4. Resolve .. segments (reject if escaping root)
|
|
28
|
-
* 5. Strip trailing slash (except root "/")
|
|
29
|
-
*
|
|
30
|
-
* @param rawPathname - The raw pathname from the request URL (percent-encoded)
|
|
31
|
-
* @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.
|
|
32
|
-
*/
|
|
33
|
-
function canonicalize(rawPathname, stripTrailingSlash = true) {
|
|
34
|
-
if (ENCODED_SEPARATOR_RE.test(rawPathname)) return {
|
|
35
|
-
ok: false,
|
|
36
|
-
status: 400
|
|
37
|
-
};
|
|
38
|
-
if (NULL_BYTE_RE.test(rawPathname)) return {
|
|
39
|
-
ok: false,
|
|
40
|
-
status: 400
|
|
41
|
-
};
|
|
42
|
-
let decoded;
|
|
43
|
-
try {
|
|
44
|
-
decoded = decodeURIComponent(rawPathname);
|
|
45
|
-
} catch {
|
|
46
|
-
return {
|
|
47
|
-
ok: false,
|
|
48
|
-
status: 400
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
if (decoded.includes("\0")) return {
|
|
52
|
-
ok: false,
|
|
53
|
-
status: 400
|
|
54
|
-
};
|
|
55
|
-
let pathname = decoded.replace(/\/\/+/g, "/");
|
|
56
|
-
const segments = pathname.split("/");
|
|
57
|
-
const resolved = [];
|
|
58
|
-
for (const seg of segments) if (seg === "..") {
|
|
59
|
-
if (resolved.length <= 1) return {
|
|
60
|
-
ok: false,
|
|
61
|
-
status: 400
|
|
62
|
-
};
|
|
63
|
-
resolved.pop();
|
|
64
|
-
} else if (seg !== ".") resolved.push(seg);
|
|
65
|
-
pathname = resolved.join("/") || "/";
|
|
66
|
-
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
67
|
-
return {
|
|
68
|
-
ok: true,
|
|
69
|
-
pathname
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region src/server/proxy.ts
|
|
74
|
-
/**
|
|
75
|
-
* Run the proxy pipeline.
|
|
76
|
-
*
|
|
77
|
-
* @param proxyExport - The default export from proxy.ts (function or array)
|
|
78
|
-
* @param req - The incoming request
|
|
79
|
-
* @param next - The continuation that proceeds to route matching and rendering
|
|
80
|
-
* @returns The final response
|
|
81
|
-
*/
|
|
82
|
-
async function runProxy(proxyExport, req, next) {
|
|
83
|
-
const fns = Array.isArray(proxyExport) ? proxyExport : [proxyExport];
|
|
84
|
-
let i = fns.length;
|
|
85
|
-
let composed = next;
|
|
86
|
-
while (i--) {
|
|
87
|
-
const fn = fns[i];
|
|
88
|
-
const downstream = composed;
|
|
89
|
-
composed = () => Promise.resolve(fn(req, downstream));
|
|
90
|
-
}
|
|
91
|
-
return composed();
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/server/middleware-runner.ts
|
|
95
|
-
/**
|
|
96
|
-
* Run a route's middleware function.
|
|
97
|
-
*
|
|
98
|
-
* @param middlewareFn - The default export from the route's middleware.ts
|
|
99
|
-
* @param ctx - The middleware context (req, params, headers, requestHeaders, searchParams)
|
|
100
|
-
* @returns A Response if middleware short-circuited, or undefined to continue
|
|
101
|
-
*/
|
|
102
|
-
async function runMiddleware(middlewareFn, ctx) {
|
|
103
|
-
const result = await middlewareFn(ctx);
|
|
104
|
-
if (result instanceof Response) return result;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Run all middleware functions in the segment chain, root to leaf.
|
|
108
|
-
*
|
|
109
|
-
* Execution is top-down: root middleware runs first, leaf middleware runs last.
|
|
110
|
-
* All middleware share the same MiddlewareContext — a parent that sets
|
|
111
|
-
* ctx.requestHeaders makes it visible to child middleware and downstream components.
|
|
112
|
-
*
|
|
113
|
-
* Short-circuits on the first middleware that returns a Response.
|
|
114
|
-
* Remaining middleware in the chain do not execute.
|
|
115
|
-
*
|
|
116
|
-
* @param chain - Middleware functions ordered root-to-leaf
|
|
117
|
-
* @param ctx - Shared middleware context
|
|
118
|
-
* @returns A Response if any middleware short-circuited, or undefined to continue
|
|
119
|
-
*/
|
|
120
|
-
async function runMiddlewareChain(chain, ctx) {
|
|
121
|
-
for (const fn of chain) {
|
|
122
|
-
const result = await fn(ctx);
|
|
123
|
-
if (result instanceof Response) return result;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
//#endregion
|
|
127
11
|
//#region src/server/server-timing.ts
|
|
128
12
|
/**
|
|
129
13
|
* Server-Timing header — dev-mode timing breakdowns for Chrome DevTools.
|
|
@@ -200,276 +84,6 @@ function getServerTimingHeader() {
|
|
|
200
84
|
return result || null;
|
|
201
85
|
}
|
|
202
86
|
//#endregion
|
|
203
|
-
//#region src/server/error-formatter.ts
|
|
204
|
-
/**
|
|
205
|
-
* Error Formatter — rewrites SSR/RSC error messages to surface user code.
|
|
206
|
-
*
|
|
207
|
-
* When React or Vite throw errors during SSR, stack traces reference
|
|
208
|
-
* vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)
|
|
209
|
-
* and mangled export names (`__vite_ssr_export_default__`). This module
|
|
210
|
-
* rewrites error messages and stack traces to point at user code instead.
|
|
211
|
-
*
|
|
212
|
-
* Dev-only — in production, errors go through the structured logger
|
|
213
|
-
* without formatting.
|
|
214
|
-
*/
|
|
215
|
-
/**
|
|
216
|
-
* Patterns that identify internal Vite/RSC vendor paths in stack traces.
|
|
217
|
-
* These are replaced with human-readable labels.
|
|
218
|
-
*/
|
|
219
|
-
var VENDOR_PATH_PATTERNS = [
|
|
220
|
-
{
|
|
221
|
-
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor_react-server-dom[^\s)]+/g,
|
|
222
|
-
replacement: "<react-server-dom>"
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor[^\s)]+/g,
|
|
226
|
-
replacement: "<rsc-vendor>"
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
pattern: /node_modules\/\.vite\/deps_ssr\/[^\s)]+/g,
|
|
230
|
-
replacement: "<vite-dep>"
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
pattern: /node_modules\/\.vite\/deps\/[^\s)]+/g,
|
|
234
|
-
replacement: "<vite-dep>"
|
|
235
|
-
}
|
|
236
|
-
];
|
|
237
|
-
/**
|
|
238
|
-
* Patterns that identify Vite-mangled export names in error messages.
|
|
239
|
-
*/
|
|
240
|
-
var MANGLED_NAME_PATTERNS = [{
|
|
241
|
-
pattern: /__vite_ssr_export_default__/g,
|
|
242
|
-
replacement: "<default export>"
|
|
243
|
-
}, {
|
|
244
|
-
pattern: /__vite_ssr_export_(\w+)__/g,
|
|
245
|
-
replacement: "<export $1>"
|
|
246
|
-
}];
|
|
247
|
-
/**
|
|
248
|
-
* Rewrite an error's message and stack to replace internal Vite paths
|
|
249
|
-
* and mangled names with human-readable labels.
|
|
250
|
-
*/
|
|
251
|
-
function formatSsrError(error) {
|
|
252
|
-
if (!(error instanceof Error)) return String(error);
|
|
253
|
-
let message = error.message;
|
|
254
|
-
let stack = error.stack ?? "";
|
|
255
|
-
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) message = message.replace(pattern, replacement);
|
|
256
|
-
for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
257
|
-
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
258
|
-
const hint = extractErrorHint(error.message);
|
|
259
|
-
const parts = [];
|
|
260
|
-
parts.push(message);
|
|
261
|
-
if (hint) parts.push(` → ${hint}`);
|
|
262
|
-
const userFrames = extractUserFrames(stack);
|
|
263
|
-
if (userFrames.length > 0) {
|
|
264
|
-
parts.push("");
|
|
265
|
-
parts.push(" User code in stack:");
|
|
266
|
-
for (const frame of userFrames) parts.push(` ${frame}`);
|
|
267
|
-
}
|
|
268
|
-
return parts.join("\n");
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Extract a human-readable hint from common React/RSC error messages.
|
|
272
|
-
*
|
|
273
|
-
* React error messages contain useful information but the surrounding
|
|
274
|
-
* context (vendor paths, mangled names) obscures it. This extracts the
|
|
275
|
-
* actionable part as a one-line hint.
|
|
276
|
-
*/
|
|
277
|
-
function extractErrorHint(message) {
|
|
278
|
-
if (message.match(/Functions cannot be passed directly to Client Components/)) {
|
|
279
|
-
const propMatch = message.match(/<[^>]*?\s(\w+)=\{function/);
|
|
280
|
-
if (propMatch) return `Prop "${propMatch[1]}" is a function — mark it "use server" or call it before passing`;
|
|
281
|
-
return "A function prop was passed to a Client Component — mark it \"use server\" or call it before passing";
|
|
282
|
-
}
|
|
283
|
-
if (message.includes("Objects are not valid as a React child")) return "An object was rendered as JSX children — convert to string or extract the value";
|
|
284
|
-
const nullRefMatch = message.match(/Cannot read propert(?:y|ies) of (undefined|null) \(reading '(\w+)'\)/);
|
|
285
|
-
if (nullRefMatch) return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;
|
|
286
|
-
const notFnMatch = message.match(/(\w+) is not a function/);
|
|
287
|
-
if (notFnMatch) return `"${notFnMatch[1]}" is not a function — check imports and exports`;
|
|
288
|
-
if (message.includes("Element type is invalid")) return "A component resolved to undefined/null — check default exports and import paths";
|
|
289
|
-
if (message.includes("Invalid hook call")) return "A hook was called outside of a React component render. If this is a 'use client' component, ensure the directive is at the very top of the file (before any imports) and that @vitejs/plugin-rsc is loaded correctly. Barrel re-exports from non-'use client' files do not propagate the directive.";
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Extract stack frames that reference user code (not node_modules,
|
|
294
|
-
* not framework internals).
|
|
295
|
-
*
|
|
296
|
-
* Returns at most 5 frames to keep output concise.
|
|
297
|
-
*/
|
|
298
|
-
function extractUserFrames(stack) {
|
|
299
|
-
const lines = stack.split("\n");
|
|
300
|
-
const userFrames = [];
|
|
301
|
-
for (const line of lines) {
|
|
302
|
-
const trimmed = line.trim();
|
|
303
|
-
if (!trimmed.startsWith("at ")) continue;
|
|
304
|
-
if (trimmed.includes("node_modules") || trimmed.includes("<react-server-dom>") || trimmed.includes("<rsc-vendor>") || trimmed.includes("<vite-dep>") || trimmed.includes("node:internal")) continue;
|
|
305
|
-
userFrames.push(trimmed);
|
|
306
|
-
if (userFrames.length >= 5) break;
|
|
307
|
-
}
|
|
308
|
-
return userFrames;
|
|
309
|
-
}
|
|
310
|
-
//#endregion
|
|
311
|
-
//#region src/server/default-logger.ts
|
|
312
|
-
/**
|
|
313
|
-
* DefaultLogger — human-readable stderr logging when no custom logger is configured.
|
|
314
|
-
*
|
|
315
|
-
* Ships as the fallback so production deployments always have error visibility,
|
|
316
|
-
* even without an `instrumentation.ts` logger export. Output is one line per
|
|
317
|
-
* event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
|
|
318
|
-
*
|
|
319
|
-
* Format:
|
|
320
|
-
* [timber] ERROR message key=value key=value trace_id=4bf92f35
|
|
321
|
-
* [timber] WARN message key=value key=value trace_id=4bf92f35
|
|
322
|
-
* [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
|
|
323
|
-
*
|
|
324
|
-
* Behavior:
|
|
325
|
-
* - Suppressed entirely in dev mode (dev logging handles all output)
|
|
326
|
-
* - `debug` suppressed unless TIMBER_DEBUG is set
|
|
327
|
-
* - Replaced entirely when a custom logger is set via `setLogger()`
|
|
328
|
-
*
|
|
329
|
-
* See design/17-logging.md §"DefaultLogger"
|
|
330
|
-
*/
|
|
331
|
-
/**
|
|
332
|
-
* Format data fields as `key=value` pairs for human-readable output.
|
|
333
|
-
* - `error` key is serialized via formatSsrError for stack trace cleanup
|
|
334
|
-
* - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
|
|
335
|
-
* - Other values are stringified inline
|
|
336
|
-
*/
|
|
337
|
-
function formatDataFields(data) {
|
|
338
|
-
if (!data) return "";
|
|
339
|
-
const parts = [];
|
|
340
|
-
let traceId;
|
|
341
|
-
for (const [key, value] of Object.entries(data)) {
|
|
342
|
-
if (key === "trace_id") {
|
|
343
|
-
traceId = typeof value === "string" ? value : String(value);
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
if (key === "error") {
|
|
347
|
-
parts.push(`error=${formatSsrError(value)}`);
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (value === void 0 || value === null) continue;
|
|
351
|
-
parts.push(`${key}=${value}`);
|
|
352
|
-
}
|
|
353
|
-
if (traceId) parts.push(`trace_id=${traceId.slice(0, 8)}`);
|
|
354
|
-
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
355
|
-
}
|
|
356
|
-
/** Pad level string to fixed width for alignment. */
|
|
357
|
-
function padLevel(level) {
|
|
358
|
-
return level.padEnd(5);
|
|
359
|
-
}
|
|
360
|
-
function createDefaultLogger() {
|
|
361
|
-
return {
|
|
362
|
-
error(msg, data) {
|
|
363
|
-
const fields = formatDataFields(data);
|
|
364
|
-
process.stderr.write(`[timber] ${padLevel("ERROR")} ${msg}${fields}\n`);
|
|
365
|
-
},
|
|
366
|
-
warn(msg, data) {
|
|
367
|
-
const fields = formatDataFields(data);
|
|
368
|
-
process.stderr.write(`[timber] ${padLevel("WARN")} ${msg}${fields}\n`);
|
|
369
|
-
},
|
|
370
|
-
info(msg, data) {
|
|
371
|
-
if (isDevMode()) return;
|
|
372
|
-
if (!isDebug()) return;
|
|
373
|
-
const fields = formatDataFields(data);
|
|
374
|
-
process.stderr.write(`[timber] ${padLevel("INFO")} ${msg}${fields}\n`);
|
|
375
|
-
},
|
|
376
|
-
debug(msg, data) {
|
|
377
|
-
if (isDevMode()) return;
|
|
378
|
-
if (!isDebug()) return;
|
|
379
|
-
const fields = formatDataFields(data);
|
|
380
|
-
process.stderr.write(`[timber] ${padLevel("DEBUG")} ${msg}${fields}\n`);
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/server/logger.ts
|
|
386
|
-
/**
|
|
387
|
-
* Logger — structured logging with environment-aware formatting.
|
|
388
|
-
*
|
|
389
|
-
* timber.js ships a DefaultLogger that writes human-readable lines to stderr
|
|
390
|
-
* in production. Users can export a custom logger from instrumentation.ts to
|
|
391
|
-
* replace it with pino, winston, or any TimberLogger-compatible object.
|
|
392
|
-
*
|
|
393
|
-
* See design/17-logging.md §"Production Logging"
|
|
394
|
-
*/
|
|
395
|
-
var _logger = createDefaultLogger();
|
|
396
|
-
/**
|
|
397
|
-
* Set the user-provided logger. Called by the instrumentation loader
|
|
398
|
-
* when it finds a `logger` export in instrumentation.ts. Replaces
|
|
399
|
-
* the DefaultLogger entirely.
|
|
400
|
-
*/
|
|
401
|
-
function setLogger(logger) {
|
|
402
|
-
_logger = logger;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Get the current logger. Always non-null — returns DefaultLogger when
|
|
406
|
-
* no custom logger is configured.
|
|
407
|
-
*/
|
|
408
|
-
function getLogger() {
|
|
409
|
-
return _logger;
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Inject trace_id and span_id into log data for log–trace correlation.
|
|
413
|
-
* Always injects trace_id (never undefined). Injects span_id only when OTEL is active.
|
|
414
|
-
*/
|
|
415
|
-
function withTraceContext(data) {
|
|
416
|
-
const store = getTraceStore();
|
|
417
|
-
const enriched = { ...data };
|
|
418
|
-
if (store) {
|
|
419
|
-
enriched.trace_id = store.traceId;
|
|
420
|
-
if (store.spanId) enriched.span_id = store.spanId;
|
|
421
|
-
}
|
|
422
|
-
return enriched;
|
|
423
|
-
}
|
|
424
|
-
/** Log a completed request. Level: info. */
|
|
425
|
-
function logRequestCompleted(data) {
|
|
426
|
-
_logger.info("request completed", withTraceContext(data));
|
|
427
|
-
}
|
|
428
|
-
/** Log request received. Level: debug. */
|
|
429
|
-
function logRequestReceived(data) {
|
|
430
|
-
_logger.debug("request received", withTraceContext(data));
|
|
431
|
-
}
|
|
432
|
-
/** Log a slow request warning. Level: warn. */
|
|
433
|
-
function logSlowRequest(data) {
|
|
434
|
-
_logger.warn("slow request exceeded threshold", withTraceContext(data));
|
|
435
|
-
}
|
|
436
|
-
/** Log middleware short-circuit. Level: debug. */
|
|
437
|
-
function logMiddlewareShortCircuit(data) {
|
|
438
|
-
_logger.debug("middleware short-circuited", withTraceContext(data));
|
|
439
|
-
}
|
|
440
|
-
/** Log unhandled error in middleware phase. Level: error. */
|
|
441
|
-
function logMiddlewareError(data) {
|
|
442
|
-
_logger.error("unhandled error in middleware phase", withTraceContext(data));
|
|
443
|
-
}
|
|
444
|
-
/** Log unhandled render-phase error. Level: error. */
|
|
445
|
-
function logRenderError(data) {
|
|
446
|
-
_logger.error("unhandled render-phase error", withTraceContext(data));
|
|
447
|
-
}
|
|
448
|
-
/** Log proxy.ts uncaught error. Level: error. */
|
|
449
|
-
function logProxyError(data) {
|
|
450
|
-
_logger.error("proxy.ts threw uncaught error", withTraceContext(data));
|
|
451
|
-
}
|
|
452
|
-
/** Log unhandled error in route handler. Level: error. */
|
|
453
|
-
function logRouteError(data) {
|
|
454
|
-
_logger.error("unhandled route handler error", withTraceContext(data));
|
|
455
|
-
}
|
|
456
|
-
/** Log waitUntil() adapter missing (once at startup). Level: warn. */
|
|
457
|
-
function logWaitUntilUnsupported() {
|
|
458
|
-
_logger.warn("adapter does not support waitUntil()");
|
|
459
|
-
}
|
|
460
|
-
/** Log waitUntil() promise rejection. Level: warn. */
|
|
461
|
-
function logWaitUntilRejected(data) {
|
|
462
|
-
_logger.warn("waitUntil() promise rejected", withTraceContext(data));
|
|
463
|
-
}
|
|
464
|
-
/** Log staleWhileRevalidate refetch failure. Level: warn. */
|
|
465
|
-
function logSwrRefetchFailed(data) {
|
|
466
|
-
_logger.warn("staleWhileRevalidate refetch failed", withTraceContext(data));
|
|
467
|
-
}
|
|
468
|
-
/** Log cache miss. Level: debug. */
|
|
469
|
-
function logCacheMiss(data) {
|
|
470
|
-
_logger.debug("timber.cache MISS", withTraceContext(data));
|
|
471
|
-
}
|
|
472
|
-
//#endregion
|
|
473
87
|
//#region src/server/instrumentation.ts
|
|
474
88
|
/**
|
|
475
89
|
* Instrumentation — loads and runs the user's instrumentation.ts file.
|
|
@@ -532,6 +146,292 @@ function hasOnRequestError() {
|
|
|
532
146
|
return _onRequestError !== null;
|
|
533
147
|
}
|
|
534
148
|
//#endregion
|
|
149
|
+
//#region src/server/pipeline-helpers.ts
|
|
150
|
+
/**
|
|
151
|
+
* Only __proto__ needs stripping — it has a language-level setter that
|
|
152
|
+
* changes the prototype chain of spread copies. constructor and prototype
|
|
153
|
+
* are harmless own properties on null-prototype objects.
|
|
154
|
+
*/
|
|
155
|
+
var DANGEROUS_KEYS = new Set(["__proto__"]);
|
|
156
|
+
/**
|
|
157
|
+
* Deep-walk a value returned by a segment param codec, producing a
|
|
158
|
+
* sanitized copy where every plain object is null-prototype and
|
|
159
|
+
* dangerous keys (__proto__, constructor, prototype) are stripped at
|
|
160
|
+
* every depth.
|
|
161
|
+
*
|
|
162
|
+
* Non-plain objects (Date, Map, class instances, etc.) are returned
|
|
163
|
+
* as-is — they cannot be poisoned by `{...x}` spread and may carry
|
|
164
|
+
* author-intended prototype methods.
|
|
165
|
+
*
|
|
166
|
+
* Arrays are walked element-wise.
|
|
167
|
+
*
|
|
168
|
+
* Performance: URL params are bounded by URL length (~8 KB). Realistic
|
|
169
|
+
* trees are <1 KB. The recursive walk is sub-microsecond.
|
|
170
|
+
*
|
|
171
|
+
* See TIM-655, TIM-855, TIM-873, design/13-security.md
|
|
172
|
+
*/
|
|
173
|
+
function sanitizeParamValue(value) {
|
|
174
|
+
if (value === null || typeof value !== "object") return value;
|
|
175
|
+
if (Array.isArray(value)) return value.map(sanitizeParamValue);
|
|
176
|
+
const proto = Object.getPrototypeOf(value);
|
|
177
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
178
|
+
const out = Object.create(null);
|
|
179
|
+
for (const key of Object.keys(value)) if (!DANGEROUS_KEYS.has(key)) out[key] = sanitizeParamValue(value[key]);
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build a proxy resolver closure from the declared source. Called exactly
|
|
184
|
+
* once at `createPipeline` setup time, so the hot path sees only the branch
|
|
185
|
+
* that corresponds to this pipeline's configured variant.
|
|
186
|
+
*
|
|
187
|
+
* Returns `null` when the app has no proxy.ts — the hot path short-circuits
|
|
188
|
+
* around `runProxyPhase` entirely in that case.
|
|
189
|
+
*
|
|
190
|
+
* Accepts the sugar form (a bare `ProxyExport` — function or function array)
|
|
191
|
+
* and normalises it to the static variant. Functions and arrays are
|
|
192
|
+
* structurally distinct from the tagged `{ kind: 'lazy', loader }` object,
|
|
193
|
+
* so discrimination is unambiguous.
|
|
194
|
+
*/
|
|
195
|
+
function makeProxyResolver(proxy) {
|
|
196
|
+
if (proxy === void 0) return null;
|
|
197
|
+
if (typeof proxy === "function" || Array.isArray(proxy)) {
|
|
198
|
+
const exp = proxy;
|
|
199
|
+
return () => exp;
|
|
200
|
+
}
|
|
201
|
+
if (proxy.kind === "static") {
|
|
202
|
+
const exp = proxy.export;
|
|
203
|
+
return () => exp;
|
|
204
|
+
}
|
|
205
|
+
const loader = proxy.loader;
|
|
206
|
+
return async () => (await loader()).default;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Apply all Set-Cookie headers from the cookie jar to a Headers object.
|
|
210
|
+
* Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
|
|
211
|
+
*/
|
|
212
|
+
function applyCookieJar(headers) {
|
|
213
|
+
for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Merge framework-managed response headers onto a terminal response without
|
|
217
|
+
* overwriting headers the terminal response already set itself.
|
|
218
|
+
*/
|
|
219
|
+
function mergeMissingHeaders(target, source) {
|
|
220
|
+
const existingKeys = new Set([...target.keys()].map((key) => key.toLowerCase()));
|
|
221
|
+
for (const [key, value] of source.entries()) if (!existingKeys.has(key.toLowerCase())) target.append(key, value);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Clone a Response into a fresh one whose header bag is guaranteed mutable.
|
|
225
|
+
*
|
|
226
|
+
* `Response.redirect()` and some platform-level passthrough responses (notably
|
|
227
|
+
* on Cloudflare Workers) return objects with frozen header bags. Calling
|
|
228
|
+
* `.set()` or `.append()` on them throws `TypeError: immutable`, which the
|
|
229
|
+
* pipeline can hit when it appends Set-Cookie or Server-Timing entries.
|
|
230
|
+
*
|
|
231
|
+
* The pipeline calls this at the producer sites where user-controlled
|
|
232
|
+
* responses enter the framework — `outcomeToResponse` for all phase outcomes,
|
|
233
|
+
* and `handleRequest` for metadata-route and auto-sitemap user handlers — so
|
|
234
|
+
* downstream code can write headers without runtime feature-detection.
|
|
235
|
+
*
|
|
236
|
+
* The clone is unconditional. This is a deliberate trade: we avoid a
|
|
237
|
+
* try/catch + thrown `TypeError` on every request (the previous probe-based
|
|
238
|
+
* approach paid that cost on the hot path) and accept one cheap Response
|
|
239
|
+
* rewrap at the framework boundary instead.
|
|
240
|
+
*/
|
|
241
|
+
function cloneWithMutableHeaders(response) {
|
|
242
|
+
return new Response(response.body, {
|
|
243
|
+
status: response.status,
|
|
244
|
+
statusText: response.statusText,
|
|
245
|
+
headers: new Headers(response.headers)
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Build a redirect Response from a RedirectSignal.
|
|
250
|
+
*
|
|
251
|
+
* For RSC payload requests (client navigation), returns 204 + X-Timber-Redirect
|
|
252
|
+
* so the client router can perform a soft SPA redirect. A raw 302 would be
|
|
253
|
+
* turned into an opaque redirect by fetch({redirect:'manual'}), crashing
|
|
254
|
+
* createFromFetch. See design/19-client-navigation.md.
|
|
255
|
+
*/
|
|
256
|
+
function buildRedirectResponse(signal, req, headers) {
|
|
257
|
+
if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
|
|
258
|
+
headers.set("X-Timber-Redirect", signal.location);
|
|
259
|
+
return new Response(null, {
|
|
260
|
+
status: 204,
|
|
261
|
+
headers
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
headers.set("Location", signal.location);
|
|
265
|
+
return new Response(null, {
|
|
266
|
+
status: signal.status,
|
|
267
|
+
headers
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Fire the user's onRequestError hook with request context.
|
|
272
|
+
* Extracts request info from the Request object and calls the instrumentation hook.
|
|
273
|
+
*/
|
|
274
|
+
async function fireOnRequestError(error, req, phase) {
|
|
275
|
+
const url = new URL(req.url);
|
|
276
|
+
const headersObj = {};
|
|
277
|
+
req.headers.forEach((v, k) => {
|
|
278
|
+
headersObj[k] = v;
|
|
279
|
+
});
|
|
280
|
+
await callOnRequestError(error, {
|
|
281
|
+
method: req.method,
|
|
282
|
+
path: url.pathname,
|
|
283
|
+
headers: headersObj
|
|
284
|
+
}, {
|
|
285
|
+
phase,
|
|
286
|
+
routePath: url.pathname,
|
|
287
|
+
routeType: "page",
|
|
288
|
+
traceId: getTraceId()
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/server/canonicalize.ts
|
|
293
|
+
/**
|
|
294
|
+
* Encoded separators that produce a 400 rejection.
|
|
295
|
+
* %2f (/) and %5c (\) cause path-confusion attacks.
|
|
296
|
+
*/
|
|
297
|
+
var ENCODED_SEPARATOR_RE = /%2f|%5c/i;
|
|
298
|
+
/** Null byte — rejected. */
|
|
299
|
+
var NULL_BYTE_RE = /%00/i;
|
|
300
|
+
/**
|
|
301
|
+
* Canonicalize a URL pathname.
|
|
302
|
+
*
|
|
303
|
+
* 1. Reject encoded separators (%2f, %5c) and null bytes (%00)
|
|
304
|
+
* 2. Single percent-decode
|
|
305
|
+
* 3. Collapse // → /
|
|
306
|
+
* 4. Resolve .. segments (reject if escaping root)
|
|
307
|
+
* 5. Strip trailing slash (except root "/")
|
|
308
|
+
*
|
|
309
|
+
* @param rawPathname - The raw pathname from the request URL (percent-encoded)
|
|
310
|
+
* @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.
|
|
311
|
+
*/
|
|
312
|
+
function canonicalize(rawPathname, stripTrailingSlash = true) {
|
|
313
|
+
if (ENCODED_SEPARATOR_RE.test(rawPathname)) return {
|
|
314
|
+
ok: false,
|
|
315
|
+
status: 400
|
|
316
|
+
};
|
|
317
|
+
if (NULL_BYTE_RE.test(rawPathname)) return {
|
|
318
|
+
ok: false,
|
|
319
|
+
status: 400
|
|
320
|
+
};
|
|
321
|
+
let decoded;
|
|
322
|
+
try {
|
|
323
|
+
decoded = decodeURIComponent(rawPathname);
|
|
324
|
+
} catch {
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
status: 400
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (decoded.includes("\0")) return {
|
|
331
|
+
ok: false,
|
|
332
|
+
status: 400
|
|
333
|
+
};
|
|
334
|
+
let pathname = decoded.replace(/\/\/+/g, "/");
|
|
335
|
+
const segments = pathname.split("/");
|
|
336
|
+
const resolved = [];
|
|
337
|
+
for (const seg of segments) if (seg === "..") {
|
|
338
|
+
if (resolved.length <= 1) return {
|
|
339
|
+
ok: false,
|
|
340
|
+
status: 400
|
|
341
|
+
};
|
|
342
|
+
resolved.pop();
|
|
343
|
+
} else if (seg !== ".") resolved.push(seg);
|
|
344
|
+
pathname = resolved.join("/") || "/";
|
|
345
|
+
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
pathname
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/server/proxy.ts
|
|
353
|
+
/**
|
|
354
|
+
* Run the proxy pipeline.
|
|
355
|
+
*
|
|
356
|
+
* @param proxyExport - The default export from proxy.ts (function or array)
|
|
357
|
+
* @param req - The incoming request
|
|
358
|
+
* @param next - The continuation that proceeds to route matching and rendering
|
|
359
|
+
* @returns The final response
|
|
360
|
+
*/
|
|
361
|
+
async function runProxy(proxyExport, req, next) {
|
|
362
|
+
const fns = Array.isArray(proxyExport) ? proxyExport : [proxyExport];
|
|
363
|
+
let i = fns.length;
|
|
364
|
+
let composed = next;
|
|
365
|
+
while (i--) {
|
|
366
|
+
const fn = fns[i];
|
|
367
|
+
const downstream = composed;
|
|
368
|
+
composed = () => Promise.resolve(fn(req, downstream));
|
|
369
|
+
}
|
|
370
|
+
return composed();
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/server/middleware-runner.ts
|
|
374
|
+
/**
|
|
375
|
+
* Run a route's middleware function.
|
|
376
|
+
*
|
|
377
|
+
* @param middlewareFn - The default export from the route's middleware.ts
|
|
378
|
+
* @param ctx - The middleware context (req, params, headers, requestHeaders, searchParams)
|
|
379
|
+
* @returns A Response if middleware short-circuited, or undefined to continue
|
|
380
|
+
*/
|
|
381
|
+
async function runMiddleware(middlewareFn, ctx) {
|
|
382
|
+
const result = await middlewareFn(ctx);
|
|
383
|
+
if (result instanceof Response) return result;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Run all middleware functions in the segment chain, root to leaf.
|
|
387
|
+
*
|
|
388
|
+
* Execution is top-down: root middleware runs first, leaf middleware runs last.
|
|
389
|
+
* All middleware share the same MiddlewareContext — a parent that sets
|
|
390
|
+
* ctx.requestHeaders makes it visible to child middleware and downstream components.
|
|
391
|
+
*
|
|
392
|
+
* Short-circuits on the first middleware that returns a Response.
|
|
393
|
+
* Remaining middleware in the chain do not execute.
|
|
394
|
+
*
|
|
395
|
+
* @param chain - Middleware functions ordered root-to-leaf
|
|
396
|
+
* @param ctx - Shared middleware context
|
|
397
|
+
* @returns A Response if any middleware short-circuited, or undefined to continue
|
|
398
|
+
*/
|
|
399
|
+
async function runMiddlewareChain(chain, ctx) {
|
|
400
|
+
for (const fn of chain) {
|
|
401
|
+
const result = await fn(ctx);
|
|
402
|
+
if (result instanceof Response) return result;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Per-request marker for synthetic re-render requests that should NOT
|
|
407
|
+
* re-execute `middleware.ts`. The action-dispatch wrapper runs middleware
|
|
408
|
+
* once on the inbound action POST; when validation fails on the no-JS
|
|
409
|
+
* path, it builds a synthetic GET that flows through the normal pipeline
|
|
410
|
+
* to render the page with `getFormFlash()` data. Without this marker, the
|
|
411
|
+
* pipeline would run middleware a second time on that synthetic GET.
|
|
412
|
+
*
|
|
413
|
+
* The set is keyed by the synthetic Request object itself, so the entry
|
|
414
|
+
* lives exactly as long as the request and is garbage-collected with it.
|
|
415
|
+
* Cannot be set or detected by user code — there is no header, no URL
|
|
416
|
+
* parameter, nothing on the wire that an attacker could spoof.
|
|
417
|
+
*
|
|
418
|
+
* See TIM-871.
|
|
419
|
+
*
|
|
420
|
+
* @internal — framework use only.
|
|
421
|
+
*/
|
|
422
|
+
var middlewareBypassRequests = /* @__PURE__ */ new WeakSet();
|
|
423
|
+
/**
|
|
424
|
+
* Check whether a request was marked to bypass middleware.
|
|
425
|
+
*
|
|
426
|
+
* Called by `handleRequest` in pipeline-phases.ts before invoking the
|
|
427
|
+
* middleware phase. Returns false for any request not explicitly marked.
|
|
428
|
+
*
|
|
429
|
+
* @internal
|
|
430
|
+
*/
|
|
431
|
+
function shouldBypassMiddleware(req) {
|
|
432
|
+
return middlewareBypassRequests.has(req);
|
|
433
|
+
}
|
|
434
|
+
//#endregion
|
|
535
435
|
//#region src/server/metadata-social.ts
|
|
536
436
|
/**
|
|
537
437
|
* Render Open Graph metadata into head element descriptors.
|
|
@@ -1259,20 +1159,36 @@ async function loadModule(loader) {
|
|
|
1259
1159
|
}
|
|
1260
1160
|
}
|
|
1261
1161
|
//#endregion
|
|
1262
|
-
//#region src/server/deny-
|
|
1162
|
+
//#region src/server/deny-boundary.ts
|
|
1263
1163
|
/**
|
|
1264
|
-
* Deny
|
|
1164
|
+
* Deny boundary subsystem — the in-tree DenySignal flow.
|
|
1165
|
+
*
|
|
1166
|
+
* Three things live together here because they form a single flow:
|
|
1167
|
+
*
|
|
1168
|
+
* 1. **Chain construction** (`buildDenyPageChain`) — walks the matched
|
|
1169
|
+
* segment chain at element-tree build time and produces a list of
|
|
1170
|
+
* `DenyPageEntry` records ordered by specificity (specific status →
|
|
1171
|
+
* category catch-all → `error.tsx`).
|
|
1172
|
+
*
|
|
1173
|
+
* 2. **Runtime matching** (`renderMatchingDenyPage`) — picks the first
|
|
1174
|
+
* chain entry whose status filter matches the thrown DenySignal and
|
|
1175
|
+
* returns a React element for the matching component. Used by
|
|
1176
|
+
* `AccessGate` and `PageDenyBoundary` when they catch a deny.
|
|
1265
1177
|
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1178
|
+
* 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
|
|
1179
|
+
* component that wraps a server-component page, calls it, and catches
|
|
1180
|
+
* `DenySignal` so the deny page renders in-tree (no throw reaches
|
|
1181
|
+
* React Flight, single render pass).
|
|
1270
1182
|
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1183
|
+
* Plus the ALS helpers (`setDenyStatus` / `getDenyStatus`) the boundary
|
|
1184
|
+
* uses to thread the matched status code back to the pipeline so the
|
|
1185
|
+
* HTTP status reflects the deny.
|
|
1274
1186
|
*
|
|
1275
|
-
*
|
|
1187
|
+
* Folded into one module from the former `deny-page-resolver.ts` and
|
|
1188
|
+
* `page-deny-boundary.tsx` (TIM-853) — the names were misleading and the
|
|
1189
|
+
* three pieces only made sense together.
|
|
1190
|
+
*
|
|
1191
|
+
* See design/04-authorization.md, design/10-error-handling.md, TIM-666.
|
|
1276
1192
|
*/
|
|
1277
1193
|
/**
|
|
1278
1194
|
* Find the first deny page in the chain that matches the given status code.
|
|
@@ -1616,78 +1532,411 @@ function pathnameMatchesPattern(pathname, pattern) {
|
|
|
1616
1532
|
continue;
|
|
1617
1533
|
}
|
|
1618
1534
|
}
|
|
1619
|
-
return pi === pathParts.length;
|
|
1535
|
+
return pi === pathParts.length;
|
|
1536
|
+
}
|
|
1537
|
+
//#endregion
|
|
1538
|
+
//#region src/server/param-coercion.ts
|
|
1539
|
+
/**
|
|
1540
|
+
* Run segment param coercion on the matched route's segments.
|
|
1541
|
+
*
|
|
1542
|
+
* Loads params.ts modules from segments that have them, extracts the
|
|
1543
|
+
* segmentParams definition, and coerces raw string params through codecs.
|
|
1544
|
+
* Throws ParamCoercionError if any codec fails (→ 404).
|
|
1545
|
+
*
|
|
1546
|
+
* This runs BEFORE middleware, so ctx.segmentParams is already typed.
|
|
1547
|
+
* See design/07-routing.md §"Where Coercion Runs"
|
|
1548
|
+
*/
|
|
1549
|
+
async function coerceSegmentParams(match) {
|
|
1550
|
+
const mergeTarget = Object.create(null);
|
|
1551
|
+
for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
|
|
1552
|
+
match.segmentParams = mergeTarget;
|
|
1553
|
+
for (const segment of match.segments) {
|
|
1554
|
+
if (!segment.params) continue;
|
|
1555
|
+
let mod;
|
|
1556
|
+
try {
|
|
1557
|
+
mod = await loadModule(segment.params);
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
throw new ParamCoercionError(`Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1560
|
+
}
|
|
1561
|
+
const segmentParamsDef = mod.segmentParams;
|
|
1562
|
+
if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
|
|
1563
|
+
try {
|
|
1564
|
+
const coerced = segmentParamsDef.parse(match.segmentParams);
|
|
1565
|
+
for (const key of Object.keys(coerced)) if (key !== "__proto__") mergeTarget[key] = sanitizeParamValue(coerced[key]);
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
throw new ParamCoercionError(err instanceof Error ? err.message : String(err));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
//#endregion
|
|
1572
|
+
//#region src/server/pipeline-outcome.ts
|
|
1573
|
+
/**
|
|
1574
|
+
* Pipeline outcome translator — converts a `PhaseOutcome` (the value
|
|
1575
|
+
* each phase function returns) into a final `Response`.
|
|
1576
|
+
*
|
|
1577
|
+
* Lifted out of `pipeline-phases.ts` (TIM-853) so the per-phase try /
|
|
1578
|
+
* catch logic and the terminal Response-building logic each live in
|
|
1579
|
+
* their own file. The phases produce values; this module is the single
|
|
1580
|
+
* source of truth for how those values become wire responses.
|
|
1581
|
+
*
|
|
1582
|
+
* See design/07-routing.md §"Request Lifecycle".
|
|
1583
|
+
*/
|
|
1584
|
+
/**
|
|
1585
|
+
* Terminal outcome handler — converts a `PhaseOutcome` into a final
|
|
1586
|
+
* `Response`, applying cookies, building redirects, rendering deny pages
|
|
1587
|
+
* and fallback error pages, and firing instrumentation hooks.
|
|
1588
|
+
*
|
|
1589
|
+
* This is the single source of truth for how phase outputs become wire
|
|
1590
|
+
* responses; the per-phase try/catch blocks now produce values, not
|
|
1591
|
+
* Responses, so the conversion logic lives in exactly one place.
|
|
1592
|
+
*/
|
|
1593
|
+
async function outcomeToResponse(config, outcome, ctx) {
|
|
1594
|
+
switch (outcome.kind) {
|
|
1595
|
+
case "response": {
|
|
1596
|
+
const finalResponse = cloneWithMutableHeaders(outcome.response);
|
|
1597
|
+
if (outcome.phase === "proxy") return finalResponse;
|
|
1598
|
+
if (outcome.phase === "middleware" && ctx.responseHeaders) {
|
|
1599
|
+
applyCookieJar(finalResponse.headers);
|
|
1600
|
+
mergeMissingHeaders(finalResponse.headers, ctx.responseHeaders);
|
|
1601
|
+
logMiddlewareShortCircuit({
|
|
1602
|
+
method: ctx.method,
|
|
1603
|
+
path: ctx.path,
|
|
1604
|
+
status: finalResponse.status
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (outcome.phase === "render") markResponseFlushed();
|
|
1608
|
+
return finalResponse;
|
|
1609
|
+
}
|
|
1610
|
+
case "redirect": {
|
|
1611
|
+
const headers = ctx.responseHeaders ?? new Headers();
|
|
1612
|
+
applyCookieJar(headers);
|
|
1613
|
+
return buildRedirectResponse(outcome.signal, ctx.req, headers);
|
|
1614
|
+
}
|
|
1615
|
+
case "deny": {
|
|
1616
|
+
const headers = ctx.responseHeaders ?? new Headers();
|
|
1617
|
+
applyCookieJar(headers);
|
|
1618
|
+
if (config.renderDenyFallback) try {
|
|
1619
|
+
return cloneWithMutableHeaders(await config.renderDenyFallback(outcome.signal, ctx.req, headers, ctx.match));
|
|
1620
|
+
} catch (denyRenderError) {
|
|
1621
|
+
logRenderError({
|
|
1622
|
+
method: ctx.method,
|
|
1623
|
+
path: ctx.path,
|
|
1624
|
+
error: denyRenderError
|
|
1625
|
+
});
|
|
1626
|
+
await fireOnRequestError(denyRenderError, ctx.req, "render");
|
|
1627
|
+
if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
|
|
1628
|
+
}
|
|
1629
|
+
return new Response(null, {
|
|
1630
|
+
status: outcome.signal.status,
|
|
1631
|
+
headers
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
case "error": {
|
|
1635
|
+
if (outcome.phase === "proxy") {
|
|
1636
|
+
logProxyError({ error: outcome.error });
|
|
1637
|
+
await fireOnRequestError(outcome.error, ctx.req, "proxy");
|
|
1638
|
+
if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "proxy");
|
|
1639
|
+
return new Response(null, { status: 500 });
|
|
1640
|
+
}
|
|
1641
|
+
if (outcome.phase === "middleware") {
|
|
1642
|
+
logMiddlewareError({
|
|
1643
|
+
method: ctx.method,
|
|
1644
|
+
path: ctx.path,
|
|
1645
|
+
error: outcome.error
|
|
1646
|
+
});
|
|
1647
|
+
await fireOnRequestError(outcome.error, ctx.req, "handler");
|
|
1648
|
+
if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "middleware");
|
|
1649
|
+
return new Response(null, { status: 500 });
|
|
1650
|
+
}
|
|
1651
|
+
const headers = ctx.responseHeaders ?? new Headers();
|
|
1652
|
+
applyCookieJar(headers);
|
|
1653
|
+
logRenderError({
|
|
1654
|
+
method: ctx.method,
|
|
1655
|
+
path: ctx.path,
|
|
1656
|
+
error: outcome.error
|
|
1657
|
+
});
|
|
1658
|
+
await fireOnRequestError(outcome.error, ctx.req, "render");
|
|
1659
|
+
if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "render");
|
|
1660
|
+
if (config.renderFallbackError) try {
|
|
1661
|
+
return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, headers));
|
|
1662
|
+
} catch (fallbackRenderError) {
|
|
1663
|
+
logRenderError({
|
|
1664
|
+
method: ctx.method,
|
|
1665
|
+
path: ctx.path,
|
|
1666
|
+
error: fallbackRenderError
|
|
1667
|
+
});
|
|
1668
|
+
await fireOnRequestError(fallbackRenderError, ctx.req, "render");
|
|
1669
|
+
if (config.onPipelineError && fallbackRenderError instanceof Error) config.onPipelineError(fallbackRenderError, "render");
|
|
1670
|
+
}
|
|
1671
|
+
return new Response(null, { status: 500 });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
//#endregion
|
|
1676
|
+
//#region src/server/pipeline-phases.ts
|
|
1677
|
+
/**
|
|
1678
|
+
* Pipeline phase functions — module-level free functions that take their
|
|
1679
|
+
* dependencies as explicit parameters. Each phase returns a `PhaseOutcome`
|
|
1680
|
+
* (a discriminated union over response / redirect / deny / error) defined
|
|
1681
|
+
* in `pipeline-outcome.ts`. The terminal `outcomeToResponse` (also in
|
|
1682
|
+
* `pipeline-outcome.ts`) translates outcomes into Responses.
|
|
1683
|
+
*
|
|
1684
|
+
* Lifted out of `createPipeline` so each phase can be unit-tested in
|
|
1685
|
+
* isolation. The lift is mechanical — these functions used to be closures
|
|
1686
|
+
* over `config`; they now take `config` as an explicit parameter.
|
|
1687
|
+
*
|
|
1688
|
+
* See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow".
|
|
1689
|
+
*/
|
|
1690
|
+
/**
|
|
1691
|
+
* Run the proxy.ts phase. Calls user proxy code and uses `handleRequest` as
|
|
1692
|
+
* the inner `next()` continuation. The proxy resolver was picked at pipeline
|
|
1693
|
+
* construction time so the hot path sees no per-request branching on the
|
|
1694
|
+
* `ProxyConfig` discriminant.
|
|
1695
|
+
*/
|
|
1696
|
+
async function runProxyPhase(config, getProxy, req, method, path) {
|
|
1697
|
+
const detailed = config.serverTiming === "detailed";
|
|
1698
|
+
try {
|
|
1699
|
+
const proxyExport = await getProxy();
|
|
1700
|
+
const proxyFn = () => runProxy(proxyExport, req, () => handleRequest(config, req, method, path));
|
|
1701
|
+
return {
|
|
1702
|
+
kind: "response",
|
|
1703
|
+
phase: "proxy",
|
|
1704
|
+
response: await withSpan("timber.proxy", {}, () => detailed ? withTiming("proxy", "proxy.ts", proxyFn) : proxyFn())
|
|
1705
|
+
};
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
return {
|
|
1708
|
+
kind: "error",
|
|
1709
|
+
phase: "proxy",
|
|
1710
|
+
error
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Run the middleware chain phase. If the chain short-circuits with a Response,
|
|
1716
|
+
* returns it as a 'response' outcome. Otherwise applies the request header
|
|
1717
|
+
* overlay and falls through to the render phase.
|
|
1718
|
+
*/
|
|
1719
|
+
async function runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext) {
|
|
1720
|
+
const detailed = config.serverTiming === "detailed";
|
|
1721
|
+
const ctx = {
|
|
1722
|
+
req,
|
|
1723
|
+
requestHeaders: requestHeaderOverlay,
|
|
1724
|
+
headers: responseHeaders,
|
|
1725
|
+
segmentParams: match.segmentParams,
|
|
1726
|
+
earlyHints: (hints) => {
|
|
1727
|
+
for (const hint of hints) {
|
|
1728
|
+
let value;
|
|
1729
|
+
if (hint.as !== void 0) value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
|
|
1730
|
+
else value = `<${hint.href}>; rel=${hint.rel}`;
|
|
1731
|
+
if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
|
|
1732
|
+
if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
|
|
1733
|
+
responseHeaders.append("Link", value);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
try {
|
|
1738
|
+
const chainFn = () => runMiddlewareChain(match.middlewareChain, ctx);
|
|
1739
|
+
const middlewareResponse = await (async () => {
|
|
1740
|
+
setMutableCookieContext(true);
|
|
1741
|
+
try {
|
|
1742
|
+
return await withSpan("timber.middleware", {}, () => detailed ? withTiming("mw", "middleware.ts", chainFn) : chainFn());
|
|
1743
|
+
} finally {
|
|
1744
|
+
setMutableCookieContext(false);
|
|
1745
|
+
}
|
|
1746
|
+
})();
|
|
1747
|
+
if (middlewareResponse) return {
|
|
1748
|
+
kind: "response",
|
|
1749
|
+
phase: "middleware",
|
|
1750
|
+
response: middlewareResponse
|
|
1751
|
+
};
|
|
1752
|
+
applyRequestHeaderOverlay(requestHeaderOverlay);
|
|
1753
|
+
applyCookieJar(responseHeaders);
|
|
1754
|
+
return runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
if (error instanceof RedirectSignal) return {
|
|
1757
|
+
kind: "redirect",
|
|
1758
|
+
phase: "middleware",
|
|
1759
|
+
signal: error
|
|
1760
|
+
};
|
|
1761
|
+
if (error instanceof DenySignal) return {
|
|
1762
|
+
kind: "deny",
|
|
1763
|
+
phase: "middleware",
|
|
1764
|
+
signal: error
|
|
1765
|
+
};
|
|
1766
|
+
return {
|
|
1767
|
+
kind: "error",
|
|
1768
|
+
phase: "middleware",
|
|
1769
|
+
error
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1620
1772
|
}
|
|
1621
|
-
//#endregion
|
|
1622
|
-
//#region src/server/pipeline.ts
|
|
1623
|
-
/**
|
|
1624
|
-
* Request pipeline — the central dispatch for all timber.js requests.
|
|
1625
|
-
*
|
|
1626
|
-
* Pipeline stages (in order):
|
|
1627
|
-
* proxy.ts → canonicalize → route match → 103 Early Hints → middleware.ts → render
|
|
1628
|
-
*
|
|
1629
|
-
* Each stage is a pure function or returns a Response to short-circuit.
|
|
1630
|
-
* Each request gets a trace ID, structured logging, and OTEL spans.
|
|
1631
|
-
*
|
|
1632
|
-
* See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow",
|
|
1633
|
-
* and design/17-logging.md §"Production Logging"
|
|
1634
|
-
*/
|
|
1635
|
-
/** Keys that must never be merged via Object.assign — they pollute Object.prototype. */
|
|
1636
|
-
var DANGEROUS_KEYS = new Set([
|
|
1637
|
-
"__proto__",
|
|
1638
|
-
"constructor",
|
|
1639
|
-
"prototype"
|
|
1640
|
-
]);
|
|
1641
1773
|
/**
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
* Used instead of Object.assign when the source object comes from
|
|
1645
|
-
* user-authored codec output (segmentParams.parse), which could
|
|
1646
|
-
* contain __proto__, constructor, or prototype keys.
|
|
1647
|
-
*
|
|
1648
|
-
* See TIM-655, design/13-security.md
|
|
1774
|
+
* Run the render phase. Wraps the configured renderer in a span and a
|
|
1775
|
+
* timing scope, and translates thrown signals into outcome variants.
|
|
1649
1776
|
*/
|
|
1650
|
-
function
|
|
1651
|
-
|
|
1777
|
+
async function runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, { canonicalPathname, interception }) {
|
|
1778
|
+
const detailed = config.serverTiming === "detailed";
|
|
1779
|
+
try {
|
|
1780
|
+
const renderFn = () => config.render(req, match, responseHeaders, requestHeaderOverlay, interception);
|
|
1781
|
+
return {
|
|
1782
|
+
kind: "response",
|
|
1783
|
+
phase: "render",
|
|
1784
|
+
response: await withSpan("timber.render", { "http.route": canonicalPathname }, () => detailed ? withTiming("render", "RSC + SSR render", renderFn) : renderFn())
|
|
1785
|
+
};
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
if (error instanceof DenySignal) return {
|
|
1788
|
+
kind: "deny",
|
|
1789
|
+
phase: "render",
|
|
1790
|
+
signal: error
|
|
1791
|
+
};
|
|
1792
|
+
if (error instanceof RedirectSignal) return {
|
|
1793
|
+
kind: "redirect",
|
|
1794
|
+
phase: "render",
|
|
1795
|
+
signal: error
|
|
1796
|
+
};
|
|
1797
|
+
return {
|
|
1798
|
+
kind: "error",
|
|
1799
|
+
phase: "render",
|
|
1800
|
+
error
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1652
1803
|
}
|
|
1653
1804
|
/**
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1805
|
+
* Process a single request from canonicalization through phase dispatch.
|
|
1806
|
+
*
|
|
1807
|
+
* Stages: canonicalize → metadata routes → auto-sitemap → version skew →
|
|
1808
|
+
* route match → interception → early hints → param coercion → middleware →
|
|
1809
|
+
* render → outcome translation. Pre-routing short-circuits return Responses
|
|
1810
|
+
* directly; post-match dispatch goes through `outcomeToResponse`.
|
|
1811
|
+
*
|
|
1812
|
+
* Used both as the top-level entry (when no proxy.ts is configured) and as
|
|
1813
|
+
* the `next()` continuation passed to `runProxy()`.
|
|
1814
|
+
*/
|
|
1815
|
+
async function handleRequest(config, req, method, path) {
|
|
1816
|
+
const stripTrailingSlash = config.stripTrailingSlash ?? true;
|
|
1817
|
+
const result = canonicalize(new URL(req.url).pathname, stripTrailingSlash);
|
|
1818
|
+
if (!result.ok) return new Response(null, { status: result.status });
|
|
1819
|
+
const canonicalPathname = result.pathname;
|
|
1820
|
+
if (config.matchMetadataRoute) {
|
|
1821
|
+
const metaMatch = config.matchMetadataRoute(canonicalPathname);
|
|
1822
|
+
if (metaMatch) try {
|
|
1823
|
+
if (metaMatch.isStatic) return await serveStaticMetadataFile(metaMatch);
|
|
1824
|
+
const mod = await loadModule(metaMatch.file);
|
|
1825
|
+
if (typeof mod.default !== "function") return new Response("Metadata route must export a default function", { status: 500 });
|
|
1826
|
+
const handlerResult = await mod.default();
|
|
1827
|
+
if (handlerResult instanceof Response) return cloneWithMutableHeaders(handlerResult);
|
|
1828
|
+
const contentType = metaMatch.contentType;
|
|
1829
|
+
let body;
|
|
1830
|
+
if (typeof handlerResult === "string") body = handlerResult;
|
|
1831
|
+
else if (contentType === "application/xml") body = serializeSitemap(handlerResult);
|
|
1832
|
+
else if (contentType === "application/manifest+json") body = JSON.stringify(handlerResult, null, 2);
|
|
1833
|
+
else body = String(handlerResult);
|
|
1834
|
+
return new Response(body, {
|
|
1835
|
+
status: 200,
|
|
1836
|
+
headers: { "Content-Type": `${contentType}; charset=utf-8` }
|
|
1837
|
+
});
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
logRenderError({
|
|
1840
|
+
method,
|
|
1841
|
+
path,
|
|
1842
|
+
error
|
|
1843
|
+
});
|
|
1844
|
+
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "metadata-route");
|
|
1845
|
+
return new Response(null, { status: 500 });
|
|
1672
1846
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1847
|
+
}
|
|
1848
|
+
if (config.autoSitemapHandler) try {
|
|
1849
|
+
const sitemapResponse = await config.autoSitemapHandler(canonicalPathname);
|
|
1850
|
+
if (sitemapResponse) return cloneWithMutableHeaders(sitemapResponse);
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
logRenderError({
|
|
1853
|
+
method,
|
|
1854
|
+
path,
|
|
1855
|
+
error
|
|
1856
|
+
});
|
|
1857
|
+
if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
|
|
1858
|
+
return new Response(null, { status: 500 });
|
|
1859
|
+
}
|
|
1860
|
+
if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
|
|
1861
|
+
if (!checkVersionSkew(req).ok) {
|
|
1862
|
+
const reloadHeaders = new Headers();
|
|
1863
|
+
applyReloadHeaders(reloadHeaders);
|
|
1864
|
+
return new Response(null, {
|
|
1865
|
+
status: 204,
|
|
1866
|
+
headers: reloadHeaders
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
let match = config.matchRoute(canonicalPathname);
|
|
1871
|
+
let interception;
|
|
1872
|
+
const sourceUrl = req.headers.get("X-Timber-URL");
|
|
1873
|
+
if (sourceUrl && config.interceptionRewrites?.length) {
|
|
1874
|
+
const intercepted = findInterceptionMatch(canonicalPathname, sourceUrl, config.interceptionRewrites);
|
|
1875
|
+
if (intercepted) {
|
|
1876
|
+
const sourceMatch = config.matchRoute(intercepted.sourcePathname);
|
|
1877
|
+
if (sourceMatch) {
|
|
1878
|
+
match = sourceMatch;
|
|
1879
|
+
interception = { targetPathname: canonicalPathname };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (!match) {
|
|
1884
|
+
if (config.renderNoMatch) {
|
|
1885
|
+
const responseHeaders = new Headers();
|
|
1886
|
+
return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
|
|
1887
|
+
}
|
|
1888
|
+
return new Response(null, { status: 404 });
|
|
1889
|
+
}
|
|
1890
|
+
const responseHeaders = new Headers();
|
|
1891
|
+
const requestHeaderOverlay = new Headers();
|
|
1892
|
+
responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
|
|
1893
|
+
if (config.earlyHints) try {
|
|
1894
|
+
await config.earlyHints(match, req, responseHeaders);
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
swallow(err, "early hints hook threw");
|
|
1897
|
+
}
|
|
1898
|
+
try {
|
|
1899
|
+
await coerceSegmentParams(match);
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
if (error instanceof ParamCoercionError) {
|
|
1902
|
+
const leafSegment = match.segments[match.segments.length - 1];
|
|
1903
|
+
if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
|
|
1904
|
+
if (config.renderNoMatch) return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
|
|
1905
|
+
return new Response(null, { status: 404 });
|
|
1680
1906
|
}
|
|
1907
|
+
throw error;
|
|
1681
1908
|
}
|
|
1909
|
+
setSegmentParams(match.segmentParams);
|
|
1910
|
+
return outcomeToResponse(config, !shouldBypassMiddleware(req) && match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, {
|
|
1911
|
+
canonicalPathname,
|
|
1912
|
+
interception
|
|
1913
|
+
}) : await runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, {
|
|
1914
|
+
canonicalPathname,
|
|
1915
|
+
interception
|
|
1916
|
+
}), {
|
|
1917
|
+
req,
|
|
1918
|
+
method,
|
|
1919
|
+
path,
|
|
1920
|
+
responseHeaders,
|
|
1921
|
+
match
|
|
1922
|
+
});
|
|
1682
1923
|
}
|
|
1924
|
+
//#endregion
|
|
1925
|
+
//#region src/server/pipeline.ts
|
|
1683
1926
|
/**
|
|
1684
1927
|
* Create the request handler from a pipeline configuration.
|
|
1685
1928
|
*
|
|
1686
|
-
* Returns a function that processes an incoming Request through all pipeline
|
|
1687
|
-
* and produces a Response. This is the top-level entry point for the
|
|
1929
|
+
* Returns a function that processes an incoming Request through all pipeline
|
|
1930
|
+
* stages and produces a Response. This is the top-level entry point for the
|
|
1931
|
+
* server. The body is intentionally small — phase logic lives in
|
|
1932
|
+
* `pipeline-phases.ts`. This function only owns the per-request setup that
|
|
1933
|
+
* has to wrap the entire dispatch: trace ID, request context ALS, span
|
|
1934
|
+
* scope, Server-Timing header emission, and the active-request counter.
|
|
1688
1935
|
*/
|
|
1689
1936
|
function createPipeline(config) {
|
|
1690
|
-
const
|
|
1937
|
+
const proxyResolver = makeProxyResolver(config.proxy);
|
|
1938
|
+
const slowRequestMs = config.slowRequestMs ?? 3e3;
|
|
1939
|
+
const serverTiming = config.serverTiming ?? "total";
|
|
1691
1940
|
let activeRequests = 0;
|
|
1692
1941
|
return async (req) => {
|
|
1693
1942
|
const url = new URL(req.url);
|
|
@@ -1709,18 +1958,18 @@ function createPipeline(config) {
|
|
|
1709
1958
|
const otelIds = await getOtelTraceId();
|
|
1710
1959
|
if (otelIds) replaceTraceId(otelIds.traceId, otelIds.spanId);
|
|
1711
1960
|
let result;
|
|
1712
|
-
if (
|
|
1713
|
-
|
|
1961
|
+
if (proxyResolver) result = await outcomeToResponse(config, await runProxyPhase(config, proxyResolver, req, method, path), {
|
|
1962
|
+
req,
|
|
1963
|
+
method,
|
|
1964
|
+
path
|
|
1965
|
+
});
|
|
1966
|
+
else result = await handleRequest(config, req, method, path);
|
|
1714
1967
|
await setSpanAttribute("http.response.status_code", result.status);
|
|
1715
1968
|
if (serverTiming === "detailed") {
|
|
1716
1969
|
const timingHeader = getServerTimingHeader();
|
|
1717
|
-
if (timingHeader)
|
|
1718
|
-
result = ensureMutableResponse(result);
|
|
1719
|
-
result.headers.set("Server-Timing", timingHeader);
|
|
1720
|
-
}
|
|
1970
|
+
if (timingHeader) result.headers.set("Server-Timing", timingHeader);
|
|
1721
1971
|
} else if (serverTiming === "total") {
|
|
1722
1972
|
const totalMs = Math.round(performance.now() - startTime);
|
|
1723
|
-
result = ensureMutableResponse(result);
|
|
1724
1973
|
result.headers.set("Server-Timing", `total;dur=${totalMs}`);
|
|
1725
1974
|
}
|
|
1726
1975
|
return result;
|
|
@@ -1749,276 +1998,6 @@ function createPipeline(config) {
|
|
|
1749
1998
|
});
|
|
1750
1999
|
});
|
|
1751
2000
|
};
|
|
1752
|
-
async function runProxyPhase(req, method, path) {
|
|
1753
|
-
try {
|
|
1754
|
-
let proxyExport;
|
|
1755
|
-
if (config.proxyLoader) proxyExport = (await config.proxyLoader()).default;
|
|
1756
|
-
else proxyExport = config.proxy;
|
|
1757
|
-
const proxyFn = () => runProxy(proxyExport, req, () => handleRequest(req, method, path));
|
|
1758
|
-
return await withSpan("timber.proxy", {}, () => serverTiming === "detailed" ? withTiming("proxy", "proxy.ts", proxyFn) : proxyFn());
|
|
1759
|
-
} catch (error) {
|
|
1760
|
-
logProxyError({ error });
|
|
1761
|
-
await fireOnRequestError(error, req, "proxy");
|
|
1762
|
-
if (onPipelineError && error instanceof Error) onPipelineError(error, "proxy");
|
|
1763
|
-
return new Response(null, { status: 500 });
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Build a redirect Response from a RedirectSignal.
|
|
1768
|
-
*
|
|
1769
|
-
* For RSC payload requests (client navigation), returns 204 + X-Timber-Redirect
|
|
1770
|
-
* so the client router can perform a soft SPA redirect. A raw 302 would be
|
|
1771
|
-
* turned into an opaque redirect by fetch({redirect:'manual'}), crashing
|
|
1772
|
-
* createFromFetch. See design/19-client-navigation.md.
|
|
1773
|
-
*/
|
|
1774
|
-
function buildRedirectResponse(signal, req, headers) {
|
|
1775
|
-
if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
|
|
1776
|
-
headers.set("X-Timber-Redirect", signal.location);
|
|
1777
|
-
return new Response(null, {
|
|
1778
|
-
status: 204,
|
|
1779
|
-
headers
|
|
1780
|
-
});
|
|
1781
|
-
}
|
|
1782
|
-
headers.set("Location", signal.location);
|
|
1783
|
-
return new Response(null, {
|
|
1784
|
-
status: signal.status,
|
|
1785
|
-
headers
|
|
1786
|
-
});
|
|
1787
|
-
}
|
|
1788
|
-
async function handleRequest(req, method, path) {
|
|
1789
|
-
const result = canonicalize(new URL(req.url).pathname, stripTrailingSlash);
|
|
1790
|
-
if (!result.ok) return new Response(null, { status: result.status });
|
|
1791
|
-
const canonicalPathname = result.pathname;
|
|
1792
|
-
if (config.matchMetadataRoute) {
|
|
1793
|
-
const metaMatch = config.matchMetadataRoute(canonicalPathname);
|
|
1794
|
-
if (metaMatch) try {
|
|
1795
|
-
if (metaMatch.isStatic) return await serveStaticMetadataFile(metaMatch);
|
|
1796
|
-
const mod = await loadModule(metaMatch.file);
|
|
1797
|
-
if (typeof mod.default !== "function") return new Response("Metadata route must export a default function", { status: 500 });
|
|
1798
|
-
const handlerResult = await mod.default();
|
|
1799
|
-
if (handlerResult instanceof Response) return handlerResult;
|
|
1800
|
-
const contentType = metaMatch.contentType;
|
|
1801
|
-
let body;
|
|
1802
|
-
if (typeof handlerResult === "string") body = handlerResult;
|
|
1803
|
-
else if (contentType === "application/xml") body = serializeSitemap(handlerResult);
|
|
1804
|
-
else if (contentType === "application/manifest+json") body = JSON.stringify(handlerResult, null, 2);
|
|
1805
|
-
else body = typeof handlerResult === "string" ? handlerResult : String(handlerResult);
|
|
1806
|
-
return new Response(body, {
|
|
1807
|
-
status: 200,
|
|
1808
|
-
headers: { "Content-Type": `${contentType}; charset=utf-8` }
|
|
1809
|
-
});
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
logRenderError({
|
|
1812
|
-
method,
|
|
1813
|
-
path,
|
|
1814
|
-
error
|
|
1815
|
-
});
|
|
1816
|
-
if (onPipelineError && error instanceof Error) onPipelineError(error, "metadata-route");
|
|
1817
|
-
return new Response(null, { status: 500 });
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
if (config.autoSitemapHandler) try {
|
|
1821
|
-
const sitemapResponse = await config.autoSitemapHandler(canonicalPathname);
|
|
1822
|
-
if (sitemapResponse) return sitemapResponse;
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
logRenderError({
|
|
1825
|
-
method,
|
|
1826
|
-
path,
|
|
1827
|
-
error
|
|
1828
|
-
});
|
|
1829
|
-
if (onPipelineError && error instanceof Error) onPipelineError(error, "auto-sitemap");
|
|
1830
|
-
return new Response(null, { status: 500 });
|
|
1831
|
-
}
|
|
1832
|
-
if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
|
|
1833
|
-
if (!checkVersionSkew(req).ok) {
|
|
1834
|
-
const reloadHeaders = new Headers();
|
|
1835
|
-
applyReloadHeaders(reloadHeaders);
|
|
1836
|
-
return new Response(null, {
|
|
1837
|
-
status: 204,
|
|
1838
|
-
headers: reloadHeaders
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
let match = matchRoute(canonicalPathname);
|
|
1843
|
-
let interception;
|
|
1844
|
-
const sourceUrl = req.headers.get("X-Timber-URL");
|
|
1845
|
-
if (sourceUrl && config.interceptionRewrites?.length) {
|
|
1846
|
-
const intercepted = findInterceptionMatch(canonicalPathname, sourceUrl, config.interceptionRewrites);
|
|
1847
|
-
if (intercepted) {
|
|
1848
|
-
const sourceMatch = matchRoute(intercepted.sourcePathname);
|
|
1849
|
-
if (sourceMatch) {
|
|
1850
|
-
match = sourceMatch;
|
|
1851
|
-
interception = { targetPathname: canonicalPathname };
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
if (!match) {
|
|
1856
|
-
if (config.renderNoMatch) {
|
|
1857
|
-
const responseHeaders = new Headers();
|
|
1858
|
-
return config.renderNoMatch(req, responseHeaders);
|
|
1859
|
-
}
|
|
1860
|
-
return new Response(null, { status: 404 });
|
|
1861
|
-
}
|
|
1862
|
-
const responseHeaders = new Headers();
|
|
1863
|
-
const requestHeaderOverlay = new Headers();
|
|
1864
|
-
responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
|
|
1865
|
-
if (earlyHints) try {
|
|
1866
|
-
await earlyHints(match, req, responseHeaders);
|
|
1867
|
-
} catch {}
|
|
1868
|
-
try {
|
|
1869
|
-
await coerceSegmentParams(match);
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
if (error instanceof ParamCoercionError) {
|
|
1872
|
-
const leafSegment = match.segments[match.segments.length - 1];
|
|
1873
|
-
if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
|
|
1874
|
-
if (config.renderNoMatch) return config.renderNoMatch(req, responseHeaders);
|
|
1875
|
-
return new Response(null, { status: 404 });
|
|
1876
|
-
}
|
|
1877
|
-
throw error;
|
|
1878
|
-
}
|
|
1879
|
-
setSegmentParams(match.segmentParams);
|
|
1880
|
-
if (match.middlewareChain.length > 0) {
|
|
1881
|
-
const ctx = {
|
|
1882
|
-
req,
|
|
1883
|
-
requestHeaders: requestHeaderOverlay,
|
|
1884
|
-
headers: responseHeaders,
|
|
1885
|
-
segmentParams: match.segmentParams,
|
|
1886
|
-
earlyHints: (hints) => {
|
|
1887
|
-
for (const hint of hints) {
|
|
1888
|
-
let value;
|
|
1889
|
-
if (hint.as !== void 0) value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
|
|
1890
|
-
else value = `<${hint.href}>; rel=${hint.rel}`;
|
|
1891
|
-
if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
|
|
1892
|
-
if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
|
|
1893
|
-
responseHeaders.append("Link", value);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
};
|
|
1897
|
-
try {
|
|
1898
|
-
setMutableCookieContext(true);
|
|
1899
|
-
const chainFn = () => runMiddlewareChain(match.middlewareChain, ctx);
|
|
1900
|
-
const middlewareResponse = await withSpan("timber.middleware", {}, () => serverTiming === "detailed" ? withTiming("mw", "middleware.ts", chainFn) : chainFn());
|
|
1901
|
-
setMutableCookieContext(false);
|
|
1902
|
-
if (middlewareResponse) {
|
|
1903
|
-
const finalResponse = ensureMutableResponse(middlewareResponse);
|
|
1904
|
-
applyCookieJar(finalResponse.headers);
|
|
1905
|
-
const existingKeys = new Set([...finalResponse.headers.keys()].map((k) => k.toLowerCase()));
|
|
1906
|
-
for (const [key, value] of responseHeaders.entries()) if (!existingKeys.has(key.toLowerCase())) finalResponse.headers.append(key, value);
|
|
1907
|
-
logMiddlewareShortCircuit({
|
|
1908
|
-
method,
|
|
1909
|
-
path,
|
|
1910
|
-
status: finalResponse.status
|
|
1911
|
-
});
|
|
1912
|
-
return finalResponse;
|
|
1913
|
-
}
|
|
1914
|
-
applyRequestHeaderOverlay(requestHeaderOverlay);
|
|
1915
|
-
} catch (error) {
|
|
1916
|
-
setMutableCookieContext(false);
|
|
1917
|
-
if (error instanceof RedirectSignal) {
|
|
1918
|
-
applyCookieJar(responseHeaders);
|
|
1919
|
-
return buildRedirectResponse(error, req, responseHeaders);
|
|
1920
|
-
}
|
|
1921
|
-
if (error instanceof DenySignal) {
|
|
1922
|
-
applyCookieJar(responseHeaders);
|
|
1923
|
-
if (config.renderDenyFallback) try {
|
|
1924
|
-
return await config.renderDenyFallback(error, req, responseHeaders, match);
|
|
1925
|
-
} catch {}
|
|
1926
|
-
return new Response(null, {
|
|
1927
|
-
status: error.status,
|
|
1928
|
-
headers: responseHeaders
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
logMiddlewareError({
|
|
1932
|
-
method,
|
|
1933
|
-
path,
|
|
1934
|
-
error
|
|
1935
|
-
});
|
|
1936
|
-
await fireOnRequestError(error, req, "handler");
|
|
1937
|
-
if (onPipelineError && error instanceof Error) onPipelineError(error, "middleware");
|
|
1938
|
-
return new Response(null, { status: 500 });
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
applyCookieJar(responseHeaders);
|
|
1942
|
-
try {
|
|
1943
|
-
const renderFn = () => render(req, match, responseHeaders, requestHeaderOverlay, interception);
|
|
1944
|
-
const response = await withSpan("timber.render", { "http.route": canonicalPathname }, () => serverTiming === "detailed" ? withTiming("render", "RSC + SSR render", renderFn) : renderFn());
|
|
1945
|
-
markResponseFlushed();
|
|
1946
|
-
return response;
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
if (error instanceof DenySignal) {
|
|
1949
|
-
if (config.renderDenyFallback) try {
|
|
1950
|
-
return await config.renderDenyFallback(error, req, responseHeaders, match);
|
|
1951
|
-
} catch {}
|
|
1952
|
-
return new Response(null, {
|
|
1953
|
-
status: error.status,
|
|
1954
|
-
headers: responseHeaders
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
if (error instanceof RedirectSignal) return buildRedirectResponse(error, req, responseHeaders);
|
|
1958
|
-
logRenderError({
|
|
1959
|
-
method,
|
|
1960
|
-
path,
|
|
1961
|
-
error
|
|
1962
|
-
});
|
|
1963
|
-
await fireOnRequestError(error, req, "render");
|
|
1964
|
-
if (onPipelineError && error instanceof Error) onPipelineError(error, "render");
|
|
1965
|
-
if (config.renderFallbackError) try {
|
|
1966
|
-
return await config.renderFallbackError(error, req, responseHeaders);
|
|
1967
|
-
} catch {}
|
|
1968
|
-
return new Response(null, { status: 500 });
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
/**
|
|
1973
|
-
* Fire the user's onRequestError hook with request context.
|
|
1974
|
-
* Extracts request info from the Request object and calls the instrumentation hook.
|
|
1975
|
-
*/
|
|
1976
|
-
async function fireOnRequestError(error, req, phase) {
|
|
1977
|
-
const url = new URL(req.url);
|
|
1978
|
-
const headersObj = {};
|
|
1979
|
-
req.headers.forEach((v, k) => {
|
|
1980
|
-
headersObj[k] = v;
|
|
1981
|
-
});
|
|
1982
|
-
await callOnRequestError(error, {
|
|
1983
|
-
method: req.method,
|
|
1984
|
-
path: url.pathname,
|
|
1985
|
-
headers: headersObj
|
|
1986
|
-
}, {
|
|
1987
|
-
phase,
|
|
1988
|
-
routePath: url.pathname,
|
|
1989
|
-
routeType: "page",
|
|
1990
|
-
traceId: getTraceId()
|
|
1991
|
-
});
|
|
1992
|
-
}
|
|
1993
|
-
/**
|
|
1994
|
-
* Apply all Set-Cookie headers from the cookie jar to a Headers object.
|
|
1995
|
-
* Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
|
|
1996
|
-
*/
|
|
1997
|
-
function applyCookieJar(headers) {
|
|
1998
|
-
for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
|
|
1999
|
-
}
|
|
2000
|
-
/**
|
|
2001
|
-
* Ensure a Response has mutable headers so the pipeline can safely append
|
|
2002
|
-
* Set-Cookie and Server-Timing entries.
|
|
2003
|
-
*
|
|
2004
|
-
* `Response.redirect()` and some platform-level responses return objects
|
|
2005
|
-
* with immutable headers. Calling `.set()` or `.append()` on them throws
|
|
2006
|
-
* `TypeError: immutable`. This helper detects the immutable case by
|
|
2007
|
-
* attempting a no-op write and, on failure, clones into a fresh Response
|
|
2008
|
-
* with mutable headers.
|
|
2009
|
-
*/
|
|
2010
|
-
function ensureMutableResponse(response) {
|
|
2011
|
-
try {
|
|
2012
|
-
response.headers.set("X-Timber-Probe", "1");
|
|
2013
|
-
response.headers.delete("X-Timber-Probe");
|
|
2014
|
-
return response;
|
|
2015
|
-
} catch {
|
|
2016
|
-
return new Response(response.body, {
|
|
2017
|
-
status: response.status,
|
|
2018
|
-
statusText: response.statusText,
|
|
2019
|
-
headers: new Headers(response.headers)
|
|
2020
|
-
});
|
|
2021
|
-
}
|
|
2022
2001
|
}
|
|
2023
2002
|
//#endregion
|
|
2024
2003
|
//#region src/server/build-manifest.ts
|
|
@@ -2221,10 +2200,46 @@ function sendEarlyHints103(links) {
|
|
|
2221
2200
|
if (!sender) return;
|
|
2222
2201
|
try {
|
|
2223
2202
|
sender(links);
|
|
2224
|
-
} catch {
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
swallow(err, "early hints 103 send failed");
|
|
2205
|
+
}
|
|
2225
2206
|
}
|
|
2226
2207
|
//#endregion
|
|
2227
2208
|
//#region src/server/tree-builder.ts
|
|
2209
|
+
var REACT_COMPONENT_TYPE_MARKERS = new Set([
|
|
2210
|
+
Symbol.for("react.forward_ref"),
|
|
2211
|
+
Symbol.for("react.memo"),
|
|
2212
|
+
Symbol.for("react.lazy"),
|
|
2213
|
+
Symbol.for("react.provider"),
|
|
2214
|
+
Symbol.for("react.context"),
|
|
2215
|
+
Symbol.for("react.suspense"),
|
|
2216
|
+
Symbol.for("react.suspense_list"),
|
|
2217
|
+
Symbol.for("react.client.reference")
|
|
2218
|
+
]);
|
|
2219
|
+
/**
|
|
2220
|
+
* Validate that a loaded module's `default` export is something React
|
|
2221
|
+
* accepts as the first argument to `createElement` — i.e. a valid component
|
|
2222
|
+
* type. React doesn't export `isValidElementType` (only `isValidElement`,
|
|
2223
|
+
* which checks for *elements*, not *component types*), so this mirrors
|
|
2224
|
+
* React's internal check:
|
|
2225
|
+
*
|
|
2226
|
+
* - functions → function or class components
|
|
2227
|
+
* - objects with a `$$typeof` matching one of React's known component
|
|
2228
|
+
* markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
|
|
2229
|
+
* suspense, client references via `@vitejs/plugin-rsc`)
|
|
2230
|
+
*
|
|
2231
|
+
* Strings (HTML tag names) are valid for `createElement` but never appear
|
|
2232
|
+
* as a route module's default export, so they're not recognized here.
|
|
2233
|
+
*
|
|
2234
|
+
* Anything else (numbers, plain config objects, JSON, etc.) is rejected so
|
|
2235
|
+
* the boundary wrapper is skipped rather than crashing inside React.
|
|
2236
|
+
*/
|
|
2237
|
+
function isValidElementType(value) {
|
|
2238
|
+
if (typeof value === "function") return true;
|
|
2239
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2240
|
+
const marker = value.$$typeof;
|
|
2241
|
+
return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
|
|
2242
|
+
}
|
|
2228
2243
|
/**
|
|
2229
2244
|
* Build the unified element tree from a matched segment chain.
|
|
2230
2245
|
*
|
|
@@ -2263,7 +2278,11 @@ async function buildElementTree(config) {
|
|
|
2263
2278
|
const LayoutComponent = (await loadModule(segment.layout)).default;
|
|
2264
2279
|
if (LayoutComponent) {
|
|
2265
2280
|
const slotProps = {};
|
|
2266
|
-
|
|
2281
|
+
const slotNames = Object.keys(segment.slots);
|
|
2282
|
+
if (slotNames.length > 0) for (const slotName of slotNames) {
|
|
2283
|
+
const slotNode = segment.slots[slotName];
|
|
2284
|
+
slotProps[slotName] = await buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent);
|
|
2285
|
+
}
|
|
2267
2286
|
element = createElement(LayoutComponent, {
|
|
2268
2287
|
...slotProps,
|
|
2269
2288
|
children: element
|
|
@@ -2332,10 +2351,11 @@ function isMdxFile(file) {
|
|
|
2332
2351
|
*/
|
|
2333
2352
|
async function wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent) {
|
|
2334
2353
|
if (segment.statusFiles) {
|
|
2335
|
-
for (const [key, file] of segment.statusFiles) if (key !== "4xx" && key !== "5xx") {
|
|
2354
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) if (key !== "4xx" && key !== "5xx") {
|
|
2336
2355
|
const status = parseInt(key, 10);
|
|
2337
2356
|
if (!isNaN(status)) {
|
|
2338
|
-
const
|
|
2357
|
+
const mod = await loadModule(file);
|
|
2358
|
+
const Component = isValidElementType(mod.default) ? mod.default : null;
|
|
2339
2359
|
if (Component) element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
|
|
2340
2360
|
fallbackElement: createElement(Component, { status }),
|
|
2341
2361
|
status,
|
|
@@ -2347,8 +2367,9 @@ async function wrapWithErrorBoundaries(segment, element, loadModule, createEleme
|
|
|
2347
2367
|
});
|
|
2348
2368
|
}
|
|
2349
2369
|
}
|
|
2350
|
-
for (const [key, file] of segment.statusFiles) if (key === "4xx" || key === "5xx") {
|
|
2351
|
-
const
|
|
2370
|
+
for (const [key, file] of Object.entries(segment.statusFiles)) if (key === "4xx" || key === "5xx") {
|
|
2371
|
+
const mod = await loadModule(file);
|
|
2372
|
+
const Component = isValidElementType(mod.default) ? mod.default : null;
|
|
2352
2373
|
if (Component) {
|
|
2353
2374
|
const categoryStatus = key === "4xx" ? 400 : 500;
|
|
2354
2375
|
element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
|
|
@@ -2364,7 +2385,8 @@ async function wrapWithErrorBoundaries(segment, element, loadModule, createEleme
|
|
|
2364
2385
|
}
|
|
2365
2386
|
}
|
|
2366
2387
|
if (segment.error) {
|
|
2367
|
-
const
|
|
2388
|
+
const errorModule = await loadModule(segment.error);
|
|
2389
|
+
const ErrorComponent = isValidElementType(errorModule.default) ? errorModule.default : null;
|
|
2368
2390
|
if (ErrorComponent) element = createElement(errorBoundaryComponent, isMdxFile(segment.error) ? {
|
|
2369
2391
|
fallbackElement: createElement(ErrorComponent, {}),
|
|
2370
2392
|
children: element
|
|
@@ -2377,15 +2399,54 @@ async function wrapWithErrorBoundaries(segment, element, loadModule, createEleme
|
|
|
2377
2399
|
}
|
|
2378
2400
|
//#endregion
|
|
2379
2401
|
//#region src/server/status-code-resolver.ts
|
|
2380
|
-
/**
|
|
2381
|
-
|
|
2382
|
-
* Only used in the 4xx component fallback chain.
|
|
2383
|
-
*/
|
|
2384
|
-
var LEGACY_FILE_TO_STATUS = {
|
|
2402
|
+
/** Reverse index: status code → legacy file name. Built once at module load. */
|
|
2403
|
+
var STATUS_TO_LEGACY_FILE = Object.fromEntries(Object.entries({
|
|
2385
2404
|
"not-found": 404,
|
|
2386
2405
|
"forbidden": 403,
|
|
2387
2406
|
"unauthorized": 401
|
|
2388
|
-
};
|
|
2407
|
+
}).map(([name, status]) => [status, name]));
|
|
2408
|
+
/**
|
|
2409
|
+
* Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
|
|
2410
|
+
* status-file group on a single segment. Shared by all three fallback
|
|
2411
|
+
* chains — the only structural difference between component 4xx,
|
|
2412
|
+
* component 5xx, and JSON resolution is *which* group is searched and
|
|
2413
|
+
* how the per-segment loop is layered around it.
|
|
2414
|
+
*/
|
|
2415
|
+
function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
|
|
2416
|
+
if (!group) return null;
|
|
2417
|
+
const exact = group[statusStr];
|
|
2418
|
+
if (exact) return {
|
|
2419
|
+
file: exact,
|
|
2420
|
+
status,
|
|
2421
|
+
kind: "exact",
|
|
2422
|
+
segmentIndex
|
|
2423
|
+
};
|
|
2424
|
+
const category = group[categoryKey];
|
|
2425
|
+
if (category) return {
|
|
2426
|
+
file: category,
|
|
2427
|
+
status,
|
|
2428
|
+
kind: "category",
|
|
2429
|
+
segmentIndex
|
|
2430
|
+
};
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Look up the legacy convention file (`not-found.tsx` / `forbidden.tsx` /
|
|
2435
|
+
* `unauthorized.tsx`) for `status` on a single segment. Returns null if
|
|
2436
|
+
* `status` has no legacy mapping or the file isn't present.
|
|
2437
|
+
*/
|
|
2438
|
+
function lookupLegacy(group, status, segmentIndex) {
|
|
2439
|
+
if (!group) return null;
|
|
2440
|
+
const name = STATUS_TO_LEGACY_FILE[status];
|
|
2441
|
+
if (!name) return null;
|
|
2442
|
+
const file = group[name];
|
|
2443
|
+
return file ? {
|
|
2444
|
+
file,
|
|
2445
|
+
status,
|
|
2446
|
+
kind: "legacy",
|
|
2447
|
+
segmentIndex
|
|
2448
|
+
} : null;
|
|
2449
|
+
}
|
|
2389
2450
|
/**
|
|
2390
2451
|
* Resolve the status-code file to render for a given HTTP status code.
|
|
2391
2452
|
*
|
|
@@ -2398,108 +2459,58 @@ var LEGACY_FILE_TO_STATUS = {
|
|
|
2398
2459
|
* @param format - The response format family ('component' or 'json'). Defaults to 'component'.
|
|
2399
2460
|
*/
|
|
2400
2461
|
function resolveStatusFile(status, segments, format = "component") {
|
|
2401
|
-
if (status
|
|
2402
|
-
if (
|
|
2403
|
-
return
|
|
2462
|
+
if (status < 400 || status > 599) return null;
|
|
2463
|
+
if (format === "json") return resolveJson(status, segments);
|
|
2464
|
+
if (status <= 499) return resolve4xx(status, segments);
|
|
2465
|
+
return resolve5xx(status, segments);
|
|
2404
2466
|
}
|
|
2405
2467
|
/**
|
|
2406
|
-
* 4xx component fallback chain
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2468
|
+
* 4xx component fallback chain — three separate full passes leaf→root.
|
|
2469
|
+
*
|
|
2470
|
+
* The passes must be separate (not interleaved per-segment) so that a
|
|
2471
|
+
* root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
|
|
2472
|
+
* inverts this and is per-segment: a leaf's `error.tsx` beats a root's
|
|
2473
|
+
* `5xx.tsx`. This asymmetry is the only reason these two functions exist
|
|
2474
|
+
* separately.
|
|
2475
|
+
*
|
|
2476
|
+
* Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
|
|
2477
|
+
* Pass 2 — not-found / forbidden / unauthorized (legacyStatusFiles)
|
|
2478
|
+
* Pass 3 — error.tsx (error)
|
|
2410
2479
|
*/
|
|
2411
2480
|
function resolve4xx(status, segments) {
|
|
2412
2481
|
const statusStr = String(status);
|
|
2413
2482
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2414
|
-
const
|
|
2415
|
-
if (
|
|
2416
|
-
const exact = segment.statusFiles.get(statusStr);
|
|
2417
|
-
if (exact) return {
|
|
2418
|
-
file: exact,
|
|
2419
|
-
status,
|
|
2420
|
-
kind: "exact",
|
|
2421
|
-
segmentIndex: i
|
|
2422
|
-
};
|
|
2423
|
-
const category = segment.statusFiles.get("4xx");
|
|
2424
|
-
if (category) return {
|
|
2425
|
-
file: category,
|
|
2426
|
-
status,
|
|
2427
|
-
kind: "category",
|
|
2428
|
-
segmentIndex: i
|
|
2429
|
-
};
|
|
2483
|
+
const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
|
|
2484
|
+
if (r) return r;
|
|
2430
2485
|
}
|
|
2431
2486
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2432
|
-
const
|
|
2433
|
-
if (
|
|
2434
|
-
for (const [name, legacyStatus] of Object.entries(LEGACY_FILE_TO_STATUS)) if (legacyStatus === status) {
|
|
2435
|
-
const file = segment.legacyStatusFiles.get(name);
|
|
2436
|
-
if (file) return {
|
|
2437
|
-
file,
|
|
2438
|
-
status,
|
|
2439
|
-
kind: "legacy",
|
|
2440
|
-
segmentIndex: i
|
|
2441
|
-
};
|
|
2442
|
-
}
|
|
2487
|
+
const r = lookupLegacy(segments[i].legacyStatusFiles, status, i);
|
|
2488
|
+
if (r) return r;
|
|
2443
2489
|
}
|
|
2444
|
-
for (let i = segments.length - 1; i >= 0; i--) if (segments[i].error) return {
|
|
2445
|
-
file: segments[i].error,
|
|
2446
|
-
status,
|
|
2447
|
-
kind: "error",
|
|
2448
|
-
segmentIndex: i
|
|
2449
|
-
};
|
|
2450
|
-
return null;
|
|
2451
|
-
}
|
|
2452
|
-
/**
|
|
2453
|
-
* 4xx JSON fallback chain (single pass):
|
|
2454
|
-
* Pass 1 — json status files (leaf → root): {status}.json → 4xx.json
|
|
2455
|
-
* No legacy compat, no error.tsx — JSON chain terminates at category catch-all.
|
|
2456
|
-
*/
|
|
2457
|
-
function resolve4xxJson(status, segments) {
|
|
2458
|
-
const statusStr = String(status);
|
|
2459
2490
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2460
|
-
const
|
|
2461
|
-
if (
|
|
2462
|
-
|
|
2463
|
-
if (exact) return {
|
|
2464
|
-
file: exact,
|
|
2491
|
+
const errorFile = segments[i].error;
|
|
2492
|
+
if (errorFile) return {
|
|
2493
|
+
file: errorFile,
|
|
2465
2494
|
status,
|
|
2466
|
-
kind: "
|
|
2467
|
-
segmentIndex: i
|
|
2468
|
-
};
|
|
2469
|
-
const category = segment.jsonStatusFiles.get("4xx");
|
|
2470
|
-
if (category) return {
|
|
2471
|
-
file: category,
|
|
2472
|
-
status,
|
|
2473
|
-
kind: "category",
|
|
2495
|
+
kind: "error",
|
|
2474
2496
|
segmentIndex: i
|
|
2475
2497
|
};
|
|
2476
2498
|
}
|
|
2477
2499
|
return null;
|
|
2478
2500
|
}
|
|
2479
2501
|
/**
|
|
2480
|
-
* 5xx component fallback chain
|
|
2481
|
-
*
|
|
2502
|
+
* 5xx component fallback chain — single pass, per-segment leaf→root.
|
|
2503
|
+
*
|
|
2504
|
+
* At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
|
|
2505
|
+
* `error.tsx` therefore beats a root's `5xx.tsx`, which is the
|
|
2506
|
+
* intentional inverse of the 4xx chain.
|
|
2482
2507
|
*/
|
|
2483
2508
|
function resolve5xx(status, segments) {
|
|
2484
2509
|
const statusStr = String(status);
|
|
2485
2510
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2486
2511
|
const segment = segments[i];
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
if (exact) return {
|
|
2490
|
-
file: exact,
|
|
2491
|
-
status,
|
|
2492
|
-
kind: "exact",
|
|
2493
|
-
segmentIndex: i
|
|
2494
|
-
};
|
|
2495
|
-
const category = segment.statusFiles.get("5xx");
|
|
2496
|
-
if (category) return {
|
|
2497
|
-
file: category,
|
|
2498
|
-
status,
|
|
2499
|
-
kind: "category",
|
|
2500
|
-
segmentIndex: i
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
2512
|
+
const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
|
|
2513
|
+
if (r) return r;
|
|
2503
2514
|
if (segment.error) return {
|
|
2504
2515
|
file: segment.error,
|
|
2505
2516
|
status,
|
|
@@ -2510,29 +2521,18 @@ function resolve5xx(status, segments) {
|
|
|
2510
2521
|
return null;
|
|
2511
2522
|
}
|
|
2512
2523
|
/**
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
2515
|
-
*
|
|
2524
|
+
* JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
|
|
2525
|
+
*
|
|
2526
|
+
* At each segment: {status}.json → {category}.json. No legacy compat,
|
|
2527
|
+
* no error.tsx — the JSON chain terminates at the category catch-all
|
|
2528
|
+
* and the caller falls back to a bare-JSON framework default.
|
|
2516
2529
|
*/
|
|
2517
|
-
function
|
|
2530
|
+
function resolveJson(status, segments) {
|
|
2518
2531
|
const statusStr = String(status);
|
|
2532
|
+
const categoryKey = status >= 500 ? "5xx" : "4xx";
|
|
2519
2533
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2520
|
-
const
|
|
2521
|
-
if (
|
|
2522
|
-
const exact = segment.jsonStatusFiles.get(statusStr);
|
|
2523
|
-
if (exact) return {
|
|
2524
|
-
file: exact,
|
|
2525
|
-
status,
|
|
2526
|
-
kind: "exact",
|
|
2527
|
-
segmentIndex: i
|
|
2528
|
-
};
|
|
2529
|
-
const category = segment.jsonStatusFiles.get("5xx");
|
|
2530
|
-
if (category) return {
|
|
2531
|
-
file: category,
|
|
2532
|
-
status,
|
|
2533
|
-
kind: "category",
|
|
2534
|
-
segmentIndex: i
|
|
2535
|
-
};
|
|
2534
|
+
const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
|
|
2535
|
+
if (r) return r;
|
|
2536
2536
|
}
|
|
2537
2537
|
return null;
|
|
2538
2538
|
}
|
|
@@ -2895,6 +2895,6 @@ var RenderTimeoutError = class extends Error {
|
|
|
2895
2895
|
}
|
|
2896
2896
|
};
|
|
2897
2897
|
//#endregion
|
|
2898
|
-
export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderError, RenderTimeoutError, SlotAccessGate, WarningId, buildElementTree, buildNoJsResponse, callOnRequestError, canonicalize, classifyMetadataRoute, collectEarlyHintHeaders, createPipeline, enforceBodyLimits, executeAction, flushResponse, formatLinkHeader, generateTraceId, getLogger, getMetadataRouteAutoLink, getMetadataRouteServePath, getSetCookieHeaders, handleRouteRequest, hasOnRequestError, isRscActionRequest, loadInstrumentation, logCacheMiss, logMiddlewareError, logMiddlewareShortCircuit, logProxyError, logRenderError, logRequestCompleted, logRequestReceived, logSlowRequest, logSwrRefetchFailed, logWaitUntilRejected, logWaitUntilUnsupported, markResponseFlushed, parseBodySize, renderMetadataToElements, replaceTraceId, resolveAllowedMethods, resolveMetadata, resolveMetadataUrls, resolveSlotDenied, resolveStatusFile, resolveTitle, runMiddleware, runProxy, runWithEarlyHintsSender, runWithRequestContext, runWithTraceId, sendEarlyHints103, setLogger, setMutableCookieContext, setSegmentParams, setViteServer, validateCsrf, warnDenyInSuspense, warnRedirectInAccess, warnRedirectInSuspense, warnSlowSlotWithoutSuspense, warnStaticRequestApi, warnSuspenseWrappingChildren };
|
|
2898
|
+
export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderError, RenderTimeoutError, SlotAccessGate, WarningId, buildElementTree, buildNoJsResponse, callOnRequestError, canonicalize, classifyMetadataRoute, coerce, collectEarlyHintHeaders, createPipeline, enforceBodyLimits, executeAction, flushResponse, formatLinkHeader, generateTraceId, getCookie, getHeader, getLogger, getMetadataRouteAutoLink, getMetadataRouteServePath, getSearchParams, getSegmentParams, getSetCookieHeaders, handleRouteRequest, hasOnRequestError, isRscActionRequest, loadInstrumentation, logCacheMiss, logMiddlewareError, logMiddlewareShortCircuit, logProxyError, logRenderError, logRequestCompleted, logRequestReceived, logSlowRequest, logSwrRefetchFailed, logWaitUntilRejected, logWaitUntilUnsupported, markResponseFlushed, parseBodySize, renderMetadataToElements, replaceTraceId, resolveAllowedMethods, resolveMetadata, resolveMetadataUrls, resolveSlotDenied, resolveStatusFile, resolveTitle, runMiddleware, runProxy, runWithEarlyHintsSender, runWithRequestContext, runWithTraceId, sendEarlyHints103, setLogger, setMutableCookieContext, setSegmentParams, setViteServer, validateCsrf, warnDenyInSuspense, warnRedirectInAccess, warnRedirectInSuspense, warnSlowSlotWithoutSuspense, warnStaticRequestApi, warnSuspenseWrappingChildren };
|
|
2899
2899
|
|
|
2900
2900
|
//# sourceMappingURL=internal.js.map
|