@timber-js/app 0.2.0-alpha.121 → 0.2.0-alpha.123
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/LICENSE +8 -0
- package/dist/_chunks/{actions-Dy8vOjFt.js → actions-ZlcKx0H9.js} +5 -5
- package/dist/_chunks/actions-ZlcKx0H9.js.map +1 -0
- package/dist/_chunks/{build-output-helper-B1n0sUqj.js → build-output-helper-DYOWsPjZ.js} +47 -6
- package/dist/_chunks/build-output-helper-DYOWsPjZ.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-4HcQIziG.js +2 -0
- package/dist/_chunks/{cli-schema-sync-ByZPGGmS.js → cli-schema-sync-VcCf1TFY.js} +17 -3
- package/dist/_chunks/{cli-schema-sync-ByZPGGmS.js.map → cli-schema-sync-VcCf1TFY.js.map} +1 -1
- package/dist/_chunks/{define-CPVQaWbx.js → define-C0V_eKiW.js} +6 -18
- package/dist/_chunks/define-C0V_eKiW.js.map +1 -0
- package/dist/_chunks/{handler-store-CcwNX-gk.js → handler-store-bIAstrxQ.js} +1 -1
- package/dist/_chunks/{handler-store-CcwNX-gk.js.map → handler-store-bIAstrxQ.js.map} +1 -1
- package/dist/_chunks/{merge-search-params-az6ZiBeU.js → merge-search-params-Cm1j99HC.js} +1 -1
- package/dist/_chunks/{merge-search-params-az6ZiBeU.js.map → merge-search-params-Cm1j99HC.js.map} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-DESG8uk5.js} +32 -19
- package/dist/_chunks/metadata-routes-DESG8uk5.js.map +1 -0
- package/dist/_chunks/{navigation-root-DoRTeBm5.js → navigation-root-Ds_hDtRW.js} +2 -2
- package/dist/_chunks/{navigation-root-DoRTeBm5.js.map → navigation-root-Ds_hDtRW.js.map} +1 -1
- package/dist/_chunks/{resolve-schema-Bf8eC6q6.js → resolve-schema-DU2nbVOi.js} +5 -1
- package/dist/_chunks/resolve-schema-DU2nbVOi.js.map +1 -0
- package/dist/_chunks/{router-ref-CpvjBqdu.js → router-ref-B2-2kIIC.js} +2 -2
- package/dist/_chunks/{router-ref-CpvjBqdu.js.map → router-ref-B2-2kIIC.js.map} +1 -1
- package/dist/_chunks/{segment-classify-CnfLLvir.js → segment-classify-BjfuctV2.js} +1 -1
- package/dist/_chunks/{segment-classify-CnfLLvir.js.map → segment-classify-BjfuctV2.js.map} +1 -1
- package/dist/_chunks/{segment-context-EsfQpS--.js → segment-context-Bg0pFbLT.js} +1 -1
- package/dist/_chunks/{segment-context-EsfQpS--.js.map → segment-context-Bg0pFbLT.js.map} +1 -1
- package/dist/_chunks/{ssr-data-m9-cNEIw.js → ssr-data-DVX8S7vj.js} +1 -1
- package/dist/_chunks/{ssr-data-m9-cNEIw.js.map → ssr-data-DVX8S7vj.js.map} +1 -1
- package/dist/_chunks/{use-segment-params-CtIAjqK0.js → use-segment-params-Du7u25cK.js} +3 -8
- package/dist/_chunks/{use-segment-params-CtIAjqK0.js.map → use-segment-params-Du7u25cK.js.map} +1 -1
- package/dist/_chunks/{validation-BMU5MPzP.js → validation-AfEFM5ze.js} +1 -1
- package/dist/_chunks/{validation-BMU5MPzP.js.map → validation-AfEFM5ze.js.map} +1 -1
- package/dist/_chunks/{walkers-DNp9tHyV.js → walkers-tkw7sDlj.js} +12 -9
- package/dist/_chunks/walkers-tkw7sDlj.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +48 -15
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +67 -32
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +9 -5
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +8 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/shallow-url.d.ts +10 -0
- package/dist/client/shallow-url.d.ts.map +1 -0
- package/dist/cookies/index.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -10
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/cloudflare-wasm.d.ts +18 -0
- package/dist/plugins/cloudflare-wasm.d.ts.map +1 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +5 -4
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/actions.d.ts +3 -8
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +6 -0
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.d.ts +1 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +15 -6
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-routes.d.ts +23 -7
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +4 -2
- package/dist/server/route-matcher.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.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/tree-match.d.ts +12 -4
- package/dist/server/tree-match.d.ts.map +1 -1
- package/dist/server/types.d.ts +2 -2
- package/package.json +6 -7
- package/src/adapters/build-output-helper.ts +67 -5
- package/src/cli-schema-sync.ts +25 -4
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/action-dispatch.ts +9 -10
- package/src/client/browser-entry/index.ts +1 -1
- package/src/client/browser-entry/post-hydration.ts +12 -6
- package/src/client/index.ts +1 -5
- package/src/client/link.tsx +34 -21
- package/src/client/router.ts +74 -25
- package/src/client/rsc-fetch.ts +20 -5
- package/src/client/shallow-url.ts +22 -0
- package/src/index.ts +2 -0
- package/src/params/resolve-schema.ts +15 -0
- package/src/plugins/build-manifest.ts +57 -5
- package/src/plugins/cloudflare-wasm.ts +103 -0
- package/src/plugins/dev-server.ts +4 -2
- package/src/plugins/routing.ts +1 -1
- package/src/routing/codegen-shared.ts +8 -5
- package/src/routing/codegen.ts +5 -0
- package/src/routing/link-codegen.ts +1 -1
- package/src/segment-params/define.ts +2 -29
- package/src/server/actions.ts +4 -14
- package/src/server/build-manifest.ts +7 -0
- package/src/server/internal.ts +7 -1
- package/src/server/metadata-routes.ts +44 -19
- package/src/server/pipeline-phases.ts +8 -0
- package/src/server/request-context.ts +0 -4
- package/src/server/route-element-builder.ts +57 -11
- package/src/server/route-matcher.ts +81 -49
- package/src/server/rsc-entry/index.ts +7 -1
- package/src/server/rsc-entry/render-route.ts +7 -1
- package/src/server/ssr-entry.ts +6 -0
- package/src/server/tree-match.ts +23 -11
- package/src/server/types.ts +2 -2
- package/dist/_chunks/actions-Dy8vOjFt.js.map +0 -1
- package/dist/_chunks/build-output-helper-B1n0sUqj.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DHQLjFu3.js +0 -2
- package/dist/_chunks/define-CPVQaWbx.js.map +0 -1
- package/dist/_chunks/metadata-routes-Cjmvi3rQ.js.map +0 -1
- package/dist/_chunks/resolve-schema-Bf8eC6q6.js.map +0 -1
- package/dist/_chunks/walkers-DNp9tHyV.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-schema-sync-ByZPGGmS.js","names":[],"sources":["../../src/routing/scanner.ts","../../src/utils/startup-timer.ts","../../src/plugin-context.ts","../../src/cli-schema-sync.ts"],"sourcesContent":["/**\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 { classifySegment } from './segment-classify.js';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.js';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';\nimport { swallow } from '../server/logger.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 // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(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: {},\n };\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 (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\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 = {};\n }\n node.jsonStatusFiles[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 = {};\n }\n node.statusFiles[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 = {};\n }\n node.legacyStatusFiles[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 = {};\n }\n const existing = node.metadataRoutes[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[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[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 (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\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 record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[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 Object.values(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 Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {\n const existing = forms.get(node.paramName);\n if (existing && existing.segmentType !== node.segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${node.paramName}]`,\n 'catch-all': `[...${node.paramName}]`,\n 'optional-catch-all': `[[...${node.paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${node.paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${node.paramName}] → [${node.paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(node.paramName, {\n segmentType: node.segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\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 * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createRequire } from 'node:module';\nimport type { RouteTree } from './routing/types';\nimport type { BuildManifest } from './server/build-manifest';\nimport type { StartupTimer } from './utils/startup-timer';\nimport { createStartupTimer } from './utils/startup-timer';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\nimport type { HoldingServer } from './dev-tools/holding-server.js';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /** CSS build manifest (populated by adapter after client build, null in dev) */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * Used by adapter-build and cli preview.\n */\n buildDir: string;\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return join(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return join(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n *\n * Uses require() which works for ESM modules on Node 22.12+.\n * This keeps timber() synchronous — no async config loading needed.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. The `output` key is excluded because it defaults to 'server'\n * in createPluginContext and would always appear as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Merge file-based config into ctx.config. Inline config (already in ctx.config)\n * takes precedence — file config only fills in missing fields.\n */\nexport function mergeFileConfig(ctx: PluginContext, fileConfig: TimberUserConfig): void {\n const inline = ctx.config;\n warnConfigConflicts(inline, fileConfig);\n\n ctx.config = {\n ...fileConfig,\n ...inline,\n ...(fileConfig.limits && inline.limits\n ? { limits: { ...fileConfig.limits, ...inline.limits } }\n : {}),\n ...(fileConfig.dev && inline.dev ? { dev: { ...fileConfig.dev, ...inline.dev } } : {}),\n ...(fileConfig.mdx && inline.mdx ? { mdx: { ...fileConfig.mdx, ...inline.mdx } } : {}),\n };\n}\n","/**\n * `timber schema sync` — scan filesystem for dynamic segments and append\n * missing entries to app/schema.ts with `codec.string` defaults.\n *\n * Only adds, never removes or modifies existing entries.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.js';\nimport type { SegmentNode } from './routing/types.js';\nimport { resolveAppDir } from './plugin-context.js';\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Reconstruct bracket key from segmentName (already has brackets)\n segments.add(node.segmentName);\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Uses regex to find keys in segmentParams: { '[key]': ... } blocks.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?\\w+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': codec.string,`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nfunction appendToSchema(content: string, newSegments: string[]): string {\n // Find the closing brace of segmentParams object.\n // Strategy: find the last entry in segmentParams block, insert after it.\n // Look for the pattern: segmentParams: { ... }\n // We find the closing `},` or `}` that ends segmentParams.\n\n // Find \"segmentParams\" key position\n const spIndex = content.indexOf('segmentParams');\n if (spIndex === -1) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n\n // Find the opening brace after segmentParams\n const openBrace = content.indexOf('{', spIndex + 'segmentParams'.length);\n if (openBrace === -1) {\n throw new Error('[timber] Could not find opening brace for segmentParams.');\n }\n\n // Count braces to find the matching close\n let depth = 1;\n let i = openBrace + 1;\n while (i < content.length && depth > 0) {\n if (content[i] === '{') depth++;\n else if (content[i] === '}') depth--;\n i++;\n }\n // i now points to just after the closing brace\n const closeBrace = i - 1;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments.map((seg) => `${indent}'${seg}': codec.string,`).join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n if (filesystemSegments.length === 0) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n created: false,\n };\n }\n\n const schemaPath = findSchemaFile(appDir);\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n created: true,\n };\n }\n\n // Read existing schema and find missing segments\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n\n if (missing.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n created: false,\n };\n }\n\n // Append missing segments\n const updated = appendToSchema(content, missing);\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n created: false,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli-schema-sync-VcCf1TFY.js","names":[],"sources":["../../src/routing/scanner.ts","../../src/utils/startup-timer.ts","../../src/plugin-context.ts","../../src/cli-schema-sync.ts"],"sourcesContent":["/**\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 { classifySegment } from './segment-classify.js';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.js';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';\nimport { swallow } from '../server/logger.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 // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(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: {},\n };\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 (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\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 = {};\n }\n node.jsonStatusFiles[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 = {};\n }\n node.statusFiles[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 = {};\n }\n node.legacyStatusFiles[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 = {};\n }\n const existing = node.metadataRoutes[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[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[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 (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\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 record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[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 Object.values(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 Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {\n const existing = forms.get(node.paramName);\n if (existing && existing.segmentType !== node.segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${node.paramName}]`,\n 'catch-all': `[...${node.paramName}]`,\n 'optional-catch-all': `[[...${node.paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${node.paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${node.paramName}] → [${node.paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(node.paramName, {\n segmentType: node.segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\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 * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createRequire } from 'node:module';\nimport type { RouteTree } from './routing/types';\nimport type { BuildManifest } from './server/build-manifest';\nimport type { StartupTimer } from './utils/startup-timer';\nimport { createStartupTimer } from './utils/startup-timer';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\nimport type { HoldingServer } from './dev-tools/holding-server.js';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /** CSS build manifest (populated by adapter after client build, null in dev) */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * Used by adapter-build and cli preview.\n */\n buildDir: string;\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return join(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return join(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n *\n * Uses require() which works for ESM modules on Node 22.12+.\n * This keeps timber() synchronous — no async config loading needed.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. The `output` key is excluded because it defaults to 'server'\n * in createPluginContext and would always appear as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Merge file-based config into ctx.config. Inline config (already in ctx.config)\n * takes precedence — file config only fills in missing fields.\n */\nexport function mergeFileConfig(ctx: PluginContext, fileConfig: TimberUserConfig): void {\n const inline = ctx.config;\n warnConfigConflicts(inline, fileConfig);\n\n ctx.config = {\n ...fileConfig,\n ...inline,\n ...(fileConfig.limits && inline.limits\n ? { limits: { ...fileConfig.limits, ...inline.limits } }\n : {}),\n ...(fileConfig.dev && inline.dev ? { dev: { ...fileConfig.dev, ...inline.dev } } : {}),\n ...(fileConfig.mdx && inline.mdx ? { mdx: { ...fileConfig.mdx, ...inline.mdx } } : {}),\n };\n}\n","/**\n * `timber schema sync` — scan filesystem for dynamic segments and append\n * missing entries to app/schema.ts with `codec.string` defaults.\n *\n * Only adds, never removes or modifies existing entries.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.js';\nimport type { SegmentNode } from './routing/types.js';\nimport { resolveAppDir } from './plugin-context.js';\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Reconstruct bracket key from segmentName (already has brackets)\n segments.add(node.segmentName);\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Uses regex to find keys in segmentParams: { '[key]': ... } blocks.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?\\w+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': codec.string,`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nconst CODEC_IMPORT = \"import { codec } from '@timber-js/app/codec';\";\n\nfunction hasCodecImport(content: string): boolean {\n return /import\\s+\\{[^}]*\\bcodec\\b[^}]*\\}\\s+from\\s+['\"]@timber-js\\/app\\/codec['\"]/.test(content);\n}\n\nfunction ensureCodecImport(content: string): string {\n if (hasCodecImport(content)) return content;\n\n // Insert after the last existing import statement\n const lines = content.split('\\n');\n let lastImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*import\\s/.test(lines[i])) lastImportIndex = i;\n }\n\n if (lastImportIndex === -1) {\n return CODEC_IMPORT + '\\n' + content;\n }\n\n lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);\n return lines.join('\\n');\n}\n\nfunction appendToSchema(content: string, newSegments: string[]): string {\n content = ensureCodecImport(content);\n\n // Find \"segmentParams\" key position\n const spIndex = content.indexOf('segmentParams');\n if (spIndex === -1) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n\n // Find the opening brace after segmentParams\n const openBrace = content.indexOf('{', spIndex + 'segmentParams'.length);\n if (openBrace === -1) {\n throw new Error('[timber] Could not find opening brace for segmentParams.');\n }\n\n // Count braces to find the matching close\n let depth = 1;\n let i = openBrace + 1;\n while (i < content.length && depth > 0) {\n if (content[i] === '{') depth++;\n else if (content[i] === '}') depth--;\n i++;\n }\n // i now points to just after the closing brace\n const closeBrace = i - 1;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments.map((seg) => `${indent}'${seg}': codec.string,`).join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n if (filesystemSegments.length === 0) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n created: false,\n };\n }\n\n const schemaPath = findSchemaFile(appDir);\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n created: true,\n };\n }\n\n // Read existing schema and find missing segments\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n\n if (missing.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n created: false,\n };\n }\n\n // Append missing segments\n const updated = appendToSchema(content, missing);\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n created: false,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { a as validateSync, i as isStandardSchema, r as isCodec } from "./schema-bridge-o0SwFtXW.js";
|
|
2
|
+
import { n as useSegmentParams } from "./use-segment-params-Du7u25cK.js";
|
|
2
3
|
//#region src/segment-params/define.ts
|
|
3
4
|
var _getSegmentParamsFn;
|
|
4
|
-
var _useSegmentParamsHook;
|
|
5
|
-
/**
|
|
6
|
-
* Register the client useSegmentParams hook.
|
|
7
|
-
* Called by client entry at module load time to avoid require() at call time.
|
|
8
|
-
* @internal
|
|
9
|
-
*/
|
|
10
|
-
function _registerUseSegmentParams(hook) {
|
|
11
|
-
_useSegmentParamsHook = hook;
|
|
12
|
-
}
|
|
13
|
-
if (typeof window !== "undefined" && !_useSegmentParamsHook) import("./use-segment-params-CtIAjqK0.js").then((n) => n.r).then((mod) => {
|
|
14
|
-
if (!_useSegmentParamsHook) _useSegmentParamsHook = mod.useSegmentParams;
|
|
15
|
-
});
|
|
16
5
|
/**
|
|
17
6
|
* Register the getSegmentParams function. Called once at module load time
|
|
18
7
|
* from request-context.ts to avoid dynamic import at call time.
|
|
@@ -106,19 +95,18 @@ function defineSegmentParams(codecs) {
|
|
|
106
95
|
if (typeof window !== "undefined") throw new Error("[timber] segmentParams.get() is server-only. Use segmentParams.useSegmentParams() on the client.");
|
|
107
96
|
return getSegmentParamsFromAls();
|
|
108
97
|
}
|
|
109
|
-
function useSegmentParams() {
|
|
110
|
-
|
|
111
|
-
return _useSegmentParamsHook();
|
|
98
|
+
function useSegmentParams$1() {
|
|
99
|
+
return useSegmentParams();
|
|
112
100
|
}
|
|
113
101
|
return {
|
|
114
102
|
parse,
|
|
115
103
|
serialize,
|
|
116
104
|
get,
|
|
117
|
-
useSegmentParams,
|
|
105
|
+
useSegmentParams: useSegmentParams$1,
|
|
118
106
|
codecs: resolvedCodecs
|
|
119
107
|
};
|
|
120
108
|
}
|
|
121
109
|
//#endregion
|
|
122
|
-
export {
|
|
110
|
+
export { defineSegmentParams as n, _setGetSegmentParamsFn as t };
|
|
123
111
|
|
|
124
|
-
//# sourceMappingURL=define-
|
|
112
|
+
//# sourceMappingURL=define-C0V_eKiW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-C0V_eKiW.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';\nimport { useSegmentParams as clientUseSegmentParams } from '../client/use-segment-params.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: (() => 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: () => Record<string, string | string[]>): void {\n _getSegmentParamsFn = fn;\n}\n\nfunction getSegmentParamsFromAls(): Record<string, string | string[]> {\n if (!_getSegmentParamsFn) {\n throw new Error(\n '[timber] segmentParams.get() is only available on the server. ' +\n 'Use segmentParams.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, sync.\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 function Page() {\n * const { id } = segmentParams.get() // id: number\n * }\n * ```\n */\n get(): T;\n\n /**\n * Client hook for accessing typed segment params reactively.\n *\n * ```tsx\n * 'use client'\n * import { segmentParams } from './params'\n * export function ProductHeader() {\n * const { id } = segmentParams.useSegmentParams()\n * }\n * ```\n */\n useSegmentParams(): 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 // Skip params not present in the URL at this route depth.\n // This supports TIM-834 inheritance: a parent params.ts can declare\n // codecs for descendant segments. At shallower depths those params\n // don't exist yet — skip them instead of passing undefined to the codec.\n if (rawValue === undefined) continue;\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, sync.\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 function get(): T {\n if (typeof window !== 'undefined') {\n throw new Error(\n '[timber] segmentParams.get() is server-only. ' +\n 'Use segmentParams.useSegmentParams() on the client.'\n );\n }\n const params = getSegmentParamsFromAls();\n // params are already coerced by the pipeline — return as-is.\n return params as unknown as T;\n }\n\n // ---- useSegmentParams ----\n // Client hook that reads typed params from the navigation context.\n // Delegates to the existing useSegmentParams hook from use-segment-params.ts.\n function useSegmentParams(): T {\n const raw = clientUseSegmentParams();\n // Params are already coerced by the server pipeline before being sent\n // to the client — return as-is. Re-parsing would break non-idempotent\n // codecs (e.g., z.coerce.number() on an already-parsed number). See TIM-574.\n return raw as unknown as T;\n }\n\n const definition: ParamsDefinition<T> = {\n parse,\n serialize,\n get,\n useSegmentParams,\n codecs: resolvedCodecs as { [K in keyof T]: Codec<T[K]> },\n };\n\n return definition;\n}\n"],"mappings":";;;AA8BA,IAAI;;;;;;AAOJ,SAAgB,uBAAuB,IAAmD;AACxF,uBAAsB;;AAGxB,SAAS,0BAA6D;AACpE,KAAI,CAAC,oBACH,OAAM,IAAI,MACR,oHAED;AAEH,QAAO,qBAAqB;;;;;;;;;AA2E9B,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;AAKrB,OAAI,aAAa,KAAA,EAAW;AAC5B,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,SAAS,MAAS;AAChB,MAAI,OAAO,WAAW,YACpB,OAAM,IAAI,MACR,mGAED;AAIH,SAFe,yBAAyB;;CAQ1C,SAAS,qBAAsB;AAK7B,SAJY,kBAAwB;;AAetC,QARwC;EACtC;EACA;EACA;EACA,kBAAA;EACA,QAAQ;EACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-store-
|
|
1
|
+
{"version":3,"file":"handler-store-bIAstrxQ.js","names":[],"sources":["../../src/cache/handler-store.ts"],"sourcesContent":["/**\n * Module-level cache handler singleton.\n *\n * Lazily initialized to MemoryCacheHandler on first access. The framework\n * replaces this at boot from timber.config.ts via setCacheHandler().\n *\n * This module avoids importing from ./index to prevent circular dependencies.\n */\n\n// Inline the interface to avoid circular import with index.ts\ninterface CacheHandlerLike {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\nlet handler: CacheHandlerLike | null = null;\n\n/** Replace the active cache handler. Called by the framework at boot. */\nexport function setCacheHandler(h: CacheHandlerLike): void {\n handler = h;\n}\n\n/**\n * Get the active cache handler. Creates a default MemoryCacheHandler on\n * first access if none has been set via setCacheHandler().\n */\nexport function getCacheHandler(): CacheHandlerLike {\n if (!handler) {\n // Inline a minimal LRU cache to avoid circular dep with index.ts.\n // In production, the framework always calls setCacheHandler() at boot.\n handler = createDefaultHandler();\n }\n return handler;\n}\n\nfunction createDefaultHandler(): CacheHandlerLike {\n const store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n const maxEntries = 1000;\n\n return {\n async get(key) {\n const entry = store.get(key);\n if (!entry) return null;\n store.delete(key);\n store.set(key, entry);\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n },\n async set(key, value, opts) {\n if (store.has(key)) store.delete(key);\n while (store.size >= maxEntries) {\n const oldest = store.keys().next().value;\n if (oldest !== undefined) store.delete(oldest);\n else break;\n }\n store.set(key, { value, expiresAt: Date.now() + opts.ttl * 1000, tags: opts.tags });\n },\n async invalidate(opts) {\n if (opts.key) store.delete(opts.key);\n if (opts.tag) {\n for (const [key, entry] of store) {\n if (entry.tags.includes(opts.tag)) store.delete(key);\n }\n }\n },\n };\n}\n"],"mappings":";AAgBA,IAAI,UAAmC;;AAGvC,SAAgB,gBAAgB,GAA2B;AACzD,WAAU;;;;;;AAOZ,SAAgB,kBAAoC;AAClD,KAAI,CAAC,QAGH,WAAU,sBAAsB;AAElC,QAAO;;AAGT,SAAS,uBAAyC;CAChD,MAAM,wBAAQ,IAAI,KAAoE;CACtF,MAAM,aAAa;AAEnB,QAAO;EACL,MAAM,IAAI,KAAK;GACb,MAAM,QAAQ,MAAM,IAAI,IAAI;AAC5B,OAAI,CAAC,MAAO,QAAO;AACnB,SAAM,OAAO,IAAI;AACjB,SAAM,IAAI,KAAK,MAAM;GACrB,MAAM,QAAQ,KAAK,KAAK,GAAG,MAAM;AACjC,UAAO;IAAE,OAAO,MAAM;IAAO;IAAO;;EAEtC,MAAM,IAAI,KAAK,OAAO,MAAM;AAC1B,OAAI,MAAM,IAAI,IAAI,CAAE,OAAM,OAAO,IAAI;AACrC,UAAO,MAAM,QAAQ,YAAY;IAC/B,MAAM,SAAS,MAAM,MAAM,CAAC,MAAM,CAAC;AACnC,QAAI,WAAW,KAAA,EAAW,OAAM,OAAO,OAAO;QACzC;;AAEP,SAAM,IAAI,KAAK;IAAE;IAAO,WAAW,KAAK,KAAK,GAAG,KAAK,MAAM;IAAM,MAAM,KAAK;IAAM,CAAC;;EAErF,MAAM,WAAW,MAAM;AACrB,OAAI,KAAK,IAAK,OAAM,OAAO,KAAK,IAAI;AACpC,OAAI,KAAK;SACF,MAAM,CAAC,KAAK,UAAU,MACzB,KAAI,MAAM,KAAK,SAAS,KAAK,IAAI,CAAE,OAAM,OAAO,IAAI;;;EAI3D"}
|
package/dist/_chunks/{merge-search-params-az6ZiBeU.js.map → merge-search-params-Cm1j99HC.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-search-params-
|
|
1
|
+
{"version":3,"file":"merge-search-params-Cm1j99HC.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"}
|
|
@@ -38,7 +38,7 @@ var METADATA_ROUTE_CONVENTIONS = {
|
|
|
38
38
|
contentType: "image/x-icon",
|
|
39
39
|
nestable: false,
|
|
40
40
|
staticExtensions: ["ico"],
|
|
41
|
-
dynamicExtensions: [],
|
|
41
|
+
dynamicExtensions: ["ts", "tsx"],
|
|
42
42
|
servePath: "favicon.ico"
|
|
43
43
|
},
|
|
44
44
|
"icon": {
|
|
@@ -61,14 +61,6 @@ var METADATA_ROUTE_CONVENTIONS = {
|
|
|
61
61
|
dynamicExtensions: ["ts", "tsx"],
|
|
62
62
|
servePath: "opengraph-image"
|
|
63
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
64
|
"apple-icon": {
|
|
73
65
|
type: "apple-icon",
|
|
74
66
|
contentType: "image/*",
|
|
@@ -122,32 +114,53 @@ function getMetadataRouteServePath(type) {
|
|
|
122
114
|
for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) if (convention.type === type) return convention.servePath;
|
|
123
115
|
throw new Error(`[timber] Unknown metadata route type: ${type}`);
|
|
124
116
|
}
|
|
117
|
+
var METADATA_SERVE_PATHS = new Set(Object.values(METADATA_ROUTE_CONVENTIONS).map((c) => c.servePath));
|
|
118
|
+
function isMetadataRouteServePath(pathname) {
|
|
119
|
+
const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
120
|
+
return METADATA_SERVE_PATHS.has(lastSegment);
|
|
121
|
+
}
|
|
125
122
|
/**
|
|
126
123
|
* Get the auto-link tags to inject into <head> for metadata route files
|
|
127
124
|
* discovered in a segment.
|
|
128
125
|
*
|
|
126
|
+
* Returns link tags for icon/apple-icon/manifest, and meta tags for
|
|
127
|
+
* opengraph-image (emits both og:image and twitter:image). Returns null
|
|
128
|
+
* for types that don't auto-link (favicon, sitemap, robots).
|
|
129
|
+
*
|
|
129
130
|
* @param type - The metadata route type
|
|
130
131
|
* @param href - The resolved URL path to the metadata route
|
|
131
|
-
* @returns
|
|
132
|
+
* @returns Tag descriptor(s) for the <head>, or null if no auto-link
|
|
132
133
|
*/
|
|
133
134
|
function getMetadataRouteAutoLink(type, href) {
|
|
134
135
|
switch (type) {
|
|
135
|
-
case "icon": return {
|
|
136
|
+
case "icon": return [{
|
|
137
|
+
tag: "link",
|
|
136
138
|
rel: "icon",
|
|
137
139
|
href
|
|
138
|
-
};
|
|
139
|
-
case "apple-icon": return {
|
|
140
|
+
}];
|
|
141
|
+
case "apple-icon": return [{
|
|
142
|
+
tag: "link",
|
|
140
143
|
rel: "apple-touch-icon",
|
|
141
144
|
href
|
|
142
|
-
};
|
|
143
|
-
case "manifest": return {
|
|
145
|
+
}];
|
|
146
|
+
case "manifest": return [{
|
|
147
|
+
tag: "link",
|
|
144
148
|
rel: "manifest",
|
|
145
149
|
href
|
|
146
|
-
};
|
|
147
|
-
|
|
150
|
+
}];
|
|
151
|
+
case "opengraph-image": return [{
|
|
152
|
+
tag: "meta",
|
|
153
|
+
property: "og:image",
|
|
154
|
+
content: href
|
|
155
|
+
}, {
|
|
156
|
+
tag: "meta",
|
|
157
|
+
name: "twitter:image",
|
|
158
|
+
content: href
|
|
159
|
+
}];
|
|
160
|
+
default: return [];
|
|
148
161
|
}
|
|
149
162
|
}
|
|
150
163
|
//#endregion
|
|
151
|
-
export { isDynamicMetadataExtension as a, getMetadataRouteServePath as i, classifyMetadataRoute as n, getMetadataRouteAutoLink as r, METADATA_ROUTE_CONVENTIONS as t };
|
|
164
|
+
export { isDynamicMetadataExtension as a, getMetadataRouteServePath as i, classifyMetadataRoute as n, isMetadataRouteServePath as o, getMetadataRouteAutoLink as r, METADATA_ROUTE_CONVENTIONS as t };
|
|
152
165
|
|
|
153
|
-
//# sourceMappingURL=metadata-routes-
|
|
166
|
+
//# sourceMappingURL=metadata-routes-DESG8uk5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-routes-DESG8uk5.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 | '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: ['ts', 'tsx'],\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\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// ─── MIME Type Resolution ─────────────────────────────────────────────────────\n\n/**\n * Map of file extensions to MIME types for static metadata route files.\n * Used to resolve the generic `image/*` content type for static image files.\n */\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n xml: 'application/xml',\n txt: 'text/plain',\n json: 'application/json',\n ico: 'image/x-icon',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\n\n/**\n * Resolve the concrete MIME type for a static metadata route file.\n *\n * For generic content types like `image/*`, this resolves to the actual\n * MIME type based on the file extension (e.g. `image/png` for `.png`).\n *\n * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)\n * @param extension - The file extension without leading dot (e.g. \"png\", \"xml\")\n * @returns The resolved MIME type\n */\nexport function resolveStaticContentType(conventionContentType: string, extension: string): string {\n if (conventionContentType.includes('*')) {\n return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';\n }\n return conventionContentType;\n}\n\n/**\n * Check if a file extension represents a static (non-code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"xml\", \"png\", \"ts\")\n * @returns true if this is a static file, false if dynamic or unrecognized\n */\nexport function isStaticMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.staticExtensions.includes(extension);\n}\n\n/**\n * Check if a file extension represents a dynamic (code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"ts\", \"tsx\")\n * @returns true if this is a dynamic file, false if static or unrecognized\n */\nexport function isDynamicMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.dynamicExtensions.includes(extension);\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\nconst METADATA_SERVE_PATHS = new Set(\n Object.values(METADATA_ROUTE_CONVENTIONS).map((c) => c.servePath)\n);\n\nexport function isMetadataRouteServePath(pathname: string): boolean {\n const lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);\n return METADATA_SERVE_PATHS.has(lastSegment);\n}\n\n/** A <link> auto-link tag. */\nexport interface AutoLinkLink {\n tag: 'link';\n rel: string;\n href: string;\n type?: string;\n}\n\n/** A <meta> auto-link tag. */\nexport interface AutoLinkMeta {\n tag: 'meta';\n property?: string;\n name?: string;\n content: string;\n}\n\nexport type AutoLinkTag = AutoLinkLink | AutoLinkMeta;\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * Returns link tags for icon/apple-icon/manifest, and meta tags for\n * opengraph-image (emits both og:image and twitter:image). Returns null\n * for types that don't auto-link (favicon, sitemap, robots).\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns Tag descriptor(s) for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(type: MetadataRouteType, href: string): AutoLinkTag[] {\n switch (type) {\n case 'icon':\n return [{ tag: 'link', rel: 'icon', href }];\n case 'apple-icon':\n return [{ tag: 'link', rel: 'apple-touch-icon', href }];\n case 'manifest':\n return [{ tag: 'link', rel: 'manifest', href }];\n case 'opengraph-image':\n return [\n { tag: 'meta', property: 'og:image', content: href },\n { tag: 'meta', name: 'twitter:image', content: href },\n ];\n default:\n return [];\n }\n}\n"],"mappings":";;;;;;;;;;AA4CA,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,CAAC,MAAM,MAAM;EAChC,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;CAED,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACF;;;;;;;;AAyDD,SAAgB,2BAA2B,UAAkB,WAA4B;CACvF,MAAM,aAAa,2BAA2B;AAC9C,KAAI,CAAC,WAAY,QAAO;AACxB,QAAO,WAAW,kBAAkB,SAAS,UAAU;;;;;;;;AAWzD,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;;AAGlE,IAAM,uBAAuB,IAAI,IAC/B,OAAO,OAAO,2BAA2B,CAAC,KAAK,MAAM,EAAE,UAAU,CAClE;AAED,SAAgB,yBAAyB,UAA2B;CAClE,MAAM,cAAc,SAAS,MAAM,SAAS,YAAY,IAAI,GAAG,EAAE;AACjE,QAAO,qBAAqB,IAAI,YAAY;;;;;;;;;;;;;;AAiC9C,SAAgB,yBAAyB,MAAyB,MAA6B;AAC7F,SAAQ,MAAR;EACE,KAAK,OACH,QAAO,CAAC;GAAE,KAAK;GAAQ,KAAK;GAAQ;GAAM,CAAC;EAC7C,KAAK,aACH,QAAO,CAAC;GAAE,KAAK;GAAQ,KAAK;GAAoB;GAAM,CAAC;EACzD,KAAK,WACH,QAAO,CAAC;GAAE,KAAK;GAAQ,KAAK;GAAY;GAAM,CAAC;EACjD,KAAK,kBACH,QAAO,CACL;GAAE,KAAK;GAAQ,UAAU;GAAY,SAAS;GAAM,EACpD;GAAE,KAAK;GAAQ,MAAM;GAAiB,SAAS;GAAM,CACtD;EACH,QACE,QAAO,EAAE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./use-segment-params-
|
|
1
|
+
import "./use-segment-params-Du7u25cK.js";
|
|
2
2
|
import "react";
|
|
3
3
|
//#region src/client/link-pending-store.ts
|
|
4
4
|
var LINK_PENDING_KEY = Symbol.for("__timber_link_pending");
|
|
@@ -93,4 +93,4 @@ function setHardNavigating(value) {
|
|
|
93
93
|
//#endregion
|
|
94
94
|
export { unmountLinkForCurrentNavigation as i, LINK_IDLE as n, setLinkForCurrentNavigation as r, setHardNavigating as t };
|
|
95
95
|
|
|
96
|
-
//# sourceMappingURL=navigation-root-
|
|
96
|
+
//# sourceMappingURL=navigation-root-Ds_hDtRW.js.map
|