@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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"debug-ECi_61pb.js","names":[],"sources":["../../src/server/debug.ts"],"sourcesContent":["/**\n * Runtime debug flag for timber.js.\n *\n * Two distinct functions for two distinct security levels:\n *\n * ## `isDebug()` — server-side logging only\n *\n * Returns true when timber's debug/warning messages should be written to\n * stderr / the server console. This NEVER affects what is sent to the\n * client (no error details, no timing headers, no stack traces).\n *\n * Active when any of:\n * - `NODE_ENV !== 'production'` (standard dev mode)\n * - `TIMBER_DEBUG` env var is set to a truthy value at runtime\n * - `timber.config.ts` has `debug: true`\n *\n * ## `isDevMode()` — client-visible dev behavior\n *\n * Returns true ONLY when `NODE_ENV !== 'production'`. This gates anything\n * that changes what clients can observe:\n * - Dev error pages with stack traces (fallback-error.ts)\n * - Detailed Server-Timing headers (pipeline.ts)\n * - Error messages in action INTERNAL_ERROR payloads (action-client.ts)\n * - Pipeline error handler wiring (Vite overlay)\n *\n * `isDevMode()` is statically replaced in production builds → the guarded\n * code is tree-shaken to zero bytes. TIMBER_DEBUG cannot enable it.\n *\n * Usage:\n * In Cloudflare Workers wrangler.toml:\n * [vars]\n * TIMBER_DEBUG = \"1\"\n *\n * In Node.js:\n * TIMBER_DEBUG=1 node server.js\n *\n * In timber.config.ts:\n * export default { debug: true }\n *\n * See design/13-security.md for the security taxonomy.\n * See design/18-build-system.md for build pipeline details.\n */\n\n// ─── Dev Mode (client-visible) ──────────────────────────────────────────────\n\n/**\n * Check if the application is running in development mode.\n *\n * This is the ONLY function that should gate client-visible dev behavior:\n * - Dev error pages with stack traces\n * - Server-Timing mode default (`'detailed'` in dev, `'total'` in prod)\n * - Error messages in action `INTERNAL_ERROR` payloads\n * - Pipeline error handler wiring (Vite overlay)\n *\n * Returns `process.env.NODE_ENV !== 'production'`, which is statically\n * replaced by the bundler in production builds. Code guarded by this\n * function is tree-shaken to zero bytes in production.\n *\n * TIMBER_DEBUG does NOT enable this — that would leak server internals\n * to clients. Use `isDebug()` for server-side-only logging.\n */\nexport function isDevMode(): boolean {\n return process.env.NODE_ENV !== 'production';\n}\n\n// ─── Debug Flag (server-side logging only) ──────────────────────────────────\n\n/**\n * Config-level debug override. Set via `setDebugFromConfig()` during\n * initialization when timber.config.ts has `debug: true`.\n */\nlet _configDebug = false;\n\n/**\n * Set the debug flag from timber.config.ts.\n * Called during handler initialization.\n */\nexport function setDebugFromConfig(debug: boolean): void {\n _configDebug = debug;\n}\n\n/**\n * Check if timber debug logging is active (server-side only).\n *\n * Returns true if ANY of these conditions hold:\n * - NODE_ENV is not 'production' (standard dev mode)\n * - TIMBER_DEBUG environment variable is set to a truthy value at runtime\n * - timber.config.ts has `debug: true`\n *\n * This function controls ONLY server-side logging — messages written to\n * stderr or the server console. It NEVER affects client-visible behavior\n * (error pages, response headers, action payloads). For client-visible\n * behavior, use `isDevMode()`.\n *\n * The TIMBER_DEBUG check is deliberately written as a dynamic property\n * access so bundlers cannot statically replace it.\n */\nexport function isDebug(): boolean {\n // Fast path: dev mode (statically replaced to `true` in dev, `false` in prod)\n if (process.env.NODE_ENV !== 'production') return true;\n\n // Config override\n if (_configDebug) return true;\n\n // Runtime env var check — uses dynamic access to prevent static replacement.\n // In production builds, process.env.NODE_ENV is statically replaced, but\n // TIMBER_DEBUG must survive as a runtime check. The dynamic key access\n // pattern ensures the bundler treats this as opaque.\n return _readTimberDebugEnv();\n}\n\n/**\n * Read TIMBER_DEBUG from the environment at runtime.\n *\n * Extracted to a separate function to:\n * 1. Prevent bundler inlining (cross-module function calls are not inlined)\n * 2. Handle platforms where `process` may not exist (Cloudflare Workers)\n * 3. Support globalThis.__TIMBER_DEBUG for programmatic control\n */\nfunction _readTimberDebugEnv(): boolean {\n // globalThis override — useful for programmatic control and testing\n if ((globalThis as Record<string, unknown>).__TIMBER_DEBUG) return true;\n\n // process.env — works in Node.js and platforms that polyfill process\n try {\n const key = 'TIMBER_DEBUG';\n const val =\n typeof process !== 'undefined' && process.env\n ? (process.env as Record<string, string | undefined>)[key]\n : undefined;\n if (val && val !== '0' && val !== 'false') return true;\n } catch {\n // process may not exist or env may throw — safe to ignore\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,YAAqB;AACnC,QAAA,QAAA,IAAA,aAAgC;;;;;;AASlC,IAAI,eAAe;;;;;;;;;;;;;;;;;AA0BnB,SAAgB,UAAmB;AAEjC,KAAA,QAAA,IAAA,aAA6B,aAAc,QAAO;AAGlD,KAAI,aAAc,QAAO;AAMzB,QAAO,qBAAqB;;;;;;;;;;AAW9B,SAAS,sBAA+B;AAEtC,KAAK,WAAuC,eAAgB,QAAO;AAGnE,KAAI;EAEF,MAAM,MACJ,OAAO,YAAY,eAAe,QAAQ,MACrC,QAAQ,IAHH,kBAIN,KAAA;AACN,MAAI,OAAO,QAAQ,OAAO,QAAQ,QAAS,QAAO;SAC5C;AAIR,QAAO"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-C77ScO0m.js","names":[],"sources":["../../src/segment-params/define.ts"],"sourcesContent":["/**\n * defineSegmentParams — factory for typed route param coercion.\n *\n * Creates a ParamsDefinition that coerces raw string params from the\n * URL into typed values. Used by exporting from params.ts (segment-level)\n * convention file.\n *\n * Reuses the shared Codec<T> protocol with Standard Schema auto-detection,\n * same pattern as defineSearchParams. Runtime constraints are stricter:\n * - serialize must return string (not null — path segments can't be omitted)\n * - parse throwing → 404 (invalid param value)\n *\n * Design doc: design/07a-route-params-triage.md\n */\n\nimport type { Codec } from '../codec.js';\nimport {\n type StandardSchemaV1,\n validateSync,\n isStandardSchema,\n isCodec,\n} from '../schema-bridge.js';\n\n// ---------------------------------------------------------------------------\n// Server-only ALS reference for .get()\n// ---------------------------------------------------------------------------\n\n// Same pattern as search-params: eagerly registered at server startup\n// to avoid dynamic imports that lose ALS context. See TIM-523.\nlet _getSegmentParamsFn: (() => Promise<Record<string, string | string[]>>) | undefined;\n\n/**\n * Register the getSegmentParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setGetSegmentParamsFn(fn: () => Promise<Record<string, string | string[]>>): void {\n _getSegmentParamsFn = fn;\n}\n\nfunction getSegmentParamsFromAls(): Promise<Record<string, string | string[]>> {\n if (!_getSegmentParamsFn) {\n throw new Error(\n '[timber] segmentParams.get() is only available on the server. ' +\n 'Use useSegmentParams() on the client.'\n );\n }\n return _getSegmentParamsFn();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from a Codec or StandardSchemaV1. */\nexport type InferParamField<V> =\n V extends Codec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineParams: a Codec or a Standard Schema. */\nexport type ParamField<T = unknown> = Codec<T> | StandardSchemaV1<T>;\n\nexport type { StandardSchemaV1 };\n\n/**\n * A typed route params definition.\n *\n * Returned by defineParams(). Provides parse (string → typed) and\n * serialize (typed → string) for each declared param.\n */\nexport interface ParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw string params into typed values. Throws on invalid values. */\n parse(raw: Record<string, string | string[]>): T;\n\n /** Serialize typed values back to strings for URL construction. */\n serialize(values: T): Record<string, string>;\n\n /**\n * Get typed segment params from the current request context (ALS).\n *\n * Server-only. Reads getSegmentParams() from ALS and coerces through\n * this definition's codecs, returning fully typed params.\n *\n * ```ts\n * // app/products/[id]/params.ts\n * export const segmentParams = defineSegmentParams({ id: z.coerce.number() })\n *\n * // app/products/[id]/page.tsx\n * import { segmentParams } from './params'\n * export default async function Page() {\n * const { id } = await segmentParams.get() // id: number\n * }\n * ```\n */\n get(): Promise<T>;\n\n /** Read-only codec map. */\n codecs: { [K in keyof T]: Codec<T[K]> };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a Standard Schema into a Codec for route params.\n *\n * Unlike fromSchema for search params:\n * - Parse throws on failure (no fallback to default)\n * - Serialize returns string (not null)\n */\nfunction fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {\n return {\n parse(value: string | string[] | undefined): T {\n // Route params are always strings (single segment) or string[] (catch-all)\n const input = Array.isArray(value) ? value : value;\n\n const result = validateSync(schema, input);\n if (!result.issues) {\n return result.value;\n }\n\n // For route params, parse failure means the param is invalid → throw\n const messages = result.issues.map((i) => i.message).join(', ');\n throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);\n },\n\n serialize(value: T): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n // Catch-all segments produce arrays — join with '/' for path reconstruction\n if (Array.isArray(value)) {\n return value.join('/');\n }\n return String(value);\n },\n };\n}\n\n/**\n * Resolve a field value to a Codec. Auto-detects Standard Schema objects.\n */\nfunction resolveField(fieldName: string, value: ParamField): Codec<unknown> {\n if (isCodec(value)) {\n return value;\n }\n\n if (isStandardSchema(value)) {\n return fromParamSchema(fieldName, value);\n }\n\n throw new Error(\n `[timber] defineSegmentParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that no codec's serialize returns null.\n * Route params are structural — they must produce a valid path segment.\n */\nfunction validateSerialize(codecMap: Record<string, Codec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n // Test serialize with a sample parsed value to check for null\n // We can't exhaustively test, but we can check that serialize(parse(\"test\"))\n // doesn't return null for a basic input.\n try {\n const testValue = codec.parse('test');\n const serialized = codec.serialize(testValue);\n if (serialized === null) {\n throw new Error(\n `[timber] defineSegmentParams: field '${key}' codec.serialize() returned null.\\n` +\n ` Route params are path segments — they cannot be omitted.\\n` +\n ` Ensure serialize() always returns a string.`\n );\n }\n } catch (e) {\n // parse('test') may throw for strict codecs (e.g., number-only).\n // That's fine — it means the codec validates. We only care about\n // serialize returning null, which we can't test without a valid value.\n if (e instanceof Error && e.message.includes('returned null')) {\n throw e;\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a ParamsDefinition from a map of codecs and/or Standard Schema objects.\n *\n * ```ts\n * // app/products/[id]/layout.tsx\n * import { defineSegmentParams } from '@timber-js/app/segment-params'\n * import { z } from 'zod/v4'\n *\n * export const segmentParams = defineSegmentParams({\n * id: z.coerce.number().int().positive(),\n * })\n * ```\n */\nexport function defineSegmentParams<C extends Record<string, ParamField>>(\n codecs: C\n): ParamsDefinition<{ [K in keyof C]: InferParamField<C[K]> }> {\n type T = { [K in keyof C]: InferParamField<C[K]> };\n\n const resolvedCodecs: Record<string, Codec<unknown>> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n resolvedCodecs[key] = resolveField(key, value as ParamField);\n }\n\n // Validate that serialize doesn't return null\n validateSerialize(resolvedCodecs);\n\n // ---- parse ----\n function parse(raw: Record<string, string | string[]>): T {\n const result: Record<string, unknown> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const rawValue = raw[key];\n // Route params are always present (the route matched)\n result[key] = codec.parse(rawValue);\n }\n\n return result as T;\n }\n\n // ---- serialize ----\n function serialize(values: T): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const [key, codec] of Object.entries(resolvedCodecs)) {\n const serialized = codec.serialize(values[key as keyof T] as unknown);\n if (serialized === null) {\n throw new Error(\n `[timber] params.serialize: field '${key}' serialized to null. ` +\n `Route params must produce a valid path segment.`\n );\n }\n result[key] = serialized;\n }\n\n return result;\n }\n\n // ---- get ----\n // ALS-backed: reads segment params from the current request context.\n // Server-only — throws on client.\n //\n // The pipeline already coerces params via coerceSegmentParams() which\n // calls parse() and stores typed values in ALS via setSegmentParams().\n // We return those directly instead of re-parsing, because codecs may\n // not be idempotent (e.g., a codec that only accepts raw strings would\n // throw if given an already-parsed value). See TIM-574.\n async function get(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] segmentParams.get() is server-only. ' + 'Use useSegmentParams() on the client.'\n );\n }\n const params = await getSegmentParamsFromAls();\n // params are already coerced by the pipeline — return as-is.\n return params as unknown as T;\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n get,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";;AA6BA,IAAI;;;;;;AAOJ,SAAgB,uBAAuB,IAA4D;AACjG,uBAAsB;;AAGxB,SAAS,0BAAsE;AAC7E,KAAI,CAAC,oBACH,OAAM,IAAI,MACR,sGAED;AAEH,QAAO,qBAAqB;;;;;;;;;AA+D9B,SAAS,gBAAmB,WAAmB,QAAuC;AACpF,QAAO;EACL,MAAM,OAAyC;GAI7C,MAAM,SAAS,aAAa,QAFd,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAEH;AAC1C,OAAI,CAAC,OAAO,OACV,QAAO,OAAO;GAIhB,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;AAC/D,SAAM,IAAI,MAAM,mBAAmB,UAAU,qBAAqB,WAAW;;EAG/E,UAAU,OAAyB;AACjC,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;AAGT,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,IAAI;AAExB,UAAO,OAAO,MAAM;;EAEvB;;;;;AAMH,SAAS,aAAa,WAAmB,OAAmC;AAC1E,KAAI,QAAQ,MAAM,CAChB,QAAO;AAGT,KAAI,iBAAiB,MAAM,CACzB,QAAO,gBAAgB,WAAW,MAAM;AAG1C,OAAM,IAAI,MACR,wCAAwC,UAAU,uJAGnD;;;;;;AAOH,SAAS,kBAAkB,UAAgD;AACzE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAIjD,KAAI;EACF,MAAM,YAAY,MAAM,MAAM,OAAO;AAErC,MADmB,MAAM,UAAU,UAAU,KAC1B,KACjB,OAAM,IAAI,MACR,wCAAwC,IAAI,+IAG7C;UAEI,GAAG;AAIV,MAAI,aAAa,SAAS,EAAE,QAAQ,SAAS,gBAAgB,CAC3D,OAAM;;;;;;;;;;;;;;;;AAuBd,SAAgB,oBACd,QAC6D;CAG7D,MAAM,iBAAiD,EAAE;AAEzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,gBAAe,OAAO,aAAa,KAAK,MAAoB;AAI9D,mBAAkB,eAAe;CAGjC,SAAS,MAAM,KAA2C;EACxD,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,WAAW,IAAI;AAErB,UAAO,OAAO,MAAM,MAAM,SAAS;;AAGrC,SAAO;;CAIT,SAAS,UAAU,QAAmC;EACpD,MAAM,SAAiC,EAAE;AAEzC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,eAAe,EAAE;GACzD,MAAM,aAAa,MAAM,UAAU,OAAO,KAA2B;AACrE,OAAI,eAAe,KACjB,OAAM,IAAI,MACR,qCAAqC,IAAI,uEAE1C;AAEH,UAAO,OAAO;;AAGhB,SAAO;;CAYT,eAAe,MAAkB;AAC/B,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,qFACD;AAIH,SAFe,MAAM,yBAAyB;;AAYhD,QAPwC;EACtC;EACA;EACA;EACA,QAAQ;EACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-Itxvcd7F.js","names":[],"sources":["../../src/search-params/define.ts"],"sourcesContent":["/**\n * defineSearchParams — factory for SearchParamsDefinition<T>.\n *\n * Creates a typed, composable definition for a route's search parameters.\n * Accepts both SearchParamCodec values and Standard Schema objects (Zod,\n * Valibot, ArkType) with auto-detection. Supports URL key aliasing via\n * withUrlKey(), default-omission serialization, and composition via\n * .extend() / .pick().\n *\n * Design doc: design/23-search-params.md §\"defineSearchParams — The Factory\"\n */\n\nimport { useQueryStates as clientUseQueryStates } from '../client/use-query-states.js';\nimport { fromSchema, isStandardSchema, isCodec } from '../schema-bridge.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\nimport type { Codec } from '../codec.js';\n\n// Server-only reference for .get() — avoids pulling server ALS into client bundles.\n// In client environments, .get() throws before reaching this code path.\n//\n// IMPORTANT: This is set eagerly via _setGetSearchParamsFn() at server startup\n// (called from request-context.ts module initialization). It must NOT use\n// dynamic `await import()` at call time because the async microtask from the\n// dynamic import loses AsyncLocalStorage context in React's RSC Flight renderer,\n// breaking getSearchParams() in parallel slot pages. See TIM-523.\nlet _getSearchParamsFn: (() => Promise<URLSearchParams>) | undefined;\n\n/**\n * Register the getSearchParams function. Called once at module load time\n * from request-context.ts to avoid dynamic import at call time.\n * @internal\n */\nexport function _setGetSearchParamsFn(fn: () => Promise<URLSearchParams>): void {\n _getSearchParamsFn = fn;\n}\n\nfunction getSearchParamsFromAls(): Promise<URLSearchParams> {\n if (!_getSearchParamsFn) {\n throw new Error(\n '[timber] searchParams.get() is only available on the server. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n return _getSearchParamsFn();\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A codec that converts between URL string values and typed values.\n *\n * nuqs parsers implement this interface natively — no adapter needed.\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected\n * by defineSearchParams and wrapped via fromSchema.\n */\nexport interface SearchParamCodec<T> extends Codec<T> {\n /** Optional URL key alias, set by withUrlKey(). */\n urlKey?: string;\n}\n\n/** A codec with a URL key alias attached via withUrlKey(). */\nexport interface SearchParamCodecWithUrlKey<T> extends SearchParamCodec<T> {\n urlKey: string;\n}\n\n/** Infer the output type of a codec. */\nexport type InferCodec<C> = C extends SearchParamCodec<infer T> ? T : never;\n\n/** Map of property names to codecs. */\nexport type CodecMap<T extends Record<string, unknown>> = {\n [K in keyof T]: SearchParamCodec<T[K]>;\n};\n\n/** Options for useQueryStates setter. */\nexport interface SetParamsOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/** Setter function returned by useQueryStates. */\nexport type SetParams<T> = (values: Partial<T>, options?: SetParamsOptions) => void;\n\n/** Options for useQueryStates hook. */\nexport interface QueryStatesOptions {\n /** Update URL without server roundtrip (default: false). */\n shallow?: boolean;\n /** Scroll to top after update (default: true). */\n scroll?: boolean;\n /** 'push' (default) or 'replace' for history state. */\n history?: 'push' | 'replace';\n}\n\n/**\n * A fully typed, composable search params definition.\n *\n * Returned by defineSearchParams(). Carries a phantom _type property\n * for build-time type extraction.\n */\nexport interface SearchParamsDefinition<T extends Record<string, unknown>> {\n /** Parse raw URL search params into typed values. */\n parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n /** Parse a Promise of URLSearchParams (e.g., from the ALS `searchParams()` API). */\n parse(raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>): Promise<T>;\n\n /**\n * Get typed search params from the current request context (ALS-backed).\n *\n * Server-only — reads getSearchParams() from ALS and parses through codecs.\n * Throws on client. Eliminates the naming conflict between the definition\n * export and the server helper.\n *\n * ```tsx\n * // app/products/page.tsx\n * import { searchParams } from './params'\n * export default async function Page() {\n * const { page, category } = await searchParams.get()\n * }\n * ```\n */\n get(): Promise<T>;\n\n /** Client hook — reads current URL params and returns typed values + setter. */\n useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>];\n\n /** Extend with additional codecs or Standard Schema objects. */\n extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n codecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }>;\n\n /** Pick a subset of keys. Preserves codecs and aliases. */\n pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>>;\n\n /** Serialize values to a query string (no leading '?'), omitting defaults. */\n serialize(values: Partial<T>): string;\n\n /** Build a full path with query string, omitting defaults. */\n href(pathname: string, values: Partial<T>): string;\n\n /** Build a URLSearchParams instance, omitting defaults. */\n toSearchParams(values: Partial<T>): URLSearchParams;\n\n /** Read-only codec map for spreading into .extend(). */\n codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n\n /** Read-only URL key alias map. Maps property names to URL query parameter keys. */\n readonly urlKeys: Readonly<Record<string, string>>;\n\n /**\n * Phantom property for build-time type extraction.\n * Never set at runtime — exists only in the type system.\n */\n readonly _type?: T;\n}\n\n// StandardSchemaV1 is imported from schema-bridge.ts — single source of truth.\n// Re-export for consumers that import it from this module.\nexport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n// ---------------------------------------------------------------------------\n// Type-level helpers\n// ---------------------------------------------------------------------------\n\n/** Infer the output type from either a SearchParamCodec or a StandardSchemaV1. */\nexport type InferField<V> =\n V extends SearchParamCodec<infer T> ? T : V extends StandardSchemaV1<infer T> ? T : never;\n\n/** Acceptable field value for defineSearchParams: a codec or a Standard Schema. */\nexport type SearchParamField<T = unknown> = SearchParamCodec<T> | StandardSchemaV1<T>;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert URLSearchParams or a plain record to a normalized record\n * where repeated keys produce arrays.\n */\nfunction normalizeRaw(\n raw: URLSearchParams | Record<string, string | string[] | undefined>\n): Record<string, string | string[] | undefined> {\n if (raw instanceof URLSearchParams) {\n const result: Record<string, string | string[] | undefined> = {};\n for (const key of new Set(raw.keys())) {\n const values = raw.getAll(key);\n result[key] = values.length === 1 ? values[0] : values;\n }\n return result;\n }\n return raw;\n}\n\n/**\n * Compute the serialized default value for a codec. Used for\n * default-omission: when serialize(value) === serialize(parse(undefined)),\n * the field is omitted from the URL.\n */\nfunction getDefaultSerialized<T>(codec: SearchParamCodec<T>): string | null {\n return codec.serialize(codec.parse(undefined));\n}\n\n// isStandardSchema and isCodec are imported from schema-bridge.ts.\n\n/**\n * Resolve a field value to a SearchParamCodec. Auto-detects Standard Schema\n * objects and wraps them with fromSchema. Reads .urlKey from codecs.\n */\nfunction resolveField(\n fieldName: string,\n value: SearchParamField\n): { codec: SearchParamCodec<unknown>; urlKey?: string } {\n // Check for codec first (codecs may also have '~standard' if they're nuqs parsers)\n if (isCodec(value)) {\n return { codec: value, urlKey: value.urlKey };\n }\n\n // Auto-detect Standard Schema\n if (isStandardSchema(value)) {\n return { codec: fromSchema(value) };\n }\n\n throw new Error(\n `[timber] defineSearchParams: field '${fieldName}' is not a valid codec or Standard Schema. ` +\n `Expected an object with { parse, serialize } methods, or a Standard Schema object ` +\n `(Zod, Valibot, ArkType).`\n );\n}\n\n/**\n * Validate that all codecs handle absent params (parse(undefined) doesn't throw).\n * Catches schemas that throw on missing input. `undefined` and `null` are both\n * valid defaults — `undefined` is correct for optional fields (e.g., `z.string().optional()`).\n */\nfunction validateDefaults(codecMap: Record<string, SearchParamCodec<unknown>>): void {\n for (const [key, codec] of Object.entries(codecMap)) {\n try {\n codec.parse(undefined);\n } catch {\n throw new Error(\n `[timber] defineSearchParams: field '${key}' throws when the param is absent.\\n` +\n ` Search params are optional — the URL might not contain ?${key}=anything.\\n` +\n ` Add .default() or .optional() to your schema, or wrap with withDefault().`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a SearchParamsDefinition from a map of codecs and/or Standard Schema\n * objects. Accepts both SearchParamCodec values and raw Zod/Valibot/ArkType\n * schemas with auto-detection.\n *\n * ```ts\n * import { defineSearchParams, withDefault, withUrlKey } from '@timber-js/app/search-params'\n * import { parseAsString, parseAsStringEnum } from 'nuqs'\n * import { z } from 'zod/v4'\n *\n * export const searchParams = defineSearchParams({\n * page: z.coerce.number().int().min(1).default(1), // Standard Schema — auto-wrapped\n * q: withUrlKey(parseAsString, 'search'), // nuqs codec with URL alias\n * sort: withDefault(parseAsStringEnum(['price', 'name']), 'price'),\n * })\n * ```\n */\nexport function defineSearchParams<C extends Record<string, SearchParamField>>(\n codecs: C\n): SearchParamsDefinition<{ [K in keyof C]: InferField<C[K]> }> {\n type T = { [K in keyof C]: InferField<C[K]> };\n\n const resolvedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const urlKeys: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(codecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n urlKeys[key] = resolved.urlKey;\n }\n }\n\n // Validate that all codecs handle absent params\n validateDefaults(resolvedCodecs);\n\n return buildDefinition<T>(resolvedCodecs as unknown as CodecMap<T>, urlKeys);\n}\n\n// ---------------------------------------------------------------------------\n// Internal: build the definition object\n// ---------------------------------------------------------------------------\n\n/**\n * Internal: build a SearchParamsDefinition from a typed codec map and url keys.\n */\nfunction buildDefinition<T extends Record<string, unknown>>(\n codecMap: CodecMap<T>,\n urlKeys: Record<string, string>\n): SearchParamsDefinition<T> {\n // Pre-compute default serialized values for omission check\n const defaultSerialized: Record<string, string | null> = {};\n for (const key of Object.keys(codecMap)) {\n defaultSerialized[key] = getDefaultSerialized(codecMap[key as keyof T]);\n }\n\n function getUrlKey(prop: string): string {\n return urlKeys[prop] ?? prop;\n }\n\n // ---- parse ----\n function parseSync(raw: URLSearchParams | Record<string, string | string[] | undefined>): T {\n const normalized = normalizeRaw(raw);\n const result: Record<string, unknown> = {};\n\n for (const prop of Object.keys(codecMap)) {\n const urlKey = getUrlKey(prop);\n const rawValue = normalized[urlKey];\n result[prop] = (codecMap[prop as keyof T] as SearchParamCodec<unknown>).parse(rawValue);\n }\n\n return result as T;\n }\n\n // Overloaded parse: sync when given raw params, async when given a Promise.\n // This enables the ergonomic pattern: await def.parse(searchParams())\n function parse(raw: URLSearchParams | Record<string, string | string[] | undefined>): T;\n function parse(\n raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): Promise<T>;\n function parse(\n raw:\n | URLSearchParams\n | Record<string, string | string[] | undefined>\n | Promise<URLSearchParams | Record<string, string | string[] | undefined>>\n ): T | Promise<T> {\n if (raw instanceof Promise) {\n return raw.then(parseSync);\n }\n return parseSync(raw);\n }\n\n // ---- serialize ----\n function serialize(values: Partial<T>): string {\n const parts: string[] = [];\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n // Omit if serialized value matches the default\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n parts.push(`${encodeURIComponent(getUrlKey(prop))}=${encodeURIComponent(serialized)}`);\n }\n\n return parts.join('&');\n }\n\n // ---- href ----\n function href(pathname: string, values: Partial<T>): string {\n const qs = serialize(values);\n return qs ? `${pathname}?${qs}` : pathname;\n }\n\n // ---- toSearchParams ----\n function toSearchParams(values: Partial<T>): URLSearchParams {\n const usp = new URLSearchParams();\n\n for (const prop of Object.keys(codecMap)) {\n if (!(prop in values)) continue;\n const codec = codecMap[prop as keyof T] as SearchParamCodec<unknown>;\n const serialized = codec.serialize(values[prop as keyof T] as unknown);\n\n if (serialized === defaultSerialized[prop]) continue;\n if (serialized === null) continue;\n\n usp.set(getUrlKey(prop), serialized);\n }\n\n return usp;\n }\n\n // ---- extend ----\n function extend<U extends Record<string, SearchParamCodec<unknown> | StandardSchemaV1<unknown>>>(\n newCodecs: U\n ): SearchParamsDefinition<T & { [K in keyof U]: InferField<U[K]> }> {\n type Combined = T & { [K in keyof U]: InferField<U[K]> };\n\n // Resolve any Standard Schema objects in the extension\n const resolvedNewCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const newUrlKeys: Record<string, string> = {};\n for (const [key, value] of Object.entries(newCodecs)) {\n const resolved = resolveField(key, value as SearchParamField);\n resolvedNewCodecs[key] = resolved.codec;\n if (resolved.urlKey) {\n newUrlKeys[key] = resolved.urlKey;\n }\n }\n\n const combinedCodecs = {\n ...codecMap,\n ...resolvedNewCodecs,\n } as unknown as CodecMap<Combined>;\n\n // Merge URL keys: base keys + new codec urlKeys from withUrlKey\n const combinedUrlKeys: Record<string, string> = { ...urlKeys, ...newUrlKeys };\n\n return buildDefinition<Combined>(combinedCodecs, combinedUrlKeys);\n }\n\n // ---- pick ----\n function pick<K extends keyof T & string>(...keys: K[]): SearchParamsDefinition<Pick<T, K>> {\n const pickedCodecs: Record<string, SearchParamCodec<unknown>> = {};\n const pickedUrlKeys: Record<string, string> = {};\n\n for (const key of keys) {\n pickedCodecs[key] = codecMap[key] as SearchParamCodec<unknown>;\n if (key in urlKeys) {\n pickedUrlKeys[key] = urlKeys[key];\n }\n }\n\n return buildDefinition<Pick<T, K>>(\n pickedCodecs as unknown as CodecMap<Pick<T, K>>,\n pickedUrlKeys\n );\n }\n\n // ---- useQueryStates ----\n // Delegates to the 'use client' implementation from use-query-states.ts.\n //\n // In the RSC environment: use-query-states.ts is transformed by the RSC\n // plugin into a client reference proxy. Calling it throws — correct,\n // because hooks can't run during server component rendering.\n // In SSR: use-query-states.ts is the real nuqs-backed function. Hooks\n // work during SSR's renderToReadableStream, so this works correctly.\n // On the client: same as SSR — the real function is available.\n function useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>] {\n return clientUseQueryStates(codecMap, options, Object.freeze({ ...urlKeys })) as [\n T,\n SetParams<T>,\n ];\n }\n\n // ---- get ----\n // ALS-backed: reads getSearchParams() from the current request context\n // and parses through codecs. Server-only — throws on client.\n async function get(): Promise<T> {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] searchParams.get() is server-only. ' +\n 'Use searchParams.useQueryStates() on the client.'\n );\n }\n const raw = await getSearchParamsFromAls();\n return parseSync(raw);\n }\n\n const definition: SearchParamsDefinition<T> = {\n parse,\n get,\n useQueryStates,\n extend,\n pick,\n serialize,\n href,\n toSearchParams,\n codecs: codecMap,\n urlKeys: Object.freeze({ ...urlKeys }),\n };\n\n return definition;\n}\n"],"mappings":"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"}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
//#region src/cookies/define-cookie.ts
|
|
2
|
-
var _useCookieModule;
|
|
3
|
-
function getUseCookieModule() {
|
|
4
|
-
if (!_useCookieModule) throw new Error("[timber] defineCookie().useCookie() requires @timber-js/app/client to be loaded. This hook can only be used in client components.");
|
|
5
|
-
return _useCookieModule;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Register the client cookie module. Called by the client entry to wire
|
|
9
|
-
* up the lazy reference without a top-level import.
|
|
10
|
-
*
|
|
11
|
-
* @internal — framework use only
|
|
12
|
-
*/
|
|
13
|
-
function _registerUseCookieModule(mod) {
|
|
14
|
-
_useCookieModule = mod;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Define a typed cookie.
|
|
18
|
-
*
|
|
19
|
-
* ```ts
|
|
20
|
-
* import { defineCookie } from '@timber-js/app/cookies';
|
|
21
|
-
* import { fromSchema } from '@timber-js/app/codec';
|
|
22
|
-
* import { z } from 'zod/v4';
|
|
23
|
-
*
|
|
24
|
-
* export const themeCookie = defineCookie('theme', {
|
|
25
|
-
* codec: fromSchema(z.enum(['light', 'dark', 'system']).default('system')),
|
|
26
|
-
* httpOnly: false,
|
|
27
|
-
* maxAge: 60 * 60 * 24 * 365,
|
|
28
|
-
* });
|
|
29
|
-
*
|
|
30
|
-
* // Server
|
|
31
|
-
* const theme = await themeCookie.getCookie();
|
|
32
|
-
* await themeCookie.setCookie('dark');
|
|
33
|
-
*
|
|
34
|
-
* // Client
|
|
35
|
-
* const [theme, setTheme] = themeCookie.useCookie();
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
function defineCookie(name, options) {
|
|
39
|
-
const { codec, ...cookieOpts } = options;
|
|
40
|
-
const resolvedOptions = { ...cookieOpts };
|
|
41
|
-
return {
|
|
42
|
-
name,
|
|
43
|
-
options: resolvedOptions,
|
|
44
|
-
codec,
|
|
45
|
-
async getCookie() {
|
|
46
|
-
const { getCookies } = await import("./request-context-CK5tZqIP.js").then((n) => n.d);
|
|
47
|
-
const raw = (await getCookies()).get(name);
|
|
48
|
-
return codec.parse(raw);
|
|
49
|
-
},
|
|
50
|
-
async setCookie(value) {
|
|
51
|
-
const { getCookies } = await import("./request-context-CK5tZqIP.js").then((n) => n.d);
|
|
52
|
-
const jar = await getCookies();
|
|
53
|
-
const serialized = codec.serialize(value);
|
|
54
|
-
if (serialized === null) jar.delete(name, {
|
|
55
|
-
path: resolvedOptions.path,
|
|
56
|
-
domain: resolvedOptions.domain
|
|
57
|
-
});
|
|
58
|
-
else jar.set(name, serialized, resolvedOptions);
|
|
59
|
-
},
|
|
60
|
-
async deleteCookie() {
|
|
61
|
-
const { getCookies } = await import("./request-context-CK5tZqIP.js").then((n) => n.d);
|
|
62
|
-
(await getCookies()).delete(name, {
|
|
63
|
-
path: resolvedOptions.path,
|
|
64
|
-
domain: resolvedOptions.domain
|
|
65
|
-
});
|
|
66
|
-
},
|
|
67
|
-
useCookie() {
|
|
68
|
-
const { useCookie: useRawCookie } = getUseCookieModule();
|
|
69
|
-
const [raw, setRaw, deleteRaw] = useRawCookie(name, {
|
|
70
|
-
path: resolvedOptions.path,
|
|
71
|
-
domain: resolvedOptions.domain,
|
|
72
|
-
maxAge: resolvedOptions.maxAge,
|
|
73
|
-
expires: resolvedOptions.expires,
|
|
74
|
-
sameSite: resolvedOptions.sameSite,
|
|
75
|
-
secure: resolvedOptions.secure
|
|
76
|
-
});
|
|
77
|
-
const parsed = codec.parse(raw);
|
|
78
|
-
const setTyped = (value) => {
|
|
79
|
-
const serialized = codec.serialize(value);
|
|
80
|
-
if (serialized === null) deleteRaw();
|
|
81
|
-
else setRaw(serialized);
|
|
82
|
-
};
|
|
83
|
-
return [
|
|
84
|
-
parsed,
|
|
85
|
-
setTyped,
|
|
86
|
-
deleteRaw
|
|
87
|
-
];
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
//#endregion
|
|
92
|
-
export { defineCookie as n, _registerUseCookieModule as t };
|
|
93
|
-
|
|
94
|
-
//# sourceMappingURL=define-cookie-BowvzoP0.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-cookie-BowvzoP0.js","names":[],"sources":["../../src/cookies/define-cookie.ts"],"sourcesContent":["/**\n * defineCookie — typed cookie definitions.\n *\n * Bundles name + codec + options into a reusable CookieDefinition<T>\n * with async .getCookie(), .setCookie(), .deleteCookie() server methods\n * and a sync .useCookie() client hook.\n *\n * Server methods are async to future-proof the API for v2 features\n * (signed cookies via crypto.subtle, encrypted cookies, external stores).\n *\n * Reuses the SearchParamCodec protocol via fromSchema() bridge.\n * Validation on read returns the codec default (never throws).\n *\n * IMPORTANT: This module must NOT have top-level value imports from either\n * server or client modules. Server methods lazy-import request-context;\n * useCookie() lazy-imports use-cookie. This ensures:\n * - Client bundles don't pull in ALS/server code\n * - RSC bundles don't pull in useSyncExternalStore/client code\n * - Tree-shaking is not required for correctness\n *\n * See design/29-cookies.md §\"Typed Cookies with Schema Validation\"\n */\n\nimport type { CookieOptions } from '../server/request-context.js';\nimport type { ClientCookieOptions } from '../client/use-cookie.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nimport type { Codec } from '../codec.js';\n\n/**\n * A codec that converts between string cookie values and typed values.\n * Type alias for the shared Codec<T> protocol.\n */\nexport type CookieCodec<T> = Codec<T>;\n\n/** Options for defineCookie: codec + CookieOptions merged. */\nexport interface DefineCookieOptions<T> extends CookieOptions {\n /** Codec for parsing/serializing the cookie value. */\n codec: CookieCodec<T>;\n}\n\n/** A fully typed cookie definition with server and client methods. */\nexport interface CookieDefinition<T> {\n /** The cookie name. */\n readonly name: string;\n /** The resolved cookie options (without codec). */\n readonly options: CookieOptions;\n /** The codec used for parsing/serializing. */\n readonly codec: CookieCodec<T>;\n\n /** Server: read the typed value from the current request. */\n getCookie(): Promise<T>;\n /** Server: set the typed value on the response. */\n setCookie(value: T): Promise<void>;\n /** Server: delete the cookie. */\n deleteCookie(): Promise<void>;\n\n /** Client: React hook for reading/writing this cookie. Returns [value, setter, deleter]. */\n useCookie(): [T, (value: T) => void, () => void];\n}\n\n// ─── Lazy Module References ───────────────────────────────────────────────\n//\n// These are resolved on first use, not at module load time. This prevents\n// the server module graph from pulling in client code and vice versa.\n// The dynamic import() in server methods is natural (they're async).\n// For useCookie() (sync), we cache the module reference after first load.\n\nlet _useCookieModule: typeof import('../client/use-cookie.js') | undefined;\n\nfunction getUseCookieModule(): typeof import('../client/use-cookie.js') {\n if (!_useCookieModule) {\n // In the client/SSR environment, this module is already in the module\n // graph (imported by the client entry). The throw is a safeguard —\n // if useCookie() is somehow called before the module is available,\n // the developer gets a clear error instead of a silent failure.\n throw new Error(\n '[timber] defineCookie().useCookie() requires @timber-js/app/client to be loaded. ' +\n 'This hook can only be used in client components.'\n );\n }\n return _useCookieModule;\n}\n\n/**\n * Register the client cookie module. Called by the client entry to wire\n * up the lazy reference without a top-level import.\n *\n * @internal — framework use only\n */\nexport function _registerUseCookieModule(mod: typeof import('../client/use-cookie.js')): void {\n _useCookieModule = mod;\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────\n\n/**\n * Define a typed cookie.\n *\n * ```ts\n * import { defineCookie } from '@timber-js/app/cookies';\n * import { fromSchema } from '@timber-js/app/codec';\n * import { z } from 'zod/v4';\n *\n * export const themeCookie = defineCookie('theme', {\n * codec: fromSchema(z.enum(['light', 'dark', 'system']).default('system')),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * // Server\n * const theme = await themeCookie.getCookie();\n * await themeCookie.setCookie('dark');\n *\n * // Client\n * const [theme, setTheme] = themeCookie.useCookie();\n * ```\n */\nexport function defineCookie<T>(\n name: string,\n options: DefineCookieOptions<T>\n): CookieDefinition<T> {\n const { codec, ...cookieOpts } = options;\n const resolvedOptions: CookieOptions = { ...cookieOpts };\n\n return {\n name,\n options: resolvedOptions,\n codec,\n\n async getCookie(): Promise<T> {\n const { getCookies } = await import('../server/request-context.js');\n const jar = await getCookies();\n const raw = jar.get(name);\n return codec.parse(raw);\n },\n\n async setCookie(value: T): Promise<void> {\n const { getCookies } = await import('../server/request-context.js');\n const jar = await getCookies();\n const serialized = codec.serialize(value);\n if (serialized === null) {\n jar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n } else {\n jar.set(name, serialized, resolvedOptions);\n }\n },\n\n async deleteCookie(): Promise<void> {\n const { getCookies } = await import('../server/request-context.js');\n const jar = await getCookies();\n jar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n },\n\n useCookie(): [T, (value: T) => void, () => void] {\n const { useCookie: useRawCookie } = getUseCookieModule();\n\n // Extract client-safe options (no httpOnly — client cookies can't be httpOnly)\n const clientOpts: ClientCookieOptions = {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n maxAge: resolvedOptions.maxAge,\n expires: resolvedOptions.expires,\n sameSite: resolvedOptions.sameSite,\n secure: resolvedOptions.secure,\n };\n\n const [raw, setRaw, deleteRaw] = useRawCookie(name, clientOpts);\n const parsed = codec.parse(raw);\n\n const setTyped = (value: T): void => {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteRaw();\n } else {\n setRaw(serialized);\n }\n };\n\n return [parsed, setTyped, deleteRaw];\n },\n };\n}\n"],"mappings":";AAqEA,IAAI;AAEJ,SAAS,qBAA+D;AACtE,KAAI,CAAC,iBAKH,OAAM,IAAI,MACR,oIAED;AAEH,QAAO;;;;;;;;AAST,SAAgB,yBAAyB,KAAqD;AAC5F,oBAAmB;;;;;;;;;;;;;;;;;;;;;;;;AA2BrB,SAAgB,aACd,MACA,SACqB;CACrB,MAAM,EAAE,OAAO,GAAG,eAAe;CACjC,MAAM,kBAAiC,EAAE,GAAG,YAAY;AAExD,QAAO;EACL;EACA,SAAS;EACT;EAEA,MAAM,YAAwB;GAC5B,MAAM,EAAE,eAAe,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,EAAA;GAEpC,MAAM,OADM,MAAM,YAAY,EACd,IAAI,KAAK;AACzB,UAAO,MAAM,MAAM,IAAI;;EAGzB,MAAM,UAAU,OAAyB;GACvC,MAAM,EAAE,eAAe,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,EAAA;GACpC,MAAM,MAAM,MAAM,YAAY;GAC9B,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,OAAI,eAAe,KACjB,KAAI,OAAO,MAAM;IACf,MAAM,gBAAgB;IACtB,QAAQ,gBAAgB;IACzB,CAAC;OAEF,KAAI,IAAI,MAAM,YAAY,gBAAgB;;EAI9C,MAAM,eAA8B;GAClC,MAAM,EAAE,eAAe,MAAM,OAAO,iCAAA,MAAA,MAAA,EAAA,EAAA;AAEpC,IADY,MAAM,YAAY,EAC1B,OAAO,MAAM;IACf,MAAM,gBAAgB;IACtB,QAAQ,gBAAgB;IACzB,CAAC;;EAGJ,YAAiD;GAC/C,MAAM,EAAE,WAAW,iBAAiB,oBAAoB;GAYxD,MAAM,CAAC,KAAK,QAAQ,aAAa,aAAa,MATN;IACtC,MAAM,gBAAgB;IACtB,QAAQ,gBAAgB;IACxB,QAAQ,gBAAgB;IACxB,SAAS,gBAAgB;IACzB,UAAU,gBAAgB;IAC1B,QAAQ,gBAAgB;IACzB,CAE8D;GAC/D,MAAM,SAAS,MAAM,MAAM,IAAI;GAE/B,MAAM,YAAY,UAAmB;IACnC,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,QAAI,eAAe,KACjB,YAAW;QAEX,QAAO,WAAW;;AAItB,UAAO;IAAC;IAAQ;IAAU;IAAU;;EAEvC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dev-warnings-DpGRGoDi.js","names":[],"sources":["../../src/server/dev-warnings.ts"],"sourcesContent":["/**\n * Dev-mode warnings for common timber.js misuse patterns.\n *\n * These fire in development only and are stripped from production builds.\n * Each warning targets a specific misuse identified during design review.\n *\n * Warnings are deduplicated by warningId:filePath:line so the same warning\n * is only emitted once per dev session (per unique source location).\n *\n * Warnings are written to stderr and, when a Vite dev server is available,\n * forwarded to the browser console via Vite's WebSocket.\n *\n * See design/21-dev-server.md §\"Dev-Mode Warnings\"\n * See design/11-platform.md §\"Dev Mode\"\n */\n\nimport type { ViteDevServer } from 'vite';\nimport { isDebug } from './debug.js';\n\n// ─── Warning IDs ───────────────────────────────────────────────────────────\n\nexport const WarningId = {\n SUSPENSE_WRAPS_CHILDREN: 'SUSPENSE_WRAPS_CHILDREN',\n DENY_IN_SUSPENSE: 'DENY_IN_SUSPENSE',\n REDIRECT_IN_SUSPENSE: 'REDIRECT_IN_SUSPENSE',\n REDIRECT_IN_ACCESS: 'REDIRECT_IN_ACCESS',\n STATIC_REQUEST_API: 'STATIC_REQUEST_API',\n SLOW_SLOT_NO_SUSPENSE: 'SLOW_SLOT_NO_SUSPENSE',\n} as const;\n\nexport type WarningId = (typeof WarningId)[keyof typeof WarningId];\n\n// ─── Configuration ──────────────────────────────────────────────────────────\n\n/** Configuration for dev warning behavior. */\nexport interface DevWarningConfig {\n /** Threshold in ms for \"slow slot\" warnings. Default: 200. */\n slowSlotThresholdMs?: number;\n}\n\n// ─── Deduplication & Server ─────────────────────────────────────────────────\n\nconst _emitted = new Set<string>();\n\n/** Vite dev server for forwarding warnings to browser console. */\nlet _viteServer: ViteDevServer | null = null;\n\n/**\n * Register the Vite dev server for browser console forwarding.\n * Called by timber-dev-server during configureServer.\n */\nexport function setViteServer(server: ViteDevServer | null): void {\n _viteServer = server;\n}\n\nfunction isDev(): boolean {\n return isDebug();\n}\n\n/**\n * Emit a warning only once per dedup key.\n *\n * Writes to stderr and forwards to browser console via Vite WebSocket.\n * Returns true if emitted (not deduplicated).\n */\nfunction emitOnce(\n warningId: WarningId,\n location: string,\n level: 'warn' | 'error',\n message: string\n): boolean {\n if (!isDev()) return false;\n\n const dedupKey = `${warningId}:${location}`;\n if (_emitted.has(dedupKey)) return false;\n _emitted.add(dedupKey);\n\n // Write to stderr\n const prefix = level === 'error' ? '\\x1b[31m[timber]\\x1b[0m' : '\\x1b[33m[timber]\\x1b[0m';\n process.stderr.write(`${prefix} ${message}\\n`);\n\n // Forward to browser console via Vite WebSocket\n if (_viteServer?.hot) {\n _viteServer.hot.send('timber:dev-warning', {\n warningId,\n level,\n message: `[timber] ${message}`,\n });\n }\n\n return true;\n}\n\n// ─── Warning Functions ──────────────────────────────────────────────────────\n\n/**\n * Warn when a layout wraps {children} in <Suspense>.\n *\n * This defers the page content — the primary resource — behind a fallback.\n * The page's data fetches won't affect the HTTP status code because they\n * resolve after onShellReady. If the page calls deny(404), the status code\n * is already committed as 200.\n *\n * @param layoutFile - Relative path to the layout file (e.g., \"app/(dashboard)/layout.tsx\")\n */\nexport function warnSuspenseWrappingChildren(layoutFile: string): void {\n emitOnce(\n WarningId.SUSPENSE_WRAPS_CHILDREN,\n layoutFile,\n 'warn',\n `Layout at ${layoutFile} wraps {children} in <Suspense>. ` +\n 'This prevents child pages from setting HTTP status codes. ' +\n 'Use usePendingNavigation() for loading states instead.'\n );\n}\n\n/**\n * Warn when deny() is called inside a Suspense boundary.\n *\n * After the shell has flushed and the status code is committed, deny()\n * cannot change the HTTP response. The signal will be caught by the nearest\n * error boundary instead of producing a correct status code.\n *\n * @param file - Relative path to the file\n * @param line - Line number where deny() was called\n */\nexport function warnDenyInSuspense(file: string, line?: number): void {\n const location = line ? `${file}:${line}` : file;\n emitOnce(\n WarningId.DENY_IN_SUSPENSE,\n location,\n 'error',\n `deny() called inside <Suspense> at ${location}. ` +\n 'The HTTP status is already committed — this will trigger an error boundary with a 200 status. ' +\n 'Move deny() outside <Suspense> for correct HTTP semantics.'\n );\n}\n\n/**\n * Warn when redirect() is called inside a Suspense boundary.\n *\n * This will perform a client-side navigation instead of an HTTP redirect.\n *\n * @param file - Relative path to the file\n * @param line - Line number where redirect() was called\n */\nexport function warnRedirectInSuspense(file: string, line?: number): void {\n const location = line ? `${file}:${line}` : file;\n emitOnce(\n WarningId.REDIRECT_IN_SUSPENSE,\n location,\n 'error',\n `redirect() called inside <Suspense> at ${location}. ` +\n 'This will perform a client-side navigation instead of an HTTP redirect.'\n );\n}\n\n/**\n * Warn when redirect() is called in a slot's access.ts.\n *\n * Slots use deny() for graceful degradation. Redirecting from a slot would\n * redirect the entire page, breaking the contract that slot failure is\n * isolated to the slot.\n *\n * @param accessFile - Relative path to the access.ts file\n * @param line - Line number where redirect() was called\n */\nexport function warnRedirectInAccess(accessFile: string, line?: number): void {\n const location = line ? `${accessFile}:${line}` : accessFile;\n emitOnce(\n WarningId.REDIRECT_IN_ACCESS,\n location,\n 'error',\n `redirect() called in access.ts at ${location}. ` +\n 'Only deny() is valid in slot access checks. ' +\n 'Use deny() to block access or move redirect() to middleware.ts.'\n );\n}\n\n/**\n * Warn when getCookies() or getHeaders() is called during a static build.\n *\n * In output: 'static' mode, there is no per-request context — these APIs\n * read build-time values only. This is almost always a mistake.\n *\n * @param api - The dynamic API name (\"cookies\" or \"headers\")\n * @param file - Relative path to the file calling the API\n */\nexport function warnStaticRequestApi(api: 'cookies' | 'headers', file: string): void {\n emitOnce(\n WarningId.STATIC_REQUEST_API,\n `${api}:${file}`,\n 'error',\n `${api}() called during static generation of ${file}. ` +\n 'Dynamic request APIs are not available during prerendering.'\n );\n}\n\n// NOTE: warnCacheRequestProps removed — 'use cache' directive is a future feature.\n// See design/06-caching.md.\n\n/**\n * Warn when a parallel slot resolves slowly without a <Suspense> wrapper.\n *\n * A slow slot without Suspense blocks onShellReady — and therefore the\n * status code commit — for the entire page. Wrapping it in <Suspense>\n * lets the shell flush without waiting for the slot.\n *\n * @param slotName - The slot name (e.g., \"@admin\")\n * @param durationMs - How long the slot took to resolve\n */\nexport function warnSlowSlotWithoutSuspense(slotName: string, durationMs: number): void {\n emitOnce(\n WarningId.SLOW_SLOT_NO_SUSPENSE,\n slotName,\n 'warn',\n `Slot ${slotName} resolved in ${durationMs}ms and is not wrapped in <Suspense>. ` +\n 'Consider wrapping to avoid blocking the flush.'\n );\n}\n\n// ─── Testing ────────────────────────────────────────────────────────────────\n\n/**\n * Reset emitted warnings. For testing only.\n * @internal\n */\nexport function _resetWarnings(): void {\n _emitted.clear();\n}\n\n/**\n * Get the set of emitted dedup keys. For testing only.\n * @internal\n */\nexport function _getEmitted(): ReadonlySet<string> {\n return _emitted;\n}\n"],"mappings":";;AAqBA,IAAa,YAAY;CACvB,yBAAyB;CACzB,kBAAkB;CAClB,sBAAsB;CACtB,oBAAoB;CACpB,oBAAoB;CACpB,uBAAuB;CACxB;AAcD,IAAM,2BAAW,IAAI,KAAa;;AAGlC,IAAI,cAAoC;;;;;AAMxC,SAAgB,cAAc,QAAoC;AAChE,eAAc;;AAGhB,SAAS,QAAiB;AACxB,QAAO,SAAS;;;;;;;;AASlB,SAAS,SACP,WACA,UACA,OACA,SACS;AACT,KAAI,CAAC,OAAO,CAAE,QAAO;CAErB,MAAM,WAAW,GAAG,UAAU,GAAG;AACjC,KAAI,SAAS,IAAI,SAAS,CAAE,QAAO;AACnC,UAAS,IAAI,SAAS;CAGtB,MAAM,SAAS,UAAU,UAAU,4BAA4B;AAC/D,SAAQ,OAAO,MAAM,GAAG,OAAO,GAAG,QAAQ,IAAI;AAG9C,KAAI,aAAa,IACf,aAAY,IAAI,KAAK,sBAAsB;EACzC;EACA;EACA,SAAS,YAAY;EACtB,CAAC;AAGJ,QAAO;;;;;;;;;;;;AAeT,SAAgB,6BAA6B,YAA0B;AACrE,UACE,UAAU,yBACV,YACA,QACA,aAAa,WAAW,mJAGzB;;;;;;;;;;;;AAaH,SAAgB,mBAAmB,MAAc,MAAqB;CACpE,MAAM,WAAW,OAAO,GAAG,KAAK,GAAG,SAAS;AAC5C,UACE,UAAU,kBACV,UACA,SACA,sCAAsC,SAAS,4JAGhD;;;;;;;;;;AAWH,SAAgB,uBAAuB,MAAc,MAAqB;CACxE,MAAM,WAAW,OAAO,GAAG,KAAK,GAAG,SAAS;AAC5C,UACE,UAAU,sBACV,UACA,SACA,0CAA0C,SAAS,2EAEpD;;;;;;;;;;;;AAaH,SAAgB,qBAAqB,YAAoB,MAAqB;CAC5E,MAAM,WAAW,OAAO,GAAG,WAAW,GAAG,SAAS;AAClD,UACE,UAAU,oBACV,UACA,SACA,qCAAqC,SAAS,+GAG/C;;;;;;;;;;;AAYH,SAAgB,qBAAqB,KAA4B,MAAoB;AACnF,UACE,UAAU,oBACV,GAAG,IAAI,GAAG,QACV,SACA,GAAG,IAAI,wCAAwC,KAAK,+DAErD;;;;;;;;;;;;AAgBH,SAAgB,4BAA4B,UAAkB,YAA0B;AACtF,UACE,UAAU,uBACV,UACA,QACA,QAAQ,SAAS,eAAe,WAAW,qFAE5C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"interception-BbqMCVXa.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/scanner.ts","../../src/routing/codegen-shared.ts","../../src/routing/link-codegen.ts","../../src/routing/codegen.ts","../../src/routing/interception.ts"],"sourcesContent":["/**\n * Route tree types for timber.js file-system routing.\n *\n * The route tree is built by scanning the app/ directory and recognizing\n * file conventions (page.*, layout.*, middleware.ts, access.ts, route.ts, etc.).\n */\n\n/** Segment type classification */\nexport type SegmentType =\n | 'static' // e.g. \"dashboard\"\n | 'dynamic' // e.g. \"[id]\"\n | 'catch-all' // e.g. \"[...slug]\"\n | 'optional-catch-all' // e.g. \"[[...slug]]\"\n | 'group' // e.g. \"(marketing)\"\n | 'slot' // e.g. \"@sidebar\"\n | 'intercepting' // e.g. \"(.)photo\", \"(..)photo\", \"(...)photo\"\n | 'private'; // e.g. \"_components\", \"_lib\" — excluded from routing\n\n/**\n * Intercepting route marker — indicates how many levels up to resolve the\n * intercepted route from the intercepting route's location.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\nexport type InterceptionMarker = '(.)' | '(..)' | '(...)' | '(..)(..)';\n\n/** All recognized interception markers, ordered longest-first for parsing. */\nexport const INTERCEPTION_MARKERS: InterceptionMarker[] = ['(..)(..)', '(.)', '(..)', '(...)'];\n\n/** A single file discovered in a route segment */\nexport interface RouteFile {\n /** Absolute path to the file */\n filePath: string;\n /** File extension without leading dot (e.g. \"tsx\", \"ts\", \"mdx\") */\n extension: string;\n}\n\n/** A node in the segment tree */\nexport interface SegmentNode {\n /** The raw directory name (e.g. \"dashboard\", \"[id]\", \"(auth)\", \"@sidebar\") */\n segmentName: string;\n /** Classified segment type */\n segmentType: SegmentType;\n /** The dynamic param name, if dynamic (e.g. \"id\" for \"[id]\", \"slug\" for \"[...slug]\") */\n paramName?: string;\n /** The URL path prefix at this segment level (e.g. \"/dashboard\") */\n urlPath: string;\n /** For intercepting segments: the marker used, e.g. \"(.)\". */\n interceptionMarker?: InterceptionMarker;\n /**\n * For intercepting segments: the segment name after stripping the marker.\n * E.g., for \"(.)photo\" this is \"photo\".\n */\n interceptedSegmentName?: string;\n\n // --- File conventions ---\n page?: RouteFile;\n layout?: RouteFile;\n middleware?: RouteFile;\n access?: RouteFile;\n route?: RouteFile;\n /**\n * params.ts — isomorphic convention file exporting segmentParams and/or searchParams.\n * Discovered by the scanner like middleware.ts and access.ts.\n * See design/07-routing.md §\"params.ts Convention File\"\n */\n params?: RouteFile;\n error?: RouteFile;\n default?: RouteFile;\n /** Status-code files: 4xx.tsx, 5xx.tsx, {status}.tsx (component format) */\n statusFiles?: Map<string, RouteFile>;\n /** JSON status-code files: 4xx.json, 5xx.json, {status}.json */\n jsonStatusFiles?: Map<string, RouteFile>;\n /** denied.tsx — slot-only denial rendering */\n denied?: RouteFile;\n /** Legacy compat: not-found.tsx (maps to 404), forbidden.tsx (403), unauthorized.tsx (401) */\n legacyStatusFiles?: Map<string, RouteFile>;\n\n /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */\n metadataRoutes?: Map<string, RouteFile>;\n\n // --- Children ---\n children: SegmentNode[];\n /** Parallel route slots (keyed by slot name without @) */\n slots: Map<string, SegmentNode>;\n}\n\n/** The full route tree output from the scanner */\nexport interface RouteTree {\n /** The root segment node (representing app/) */\n root: SegmentNode;\n /** All discovered proxy.ts files (should be at most one, in app/) */\n proxy?: RouteFile;\n /**\n * Global error page: app/global-error.{tsx,ts,jsx,js}\n *\n * Rendered as a standalone full-page replacement (no layout wrapping)\n * when no segment-level error file is found. SSR-only render path.\n * Must provide its own <html> and <body>.\n *\n * See design/10-error-handling.md §\"Tier 2 — Global Error Page\"\n */\n globalError?: RouteFile;\n}\n\n/** Configuration passed to the scanner */\nexport interface ScannerConfig {\n /** Recognized page/layout extensions (without dots). Default: ['tsx', 'ts', 'jsx', 'js'] */\n pageExtensions?: string[];\n}\n\n/** Default page extensions */\nexport const DEFAULT_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];\n","/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.js';\nimport { classifyUrlSegment } from './segment-classify.js';\nimport { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS } from './types.js';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';\n\n/**\n * Pattern matching encoded path delimiters that must be rejected during route discovery.\n * %2F / %2f (forward slash) and %5C / %5c (backslash) can cause route collisions\n * when decoded. See design/13-security.md §\"Encoded separators rejected\".\n */\nconst ENCODED_SEPARATOR_PATTERN = /%(?:2[fF]|5[cC])/;\n\n/**\n * Pattern matching encoded null bytes (%00) that must be rejected.\n * See design/13-security.md §\"Null bytes rejected\".\n */\nconst ENCODED_NULL_PATTERN = /%00/;\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);\n\n/**\n * Legacy compat status-code files.\n * Maps legacy file name → HTTP status code for the fallback chain.\n * See design/10-error-handling.md §\"Fallback Chain\".\n */\nconst LEGACY_STATUS_FILES: Record<string, number> = {\n 'not-found': 404,\n 'forbidden': 403,\n 'unauthorized': 401,\n};\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route', 'params']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Check for global-error.{tsx,ts,jsx,js} at app root.\n // Tier 2 error page — renders standalone (no layouts) when no segment-level\n // error file is found. See design/10-error-handling.md §\"Tier 2\".\n const globalErrorFile = findPageExtFile(appDir, 'global-error', extSet);\n if (globalErrorFile) {\n tree.globalError = globalErrorFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n // Validate: detect duplicate param names in nested dynamic segments\n // e.g., /[id]/items/[id] — same param name in ancestor and descendant\n validateDuplicateParamNames(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: new Map(),\n };\n}\n\n/**\n * Classify a directory name into its segment type.\n */\nexport function classifySegment(dirName: string): {\n type: SegmentType;\n paramName?: string;\n interceptionMarker?: InterceptionMarker;\n interceptedSegmentName?: string;\n} {\n // Private folder: _name (excluded from routing)\n if (dirName.startsWith('_')) {\n return { type: 'private' };\n }\n\n // Parallel route slot: @name\n if (dirName.startsWith('@')) {\n return { type: 'slot' };\n }\n\n // Intercepting routes: (.)name, (..)name, (...)name, (..)(..)name\n // Check before route groups since intercepting markers also start with (\n const interception = parseInterceptionMarker(dirName);\n if (interception) {\n return {\n type: 'intercepting',\n interceptionMarker: interception.marker,\n interceptedSegmentName: interception.segmentName,\n };\n }\n\n // Route group: (name)\n if (dirName.startsWith('(') && dirName.endsWith(')')) {\n return { type: 'group' };\n }\n\n // Bracket-syntax segments: [param], [...param], [[...param]]\n // Delegated to the shared character-based classifier. If you change\n // bracket syntax, update segment-classify.ts — not here.\n const urlSeg = classifyUrlSegment(dirName);\n if (urlSeg.kind !== 'static') {\n return { type: urlSeg.kind, paramName: urlSeg.name };\n }\n\n return { type: 'static' };\n}\n\n/**\n * Parse an interception marker from a directory name.\n *\n * Returns the marker and the remaining segment name, or null if not an\n * intercepting route. Markers are checked longest-first to avoid (..)\n * matching before (..)(..).\n *\n * Examples:\n * \"(.)photo\" → { marker: \"(.)\", segmentName: \"photo\" }\n * \"(..)feed\" → { marker: \"(..)\", segmentName: \"feed\" }\n * \"(...)photos\" → { marker: \"(...)\", segmentName: \"photos\" }\n * \"(..)(..)admin\" → { marker: \"(..)(..)\", segmentName: \"admin\" }\n * \"(marketing)\" → null (route group, not interception)\n */\nfunction parseInterceptionMarker(\n dirName: string\n): { marker: InterceptionMarker; segmentName: string } | null {\n for (const marker of INTERCEPTION_MARKERS) {\n if (dirName.startsWith(marker)) {\n const rest = dirName.slice(marker.length);\n // Must have a segment name after the marker, and the rest must not\n // be empty or end with ) (which would be a route group like \"(auth)\")\n if (rest.length > 0 && !rest.endsWith(')')) {\n return { marker, segmentName: rest };\n }\n }\n }\n return null;\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, error, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const file: RouteFile = { filePath: fullPath, extension: ext };\n switch (name) {\n case 'page':\n node.page = file;\n break;\n case 'layout':\n node.layout = file;\n break;\n case 'error':\n node.error = file;\n break;\n case 'default':\n node.default = file;\n break;\n case 'denied':\n node.denied = file;\n break;\n }\n continue;\n }\n\n // Fixed conventions (middleware, access, route) — always .ts or .tsx\n if (FIXED_CONVENTIONS.has(name) && /\\.?[jt]sx?$/.test(ext)) {\n const file: RouteFile = { filePath: fullPath, extension: ext };\n switch (name) {\n case 'middleware':\n node.middleware = file;\n break;\n case 'access':\n node.access = file;\n break;\n case 'route':\n node.route = file;\n break;\n case 'params':\n node.params = file;\n break;\n }\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = new Map();\n }\n node.jsonStatusFiles.set(name, { filePath: fullPath, extension: ext });\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx)\n if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = new Map();\n }\n node.statusFiles.set(name, { filePath: fullPath, extension: ext });\n continue;\n }\n\n // Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)\n if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {\n if (!node.legacyStatusFiles) {\n node.legacyStatusFiles = new Map();\n }\n node.legacyStatusFiles.set(name, { filePath: fullPath, extension: ext });\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.\n // When both exist for the same base name, dynamic takes precedence.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!node.metadataRoutes) {\n node.metadataRoutes = new Map();\n }\n const existing = node.metadataRoutes.get(name);\n if (existing) {\n // Dynamic > static precedence: only overwrite if the new file is dynamic\n // or the existing file is static (dynamic always wins).\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (newIsDynamic || !existingIsDynamic) {\n node.metadataRoutes.set(name, { filePath: fullPath, extension: ext });\n }\n } else {\n node.metadataRoutes.set(name, { filePath: fullPath, extension: ext });\n }\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_PATTERN.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%%2F or %%5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (ENCODED_NULL_PATTERN.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots map, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots.set(slotName, childNode);\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that route groups don't produce conflicting pages/routes at the same URL path.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Parallel slots are excluded from collision detection — they intentionally coexist at\n * the same URL path as their parent (that's the whole point of parallel routes).\n */\nfunction validateRouteGroupCollisions(root: SegmentNode): void {\n // Map from urlPath → { filePath, source } for the first page/route seen at that path\n const seen = new Map<string, { filePath: string; segmentPath: string }>();\n collectRoutableLeaves(root, seen, '', false);\n}\n\n/**\n * Walk the segment tree and collect all routable leaves (page or route files),\n * throwing on collision. Slots are tracked in their own collision space since\n * they are parallel routes that intentionally share URL paths with their parent.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string,\n insideSlot: boolean\n): void {\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n // Only check collisions for non-slot pages — slots intentionally share URL paths\n if (!insideSlot) {\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${node.urlPath}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n }\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutableLeaves(child, seen, currentPath, insideSlot);\n }\n\n // Recurse into slots — each slot is its own parallel route space\n for (const [, slotNode] of node.slots) {\n collectRoutableLeaves(slotNode, seen, currentPath, true);\n }\n}\n\n/**\n * Validate that no route chain contains duplicate dynamic param names.\n *\n * Example violation:\n * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.\n *\n * Route groups are transparent — params accumulate through them.\n * Slots are independent — duplicate detection does NOT cross slot boundaries.\n *\n * See design/07-routing.md §\"Duplicate Param Name Detection\"\n */\nfunction validateDuplicateParamNames(root: SegmentNode): void {\n walkForDuplicateParams(root, new Map());\n}\n\n/**\n * Recursively walk the segment tree, tracking seen param names → segment paths.\n * Throws on the first duplicate found.\n */\nfunction walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {\n // If this node introduces a param name, check for duplicates\n if (node.paramName) {\n const existing = seen.get(node.paramName);\n if (existing) {\n throw new Error(\n `[timber] Duplicate param name '${node.paramName}' in route chain.\\n` +\n ` First defined at: ${existing}\\n` +\n ` Duplicate at: ${node.urlPath || '/'}\\n` +\n ` Rename one of the segments to avoid ambiguity.`\n );\n }\n // Add to seen for descendants (use a new Map to avoid polluting siblings)\n seen = new Map(seen);\n seen.set(node.paramName, node.urlPath || '/');\n }\n\n // Recurse into children (they inherit the accumulated params)\n for (const child of node.children) {\n walkForDuplicateParams(child, seen);\n }\n\n // Slots are independent parallel routes — start fresh param tracking\n // (a slot's params don't conflict with the main route's params)\n for (const [, slotNode] of node.slots) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n\n/**\n * Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).\n * Used for app-root conventions like global-error that aren't per-segment.\n */\nfunction findPageExtFile(\n dirPath: string,\n name: string,\n extSet: Set<string>\n): RouteFile | undefined {\n for (const ext of extSet) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n","/**\n * Shared codegen helpers — import-path computation, codec chain type\n * builder, and searchParams type formatter.\n *\n * Extracted from `codegen.ts` so `link-codegen.ts` can use the same\n * helpers without a cyclic import.\n */\n\nimport { relative, posix } from 'node:path';\nimport type { ParamEntry, RouteEntry } from './codegen-types.js';\n\n/**\n * Compute a relative import specifier for a codec/page file, stripping\n * the .ts/.tsx extension and resolving against the codegen output dir.\n */\nexport function codecImportPath(codecFilePath: string, importBase: string | undefined): string {\n const absPath = codecFilePath.replace(/\\.(ts|tsx)$/, '');\n if (importBase) {\n return './' + relative(importBase, absPath).replace(/\\\\/g, '/');\n }\n return './' + posix.basename(absPath);\n}\n\n/** Name of the shared helper type emitted at the top of the .d.ts. */\nexport const RESOLVE_SEGMENT_FIELD_TYPE_NAME = '_TimberResolveSegmentField';\n\n/**\n * Helper type emitted once at the top of the generated `.d.ts` and\n * referenced by every codec-chain conditional. Without this shared\n * helper, the inline expansion would duplicate the fallback branch on\n * every step and grow O(2^N) in chain depth (a single deep nested route\n * could blow up the file size and TS performance). With the helper,\n * each step reuses the named type and growth is O(N).\n *\n * The helper is a 2-arg conditional: given a typeof import expression\n * (`Def`), a key (`K`), and a fallback (`F`), it returns `T[K]` if\n * `Def extends ParamsDefinition<T>` and `K extends keyof T`, otherwise\n * `F`. The codec chain composes calls to this helper.\n */\nexport function emitResolveSegmentFieldHelper(): string {\n return [\n `type ${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<Def, K extends string, F> =`,\n ` Def extends import('@timber-js/app/segment-params').ParamsDefinition<infer T>`,\n ` ? K extends keyof T ? T[K] : F`,\n ` : F;`,\n ].join('\\n');\n}\n\n/**\n * Build a TypeScript type expression that resolves a single param's\n * codec by walking a chain of params.ts files in priority order.\n *\n * Each entry in the chain emits one application of the shared\n * `_TimberResolveSegmentField` helper type. Composing N applications\n * grows linearly with chain depth (O(N) characters), unlike an inline\n * conditional that would duplicate the fallback in each branch and\n * grow O(2^N).\n *\n * The closest match (position 0 in the chain) is checked first; if its\n * `segmentParams` definition declares the key, its inferred type wins.\n * Otherwise we fall through to the next entry, and finally to the\n * provided fallback. See TIM-834.\n */\nexport function buildCodecChainType(\n p: ParamEntry,\n importBase: string | undefined,\n fallback: string\n): string {\n const files = p.codecFilePaths;\n if (!files || files.length === 0) return fallback;\n const key = JSON.stringify(p.name);\n // Compose helper applications inside-out so the closest entry\n // (files[0]) ends up as the OUTERMOST application. Each application\n // adds a constant-size wrapper around the running fallback.\n let inner = fallback;\n for (let i = files.length - 1; i >= 0; i--) {\n const importPath = codecImportPath(files[i], importBase);\n inner = `${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<(typeof import('${importPath}'))['segmentParams'], ${key}, ${inner}>`;\n }\n return inner;\n}\n\n/**\n * Format the searchParams type for a route entry.\n *\n * When a page.tsx (or params.ts) exports searchParams, we reference its\n * inferred type via an import type. The import path is relative to\n * `importBase` (the directory where the .d.ts will be written). When\n * importBase is undefined, falls back to a bare relative path.\n */\nexport function formatSearchParamsType(route: RouteEntry, importBase?: string): string {\n if (route.hasSearchParams && route.searchParamsPagePath) {\n const importPath = codecImportPath(route.searchParamsPagePath, importBase);\n // Extract the type from the named 'searchParams' export of the page module.\n return `(typeof import('${importPath}'))['searchParams'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;\n }\n return '{}';\n}\n","/**\n * Typed `<Link>` codegen — interface augmentation generation.\n *\n * Extracted from `codegen.ts` to keep that file under the project's\n * 500-line cap. This module owns everything that emits the\n * `interface LinkFunction { ... }` augmentation blocks in the generated\n * `.timber/timber-routes.d.ts`.\n *\n * Two augmentation blocks are emitted:\n *\n * 1. **Per-route discriminated union** (`formatTypedLinkOverloads`) —\n * one call signature whose props is a union keyed on `href`. TS\n * narrows by literal href and reports prop errors against the\n * matched variant. See TIM-835 and `design/09-typescript.md`.\n *\n * 2. **Catch-all overloads** (`formatLinkCatchAllOverloads`) — external\n * href literals (`http://`, `mailto:`, etc.) and a computed-string\n * `<H extends string>` signature for runtime-computed paths.\n *\n * Block ordering is critical for error UX: per-route is emitted FIRST\n * so that, after TS's \"later overload set ordered first\" merge rule,\n * the discriminated union ends up LAST in resolution order — the\n * overload TS reports against on failure.\n */\n\nimport type { ParamEntry, RouteEntry } from './codegen-types.js';\nimport { buildCodecChainType, formatSearchParamsType } from './codegen-shared.js';\n\n/** Shared Link base-props type literal used in every emitted call signature. */\nexport const LINK_BASE_PROPS_TYPE =\n \"Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }\";\n\n/**\n * Build a TypeScript template literal pattern for a dynamic route.\n * e.g. '/products/[id]' → '/products/${string}'\n * '/blog/[...slug]' → '/blog/${string}'\n * '/docs/[[...path]]' → '/docs/${string}' (also matches /docs)\n * '/[org]/[repo]' → '/${string}/${string}'\n */\nexport function buildResolvedPattern(route: RouteEntry): string | null {\n const parts = route.urlPath.split('/');\n const templateParts = parts.map((part) => {\n if (part.startsWith('[[...') && part.endsWith(']]')) {\n // Optional catch-all — matches any trailing path\n return '${string}';\n }\n if (part.startsWith('[...') && part.endsWith(']')) {\n // Catch-all — matches one or more segments\n return '${string}';\n }\n if (part.startsWith('[') && part.endsWith(']')) {\n // Dynamic segment\n return '${string}';\n }\n return part;\n });\n return templateParts.join('/');\n}\n\n/**\n * Format the segmentParams type for Link overloads.\n *\n * Link params accept `string | number` for single dynamic segments\n * (convenience — values are stringified at runtime). Catch-all and\n * optional catch-all remain `string[]` / `string[] | undefined`.\n *\n * When the segment's params chain (TIM-834) declares a typed codec, the\n * inferred type from the codec wins via a nested conditional.\n */\nexport function formatLinkParamsType(params: ParamEntry[], importBase?: string): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => {\n const fallback = p.type === 'string' ? 'string | number' : p.type;\n const codecType = buildCodecChainType(p, importBase, fallback);\n return `${p.name}: ${codecType}`;\n });\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Catch-all call signatures for `<Link>` — external hrefs and computed\n * `string` variables. Emitted from codegen in a SEPARATE\n * `declare module` block (declared AFTER the per-route block) so the TS\n * \"later overload set ordered first\" rule places these catch-all\n * signatures ahead of per-route in resolution order, leaving per-route\n * as the final overload whose error message is reported on failure.\n *\n * The conditional `string extends H ? ... : never` protection preserves\n * TIM-624's guarantee that unknown internal path literals don't match\n * the catch-all — typos like `<Link href=\"/typo\" />` still error.\n */\nexport function formatLinkCatchAllOverloads(): string[] {\n const lines: string[] = [];\n const baseProps = LINK_BASE_PROPS_TYPE;\n\n // TIM-830: the catch-all signatures accept EITHER the legacy\n // `{ definition, values }` wrapper OR a flat `Record<string, unknown>`\n // values object. External/computed hrefs can't be looked up in the\n // runtime registry, so the wrapped form is still the reliable path;\n // the flat form is kept permissive for callers migrating from typed-\n // route hrefs to computed strings. `resolveHref` discriminates at\n // runtime via the presence of a `definition` key.\n const catchAllSearchParams =\n '{ definition: SearchParamsDefinition<Record<string, unknown>>; values: Record<string, unknown> } | Record<string, unknown>';\n\n // ExternalHref inlined here rather than referenced as an exported\n // alias so the generated .d.ts stands alone without source imports.\n const externalHref =\n '`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`';\n\n lines.push(' // Typed Link overloads — catch-all (block 2 / emitted second)');\n lines.push(' interface LinkFunction {');\n\n // (1) External/literal-protocol hrefs.\n //\n // TIM-833: `segmentParams` is permissively typed as\n // `Record<string, unknown>` (not `never`) so generic wrappers that\n // forward a `LinkProps`-shaped object can compile when the wrapper\n // happens to bottom out at an external href. There are no dynamic\n // segments to interpolate for an external href, so this prop is\n // ignored at runtime — accepting an open-shape object instead of\n // forbidding it removes a footgun without changing behavior.\n lines.push(` (props: ${baseProps} & {`);\n lines.push(` href: ${externalHref}`);\n lines.push(` segmentParams?: Record<string, unknown>`);\n lines.push(` searchParams?: ${catchAllSearchParams}`);\n lines.push(` }): import('react').JSX.Element`);\n\n // (2) Computed/variable href — non-literal `string` only.\n //\n // `string extends H` is true only when H is the wide `string` type,\n // not a specific literal. For literal hrefs, this overload must NOT\n // be selectable so the per-route discriminated union (block 1) is\n // the resolution target.\n //\n // TIM-833 follow-up: the previous shape used `string extends H ? {...} : never`\n // on the WHOLE props type. For literal H, that collapsed `props` to\n // `never`, and JSX type inference then read `(never).children` as\n // `never` — producing the misleading\n // `'children' prop expects type 'never' which requires multiple\n // children, but only a single child was provided` (TS2745) error,\n // which buried the actual prop-mismatch message under a noise diagnostic.\n //\n // Fix: move the `: never` from the whole props onto just the `href`\n // field. The overload remains uncallable for literal H (since `\"/x\"`\n // is not assignable to `never`), but `children` retains its real\n // `ReactNode` type so JSX inference no longer collapses to never.\n // The diagnostic surface becomes: TS reports the per-route block's\n // error chain (the helpful \"Type 'number' is not assignable to type\n // 'string'\" message) WITHOUT a leading TS2745 noise line.\n lines.push(` <H extends string>(`);\n lines.push(` props: ${baseProps} & {`);\n lines.push(` href: string extends H ? H : never`);\n lines.push(` segmentParams?: Record<string, string | number | string[]>`);\n lines.push(` searchParams?: ${catchAllSearchParams}`);\n lines.push(` }`);\n lines.push(` ): import('react').JSX.Element`);\n\n lines.push(' }');\n return lines;\n}\n\n/**\n * Generate typed per-route Link call signatures via LinkFunction\n * interface merging.\n *\n * TIM-835: This emits a SINGLE call signature whose props is a\n * discriminated union keyed on `href`. TypeScript narrows the union by\n * the literal `href` at the call site, then checks the rest of the\n * props against the matched variant. When `segmentParams` or\n * `searchParams` is wrong, TS reports the error against the matched\n * variant — naming the user's actual `href` and the offending field.\n *\n * Before TIM-835, this function emitted N separate per-route overloads\n * (one per route, sometimes two for dynamic routes). When ALL overloads\n * failed, TS would pick an arbitrary failed overload (heuristically the\n * \"last tried\") to render the diagnostic, often pointing at a route\n * completely unrelated to the one the user wrote. The discriminated\n * union sidesteps overload-resolution heuristics entirely.\n *\n * Open property shapes (`Record<string, unknown>` instead of `never`)\n * for `segmentParams` on routes that don't declare them keep generic\n * `LinkProps`-spreading wrappers compiling. (Aligned with TIM-833.)\n *\n * TIM-832: this function still emits the per-route block FIRST and the\n * catch-all block follows, so per-route remains the LAST overload set in\n * resolution order — the one TS reports against on failure. With a\n * discriminated union there is only one call signature in this block,\n * so the resolved-template-vs-pattern ordering reduces to placing the\n * pattern variant before the resolved-template variant inside the union.\n */\nexport function formatTypedLinkOverloads(routes: RouteEntry[], importBase?: string): string[] {\n const lines: string[] = [];\n const baseProps = LINK_BASE_PROPS_TYPE;\n\n // Build the union variants. Each route contributes one variant for the\n // pattern href (e.g. '/products/[id]') and, for dynamic routes, an\n // additional variant for the resolved-template href (e.g.\n // `/products/${string}`).\n //\n // The PATTERN variant must be listed BEFORE the resolved-template\n // variant for the same route so that when the user writes the literal\n // pattern href (`<Link href=\"/products/[id]\" />`), TS narrows to the\n // pattern variant (which carries the typed `segmentParams` shape)\n // rather than the looser resolved-template variant. Without this\n // ordering, TS would silently match the resolved-template variant\n // first — swallowing typed-segmentParams type errors.\n const variants: string[] = [];\n for (const route of routes) {\n const hasDynamicParams = route.params.length > 0;\n // For routes with no dynamic params, accept an absent OR open-shape\n // segmentParams. `Record<string, unknown>` keeps generic spreads\n // compiling and gives a readable error if a non-object is passed.\n const paramsProp = hasDynamicParams\n ? `segmentParams: ${formatLinkParamsType(route.params, importBase)}`\n : 'segmentParams?: Record<string, unknown>';\n\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : null;\n // TIM-830: pattern href uses the FLAT `Partial<T>` shape — the\n // runtime registry is keyed by the un-interpolated pattern.\n //\n // TIM-833: when the route has NO searchParams definition, use\n // `Record<string, unknown>` (open shape) instead of\n // `Record<string, never>` so generic `LinkProps`-spreading wrappers\n // compile. The trade-off vs the original TIM-835 strict shape is\n // that excess searchParams keys on a no-def route no longer raise\n // a type error — but those keys are runtime no-ops anyway, and\n // wrapper compatibility is the more common pain point. Routes WITH\n // a searchParams definition still get strict `Partial<T>` typing.\n const patternSearchParamsProp = searchParamsType\n ? `searchParams?: Partial<${searchParamsType}>`\n : 'searchParams?: Record<string, unknown>';\n\n // Pattern variant FIRST (more specific).\n variants.push(\n `${baseProps} & { href: '${route.urlPath}'; ${paramsProp}; ${patternSearchParamsProp} }`\n );\n\n // Resolved-template variant SECOND (looser, matches interpolated hrefs).\n if (hasDynamicParams) {\n const templatePattern = buildResolvedPattern(route);\n if (templatePattern) {\n // TIM-830: resolved-template href keeps the WRAPPED\n // `{ definition, values }` shape — the registry can't be looked\n // up by an already-interpolated href.\n //\n // TIM-833: searchParams falls back to `Record<string, unknown>`\n // (was `Record<string, never>`) when the route has no\n // definition, mirroring the pattern-variant change above. The\n // resolved-template `segmentParams` is intentionally KEPT as\n // `Record<string, never>` below to preserve TIM-835's\n // pattern-variant typo detection: loosening segmentParams here\n // would let the resolved-template variant shadow the pattern\n // variant on literal pattern hrefs and silently swallow keyed\n // typos like `<Link href=\"/[id]\" segmentParams={{ idz: 1 }} />`.\n const resolvedSearchParamsProp = searchParamsType\n ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`\n : 'searchParams?: Record<string, unknown>';\n // TIM-835: `Record<string, never>` for segmentParams forbids ANY\n // provided keys on the resolved-template variant. Without this,\n // the resolved-template would shadow the pattern variant for\n // literal pattern hrefs (`<Link href=\"/products/[id]\" segmentParams={...} />`)\n // because both variants accept the literal `'/products/[id]'`\n // and the looser variant would silently swallow segmentParams\n // type errors.\n variants.push(\n `${baseProps} & { href: \\`${templatePattern}\\`; segmentParams?: Record<string, never>; ${resolvedSearchParamsProp} }`\n );\n }\n }\n }\n\n lines.push(' interface LinkFunction {');\n if (variants.length === 0) {\n // No page routes — emit nothing. The catch-all block in the second\n // augmentation still provides external/computed-string signatures.\n lines.push(' }');\n return lines;\n }\n lines.push(' (');\n lines.push(' props:');\n for (const variant of variants) {\n lines.push(` | (${variant})`);\n }\n lines.push(` ): import('react').JSX.Element`);\n lines.push(' }');\n\n return lines;\n}\n","/**\n * Route map codegen.\n *\n * Walks the scanned RouteTree and generates a TypeScript declaration file\n * mapping every route to its params and searchParams shapes.\n *\n * This runs at build time and in dev (regenerated on file changes).\n * No runtime overhead — purely static type generation.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport type { RouteTree, SegmentNode } from './types.js';\nimport type { ParamEntry, RouteEntry } from './codegen-types.js';\nimport {\n buildCodecChainType,\n emitResolveSegmentFieldHelper,\n formatSearchParamsType,\n} from './codegen-shared.js';\nimport { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';\n\n/** Options for route map generation. */\nexport interface CodegenOptions {\n /** Absolute path to the app/ directory. Required for page searchParams detection. */\n appDir?: string;\n /**\n * Absolute path to the directory where the .d.ts file will be written.\n * Used to compute correct relative import paths for page files.\n * Defaults to appDir when not provided (preserves backward compat for tests).\n */\n outputDir?: string;\n}\n\n/**\n * Generate a TypeScript declaration file string from a scanned route tree.\n *\n * The output is a `declare module '@timber-js/app'` block containing the Routes\n * interface that maps every route path to its params and searchParams shape.\n */\nexport function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}): string {\n const routes: RouteEntry[] = [];\n collectRoutes(tree.root, [], [], routes);\n\n // Sort routes alphabetically for deterministic output\n routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));\n\n // When outputDir differs from appDir, import paths must be relative to outputDir\n const importBase = options.outputDir ?? options.appDir;\n\n return formatDeclarationFile(routes, importBase);\n}\n\n/**\n * Recursively walk the segment tree and collect route entries.\n *\n * A route entry is created for any segment that has a `page` or `route` file.\n * Params accumulate from ancestor dynamic segments.\n */\nfunction collectRoutes(\n node: SegmentNode,\n ancestorParams: ParamEntry[],\n ancestorParamsFiles: string[],\n routes: RouteEntry[]\n): void {\n // TIM-834: Identify this segment's own params.ts (if it has a\n // segmentParams export). The full chain of params.ts files in the\n // route ancestry is threaded down via `ancestorParamsFiles`; codec\n // resolution for each ParamEntry is deferred until leaf time so that\n // descendant params.ts files can override ancestor codecs (closest-\n // to-leaf wins, matching the runtime semantics of\n // coerceSegmentParams which walks segments top-down and overwrites\n // earlier coercions).\n const ownParamsFile =\n node.params && fileHasExport(node.params.filePath, 'segmentParams')\n ? node.params.filePath\n : undefined;\n\n // Accumulate params from this segment. We attach `codecFilePaths`\n // later (at leaf time) using the FULL chain so descendant overrides\n // are visible. The legacy layout/page fallback is recorded now\n // because it is per-segment (and does not participate in inheritance).\n const params = [...ancestorParams];\n if (node.paramName) {\n const legacyFallback = ownParamsFile ? undefined : findLegacyParamsExport(node);\n params.push({\n name: node.paramName,\n type: paramTypeForSegment(node.segmentType),\n // Codec chain populated at leaf time. We carry the per-segment\n // legacy fallback (if any) so leaf-time resolution can fall back\n // to it when no params.ts in the chain declares this key.\n legacyCodecFilePath: legacyFallback,\n });\n }\n\n // Extend the chain for descendants of this segment.\n const nextAncestorFiles = ownParamsFile\n ? [...ancestorParamsFiles, ownParamsFile]\n : ancestorParamsFiles;\n\n // Check if this segment is a leaf route (has page or route file)\n const isPage = !!node.page;\n const isApiRoute = !!node.route;\n\n if (isPage || isApiRoute) {\n // TIM-834 P1 fix: at LEAF time, the full chain of params.ts files\n // (root-to-leaf) is known. Resolve every ParamEntry's\n // `codecFilePaths` to the chain in LEAF-FIRST order so the\n // closest-to-leaf entry is checked first — matching runtime\n // closest-wins semantics. The chain is shared by all params in the\n // route, so we compute it once.\n const leafFirstChain = nextAncestorFiles.length > 0 ? [...nextAncestorFiles].reverse() : [];\n const resolvedParams: ParamEntry[] = params.map((p) => {\n const codecFilePaths =\n leafFirstChain.length > 0\n ? leafFirstChain\n : p.legacyCodecFilePath\n ? [p.legacyCodecFilePath]\n : undefined;\n return {\n name: p.name,\n type: p.type,\n codecFilePaths,\n };\n });\n\n const entry: RouteEntry = {\n urlPath: node.urlPath,\n params: resolvedParams,\n hasSearchParams: false,\n isApiRoute,\n };\n\n // Detect searchParams export from params.ts (primary) or page.tsx (fallback)\n if (isPage) {\n if (node.params && fileHasExport(node.params.filePath, 'searchParams')) {\n entry.hasSearchParams = true;\n entry.searchParamsPagePath = node.params.filePath;\n } else if (node.page && fileHasExport(node.page.filePath, 'searchParams')) {\n entry.hasSearchParams = true;\n entry.searchParamsPagePath = node.page.filePath;\n }\n }\n\n routes.push(entry);\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutes(child, params, nextAncestorFiles, routes);\n }\n\n // Recurse into slots (they share the parent's URL path, but may have their own pages)\n for (const [, slot] of node.slots) {\n collectRoutes(slot, params, nextAncestorFiles, routes);\n }\n}\n\n/**\n * Determine the TypeScript type for a segment's param.\n */\nfunction paramTypeForSegment(segmentType: string): ParamEntry['type'] {\n switch (segmentType) {\n case 'catch-all':\n return 'string[]';\n case 'optional-catch-all':\n return 'string[] | undefined';\n default:\n return 'string';\n }\n}\n\n/**\n * Check if a file exports a specific named export.\n *\n * Uses a lightweight regex check — not full AST parsing. The actual type\n * extraction happens via the TypeScript compiler in the generated .d.ts.\n */\nfunction fileHasExport(filePath: string, exportName: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const content = readFileSync(filePath, 'utf-8');\n const e = exportName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n new RegExp(`export\\\\s+(const|let|var)\\\\s+${e}\\\\b`).test(content) ||\n new RegExp(`export\\\\s*\\\\{[^}]*\\\\b${e}\\\\b[^}]*\\\\}`).test(content)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Find a legacy `segmentParams` export on layout.tsx or page.tsx.\n *\n * Backward-compat shim: TIM-508 made params.ts the canonical location\n * for `segmentParams`. Layout/page exports are still accepted for the\n * OWN segment only (not inherited by descendants — see TIM-834).\n */\nfunction findLegacyParamsExport(node: SegmentNode): string | undefined {\n if (node.layout && fileHasExport(node.layout.filePath, 'segmentParams')) {\n return node.layout.filePath;\n }\n if (node.page && fileHasExport(node.page.filePath, 'segmentParams')) {\n return node.page.filePath;\n }\n return undefined;\n}\n\n/**\n * Format the collected routes into a TypeScript declaration file.\n */\nfunction formatDeclarationFile(routes: RouteEntry[], importBase?: string): string {\n const lines: string[] = [];\n\n lines.push('// This file is auto-generated by timber.js route map codegen.');\n lines.push('// Do not edit manually. Regenerated on build and in dev mode.');\n lines.push('');\n // export {} makes this file a module, so all declare module blocks are\n // augmentations rather than ambient replacements. Without this, the\n // declare module blocks would replace the original module types entirely\n // (removing exports like bindUseQueryStates that aren't listed here).\n lines.push('export {};');\n lines.push('');\n\n // TIM-834 P2: emit the shared codec-resolution helper type ONCE so the\n // per-param chain conditionals reference it instead of inlining the\n // fallback in both branches (which grows O(2^N) in chain depth).\n lines.push(emitResolveSegmentFieldHelper());\n lines.push('');\n lines.push(\"declare module '@timber-js/app' {\");\n lines.push(' interface Routes {');\n\n for (const route of routes) {\n const paramsType = formatParamsType(route.params, importBase);\n const searchParamsType = formatSearchParamsType(route, importBase);\n\n lines.push(` '${route.urlPath}': {`);\n lines.push(` segmentParams: ${paramsType}`);\n lines.push(` searchParams: ${searchParamsType}`);\n lines.push(` }`);\n }\n\n lines.push(' }');\n lines.push('}');\n lines.push('');\n\n // Generate @timber-js/app/server augmentation — typed searchParams() generic\n const pageRoutes = routes.filter((r) => !r.isApiRoute);\n\n // Note: searchParams() always returns Promise<URLSearchParams>.\n // Typed parsing is done via definition.parse(searchParams()).\n // No module augmentation needed for @timber-js/app/server.\n\n // Generate overloads for @timber-js/app/client\n const dynamicRoutes = routes.filter((r) => r.params.length > 0);\n\n if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {\n lines.push(\"declare module '@timber-js/app/client' {\");\n lines.push(\n \" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'\"\n );\n lines.push('');\n\n // useParams overloads\n if (dynamicRoutes.length > 0) {\n for (const route of dynamicRoutes) {\n const paramsType = formatParamsType(route.params, importBase);\n lines.push(` export function useSegmentParams(route: '${route.urlPath}'): ${paramsType}`);\n }\n lines.push(' export function useSegmentParams(): Record<string, string | string[]>');\n lines.push('');\n }\n\n // useQueryStates overloads\n if (pageRoutes.length > 0) {\n lines.push(...formatUseQueryStatesOverloads(pageRoutes, importBase));\n lines.push('');\n }\n\n // Typed Link overloads — per-route with DIRECT types (no conditionals).\n // Direct types preserve TypeScript's excess property checking.\n //\n // TIM-832: per-route and catch-all are emitted as TWO separate\n // augmentation blocks. Per TS's merging rule \"later overload sets\n // ordered first\", the catch-all block (declared SECOND in this file)\n // ends up FIRST in the merged call-signature list at resolution time,\n // which puts the per-route block LAST — so its error message is the\n // one TypeScript reports when no overload matches. This gives users a\n // clear prop-mismatch error (e.g. \"'string | undefined' is not\n // assignable to 'string | number' on id\") instead of the old\n // confusing \"Type 'string' is not assignable to type 'never'\" cascade.\n if (pageRoutes.length > 0) {\n lines.push(' // Typed Link overloads — per-route (block 1 / emitted first)');\n lines.push(...formatTypedLinkOverloads(pageRoutes, importBase));\n lines.push('');\n }\n\n lines.push('}');\n lines.push('');\n }\n\n // TIM-832: catch-all block — emitted as a SEPARATE `declare module`\n // augmentation so TS's \"later overload set first\" rule orders it ahead\n // of the per-route block above at resolution time, leaving per-route as\n // the \"last overload\" whose error TypeScript reports.\n lines.push(\"declare module '@timber-js/app/client' {\");\n lines.push(\" import type { SearchParamsDefinition } from '@timber-js/app/search-params'\");\n lines.push('');\n lines.push(...formatLinkCatchAllOverloads());\n lines.push('}');\n lines.push('');\n\n return lines.join('\\n');\n}\n\n/**\n * Format the params type for a route entry.\n */\nfunction formatParamsType(params: ParamEntry[], importBase?: string): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => {\n const codecType = buildCodecChainType(p, importBase, p.type);\n return `${p.name}: ${codecType}`;\n });\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Generate useQueryStates overloads.\n *\n * For each page route:\n * - Routes with search-params.ts get a typed overload returning the inferred T\n * - Routes without search-params.ts get an overload returning [{}, SetParams<{}>]\n *\n * A fallback overload for standalone codecs (existing API) is emitted last.\n */\nfunction formatUseQueryStatesOverloads(routes: RouteEntry[], importBase?: string): string[] {\n const lines: string[] = [];\n\n for (const route of routes) {\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : '{}';\n lines.push(\n ` export function useQueryStates<R extends '${route.urlPath}'>(route: R, options?: QueryStatesOptions): [${searchParamsType}, SetParams<${searchParamsType}>]`\n );\n }\n\n // Fallback: standalone codecs (existing API)\n lines.push(\n ' export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]'\n );\n\n return lines;\n}\n\n// Link overload formatters and helpers (`formatTypedLinkOverloads`,\n// `formatLinkCatchAllOverloads`, `formatLinkParamsType`,\n// `buildResolvedPattern`, `LINK_BASE_PROPS_TYPE`) were extracted to\n// `./link-codegen.ts` (TIM-835) to keep this file under the 500-line\n// cap. They are imported at the top of this file.\n","/**\n * Intercepting route utilities.\n *\n * Computes rewrite rules from the route tree that enable intercepting routes\n * to conditionally render when navigating via client-side (soft) navigation.\n *\n * The mechanism: at build time, each intercepting route directory generates a\n * conditional rewrite. On soft navigation, the client sends an `X-Timber-URL`\n * header with the current pathname. The server checks if any rewrite's source\n * (the intercepted URL) matches the target pathname AND the header matches\n * the intercepting route's parent URL. If both match, the intercepting route\n * renders instead of the normal route.\n *\n * On hard navigation (no header), no rewrite matches, and the normal route\n * renders.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\n\nimport type { SegmentNode, InterceptionMarker } from './types.js';\n\n/** A conditional rewrite rule generated from an intercepting route. */\nexport interface InterceptionRewrite {\n /**\n * The URL pattern that this rewrite intercepts (the target of navigation).\n * E.g., \"/photo/[id]\" for a (.)photo/[id] interception.\n */\n interceptedPattern: string;\n /**\n * The URL prefix that the client must be navigating FROM for this rewrite\n * to apply. Matched against the X-Timber-URL header.\n * E.g., \"/feed\" for a (.)photo/[id] inside /feed/@modal/.\n */\n interceptingPrefix: string;\n /**\n * Segments chain from root → intercepting leaf. Used to build the element\n * tree when the interception is active.\n */\n segmentPath: SegmentNode[];\n}\n\n/**\n * Collect all interception rewrite rules from the route tree.\n *\n * Walks the tree recursively. For each intercepting segment, computes the\n * intercepted URL based on the marker and the segment's position.\n */\nexport function collectInterceptionRewrites(root: SegmentNode): InterceptionRewrite[] {\n const rewrites: InterceptionRewrite[] = [];\n walkForInterceptions(root, [root], rewrites);\n return rewrites;\n}\n\n/**\n * Recursively walk the segment tree to find intercepting routes.\n */\nfunction walkForInterceptions(\n node: SegmentNode,\n ancestors: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n // Check children\n for (const child of node.children) {\n if (child.segmentType === 'intercepting' && child.interceptionMarker) {\n // Found an intercepting route — collect rewrites from its sub-tree\n collectFromInterceptingNode(child, ancestors, rewrites);\n } else {\n walkForInterceptions(child, [...ancestors, child], rewrites);\n }\n }\n\n // Check slots (intercepting routes are typically inside slots like @modal)\n for (const [, slot] of node.slots) {\n walkForInterceptions(slot, ancestors, rewrites);\n }\n}\n\n/**\n * For an intercepting segment, find all leaf pages in its sub-tree and\n * generate rewrite rules for each.\n */\nfunction collectFromInterceptingNode(\n interceptingNode: SegmentNode,\n ancestors: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n const marker = interceptingNode.interceptionMarker!;\n const segmentName = interceptingNode.interceptedSegmentName!;\n\n // Compute the intercepted URL base based on the marker\n const parentUrlPath = ancestors[ancestors.length - 1].urlPath;\n const interceptedBase = computeInterceptedBase(parentUrlPath, marker);\n const interceptedUrlBase =\n interceptedBase === '/' ? `/${segmentName}` : `${interceptedBase}/${segmentName}`;\n\n // Find all leaf pages in the intercepting sub-tree\n collectLeavesWithRewrites(\n interceptingNode,\n interceptedUrlBase,\n parentUrlPath,\n [...ancestors, interceptingNode],\n rewrites\n );\n}\n\n/**\n * Recursively find leaf pages in an intercepting sub-tree and generate\n * rewrite rules for each.\n */\nfunction collectLeavesWithRewrites(\n node: SegmentNode,\n interceptedUrlPath: string,\n interceptingPrefix: string,\n segmentPath: SegmentNode[],\n rewrites: InterceptionRewrite[]\n): void {\n if (node.page) {\n rewrites.push({\n interceptedPattern: interceptedUrlPath,\n interceptingPrefix,\n segmentPath: [...segmentPath],\n });\n }\n\n for (const child of node.children) {\n const childUrl =\n child.segmentType === 'group'\n ? interceptedUrlPath\n : `${interceptedUrlPath}/${child.segmentName}`;\n collectLeavesWithRewrites(\n child,\n childUrl,\n interceptingPrefix,\n [...segmentPath, child],\n rewrites\n );\n }\n}\n\n/**\n * Compute the base URL that an intercepting route intercepts, given the\n * parent's URL path and the interception marker.\n *\n * - (.) — same level: parent's URL path\n * - (..) — one level up: parent's parent URL path\n * - (...) — root level: /\n * - (..)(..) — two levels up: parent's grandparent URL path\n *\n * Level counting operates on URL path segments, NOT filesystem directories.\n * Route groups and parallel slots are already excluded from urlPath (they\n * don't add URL depth), so (..) correctly climbs visible segments. This\n * avoids the Vinext bug where path.dirname() on filesystem paths would\n * waste climbs on invisible route groups.\n */\nfunction computeInterceptedBase(parentUrlPath: string, marker: InterceptionMarker): string {\n switch (marker) {\n case '(.)':\n return parentUrlPath;\n case '(..)': {\n const parts = parentUrlPath.split('/').filter(Boolean);\n parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n }\n case '(...)':\n return '/';\n case '(..)(..)': {\n const parts = parentUrlPath.split('/').filter(Boolean);\n parts.pop();\n parts.pop();\n return parts.length === 0 ? '/' : `/${parts.join('/')}`;\n }\n }\n}\n"],"mappings":";;;;;;AA2BA,IAAa,uBAA6C;CAAC;CAAY;CAAO;CAAQ;CAAQ;;AAqF9F,IAAa,0BAA0B;CAAC;CAAO;CAAM;CAAO;CAAK;;;;;;;;;;;;;;;;;ACnFjE,IAAM,4BAA4B;;;;;AAMlC,IAAM,uBAAuB;;;;AAK7B,IAAM,uBAAuB,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAS;CAAW;CAAS,CAAC;;;;;;AAOtF,IAAM,sBAA8C;CAClD,aAAa;CACb,aAAa;CACb,gBAAgB;CACjB;;;;AAKD,IAAM,oBAAoB,IAAI,IAAI;CAAC;CAAc;CAAU;CAAS;CAAS,CAAC;;;;;;AAO9E,IAAM,sBAAsB;;;;;;;;AAS5B,SAAgB,WAAW,QAAgB,SAAwB,EAAE,EAAa;CAChF,MAAM,iBAAiB,OAAO,kBAAkB;CAChD,MAAM,SAAS,IAAI,IAAI,eAAe;CAEtC,MAAM,OAAkB,EACtB,MAAM,kBAAkB,IAAI,UAAU,IAAI,EAC3C;CAGD,MAAM,YAAY,cAAc,QAAQ,QAAQ;AAChD,KAAI,UACF,MAAK,QAAQ;CAMf,MAAM,kBAAkB,gBAAgB,QAAQ,gBAAgB,OAAO;AACvE,KAAI,gBACF,MAAK,cAAc;AAIrB,kBAAiB,QAAQ,KAAK,MAAM,OAAO;AAG3C,cAAa,QAAQ,KAAK,MAAM,OAAO;AAGvC,8BAA6B,KAAK,KAAK;AAIvC,6BAA4B,KAAK,KAAK;AAEtC,QAAO;;;;;AAMT,SAAS,kBACP,aACA,aACA,SACA,WACA,oBACA,wBACa;AACb,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,UAAU,EAAE;EACZ,uBAAO,IAAI,KAAK;EACjB;;;;;AAMH,SAAgB,gBAAgB,SAK9B;AAEA,KAAI,QAAQ,WAAW,IAAI,CACzB,QAAO,EAAE,MAAM,WAAW;AAI5B,KAAI,QAAQ,WAAW,IAAI,CACzB,QAAO,EAAE,MAAM,QAAQ;CAKzB,MAAM,eAAe,wBAAwB,QAAQ;AACrD,KAAI,aACF,QAAO;EACL,MAAM;EACN,oBAAoB,aAAa;EACjC,wBAAwB,aAAa;EACtC;AAIH,KAAI,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,CAClD,QAAO,EAAE,MAAM,SAAS;CAM1B,MAAM,SAAS,mBAAmB,QAAQ;AAC1C,KAAI,OAAO,SAAS,SAClB,QAAO;EAAE,MAAM,OAAO;EAAM,WAAW,OAAO;EAAM;AAGtD,QAAO,EAAE,MAAM,UAAU;;;;;;;;;;;;;;;;AAiB3B,SAAS,wBACP,SAC4D;AAC5D,MAAK,MAAM,UAAU,qBACnB,KAAI,QAAQ,WAAW,OAAO,EAAE;EAC9B,MAAM,OAAO,QAAQ,MAAM,OAAO,OAAO;AAGzC,MAAI,KAAK,SAAS,KAAK,CAAC,KAAK,SAAS,IAAI,CACxC,QAAO;GAAE;GAAQ,aAAa;GAAM;;AAI1C,QAAO;;;;;;AAOT,SAAS,eAAe,eAAuB,SAAiB,aAAkC;AAEhG,KAAI,gBAAgB,WAAW,gBAAgB,UAAU,gBAAgB,eACvE,QAAO;AAIT,QAAO,GADY,kBAAkB,MAAM,KAAK,cAC3B,GAAG;;;;;AAM1B,SAAS,iBAAiB,SAAiB,MAAmB,QAA2B;CACvF,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,QAAQ;SACxB;AACN;;AAGF,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,SAAS,MAAM;AAGrC,MAAI;AACF,OAAI,SAAS,SAAS,CAAC,aAAa,CAAE;UAChC;AACN;;EAGF,MAAM,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE;EACnC,MAAM,OAAO,SAAS,OAAO,IAAI,MAAM;AAGvC,MAAI,qBAAqB,IAAI,KAAK,IAAI,OAAO,IAAI,IAAI,EAAE;GACrD,MAAM,OAAkB;IAAE,UAAU;IAAU,WAAW;IAAK;AAC9D,WAAQ,MAAR;IACE,KAAK;AACH,UAAK,OAAO;AACZ;IACF,KAAK;AACH,UAAK,SAAS;AACd;IACF,KAAK;AACH,UAAK,QAAQ;AACb;IACF,KAAK;AACH,UAAK,UAAU;AACf;IACF,KAAK;AACH,UAAK,SAAS;AACd;;AAEJ;;AAIF,MAAI,kBAAkB,IAAI,KAAK,IAAI,cAAc,KAAK,IAAI,EAAE;GAC1D,MAAM,OAAkB;IAAE,UAAU;IAAU,WAAW;IAAK;AAC9D,WAAQ,MAAR;IACE,KAAK;AACH,UAAK,aAAa;AAClB;IACF,KAAK;AACH,UAAK,SAAS;AACd;IACF,KAAK;AACH,UAAK,QAAQ;AACb;IACF,KAAK;AACH,UAAK,SAAS;AACd;;AAEJ;;AAKF,MAAI,oBAAoB,KAAK,KAAK,IAAI,QAAQ,QAAQ;AACpD,OAAI,CAAC,KAAK,gBACR,MAAK,kCAAkB,IAAI,KAAK;AAElC,QAAK,gBAAgB,IAAI,MAAM;IAAE,UAAU;IAAU,WAAW;IAAK,CAAC;AACtE;;AAIF,MAAI,oBAAoB,KAAK,KAAK,IAAI,OAAO,IAAI,IAAI,EAAE;AACrD,OAAI,CAAC,KAAK,YACR,MAAK,8BAAc,IAAI,KAAK;AAE9B,QAAK,YAAY,IAAI,MAAM;IAAE,UAAU;IAAU,WAAW;IAAK,CAAC;AAClE;;AAIF,MAAI,QAAQ,uBAAuB,OAAO,IAAI,IAAI,EAAE;AAClD,OAAI,CAAC,KAAK,kBACR,MAAK,oCAAoB,IAAI,KAAK;AAEpC,QAAK,kBAAkB,IAAI,MAAM;IAAE,UAAU;IAAU,WAAW;IAAK,CAAC;AACxE;;AAQF,MADiB,sBAAsB,MAAM,EAC/B;AACZ,OAAI,CAAC,KAAK,eACR,MAAK,iCAAiB,IAAI,KAAK;GAEjC,MAAM,WAAW,KAAK,eAAe,IAAI,KAAK;AAC9C,OAAI,UAAU;IAGZ,MAAM,oBAAoB,2BAA2B,MAAM,SAAS,UAAU;AAE9E,QADqB,2BAA2B,MAAM,IAAI,IACtC,CAAC,kBACnB,MAAK,eAAe,IAAI,MAAM;KAAE,UAAU;KAAU,WAAW;KAAK,CAAC;SAGvE,MAAK,eAAe,IAAI,MAAM;IAAE,UAAU;IAAU,WAAW;IAAK,CAAC;;;AAM3E,KAAI,KAAK,SAAS,KAAK,KACrB,OAAM,IAAI,MACR,qFACiB,KAAK,MAAM,SAAS,gBACpB,KAAK,KAAK,SAAS,iEAErC;;;;;AAOL,SAAS,aAAa,SAAiB,YAAyB,QAA2B;CACzF,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,QAAQ;SACxB;AACN;;AAGF,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,KAAK,SAAS,MAAM;AAErC,MAAI;AACF,OAAI,CAAC,SAAS,SAAS,CAAC,aAAa,CAAE;UACjC;AACN;;AAMF,MAAI,0BAA0B,KAAK,MAAM,CACvC,OAAM,IAAI,MACR,gGACkB,SAAS,oIAG5B;AAEH,MAAI,qBAAqB,KAAK,MAAM,CAClC,OAAM,IAAI,MACR,mFACkB,SAAS,iHAG5B;EAGH,MAAM,EAAE,MAAM,WAAW,oBAAoB,2BAA2B,gBAAgB,MAAM;AAG9F,MAAI,SAAS,UAAW;EAGxB,MAAM,YAAY,kBAChB,OACA,MAHc,eAAe,WAAW,SAAS,OAAO,KAAK,EAK7D,WACA,oBACA,uBACD;AAGD,mBAAiB,UAAU,WAAW,OAAO;AAG7C,eAAa,UAAU,WAAW,OAAO;AAGzC,MAAI,SAAS,QAAQ;GACnB,MAAM,WAAW,MAAM,MAAM,EAAE;AAC/B,cAAW,MAAM,IAAI,UAAU,UAAU;QAEzC,YAAW,SAAS,KAAK,UAAU;;;;;;;;;;;;AAczC,SAAS,6BAA6B,MAAyB;AAG7D,uBAAsB,sBADT,IAAI,KAAwD,EACvC,IAAI,MAAM;;;;;;;AAQ9C,SAAS,sBACP,MACA,MACA,aACA,YACM;CACN,MAAM,cAAc,cAChB,GAAG,YAAY,GAAG,KAAK,gBACvB,KAAK,eAAe;AAGxB,KAAI,CAAC,YAAY;EACf,MAAM,eAAe,KAAK,QAAQ,KAAK;AACvC,MAAI,cAAc;GAChB,MAAM,WAAW,KAAK,IAAI,KAAK,QAAQ;AACvC,OAAI,SACF,OAAM,IAAI,MACR,gHACiB,KAAK,QAAQ,gBACb,SAAS,SAAS,QAAQ,SAAS,YAAY,iBAC/C,aAAa,SAAS,QAAQ,YAAY,8GAG5D;AAEH,QAAK,IAAI,KAAK,SAAS;IAAE,UAAU,aAAa;IAAU,aAAa;IAAa,CAAC;;;AAKzF,MAAK,MAAM,SAAS,KAAK,SACvB,uBAAsB,OAAO,MAAM,aAAa,WAAW;AAI7D,MAAK,MAAM,GAAG,aAAa,KAAK,MAC9B,uBAAsB,UAAU,MAAM,aAAa,KAAK;;;;;;;;;;;;;AAe5D,SAAS,4BAA4B,MAAyB;AAC5D,wBAAuB,sBAAM,IAAI,KAAK,CAAC;;;;;;AAOzC,SAAS,uBAAuB,MAAmB,MAAiC;AAElF,KAAI,KAAK,WAAW;EAClB,MAAM,WAAW,KAAK,IAAI,KAAK,UAAU;AACzC,MAAI,SACF,OAAM,IAAI,MACR,kCAAkC,KAAK,UAAU,yCACxB,SAAS,oBACb,KAAK,WAAW,IAAI,oDAE1C;AAGH,SAAO,IAAI,IAAI,KAAK;AACpB,OAAK,IAAI,KAAK,WAAW,KAAK,WAAW,IAAI;;AAI/C,MAAK,MAAM,SAAS,KAAK,SACvB,wBAAuB,OAAO,KAAK;AAKrC,MAAK,MAAM,GAAG,aAAa,KAAK,MAC9B,wBAAuB,UAAU,IAAI,IAAI,KAAK,CAAC;;;;;AAOnD,SAAS,cAAc,SAAiB,MAAqC;AAC3E,MAAK,MAAM,OAAO,CAAC,MAAM,MAAM,EAAE;EAC/B,MAAM,WAAW,KAAK,SAAS,GAAG,KAAK,GAAG,MAAM;AAChD,MAAI;AACF,OAAI,SAAS,SAAS,CAAC,QAAQ,CAC7B,QAAO;IAAE,UAAU;IAAU,WAAW;IAAK;UAEzC;;;;;;;AAWZ,SAAS,gBACP,SACA,MACA,QACuB;AACvB,MAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,WAAW,KAAK,SAAS,GAAG,KAAK,GAAG,MAAM;AAChD,MAAI;AACF,OAAI,SAAS,SAAS,CAAC,QAAQ,CAC7B,QAAO;IAAE,UAAU;IAAU,WAAW;IAAK;UAEzC;;;;;;;;;;;;;;;;ACriBZ,SAAgB,gBAAgB,eAAuB,YAAwC;CAC7F,MAAM,UAAU,cAAc,QAAQ,eAAe,GAAG;AACxD,KAAI,WACF,QAAO,OAAO,SAAS,YAAY,QAAQ,CAAC,QAAQ,OAAO,IAAI;AAEjE,QAAO,OAAO,MAAM,SAAS,QAAQ;;;AAIvC,IAAa,kCAAkC;;;;;;;;;;;;;;AAe/C,SAAgB,gCAAwC;AACtD,QAAO;EACL,QAAQ,gCAAgC;EACxC;EACA;EACA;EACD,CAAC,KAAK,KAAK;;;;;;;;;;;;;;;;;AAkBd,SAAgB,oBACd,GACA,YACA,UACQ;CACR,MAAM,QAAQ,EAAE;AAChB,KAAI,CAAC,SAAS,MAAM,WAAW,EAAG,QAAO;CACzC,MAAM,MAAM,KAAK,UAAU,EAAE,KAAK;CAIlC,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,IAErC,SAAQ,GAAG,gCAAgC,mBADxB,gBAAgB,MAAM,IAAI,WAAW,CACiB,wBAAwB,IAAI,IAAI,MAAM;AAEjH,QAAO;;;;;;;;;;AAWT,SAAgB,uBAAuB,OAAmB,YAA6B;AACrF,KAAI,MAAM,mBAAmB,MAAM,qBAGjC,QAAO,mBAFY,gBAAgB,MAAM,sBAAsB,WAAW,CAErC;AAEvC,QAAO;;;;;ACnET,IAAa,uBACX;;;;;;;;AASF,SAAgB,qBAAqB,OAAkC;AAiBrE,QAhBc,MAAM,QAAQ,MAAM,IAAI,CACV,KAAK,SAAS;AACxC,MAAI,KAAK,WAAW,QAAQ,IAAI,KAAK,SAAS,KAAK,CAEjD,QAAO;AAET,MAAI,KAAK,WAAW,OAAO,IAAI,KAAK,SAAS,IAAI,CAE/C,QAAO;AAET,MAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS,IAAI,CAE5C,QAAO;AAET,SAAO;GACP,CACmB,KAAK,IAAI;;;;;;;;;;;;AAahC,SAAgB,qBAAqB,QAAsB,YAA6B;AACtF,KAAI,OAAO,WAAW,EACpB,QAAO;AAQT,QAAO,KALQ,OAAO,KAAK,MAAM;EAE/B,MAAM,YAAY,oBAAoB,GAAG,YADxB,EAAE,SAAS,WAAW,oBAAoB,EAAE,KACC;AAC9D,SAAO,GAAG,EAAE,KAAK,IAAI;GACrB,CACiB,KAAK,KAAK,CAAC;;;;;;;;;;;;;;AAehC,SAAgB,8BAAwC;CACtD,MAAM,QAAkB,EAAE;CAC1B,MAAM,YAAY;CASlB,MAAM,uBACJ;CAIF,MAAM,eACJ;AAEF,OAAM,KAAK,mEAAmE;AAC9E,OAAM,KAAK,6BAA6B;AAWxC,OAAM,KAAK,eAAe,UAAU,MAAM;AAC1C,OAAM,KAAK,eAAe,eAAe;AACzC,OAAM,KAAK,gDAAgD;AAC3D,OAAM,KAAK,wBAAwB,uBAAuB;AAC1D,OAAM,KAAK,sCAAsC;AAwBjD,OAAM,KAAK,0BAA0B;AACrC,OAAM,KAAK,gBAAgB,UAAU,MAAM;AAC3C,OAAM,KAAK,6CAA6C;AACxD,OAAM,KAAK,qEAAqE;AAChF,OAAM,KAAK,0BAA0B,uBAAuB;AAC5D,OAAM,KAAK,UAAU;AACrB,OAAM,KAAK,qCAAqC;AAEhD,OAAM,KAAK,MAAM;AACjB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT,SAAgB,yBAAyB,QAAsB,YAA+B;CAC5F,MAAM,QAAkB,EAAE;CAC1B,MAAM,YAAY;CAclB,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,mBAAmB,MAAM,OAAO,SAAS;EAI/C,MAAM,aAAa,mBACf,kBAAkB,qBAAqB,MAAM,QAAQ,WAAW,KAChE;EAEJ,MAAM,mBAAmB,MAAM,kBAC3B,uBAAuB,OAAO,WAAW,GACzC;EAYJ,MAAM,0BAA0B,mBAC5B,0BAA0B,iBAAiB,KAC3C;AAGJ,WAAS,KACP,GAAG,UAAU,cAAc,MAAM,QAAQ,KAAK,WAAW,IAAI,wBAAwB,IACtF;AAGD,MAAI,kBAAkB;GACpB,MAAM,kBAAkB,qBAAqB,MAAM;AACnD,OAAI,iBAAiB;IAcnB,MAAM,2BAA2B,mBAC7B,uDAAuD,iBAAiB,qBAAqB,iBAAiB,OAC9G;AAQJ,aAAS,KACP,GAAG,UAAU,eAAe,gBAAgB,6CAA6C,yBAAyB,IACnH;;;;AAKP,OAAM,KAAK,6BAA6B;AACxC,KAAI,SAAS,WAAW,GAAG;AAGzB,QAAM,KAAK,MAAM;AACjB,SAAO;;AAET,OAAM,KAAK,QAAQ;AACnB,OAAM,KAAK,eAAe;AAC1B,MAAK,MAAM,WAAW,SACpB,OAAM,KAAK,cAAc,QAAQ,GAAG;AAEtC,OAAM,KAAK,qCAAqC;AAChD,OAAM,KAAK,MAAM;AAEjB,QAAO;;;;;;;;;;;;;;;;;;;AC9PT,SAAgB,iBAAiB,MAAiB,UAA0B,EAAE,EAAU;CACtF,MAAM,SAAuB,EAAE;AAC/B,eAAc,KAAK,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO;AAGxC,QAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,QAAQ,CAAC;AAKzD,QAAO,sBAAsB,QAFV,QAAQ,aAAa,QAAQ,OAEA;;;;;;;;AASlD,SAAS,cACP,MACA,gBACA,qBACA,QACM;CASN,MAAM,gBACJ,KAAK,UAAU,cAAc,KAAK,OAAO,UAAU,gBAAgB,GAC/D,KAAK,OAAO,WACZ,KAAA;CAMN,MAAM,SAAS,CAAC,GAAG,eAAe;AAClC,KAAI,KAAK,WAAW;EAClB,MAAM,iBAAiB,gBAAgB,KAAA,IAAY,uBAAuB,KAAK;AAC/E,SAAO,KAAK;GACV,MAAM,KAAK;GACX,MAAM,oBAAoB,KAAK,YAAY;GAI3C,qBAAqB;GACtB,CAAC;;CAIJ,MAAM,oBAAoB,gBACtB,CAAC,GAAG,qBAAqB,cAAc,GACvC;CAGJ,MAAM,SAAS,CAAC,CAAC,KAAK;CACtB,MAAM,aAAa,CAAC,CAAC,KAAK;AAE1B,KAAI,UAAU,YAAY;EAOxB,MAAM,iBAAiB,kBAAkB,SAAS,IAAI,CAAC,GAAG,kBAAkB,CAAC,SAAS,GAAG,EAAE;EAC3F,MAAM,iBAA+B,OAAO,KAAK,MAAM;GACrD,MAAM,iBACJ,eAAe,SAAS,IACpB,iBACA,EAAE,sBACA,CAAC,EAAE,oBAAoB,GACvB,KAAA;AACR,UAAO;IACL,MAAM,EAAE;IACR,MAAM,EAAE;IACR;IACD;IACD;EAEF,MAAM,QAAoB;GACxB,SAAS,KAAK;GACd,QAAQ;GACR,iBAAiB;GACjB;GACD;AAGD,MAAI;OACE,KAAK,UAAU,cAAc,KAAK,OAAO,UAAU,eAAe,EAAE;AACtE,UAAM,kBAAkB;AACxB,UAAM,uBAAuB,KAAK,OAAO;cAChC,KAAK,QAAQ,cAAc,KAAK,KAAK,UAAU,eAAe,EAAE;AACzE,UAAM,kBAAkB;AACxB,UAAM,uBAAuB,KAAK,KAAK;;;AAI3C,SAAO,KAAK,MAAM;;AAIpB,MAAK,MAAM,SAAS,KAAK,SACvB,eAAc,OAAO,QAAQ,mBAAmB,OAAO;AAIzD,MAAK,MAAM,GAAG,SAAS,KAAK,MAC1B,eAAc,MAAM,QAAQ,mBAAmB,OAAO;;;;;AAO1D,SAAS,oBAAoB,aAAyC;AACpE,SAAQ,aAAR;EACE,KAAK,YACH,QAAO;EACT,KAAK,qBACH,QAAO;EACT,QACE,QAAO;;;;;;;;;AAUb,SAAS,cAAc,UAAkB,YAA6B;AACpE,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,UAAU,aAAa,UAAU,QAAQ;EAC/C,MAAM,IAAI,WAAW,QAAQ,uBAAuB,OAAO;AAC3D,SACE,IAAI,OAAO,gCAAgC,EAAE,KAAK,CAAC,KAAK,QAAQ,IAChE,IAAI,OAAO,wBAAwB,EAAE,aAAa,CAAC,KAAK,QAAQ;SAE5D;AACN,SAAO;;;;;;;;;;AAWX,SAAS,uBAAuB,MAAuC;AACrE,KAAI,KAAK,UAAU,cAAc,KAAK,OAAO,UAAU,gBAAgB,CACrE,QAAO,KAAK,OAAO;AAErB,KAAI,KAAK,QAAQ,cAAc,KAAK,KAAK,UAAU,gBAAgB,CACjE,QAAO,KAAK,KAAK;;;;;AAQrB,SAAS,sBAAsB,QAAsB,YAA6B;CAChF,MAAM,QAAkB,EAAE;AAE1B,OAAM,KAAK,iEAAiE;AAC5E,OAAM,KAAK,iEAAiE;AAC5E,OAAM,KAAK,GAAG;AAKd,OAAM,KAAK,aAAa;AACxB,OAAM,KAAK,GAAG;AAKd,OAAM,KAAK,+BAA+B,CAAC;AAC3C,OAAM,KAAK,GAAG;AACd,OAAM,KAAK,oCAAoC;AAC/C,OAAM,KAAK,uBAAuB;AAElC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,iBAAiB,MAAM,QAAQ,WAAW;EAC7D,MAAM,mBAAmB,uBAAuB,OAAO,WAAW;AAElE,QAAM,KAAK,QAAQ,MAAM,QAAQ,MAAM;AACvC,QAAM,KAAK,wBAAwB,aAAa;AAChD,QAAM,KAAK,uBAAuB,mBAAmB;AACrD,QAAM,KAAK,QAAQ;;AAGrB,OAAM,KAAK,MAAM;AACjB,OAAM,KAAK,IAAI;AACf,OAAM,KAAK,GAAG;CAGd,MAAM,aAAa,OAAO,QAAQ,MAAM,CAAC,EAAE,WAAW;CAOtD,MAAM,gBAAgB,OAAO,QAAQ,MAAM,EAAE,OAAO,SAAS,EAAE;AAE/D,KAAI,cAAc,SAAS,KAAK,WAAW,SAAS,GAAG;AACrD,QAAM,KAAK,2CAA2C;AACtD,QAAM,KACJ,gIACD;AACD,QAAM,KAAK,GAAG;AAGd,MAAI,cAAc,SAAS,GAAG;AAC5B,QAAK,MAAM,SAAS,eAAe;IACjC,MAAM,aAAa,iBAAiB,MAAM,QAAQ,WAAW;AAC7D,UAAM,KAAK,8CAA8C,MAAM,QAAQ,MAAM,aAAa;;AAE5F,SAAM,KAAK,0EAA0E;AACrF,SAAM,KAAK,GAAG;;AAIhB,MAAI,WAAW,SAAS,GAAG;AACzB,SAAM,KAAK,GAAG,8BAA8B,YAAY,WAAW,CAAC;AACpE,SAAM,KAAK,GAAG;;AAehB,MAAI,WAAW,SAAS,GAAG;AACzB,SAAM,KAAK,kEAAkE;AAC7E,SAAM,KAAK,GAAG,yBAAyB,YAAY,WAAW,CAAC;AAC/D,SAAM,KAAK,GAAG;;AAGhB,QAAM,KAAK,IAAI;AACf,QAAM,KAAK,GAAG;;AAOhB,OAAM,KAAK,2CAA2C;AACtD,OAAM,KAAK,+EAA+E;AAC1F,OAAM,KAAK,GAAG;AACd,OAAM,KAAK,GAAG,6BAA6B,CAAC;AAC5C,OAAM,KAAK,IAAI;AACf,OAAM,KAAK,GAAG;AAEd,QAAO,MAAM,KAAK,KAAK;;;;;AAMzB,SAAS,iBAAiB,QAAsB,YAA6B;AAC3E,KAAI,OAAO,WAAW,EACpB,QAAO;AAOT,QAAO,KAJQ,OAAO,KAAK,MAAM;EAC/B,MAAM,YAAY,oBAAoB,GAAG,YAAY,EAAE,KAAK;AAC5D,SAAO,GAAG,EAAE,KAAK,IAAI;GACrB,CACiB,KAAK,KAAK,CAAC;;;;;;;;;;;AAYhC,SAAS,8BAA8B,QAAsB,YAA+B;CAC1F,MAAM,QAAkB,EAAE;AAE1B,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,mBAAmB,MAAM,kBAC3B,uBAAuB,OAAO,WAAW,GACzC;AACJ,QAAM,KACJ,+CAA+C,MAAM,QAAQ,+CAA+C,iBAAiB,cAAc,iBAAiB,IAC7J;;AAIH,OAAM,KACJ,2KACD;AAED,QAAO;;;;;;;;;;ACpTT,SAAgB,4BAA4B,MAA0C;CACpF,MAAM,WAAkC,EAAE;AAC1C,sBAAqB,MAAM,CAAC,KAAK,EAAE,SAAS;AAC5C,QAAO;;;;;AAMT,SAAS,qBACP,MACA,WACA,UACM;AAEN,MAAK,MAAM,SAAS,KAAK,SACvB,KAAI,MAAM,gBAAgB,kBAAkB,MAAM,mBAEhD,6BAA4B,OAAO,WAAW,SAAS;KAEvD,sBAAqB,OAAO,CAAC,GAAG,WAAW,MAAM,EAAE,SAAS;AAKhE,MAAK,MAAM,GAAG,SAAS,KAAK,MAC1B,sBAAqB,MAAM,WAAW,SAAS;;;;;;AAQnD,SAAS,4BACP,kBACA,WACA,UACM;CACN,MAAM,SAAS,iBAAiB;CAChC,MAAM,cAAc,iBAAiB;CAGrC,MAAM,gBAAgB,UAAU,UAAU,SAAS,GAAG;CACtD,MAAM,kBAAkB,uBAAuB,eAAe,OAAO;AAKrE,2BACE,kBAJA,oBAAoB,MAAM,IAAI,gBAAgB,GAAG,gBAAgB,GAAG,eAMpE,eACA,CAAC,GAAG,WAAW,iBAAiB,EAChC,SACD;;;;;;AAOH,SAAS,0BACP,MACA,oBACA,oBACA,aACA,UACM;AACN,KAAI,KAAK,KACP,UAAS,KAAK;EACZ,oBAAoB;EACpB;EACA,aAAa,CAAC,GAAG,YAAY;EAC9B,CAAC;AAGJ,MAAK,MAAM,SAAS,KAAK,SAKvB,2BACE,OAJA,MAAM,gBAAgB,UAClB,qBACA,GAAG,mBAAmB,GAAG,MAAM,eAInC,oBACA,CAAC,GAAG,aAAa,MAAM,EACvB,SACD;;;;;;;;;;;;;;;;;AAmBL,SAAS,uBAAuB,eAAuB,QAAoC;AACzF,SAAQ,QAAR;EACE,KAAK,MACH,QAAO;EACT,KAAK,QAAQ;GACX,MAAM,QAAQ,cAAc,MAAM,IAAI,CAAC,OAAO,QAAQ;AACtD,SAAM,KAAK;AACX,UAAO,MAAM,WAAW,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI;;EAEvD,KAAK,QACH,QAAO;EACT,KAAK,YAAY;GACf,MAAM,QAAQ,cAAc,MAAM,IAAI,CAAC,OAAO,QAAQ;AACtD,SAAM,KAAK;AACX,SAAM,KAAK;AACX,UAAO,MAAM,WAAW,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI"}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
//#region src/shared/merge-search-params.ts
|
|
2
|
-
/**
|
|
3
|
-
* Shared utility for merging preserved search params into a target URL.
|
|
4
|
-
*
|
|
5
|
-
* Used by both <Link> (client) and redirect() (server). Extracted to a shared
|
|
6
|
-
* module to avoid importing client code ('use client') from server modules.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Merge preserved search params from the current URL into a target href.
|
|
10
|
-
*
|
|
11
|
-
* When `preserve` is `true`, all current search params are merged.
|
|
12
|
-
* When `preserve` is a `string[]`, only the named params are merged.
|
|
13
|
-
*
|
|
14
|
-
* The target href's own search params take precedence — preserved params
|
|
15
|
-
* are only added if the target doesn't already define them.
|
|
16
|
-
*
|
|
17
|
-
* @param targetHref - The resolved target href (may already contain query string)
|
|
18
|
-
* @param currentSearch - The current URL's search string (e.g. "?private=access&page=2")
|
|
19
|
-
* @param preserve - `true` to preserve all, or `string[]` to preserve specific params
|
|
20
|
-
* @returns The target href with preserved search params merged in
|
|
21
|
-
*/
|
|
22
|
-
function mergePreservedSearchParams(targetHref, currentSearch, preserve) {
|
|
23
|
-
const currentParams = new URLSearchParams(currentSearch);
|
|
24
|
-
if (currentParams.size === 0) return targetHref;
|
|
25
|
-
const hashIdx = targetHref.indexOf("#");
|
|
26
|
-
const hrefWithoutHash = hashIdx >= 0 ? targetHref.slice(0, hashIdx) : targetHref;
|
|
27
|
-
const hash = hashIdx >= 0 ? targetHref.slice(hashIdx) : "";
|
|
28
|
-
const qIdx = hrefWithoutHash.indexOf("?");
|
|
29
|
-
const targetPath = qIdx >= 0 ? hrefWithoutHash.slice(0, qIdx) : hrefWithoutHash;
|
|
30
|
-
const targetParams = new URLSearchParams(qIdx >= 0 ? hrefWithoutHash.slice(qIdx + 1) : "");
|
|
31
|
-
const merged = new URLSearchParams(targetParams);
|
|
32
|
-
for (const [key, value] of currentParams) if (!targetParams.has(key)) {
|
|
33
|
-
if (preserve === true || preserve.includes(key)) merged.append(key, value);
|
|
34
|
-
}
|
|
35
|
-
const qs = merged.toString();
|
|
36
|
-
return (qs ? `${targetPath}?${qs}` : targetPath) + hash;
|
|
37
|
-
}
|
|
38
|
-
//#endregion
|
|
39
|
-
export { mergePreservedSearchParams as t };
|
|
40
|
-
|
|
41
|
-
//# sourceMappingURL=merge-search-params-Cm_KIWDX.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"merge-search-params-Cm_KIWDX.js","names":[],"sources":["../../src/shared/merge-search-params.ts"],"sourcesContent":["/**\n * Shared utility for merging preserved search params into a target URL.\n *\n * Used by both <Link> (client) and redirect() (server). Extracted to a shared\n * module to avoid importing client code ('use client') from server modules.\n */\n\n/**\n * Merge preserved search params from the current URL into a target href.\n *\n * When `preserve` is `true`, all current search params are merged.\n * When `preserve` is a `string[]`, only the named params are merged.\n *\n * The target href's own search params take precedence — preserved params\n * are only added if the target doesn't already define them.\n *\n * @param targetHref - The resolved target href (may already contain query string)\n * @param currentSearch - The current URL's search string (e.g. \"?private=access&page=2\")\n * @param preserve - `true` to preserve all, or `string[]` to preserve specific params\n * @returns The target href with preserved search params merged in\n */\nexport function mergePreservedSearchParams(\n targetHref: string,\n currentSearch: string,\n preserve: true | string[]\n): string {\n const currentParams = new URLSearchParams(currentSearch);\n if (currentParams.size === 0) return targetHref;\n\n // Split hash fragment from target before processing query params.\n // Hash must come after query string: /path?query=value#hash\n const hashIdx = targetHref.indexOf('#');\n const hrefWithoutHash = hashIdx >= 0 ? targetHref.slice(0, hashIdx) : targetHref;\n const hash = hashIdx >= 0 ? targetHref.slice(hashIdx) : '';\n\n // Split target into path and existing query\n const qIdx = hrefWithoutHash.indexOf('?');\n const targetPath = qIdx >= 0 ? hrefWithoutHash.slice(0, qIdx) : hrefWithoutHash;\n const targetParams = new URLSearchParams(qIdx >= 0 ? hrefWithoutHash.slice(qIdx + 1) : '');\n\n // Collect params to preserve (that aren't already in the target)\n const merged = new URLSearchParams(targetParams);\n for (const [key, value] of currentParams) {\n // Only preserve if: (a) not already in target, and (b) included in preserve list\n if (!targetParams.has(key)) {\n if (preserve === true || preserve.includes(key)) {\n merged.append(key, value);\n }\n }\n }\n\n const qs = merged.toString();\n const pathWithQuery = qs ? `${targetPath}?${qs}` : targetPath;\n return pathWithQuery + hash;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,2BACd,YACA,eACA,UACQ;CACR,MAAM,gBAAgB,IAAI,gBAAgB,cAAc;AACxD,KAAI,cAAc,SAAS,EAAG,QAAO;CAIrC,MAAM,UAAU,WAAW,QAAQ,IAAI;CACvC,MAAM,kBAAkB,WAAW,IAAI,WAAW,MAAM,GAAG,QAAQ,GAAG;CACtE,MAAM,OAAO,WAAW,IAAI,WAAW,MAAM,QAAQ,GAAG;CAGxD,MAAM,OAAO,gBAAgB,QAAQ,IAAI;CACzC,MAAM,aAAa,QAAQ,IAAI,gBAAgB,MAAM,GAAG,KAAK,GAAG;CAChE,MAAM,eAAe,IAAI,gBAAgB,QAAQ,IAAI,gBAAgB,MAAM,OAAO,EAAE,GAAG,GAAG;CAG1F,MAAM,SAAS,IAAI,gBAAgB,aAAa;AAChD,MAAK,MAAM,CAAC,KAAK,UAAU,cAEzB,KAAI,CAAC,aAAa,IAAI,IAAI;MACpB,aAAa,QAAQ,SAAS,SAAS,IAAI,CAC7C,QAAO,OAAO,KAAK,MAAM;;CAK/B,MAAM,KAAK,OAAO,UAAU;AAE5B,SADsB,KAAK,GAAG,WAAW,GAAG,OAAO,cAC5B"}
|