@timber-js/app 0.1.0
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/bin/timber.mjs +5 -0
- package/dist/_chunks/error-boundary-dj-WO5uq.js +121 -0
- package/dist/_chunks/error-boundary-dj-WO5uq.js.map +1 -0
- package/dist/_chunks/format-DNt20Kt8.js +163 -0
- package/dist/_chunks/format-DNt20Kt8.js.map +1 -0
- package/dist/_chunks/interception-DIaZN1bF.js +669 -0
- package/dist/_chunks/interception-DIaZN1bF.js.map +1 -0
- package/dist/_chunks/metadata-routes-BDnswgRO.js +141 -0
- package/dist/_chunks/metadata-routes-BDnswgRO.js.map +1 -0
- package/dist/_chunks/registry-DUIpYD_x.js +20 -0
- package/dist/_chunks/registry-DUIpYD_x.js.map +1 -0
- package/dist/_chunks/request-context-D6XHINkR.js +330 -0
- package/dist/_chunks/request-context-D6XHINkR.js.map +1 -0
- package/dist/_chunks/tracing-BtOwb8O6.js +174 -0
- package/dist/_chunks/tracing-BtOwb8O6.js.map +1 -0
- package/dist/_chunks/use-cookie-8ZlA0rr3.js +125 -0
- package/dist/_chunks/use-cookie-8ZlA0rr3.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +92 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +188 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/nitro.d.ts +72 -0
- package/dist/adapters/nitro.d.ts.map +1 -0
- package/dist/adapters/nitro.js +217 -0
- package/dist/adapters/nitro.js.map +1 -0
- package/dist/adapters/types.d.ts +53 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/cache/index.d.ts +52 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +283 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/redis-handler.d.ts +45 -0
- package/dist/cache/redis-handler.d.ts.map +1 -0
- package/dist/cache/register-cached-function.d.ts +17 -0
- package/dist/cache/register-cached-function.d.ts.map +1 -0
- package/dist/cache/singleflight.d.ts +11 -0
- package/dist/cache/singleflight.d.ts.map +1 -0
- package/dist/cache/stable-stringify.d.ts +7 -0
- package/dist/cache/stable-stringify.d.ts.map +1 -0
- package/dist/cache/timber-cache.d.ts +21 -0
- package/dist/cache/timber-cache.d.ts.map +1 -0
- package/dist/cli.d.ts +44 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +135 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/browser-entry.d.ts +22 -0
- package/dist/client/browser-entry.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +42 -0
- package/dist/client/error-boundary.d.ts.map +1 -0
- package/dist/client/form.d.ts +115 -0
- package/dist/client/form.d.ts.map +1 -0
- package/dist/client/head.d.ts +16 -0
- package/dist/client/head.d.ts.map +1 -0
- package/dist/client/history.d.ts +29 -0
- package/dist/client/history.d.ts.map +1 -0
- package/dist/client/index.d.ts +32 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1218 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/link-navigate-interceptor.d.ts +28 -0
- package/dist/client/link-navigate-interceptor.d.ts.map +1 -0
- package/dist/client/link-status-provider.d.ts +11 -0
- package/dist/client/link-status-provider.d.ts.map +1 -0
- package/dist/client/link.d.ts +119 -0
- package/dist/client/link.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts +11 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +11 -0
- package/dist/client/router-ref.d.ts.map +1 -0
- package/dist/client/router.d.ts +85 -0
- package/dist/client/router.d.ts.map +1 -0
- package/dist/client/segment-cache.d.ts +88 -0
- package/dist/client/segment-cache.d.ts.map +1 -0
- package/dist/client/segment-context.d.ts +32 -0
- package/dist/client/segment-context.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +64 -0
- package/dist/client/ssr-data.d.ts.map +1 -0
- package/dist/client/types.d.ts +5 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/unload-guard.d.ts +18 -0
- package/dist/client/unload-guard.d.ts.map +1 -0
- package/dist/client/use-cookie.d.ts +37 -0
- package/dist/client/use-cookie.d.ts.map +1 -0
- package/dist/client/use-link-status.d.ts +35 -0
- package/dist/client/use-link-status.d.ts.map +1 -0
- package/dist/client/use-navigation-pending.d.ts +26 -0
- package/dist/client/use-navigation-pending.d.ts.map +1 -0
- package/dist/client/use-params.d.ts +50 -0
- package/dist/client/use-params.d.ts.map +1 -0
- package/dist/client/use-pathname.d.ts +20 -0
- package/dist/client/use-pathname.d.ts.map +1 -0
- package/dist/client/use-query-states.d.ts +36 -0
- package/dist/client/use-query-states.d.ts.map +1 -0
- package/dist/client/use-router.d.ts +39 -0
- package/dist/client/use-router.d.ts.map +1 -0
- package/dist/client/use-search-params.d.ts +24 -0
- package/dist/client/use-search-params.d.ts.map +1 -0
- package/dist/client/use-selected-layout-segment.d.ts +68 -0
- package/dist/client/use-selected-layout-segment.d.ts.map +1 -0
- package/dist/content/index.d.ts +11 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +2 -0
- package/dist/cookies/define-cookie.d.ts +61 -0
- package/dist/cookies/define-cookie.d.ts.map +1 -0
- package/dist/cookies/index.d.ts +3 -0
- package/dist/cookies/index.d.ts.map +1 -0
- package/dist/cookies/index.js +82 -0
- package/dist/cookies/index.js.map +1 -0
- package/dist/fonts/ast.d.ts +38 -0
- package/dist/fonts/ast.d.ts.map +1 -0
- package/dist/fonts/css.d.ts +43 -0
- package/dist/fonts/css.d.ts.map +1 -0
- package/dist/fonts/fallbacks.d.ts +36 -0
- package/dist/fonts/fallbacks.d.ts.map +1 -0
- package/dist/fonts/google.d.ts +122 -0
- package/dist/fonts/google.d.ts.map +1 -0
- package/dist/fonts/local.d.ts +76 -0
- package/dist/fonts/local.d.ts.map +1 -0
- package/dist/fonts/types.d.ts +85 -0
- package/dist/fonts/types.d.ts.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14701 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts +18 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -0
- package/dist/plugins/build-manifest.d.ts +79 -0
- package/dist/plugins/build-manifest.d.ts.map +1 -0
- package/dist/plugins/build-report.d.ts +63 -0
- package/dist/plugins/build-report.d.ts.map +1 -0
- package/dist/plugins/cache-transform.d.ts +36 -0
- package/dist/plugins/cache-transform.d.ts.map +1 -0
- package/dist/plugins/chunks.d.ts +45 -0
- package/dist/plugins/chunks.d.ts.map +1 -0
- package/dist/plugins/content.d.ts +19 -0
- package/dist/plugins/content.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +60 -0
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -0
- package/dist/plugins/dev-logs.d.ts +46 -0
- package/dist/plugins/dev-logs.d.ts.map +1 -0
- package/dist/plugins/dev-server.d.ts +22 -0
- package/dist/plugins/dev-server.d.ts.map +1 -0
- package/dist/plugins/dynamic-transform.d.ts +72 -0
- package/dist/plugins/dynamic-transform.d.ts.map +1 -0
- package/dist/plugins/entries.d.ts +21 -0
- package/dist/plugins/entries.d.ts.map +1 -0
- package/dist/plugins/fonts.d.ts +77 -0
- package/dist/plugins/fonts.d.ts.map +1 -0
- package/dist/plugins/mdx.d.ts +21 -0
- package/dist/plugins/mdx.d.ts.map +1 -0
- package/dist/plugins/react-prod.d.ts +18 -0
- package/dist/plugins/react-prod.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts +13 -0
- package/dist/plugins/routing.d.ts.map +1 -0
- package/dist/plugins/server-action-exports.d.ts +26 -0
- package/dist/plugins/server-action-exports.d.ts.map +1 -0
- package/dist/plugins/server-bundle.d.ts +15 -0
- package/dist/plugins/server-bundle.d.ts.map +1 -0
- package/dist/plugins/shims.d.ts +18 -0
- package/dist/plugins/shims.d.ts.map +1 -0
- package/dist/plugins/static-build.d.ts +55 -0
- package/dist/plugins/static-build.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts +29 -0
- package/dist/routing/codegen.d.ts.map +1 -0
- package/dist/routing/index.d.ts +8 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +2 -0
- package/dist/routing/interception.d.ts +46 -0
- package/dist/routing/interception.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts +28 -0
- package/dist/routing/scanner.d.ts.map +1 -0
- package/dist/routing/status-file-lint.d.ts +33 -0
- package/dist/routing/status-file-lint.d.ts.map +1 -0
- package/dist/routing/types.d.ts +81 -0
- package/dist/routing/types.d.ts.map +1 -0
- package/dist/search-params/analyze.d.ts +54 -0
- package/dist/search-params/analyze.d.ts.map +1 -0
- package/dist/search-params/codecs.d.ts +53 -0
- package/dist/search-params/codecs.d.ts.map +1 -0
- package/dist/search-params/create.d.ts +106 -0
- package/dist/search-params/create.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +7 -0
- package/dist/search-params/index.d.ts.map +1 -0
- package/dist/search-params/index.js +300 -0
- package/dist/search-params/index.js.map +1 -0
- package/dist/search-params/registry.d.ts +20 -0
- package/dist/search-params/registry.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +42 -0
- package/dist/server/access-gate.d.ts.map +1 -0
- package/dist/server/action-client.d.ts +190 -0
- package/dist/server/action-client.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts +48 -0
- package/dist/server/action-handler.d.ts.map +1 -0
- package/dist/server/actions.d.ts +108 -0
- package/dist/server/actions.d.ts.map +1 -0
- package/dist/server/asset-headers.d.ts +42 -0
- package/dist/server/asset-headers.d.ts.map +1 -0
- package/dist/server/body-limits.d.ts +30 -0
- package/dist/server/body-limits.d.ts.map +1 -0
- package/dist/server/build-manifest.d.ts +120 -0
- package/dist/server/build-manifest.d.ts.map +1 -0
- package/dist/server/canonicalize.d.ts +30 -0
- package/dist/server/canonicalize.d.ts.map +1 -0
- package/dist/server/client-module-map.d.ts +47 -0
- package/dist/server/client-module-map.d.ts.map +1 -0
- package/dist/server/csrf.d.ts +34 -0
- package/dist/server/csrf.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts +49 -0
- package/dist/server/deny-renderer.d.ts.map +1 -0
- package/dist/server/dev-logger.d.ts +44 -0
- package/dist/server/dev-logger.d.ts.map +1 -0
- package/dist/server/dev-span-processor.d.ts +29 -0
- package/dist/server/dev-span-processor.d.ts.map +1 -0
- package/dist/server/dev-warnings.d.ts +129 -0
- package/dist/server/dev-warnings.d.ts.map +1 -0
- package/dist/server/early-hints-sender.d.ts +38 -0
- package/dist/server/early-hints-sender.d.ts.map +1 -0
- package/dist/server/early-hints.d.ts +83 -0
- package/dist/server/early-hints.d.ts.map +1 -0
- package/dist/server/error-boundary-wrapper.d.ts +17 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -0
- package/dist/server/error-formatter.d.ts +17 -0
- package/dist/server/error-formatter.d.ts.map +1 -0
- package/dist/server/flush.d.ts +74 -0
- package/dist/server/flush.d.ts.map +1 -0
- package/dist/server/form-data.d.ts +60 -0
- package/dist/server/form-data.d.ts.map +1 -0
- package/dist/server/form-flash.d.ts +78 -0
- package/dist/server/form-flash.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +101 -0
- package/dist/server/html-injectors.d.ts.map +1 -0
- package/dist/server/index.d.ts +54 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2925 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/instrumentation.d.ts +61 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/logger.d.ts +83 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/manifest-status-resolver.d.ts +58 -0
- package/dist/server/manifest-status-resolver.d.ts.map +1 -0
- package/dist/server/metadata-render.d.ts +20 -0
- package/dist/server/metadata-render.d.ts.map +1 -0
- package/dist/server/metadata-routes.d.ts +67 -0
- package/dist/server/metadata-routes.d.ts.map +1 -0
- package/dist/server/metadata.d.ts +67 -0
- package/dist/server/metadata.d.ts.map +1 -0
- package/dist/server/middleware-runner.d.ts +21 -0
- package/dist/server/middleware-runner.d.ts.map +1 -0
- package/dist/server/nuqs-ssr-provider.d.ts +28 -0
- package/dist/server/nuqs-ssr-provider.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +81 -0
- package/dist/server/pipeline.d.ts.map +1 -0
- package/dist/server/prerender.d.ts +77 -0
- package/dist/server/prerender.d.ts.map +1 -0
- package/dist/server/primitives.d.ts +131 -0
- package/dist/server/primitives.d.ts.map +1 -0
- package/dist/server/proxy.d.ts +23 -0
- package/dist/server/proxy.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +175 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +66 -0
- package/dist/server/route-element-builder.d.ts.map +1 -0
- package/dist/server/route-handler.d.ts +35 -0
- package/dist/server/route-handler.d.ts.map +1 -0
- package/dist/server/route-matcher.d.ts +78 -0
- package/dist/server/route-matcher.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts +11 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -0
- package/dist/server/rsc-entry/error-renderer.d.ts +30 -0
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +73 -0
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -0
- package/dist/server/rsc-entry/index.d.ts +11 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -0
- package/dist/server/rsc-entry/ssr-bridge.d.ts +6 -0
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +34 -0
- package/dist/server/slot-resolver.d.ts.map +1 -0
- package/dist/server/ssr-entry.d.ts +73 -0
- package/dist/server/ssr-entry.d.ts.map +1 -0
- package/dist/server/ssr-render.d.ts +67 -0
- package/dist/server/ssr-render.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +77 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +99 -0
- package/dist/server/tracing.d.ts.map +1 -0
- package/dist/server/tree-builder.d.ts +116 -0
- package/dist/server/tree-builder.d.ts.map +1 -0
- package/dist/server/types.d.ts +231 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/shims/font-google.d.ts +41 -0
- package/dist/shims/font-google.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +11 -0
- package/dist/shims/headers.d.ts.map +1 -0
- package/dist/shims/image.d.ts +328 -0
- package/dist/shims/image.d.ts.map +1 -0
- package/dist/shims/link.d.ts +9 -0
- package/dist/shims/link.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +25 -0
- package/dist/shims/navigation-client.d.ts.map +1 -0
- package/dist/shims/navigation.d.ts +25 -0
- package/dist/shims/navigation.d.ts.map +1 -0
- package/dist/utils/directive-parser.d.ts +70 -0
- package/dist/utils/directive-parser.d.ts.map +1 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/startup-timer.d.ts +34 -0
- package/dist/utils/startup-timer.d.ts.map +1 -0
- package/package.json +140 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interception-DIaZN1bF.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/scanner.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 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 /** prerender.ts — signals build-time pre-rendering for this segment's shell */\n prerender?: RouteFile;\n /** search-params.ts — typed search params definition for this route */\n searchParams?: RouteFile;\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\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 { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS } from './types.js';\nimport { classifyMetadataRoute } 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', 'prerender', 'search-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 // 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 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 // Optional catch-all: [[...name]]\n if (dirName.startsWith('[[...') && dirName.endsWith(']]')) {\n const paramName = dirName.slice(5, -2);\n return { type: 'optional-catch-all', paramName };\n }\n\n // Catch-all: [...name]\n if (dirName.startsWith('[...') && dirName.endsWith(']')) {\n const paramName = dirName.slice(4, -1);\n return { type: 'catch-all', paramName };\n }\n\n // Dynamic: [name]\n if (dirName.startsWith('[') && dirName.endsWith(']')) {\n const paramName = dirName.slice(1, -1);\n return { type: 'dynamic', paramName };\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 'prerender':\n node.prerender = file;\n break;\n case 'search-params':\n node.searchParams = 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 // 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 node.metadataRoutes.set(name, { filePath: fullPath, extension: ext });\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 * 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 * 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 } from 'node:fs';\nimport { join, relative, posix } from 'node:path';\nimport type { RouteTree, SegmentNode } from './types.js';\n\n/** A single route entry extracted from the segment tree. */\ninterface RouteEntry {\n /** URL path pattern (e.g. \"/products/[id]\") */\n urlPath: string;\n /** Accumulated params from all ancestor dynamic segments */\n params: ParamEntry[];\n /** Whether this route has a co-located search-params.ts */\n hasSearchParams: boolean;\n /** Absolute path to search-params.ts (for computing relative import paths) */\n searchParamsAbsPath?: string;\n /** Whether this is an API route (route.ts) vs page route */\n isApiRoute: boolean;\n}\n\ninterface ParamEntry {\n name: string;\n type: 'string' | 'string[]' | 'string[] | undefined';\n}\n\n/** Options for route map generation. */\nexport interface CodegenOptions {\n /** Absolute path to the app/ directory. Required for search-params.ts 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 search-params.ts 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/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, [], options.appDir, 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 appDir: string | undefined,\n routes: RouteEntry[]\n): void {\n // Accumulate params from this segment\n const params = [...ancestorParams];\n if (node.paramName) {\n params.push({\n name: node.paramName,\n type: paramTypeForSegment(node.segmentType),\n });\n }\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 const entry: RouteEntry = {\n urlPath: node.urlPath,\n params: [...params],\n hasSearchParams: false,\n isApiRoute,\n };\n\n // Detect co-located search-params.ts\n if (appDir && isPage) {\n const segmentDir = resolveSegmentDir(appDir, node);\n const searchParamsFile = findSearchParamsFile(segmentDir);\n if (searchParamsFile) {\n entry.hasSearchParams = true;\n entry.searchParamsAbsPath = searchParamsFile;\n }\n }\n\n routes.push(entry);\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutes(child, params, appDir, 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, appDir, 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 * Resolve the absolute directory path for a segment node.\n *\n * Reconstructs the filesystem path by walking from appDir through\n * the segment names encoded in the urlPath, accounting for groups and slots.\n */\nfunction resolveSegmentDir(appDir: string, node: SegmentNode): string {\n // The node's page/route file path gives us the actual directory\n const file = node.page ?? node.route;\n if (file) {\n // The file is in the segment directory — go up one level\n const parts = file.filePath.split('/');\n parts.pop(); // remove filename\n return parts.join('/');\n }\n // Fallback: construct from urlPath (imprecise for groups, but acceptable)\n return appDir;\n}\n\n/**\n * Find a search-params.ts file in a directory.\n */\nfunction findSearchParamsFile(dirPath: string): string | undefined {\n for (const ext of ['ts', 'tsx']) {\n const candidate = join(dirPath, `search-params.${ext}`);\n if (existsSync(candidate)) {\n return candidate;\n }\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 lines.push(\"declare module '@timber/app' {\");\n lines.push(' interface Routes {');\n\n for (const route of routes) {\n const paramsType = formatParamsType(route.params);\n const searchParamsType = formatSearchParamsType(route, importBase);\n\n lines.push(` '${route.urlPath}': {`);\n lines.push(` params: ${paramsType}`);\n lines.push(` searchParams: ${searchParamsType}`);\n lines.push(` }`);\n }\n\n lines.push(' }');\n lines.push('}');\n lines.push('');\n\n // Generate @timber/app/server augmentation — typed searchParams() generic\n const pageRoutes = routes.filter((r) => !r.isApiRoute);\n\n if (pageRoutes.length > 0) {\n lines.push(\"declare module '@timber/app/server' {\");\n lines.push(\" import type { Routes } from '@timber/app'\");\n lines.push(\n \" export function searchParams<R extends keyof Routes>(): Promise<Routes[R]['searchParams']>\"\n );\n lines.push('}');\n lines.push('');\n }\n\n // Generate overloads for @timber/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/app/client' {\");\n lines.push(\n \" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber/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);\n lines.push(` export function useParams(route: '${route.urlPath}'): ${paramsType}`);\n }\n lines.push(' export function useParams(): 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\n if (pageRoutes.length > 0) {\n lines.push(' // Typed Link props per route');\n lines.push(...formatTypedLinkOverloads(pageRoutes, importBase));\n }\n\n lines.push('}');\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format the params type for a route entry.\n */\nfunction formatParamsType(params: ParamEntry[]): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => `${p.name}: ${p.type}`);\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Format the params type for Link props.\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 * See design/07-routing.md §\"Typed params and searchParams on <Link>\"\n */\nfunction formatLinkParamsType(params: ParamEntry[]): string {\n if (params.length === 0) {\n return '{}';\n }\n\n const fields = params.map((p) => {\n // Single dynamic segments accept string | number for convenience\n const type = p.type === 'string' ? 'string | number' : p.type;\n return `${p.name}: ${type}`;\n });\n return `{ ${fields.join('; ')} }`;\n}\n\n/**\n * Format the searchParams type for a route entry.\n *\n * When a search-params.ts exists, we reference its inferred type via an import type.\n * The import path is relative to `importBase` (the directory where the .d.ts will be\n * written). When importBase is undefined, falls back to a bare relative path.\n */\nfunction formatSearchParamsType(route: RouteEntry, importBase?: string): string {\n if (route.hasSearchParams && route.searchParamsAbsPath) {\n const absPath = route.searchParamsAbsPath.replace(/\\.(ts|tsx)$/, '');\n let importPath: string;\n if (importBase) {\n // Make the path relative to the output directory, converted to posix separators\n importPath = './' + relative(importBase, absPath).replace(/\\\\/g, '/');\n } else {\n importPath = './' + posix.basename(absPath);\n }\n // Use (typeof import('...'))[' default'] instead of import('...').default\n // because with moduleResolution:\"bundler\", import('...').default is treated as\n // a namespace member access which doesn't work for default exports.\n return `(typeof import('${importPath}'))['default'] extends import('@timber/app/search-params').SearchParamsDefinition<infer T> ? T : never`;\n }\n return '{}';\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/**\n * Generate typed Link overloads.\n *\n * For each page route, we generate a Link function overload that:\n * - Constrains href to the route pattern\n * - Types the params prop based on dynamic segments\n * - Types the searchParams prop based on search-params.ts (if present)\n *\n * Routes without dynamic segments accept href as a literal string with no params.\n * Routes with dynamic segments require a params prop.\n */\nfunction formatTypedLinkOverloads(routes: RouteEntry[], importBase?: string): string[] {\n const lines: string[] = [];\n\n for (const route of routes) {\n const hasDynamicParams = route.params.length > 0;\n const paramsType = formatLinkParamsType(route.params);\n const searchParamsType = route.hasSearchParams\n ? formatSearchParamsType(route, importBase)\n : null;\n\n if (hasDynamicParams) {\n // Route with dynamic segments — params prop required\n const spProp = searchParamsType\n ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`\n : `searchParams?: never`;\n lines.push(\n ` export function Link(props: Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {`\n );\n lines.push(` href: '${route.urlPath}'`);\n lines.push(` params: ${paramsType}`);\n lines.push(` ${spProp}`);\n lines.push(` prefetch?: boolean; scroll?: boolean; children?: import('react').ReactNode`);\n lines.push(` }): import('react').JSX.Element`);\n } else {\n // Static route — no params needed\n const spProp = searchParamsType\n ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }`\n : `searchParams?: never`;\n lines.push(\n ` export function Link(props: Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {`\n );\n lines.push(` href: '${route.urlPath}'`);\n lines.push(` params?: never`);\n lines.push(` ${spProp}`);\n lines.push(` prefetch?: boolean; scroll?: boolean; children?: import('react').ReactNode`);\n lines.push(` }): import('react').JSX.Element`);\n }\n }\n\n // Fallback overload for arbitrary string hrefs (escape hatch)\n lines.push(\n ` export function Link(props: import('./client/link.js').LinkProps): import('react').JSX.Element`\n );\n\n return lines;\n}\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":"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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region src/server/metadata-routes.ts
|
|
2
|
+
/**
|
|
3
|
+
* All recognized metadata route file conventions.
|
|
4
|
+
*
|
|
5
|
+
* Each entry maps a base file name (without extension) to its route info.
|
|
6
|
+
* The extensions determine whether the file is static or dynamic.
|
|
7
|
+
*
|
|
8
|
+
* Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg
|
|
9
|
+
* Dynamic extensions: .ts, .tsx
|
|
10
|
+
*/
|
|
11
|
+
var METADATA_ROUTE_CONVENTIONS = {
|
|
12
|
+
"sitemap": {
|
|
13
|
+
type: "sitemap",
|
|
14
|
+
contentType: "application/xml",
|
|
15
|
+
nestable: true,
|
|
16
|
+
staticExtensions: ["xml"],
|
|
17
|
+
dynamicExtensions: ["ts"],
|
|
18
|
+
servePath: "sitemap.xml"
|
|
19
|
+
},
|
|
20
|
+
"robots": {
|
|
21
|
+
type: "robots",
|
|
22
|
+
contentType: "text/plain",
|
|
23
|
+
nestable: false,
|
|
24
|
+
staticExtensions: ["txt"],
|
|
25
|
+
dynamicExtensions: ["ts"],
|
|
26
|
+
servePath: "robots.txt"
|
|
27
|
+
},
|
|
28
|
+
"manifest": {
|
|
29
|
+
type: "manifest",
|
|
30
|
+
contentType: "application/manifest+json",
|
|
31
|
+
nestable: false,
|
|
32
|
+
staticExtensions: ["json"],
|
|
33
|
+
dynamicExtensions: ["ts"],
|
|
34
|
+
servePath: "manifest.webmanifest"
|
|
35
|
+
},
|
|
36
|
+
"favicon": {
|
|
37
|
+
type: "favicon",
|
|
38
|
+
contentType: "image/x-icon",
|
|
39
|
+
nestable: false,
|
|
40
|
+
staticExtensions: ["ico"],
|
|
41
|
+
dynamicExtensions: [],
|
|
42
|
+
servePath: "favicon.ico"
|
|
43
|
+
},
|
|
44
|
+
"icon": {
|
|
45
|
+
type: "icon",
|
|
46
|
+
contentType: "image/*",
|
|
47
|
+
nestable: true,
|
|
48
|
+
staticExtensions: [
|
|
49
|
+
"png",
|
|
50
|
+
"jpg",
|
|
51
|
+
"svg"
|
|
52
|
+
],
|
|
53
|
+
dynamicExtensions: ["ts", "tsx"],
|
|
54
|
+
servePath: "icon"
|
|
55
|
+
},
|
|
56
|
+
"opengraph-image": {
|
|
57
|
+
type: "opengraph-image",
|
|
58
|
+
contentType: "image/*",
|
|
59
|
+
nestable: true,
|
|
60
|
+
staticExtensions: ["png", "jpg"],
|
|
61
|
+
dynamicExtensions: ["ts", "tsx"],
|
|
62
|
+
servePath: "opengraph-image"
|
|
63
|
+
},
|
|
64
|
+
"twitter-image": {
|
|
65
|
+
type: "twitter-image",
|
|
66
|
+
contentType: "image/*",
|
|
67
|
+
nestable: true,
|
|
68
|
+
staticExtensions: ["png", "jpg"],
|
|
69
|
+
dynamicExtensions: ["ts", "tsx"],
|
|
70
|
+
servePath: "twitter-image"
|
|
71
|
+
},
|
|
72
|
+
"apple-icon": {
|
|
73
|
+
type: "apple-icon",
|
|
74
|
+
contentType: "image/*",
|
|
75
|
+
nestable: true,
|
|
76
|
+
staticExtensions: ["png"],
|
|
77
|
+
dynamicExtensions: ["ts", "tsx"],
|
|
78
|
+
servePath: "apple-icon"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Classify a file name as a metadata route, or return null if it's not one.
|
|
83
|
+
*
|
|
84
|
+
* @param fileName - The full file name including extension (e.g. "sitemap.xml", "icon.tsx")
|
|
85
|
+
* @returns Classification info, or null if not a metadata route
|
|
86
|
+
*/
|
|
87
|
+
function classifyMetadataRoute(fileName) {
|
|
88
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
89
|
+
if (dotIndex === -1) return null;
|
|
90
|
+
const baseName = fileName.slice(0, dotIndex);
|
|
91
|
+
const ext = fileName.slice(dotIndex + 1);
|
|
92
|
+
const convention = METADATA_ROUTE_CONVENTIONS[baseName];
|
|
93
|
+
if (!convention) return null;
|
|
94
|
+
const isStatic = convention.staticExtensions.includes(ext);
|
|
95
|
+
const isDynamic = convention.dynamicExtensions.includes(ext);
|
|
96
|
+
if (!isStatic && !isDynamic) return null;
|
|
97
|
+
return {
|
|
98
|
+
type: convention.type,
|
|
99
|
+
contentType: convention.contentType,
|
|
100
|
+
nestable: convention.nestable
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get the serve path for a metadata route type.
|
|
105
|
+
*
|
|
106
|
+
* @param type - The metadata route type
|
|
107
|
+
* @returns The URL path fragment this route serves at
|
|
108
|
+
*/
|
|
109
|
+
function getMetadataRouteServePath(type) {
|
|
110
|
+
for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) if (convention.type === type) return convention.servePath;
|
|
111
|
+
throw new Error(`[timber] Unknown metadata route type: ${type}`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the auto-link tags to inject into <head> for metadata route files
|
|
115
|
+
* discovered in a segment.
|
|
116
|
+
*
|
|
117
|
+
* @param type - The metadata route type
|
|
118
|
+
* @param href - The resolved URL path to the metadata route
|
|
119
|
+
* @returns An object with tag/attrs for the <head>, or null if no auto-link
|
|
120
|
+
*/
|
|
121
|
+
function getMetadataRouteAutoLink(type, href) {
|
|
122
|
+
switch (type) {
|
|
123
|
+
case "icon": return {
|
|
124
|
+
rel: "icon",
|
|
125
|
+
href
|
|
126
|
+
};
|
|
127
|
+
case "apple-icon": return {
|
|
128
|
+
rel: "apple-touch-icon",
|
|
129
|
+
href
|
|
130
|
+
};
|
|
131
|
+
case "manifest": return {
|
|
132
|
+
rel: "manifest",
|
|
133
|
+
href
|
|
134
|
+
};
|
|
135
|
+
default: return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { getMetadataRouteServePath as i, classifyMetadataRoute as n, getMetadataRouteAutoLink as r, METADATA_ROUTE_CONVENTIONS as t };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=metadata-routes-BDnswgRO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-routes-BDnswgRO.js","names":[],"sources":["../../src/server/metadata-routes.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'twitter-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /** The URL path this file serves at (relative to segment). */\n servePath: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: [],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n },\n 'twitter-image': {\n type: 'twitter-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'twitter-image',\n },\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n },\n};\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Get the serve path for a metadata route type.\n *\n * @param type - The metadata route type\n * @returns The URL path fragment this route serves at\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) return convention.servePath;\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns An object with tag/attrs for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(\n type: MetadataRouteType,\n href: string\n): { rel: string; href: string; type?: string } | null {\n switch (type) {\n case 'icon':\n return { rel: 'icon', href };\n case 'apple-icon':\n return { rel: 'apple-touch-icon', href };\n case 'manifest':\n return { rel: 'manifest', href };\n default:\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AA6CA,IAAa,6BAWT;CACF,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,YAAY;EACV,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO;EAC1B,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,EAAE;EACrB,WAAW;EACZ;CACD,QAAQ;EACN,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB;GAAC;GAAO;GAAO;GAAM;EACvC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,mBAAmB;EACjB,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,iBAAiB;EACf,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACF;;;;;;;AAUD,SAAgB,sBAAsB,UAA4C;CAChF,MAAM,WAAW,SAAS,YAAY,IAAI;AAC1C,KAAI,aAAa,GAAI,QAAO;CAE5B,MAAM,WAAW,SAAS,MAAM,GAAG,SAAS;CAC5C,MAAM,MAAM,SAAS,MAAM,WAAW,EAAE;CAExC,MAAM,aAAa,2BAA2B;AAC9C,KAAI,CAAC,WAAY,QAAO;CAExB,MAAM,WAAW,WAAW,iBAAiB,SAAS,IAAI;CAC1D,MAAM,YAAY,WAAW,kBAAkB,SAAS,IAAI;AAE5D,KAAI,CAAC,YAAY,CAAC,UAAW,QAAO;AAEpC,QAAO;EACL,MAAM,WAAW;EACjB,aAAa,WAAW;EACxB,UAAU,WAAW;EACtB;;;;;;;;AASH,SAAgB,0BAA0B,MAAiC;AACzE,MAAK,MAAM,cAAc,OAAO,OAAO,2BAA2B,CAChE,KAAI,WAAW,SAAS,KAAM,QAAO,WAAW;AAElD,OAAM,IAAI,MAAM,yCAAyC,OAAO;;;;;;;;;;AAWlE,SAAgB,yBACd,MACA,MACqD;AACrD,SAAQ,MAAR;EACE,KAAK,OACH,QAAO;GAAE,KAAK;GAAQ;GAAM;EAC9B,KAAK,aACH,QAAO;GAAE,KAAK;GAAoB;GAAM;EAC1C,KAAK,WACH,QAAO;GAAE,KAAK;GAAY;GAAM;EAClC,QACE,QAAO"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/search-params/registry.ts
|
|
2
|
+
var registry = /* @__PURE__ */ new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Register a route's search params definition.
|
|
5
|
+
* Called by the generated route manifest loader when a route's modules load.
|
|
6
|
+
*/
|
|
7
|
+
function registerSearchParams(route, definition) {
|
|
8
|
+
registry.set(route, definition);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Look up a route's search params definition.
|
|
12
|
+
* Returns undefined if the route hasn't been loaded yet.
|
|
13
|
+
*/
|
|
14
|
+
function getSearchParams(route) {
|
|
15
|
+
return registry.get(route);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { registerSearchParams as n, getSearchParams as t };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=registry-DUIpYD_x.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-DUIpYD_x.js","names":[],"sources":["../../src/search-params/registry.ts"],"sourcesContent":["/**\n * Runtime registry for route-scoped search params definitions.\n *\n * When a route's modules load, the framework registers its search-params\n * definition here. useQueryStates('/route') resolves codecs from this map.\n *\n * Design doc: design/23-search-params.md §\"Runtime: Registration at Route Load\"\n */\n\nimport type { SearchParamsDefinition } from './create.js';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<string, SearchParamsDefinition<any>>();\n\n/**\n * Register a route's search params definition.\n * Called by the generated route manifest loader when a route's modules load.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function registerSearchParams(route: string, definition: SearchParamsDefinition<any>): void {\n registry.set(route, definition);\n}\n\n/**\n * Look up a route's search params definition.\n * Returns undefined if the route hasn't been loaded yet.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getSearchParams(route: string): SearchParamsDefinition<any> | undefined {\n return registry.get(route);\n}\n"],"mappings":";AAYA,IAAM,2BAAW,IAAI,KAA0C;;;;;AAO/D,SAAgB,qBAAqB,OAAe,YAA+C;AACjG,UAAS,IAAI,OAAO,WAAW;;;;;;AAQjC,SAAgB,gBAAgB,OAAwD;AACtF,QAAO,SAAS,IAAI,MAAM"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/server/request-context.ts
|
|
4
|
+
/**
|
|
5
|
+
* Request Context — per-request ALS store for headers() and cookies().
|
|
6
|
+
*
|
|
7
|
+
* Follows the same pattern as tracing.ts: a module-level AsyncLocalStorage
|
|
8
|
+
* instance, public accessor functions that throw outside request scope,
|
|
9
|
+
* and a framework-internal `runWithRequestContext()` to establish scope.
|
|
10
|
+
*
|
|
11
|
+
* See design/04-authorization.md §"AccessContext does not include cookies or headers"
|
|
12
|
+
* and design/11-platform.md §"AsyncLocalStorage".
|
|
13
|
+
* See design/29-cookies.md for cookie mutation semantics.
|
|
14
|
+
*/
|
|
15
|
+
var requestContextAls = new AsyncLocalStorage();
|
|
16
|
+
/**
|
|
17
|
+
* Module-level cookie signing secrets. Index 0 is the newest (used for signing).
|
|
18
|
+
* All entries are tried for verification (key rotation support).
|
|
19
|
+
*
|
|
20
|
+
* Set by the framework at startup via `setCookieSecrets()`.
|
|
21
|
+
* See design/29-cookies.md §"Signed Cookies"
|
|
22
|
+
*/
|
|
23
|
+
var _cookieSecrets = [];
|
|
24
|
+
/**
|
|
25
|
+
* Configure the cookie signing secrets.
|
|
26
|
+
*
|
|
27
|
+
* Called by the framework during server initialization with values from
|
|
28
|
+
* `cookies.secret` or `cookies.secrets` in timber.config.ts.
|
|
29
|
+
*
|
|
30
|
+
* The first secret (index 0) is used for signing new cookies.
|
|
31
|
+
* All secrets are tried for verification (supports key rotation).
|
|
32
|
+
*/
|
|
33
|
+
function setCookieSecrets(secrets) {
|
|
34
|
+
_cookieSecrets = secrets.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns a read-only view of the current request's headers.
|
|
38
|
+
*
|
|
39
|
+
* Available in middleware, access checks, server components, and server actions.
|
|
40
|
+
* Throws if called outside a request context (security principle #2: no global fallback).
|
|
41
|
+
*/
|
|
42
|
+
function headers() {
|
|
43
|
+
const store = requestContextAls.getStore();
|
|
44
|
+
if (!store) throw new Error("[timber] headers() called outside of a request context. It can only be used in middleware, access checks, server components, and server actions.");
|
|
45
|
+
return store.headers;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns a cookie accessor for the current request.
|
|
49
|
+
*
|
|
50
|
+
* Available in middleware, access checks, server components, and server actions.
|
|
51
|
+
* Throws if called outside a request context (security principle #2: no global fallback).
|
|
52
|
+
*
|
|
53
|
+
* Read methods (.get, .has, .getAll) are always available and reflect
|
|
54
|
+
* read-your-own-writes from .set() calls in the same request.
|
|
55
|
+
*
|
|
56
|
+
* Mutation methods (.set, .delete, .clear) are only available in mutable
|
|
57
|
+
* contexts (middleware.ts, server actions, route.ts handlers). Calling them
|
|
58
|
+
* in read-only contexts (access.ts, server components) throws.
|
|
59
|
+
*
|
|
60
|
+
* See design/29-cookies.md
|
|
61
|
+
*/
|
|
62
|
+
function cookies() {
|
|
63
|
+
const store = requestContextAls.getStore();
|
|
64
|
+
if (!store) throw new Error("[timber] cookies() called outside of a request context. It can only be used in middleware, access checks, server components, and server actions.");
|
|
65
|
+
if (!store.parsedCookies) store.parsedCookies = parseCookieHeader(store.cookieHeader);
|
|
66
|
+
const map = store.parsedCookies;
|
|
67
|
+
return {
|
|
68
|
+
get(name) {
|
|
69
|
+
return map.get(name);
|
|
70
|
+
},
|
|
71
|
+
has(name) {
|
|
72
|
+
return map.has(name);
|
|
73
|
+
},
|
|
74
|
+
getAll() {
|
|
75
|
+
return Array.from(map.entries()).map(([name, value]) => ({
|
|
76
|
+
name,
|
|
77
|
+
value
|
|
78
|
+
}));
|
|
79
|
+
},
|
|
80
|
+
get size() {
|
|
81
|
+
return map.size;
|
|
82
|
+
},
|
|
83
|
+
getSigned(name) {
|
|
84
|
+
const raw = map.get(name);
|
|
85
|
+
if (!raw || _cookieSecrets.length === 0) return void 0;
|
|
86
|
+
return verifySignedCookie(raw, _cookieSecrets);
|
|
87
|
+
},
|
|
88
|
+
set(name, value, options) {
|
|
89
|
+
assertMutable(store, "set");
|
|
90
|
+
if (store.flushed) {
|
|
91
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[timber] warn: cookies().set('${name}') called after response headers were committed.\n The cookie will NOT be sent. Move cookie mutations to middleware.ts, a server action,\n or a route.ts handler.`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let storedValue = value;
|
|
95
|
+
if (options?.signed) {
|
|
96
|
+
if (_cookieSecrets.length === 0) throw new Error(`[timber] cookies().set('${name}', ..., { signed: true }) requires cookies.secret or cookies.secrets in timber.config.ts.`);
|
|
97
|
+
storedValue = signCookieValue(value, _cookieSecrets[0]);
|
|
98
|
+
}
|
|
99
|
+
const opts = {
|
|
100
|
+
...DEFAULT_COOKIE_OPTIONS,
|
|
101
|
+
...options
|
|
102
|
+
};
|
|
103
|
+
store.cookieJar.set(name, {
|
|
104
|
+
name,
|
|
105
|
+
value: storedValue,
|
|
106
|
+
options: opts
|
|
107
|
+
});
|
|
108
|
+
map.set(name, storedValue);
|
|
109
|
+
},
|
|
110
|
+
delete(name, options) {
|
|
111
|
+
assertMutable(store, "delete");
|
|
112
|
+
if (store.flushed) {
|
|
113
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[timber] warn: cookies().delete('${name}') called after response headers were committed.\n The cookie will NOT be deleted. Move cookie mutations to middleware.ts, a server action,\n or a route.ts handler.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const opts = {
|
|
117
|
+
...DEFAULT_COOKIE_OPTIONS,
|
|
118
|
+
...options,
|
|
119
|
+
maxAge: 0,
|
|
120
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
121
|
+
};
|
|
122
|
+
store.cookieJar.set(name, {
|
|
123
|
+
name,
|
|
124
|
+
value: "",
|
|
125
|
+
options: opts
|
|
126
|
+
});
|
|
127
|
+
map.delete(name);
|
|
128
|
+
},
|
|
129
|
+
clear() {
|
|
130
|
+
assertMutable(store, "clear");
|
|
131
|
+
if (store.flushed) return;
|
|
132
|
+
for (const name of Array.from(map.keys())) store.cookieJar.set(name, {
|
|
133
|
+
name,
|
|
134
|
+
value: "",
|
|
135
|
+
options: {
|
|
136
|
+
...DEFAULT_COOKIE_OPTIONS,
|
|
137
|
+
maxAge: 0,
|
|
138
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
map.clear();
|
|
142
|
+
},
|
|
143
|
+
toString() {
|
|
144
|
+
return Array.from(map.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function searchParams() {
|
|
149
|
+
const store = requestContextAls.getStore();
|
|
150
|
+
if (!store) throw new Error("[timber] searchParams() called outside of a request context. It can only be used in middleware, access checks, server components, and server actions.");
|
|
151
|
+
return store.searchParamsPromise;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Replace the search params Promise for the current request with one that
|
|
155
|
+
* resolves to the typed parsed result from the route's search-params.ts.
|
|
156
|
+
* Called by the framework before rendering the page — not for app code.
|
|
157
|
+
*/
|
|
158
|
+
function setParsedSearchParams(parsed) {
|
|
159
|
+
const store = requestContextAls.getStore();
|
|
160
|
+
if (store) store.searchParamsPromise = Promise.resolve(parsed);
|
|
161
|
+
}
|
|
162
|
+
var DEFAULT_COOKIE_OPTIONS = {
|
|
163
|
+
path: "/",
|
|
164
|
+
httpOnly: true,
|
|
165
|
+
secure: true,
|
|
166
|
+
sameSite: "lax"
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Run a callback within a request context. Used by the pipeline to establish
|
|
170
|
+
* per-request ALS scope so that `headers()` and `cookies()` work.
|
|
171
|
+
*
|
|
172
|
+
* @param req - The incoming Request object.
|
|
173
|
+
* @param fn - The function to run within the request context.
|
|
174
|
+
*/
|
|
175
|
+
function runWithRequestContext(req, fn) {
|
|
176
|
+
const originalCopy = new Headers(req.headers);
|
|
177
|
+
const store = {
|
|
178
|
+
headers: freezeHeaders(req.headers),
|
|
179
|
+
originalHeaders: originalCopy,
|
|
180
|
+
cookieHeader: req.headers.get("cookie") ?? "",
|
|
181
|
+
searchParamsPromise: Promise.resolve(new URL(req.url).searchParams),
|
|
182
|
+
cookieJar: /* @__PURE__ */ new Map(),
|
|
183
|
+
flushed: false,
|
|
184
|
+
mutableContext: false
|
|
185
|
+
};
|
|
186
|
+
return requestContextAls.run(store, fn);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Enable cookie mutation for the current context. Called by the framework
|
|
190
|
+
* when entering middleware.ts, server actions, or route.ts handlers.
|
|
191
|
+
*
|
|
192
|
+
* See design/29-cookies.md §"Context Tracking"
|
|
193
|
+
*/
|
|
194
|
+
function setMutableCookieContext(mutable) {
|
|
195
|
+
const store = requestContextAls.getStore();
|
|
196
|
+
if (store) store.mutableContext = mutable;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Mark the response as flushed (headers committed). After this point,
|
|
200
|
+
* cookie mutations log a warning instead of throwing.
|
|
201
|
+
*
|
|
202
|
+
* See design/29-cookies.md §"Streaming Constraint: Post-Flush Cookie Warning"
|
|
203
|
+
*/
|
|
204
|
+
function markResponseFlushed() {
|
|
205
|
+
const store = requestContextAls.getStore();
|
|
206
|
+
if (store) store.flushed = true;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Collect all Set-Cookie headers from the cookie jar.
|
|
210
|
+
* Called by the framework at flush time to apply cookies to the response.
|
|
211
|
+
*
|
|
212
|
+
* Returns an array of serialized Set-Cookie header values.
|
|
213
|
+
*/
|
|
214
|
+
function getSetCookieHeaders() {
|
|
215
|
+
const store = requestContextAls.getStore();
|
|
216
|
+
if (!store) return [];
|
|
217
|
+
return Array.from(store.cookieJar.values()).map(serializeCookieEntry);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Apply middleware-injected request headers to the current request context.
|
|
221
|
+
*
|
|
222
|
+
* Called by the pipeline after middleware.ts runs. Merges overlay headers
|
|
223
|
+
* on top of the original request headers so downstream code (access.ts,
|
|
224
|
+
* server components, server actions) sees them via `headers()`.
|
|
225
|
+
*
|
|
226
|
+
* The original request headers are never mutated — a new frozen Headers
|
|
227
|
+
* object is created with the overlay applied on top.
|
|
228
|
+
*
|
|
229
|
+
* See design/07-routing.md §"Request Header Injection"
|
|
230
|
+
*/
|
|
231
|
+
function applyRequestHeaderOverlay(overlay) {
|
|
232
|
+
const store = requestContextAls.getStore();
|
|
233
|
+
if (!store) throw new Error("[timber] applyRequestHeaderOverlay() called outside of a request context.");
|
|
234
|
+
let hasOverlay = false;
|
|
235
|
+
overlay.forEach(() => {
|
|
236
|
+
hasOverlay = true;
|
|
237
|
+
});
|
|
238
|
+
if (!hasOverlay) return;
|
|
239
|
+
const merged = new Headers(store.originalHeaders);
|
|
240
|
+
overlay.forEach((value, key) => {
|
|
241
|
+
merged.set(key, value);
|
|
242
|
+
});
|
|
243
|
+
store.headers = freezeHeaders(merged);
|
|
244
|
+
}
|
|
245
|
+
var MUTATING_METHODS = new Set([
|
|
246
|
+
"set",
|
|
247
|
+
"append",
|
|
248
|
+
"delete"
|
|
249
|
+
]);
|
|
250
|
+
/**
|
|
251
|
+
* Wrap a Headers object in a Proxy that throws on mutating methods.
|
|
252
|
+
* Object.freeze doesn't work on Headers (native internal slots), so we
|
|
253
|
+
* intercept property access and reject set/append/delete at runtime.
|
|
254
|
+
*
|
|
255
|
+
* Read methods (get, has, entries, etc.) must be bound to the underlying
|
|
256
|
+
* Headers instance because they access private #headersList slots.
|
|
257
|
+
*/
|
|
258
|
+
function freezeHeaders(source) {
|
|
259
|
+
const copy = new Headers(source);
|
|
260
|
+
return new Proxy(copy, { get(target, prop) {
|
|
261
|
+
if (typeof prop === "string" && MUTATING_METHODS.has(prop)) return () => {
|
|
262
|
+
throw new Error(`[timber] headers() returns a read-only Headers object. Calling .${prop}() is not allowed. Use ctx.requestHeaders in middleware to inject headers for downstream components.`);
|
|
263
|
+
};
|
|
264
|
+
const value = Reflect.get(target, prop);
|
|
265
|
+
if (typeof value === "function") return value.bind(target);
|
|
266
|
+
return value;
|
|
267
|
+
} });
|
|
268
|
+
}
|
|
269
|
+
/** Throw if cookie mutation is attempted in a read-only context. */
|
|
270
|
+
function assertMutable(store, method) {
|
|
271
|
+
if (!store.mutableContext) throw new Error(`[timber] cookies().${method}() cannot be called in this context.\n Set cookies in middleware.ts, server actions, or route.ts handlers.`);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Parse a Cookie header string into a Map of name → value pairs.
|
|
275
|
+
* Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.
|
|
276
|
+
*/
|
|
277
|
+
function parseCookieHeader(header) {
|
|
278
|
+
const map = /* @__PURE__ */ new Map();
|
|
279
|
+
if (!header) return map;
|
|
280
|
+
for (const pair of header.split(";")) {
|
|
281
|
+
const eqIndex = pair.indexOf("=");
|
|
282
|
+
if (eqIndex === -1) continue;
|
|
283
|
+
const name = pair.slice(0, eqIndex).trim();
|
|
284
|
+
const value = pair.slice(eqIndex + 1).trim();
|
|
285
|
+
if (name) map.set(name, value);
|
|
286
|
+
}
|
|
287
|
+
return map;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Sign a cookie value with HMAC-SHA256.
|
|
291
|
+
* Returns `value.hex_signature`.
|
|
292
|
+
*/
|
|
293
|
+
function signCookieValue(value, secret) {
|
|
294
|
+
return `${value}.${createHmac("sha256", secret).update(value).digest("hex")}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Verify a signed cookie value against an array of secrets.
|
|
298
|
+
* Returns the original value if any secret produces a matching signature,
|
|
299
|
+
* or undefined if none match. Uses timing-safe comparison.
|
|
300
|
+
*
|
|
301
|
+
* The signed format is `value.hex_signature` — split at the last `.`.
|
|
302
|
+
*/
|
|
303
|
+
function verifySignedCookie(raw, secrets) {
|
|
304
|
+
const lastDot = raw.lastIndexOf(".");
|
|
305
|
+
if (lastDot <= 0 || lastDot === raw.length - 1) return void 0;
|
|
306
|
+
const value = raw.slice(0, lastDot);
|
|
307
|
+
const signature = raw.slice(lastDot + 1);
|
|
308
|
+
if (signature.length !== 64) return void 0;
|
|
309
|
+
const signatureBuffer = Buffer.from(signature, "hex");
|
|
310
|
+
if (signatureBuffer.length !== 32) return void 0;
|
|
311
|
+
for (const secret of secrets) if (timingSafeEqual(createHmac("sha256", secret).update(value).digest(), signatureBuffer)) return value;
|
|
312
|
+
}
|
|
313
|
+
/** Serialize a CookieEntry into a Set-Cookie header value. */
|
|
314
|
+
function serializeCookieEntry(entry) {
|
|
315
|
+
const parts = [`${entry.name}=${entry.value}`];
|
|
316
|
+
const opts = entry.options;
|
|
317
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
318
|
+
if (opts.path) parts.push(`Path=${opts.path}`);
|
|
319
|
+
if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);
|
|
320
|
+
if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
|
|
321
|
+
if (opts.httpOnly) parts.push("HttpOnly");
|
|
322
|
+
if (opts.secure) parts.push("Secure");
|
|
323
|
+
if (opts.sameSite) parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);
|
|
324
|
+
if (opts.partitioned) parts.push("Partitioned");
|
|
325
|
+
return parts.join("; ");
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
export { markResponseFlushed as a, setCookieSecrets as c, headers as i, setMutableCookieContext as l, cookies as n, runWithRequestContext as o, getSetCookieHeaders as r, searchParams as s, applyRequestHeaderOverlay as t, setParsedSearchParams as u };
|
|
329
|
+
|
|
330
|
+
//# sourceMappingURL=request-context-D6XHINkR.js.map
|