@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.98

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.
Files changed (102) hide show
  1. package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
  2. package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  3. package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
  4. package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
  5. package/dist/_chunks/{interception-BbqMCVXa.js → walkers-VOXgavMF.js} +61 -85
  6. package/dist/_chunks/walkers-VOXgavMF.js.map +1 -0
  7. package/dist/adapters/nitro.d.ts.map +1 -1
  8. package/dist/adapters/nitro.js +55 -5
  9. package/dist/adapters/nitro.js.map +1 -1
  10. package/dist/client/index.js +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +189 -62
  13. package/dist/index.js.map +1 -1
  14. package/dist/plugins/build-report.d.ts +6 -4
  15. package/dist/plugins/build-report.d.ts.map +1 -1
  16. package/dist/plugins/dev-404-page.d.ts +8 -18
  17. package/dist/plugins/dev-404-page.d.ts.map +1 -1
  18. package/dist/routing/index.d.ts +5 -3
  19. package/dist/routing/index.d.ts.map +1 -1
  20. package/dist/routing/index.js +3 -3
  21. package/dist/routing/scanner.d.ts +1 -10
  22. package/dist/routing/scanner.d.ts.map +1 -1
  23. package/dist/routing/segment-classify.d.ts +37 -8
  24. package/dist/routing/segment-classify.d.ts.map +1 -1
  25. package/dist/routing/types.d.ts +63 -23
  26. package/dist/routing/types.d.ts.map +1 -1
  27. package/dist/routing/walkers.d.ts +51 -0
  28. package/dist/routing/walkers.d.ts.map +1 -0
  29. package/dist/server/action-handler.d.ts.map +1 -1
  30. package/dist/server/dev-holding-server.d.ts +4 -2
  31. package/dist/server/dev-holding-server.d.ts.map +1 -1
  32. package/dist/server/html-injector-core.d.ts +212 -0
  33. package/dist/server/html-injector-core.d.ts.map +1 -0
  34. package/dist/server/html-injectors.d.ts +59 -59
  35. package/dist/server/html-injectors.d.ts.map +1 -1
  36. package/dist/server/internal.js +710 -563
  37. package/dist/server/internal.js.map +1 -1
  38. package/dist/server/node-stream-transforms.d.ts +46 -49
  39. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  40. package/dist/server/pipeline-helpers.d.ts +88 -0
  41. package/dist/server/pipeline-helpers.d.ts.map +1 -0
  42. package/dist/server/pipeline-phases.d.ts +97 -0
  43. package/dist/server/pipeline-phases.d.ts.map +1 -0
  44. package/dist/server/pipeline.d.ts +53 -32
  45. package/dist/server/pipeline.d.ts.map +1 -1
  46. package/dist/server/port-resolution.d.ts +117 -0
  47. package/dist/server/port-resolution.d.ts.map +1 -0
  48. package/dist/server/route-matcher.d.ts +20 -47
  49. package/dist/server/route-matcher.d.ts.map +1 -1
  50. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  51. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +74 -0
  52. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
  53. package/dist/server/status-code-resolver.d.ts +16 -11
  54. package/dist/server/status-code-resolver.d.ts.map +1 -1
  55. package/dist/server/tree-builder.d.ts.map +1 -1
  56. package/dist/utils/directive-parser.d.ts +0 -45
  57. package/dist/utils/directive-parser.d.ts.map +1 -1
  58. package/package.json +7 -6
  59. package/src/adapters/nitro.ts +55 -5
  60. package/src/cli.ts +0 -0
  61. package/src/index.ts +84 -31
  62. package/src/plugins/build-report.ts +13 -22
  63. package/src/plugins/dev-404-page.ts +15 -41
  64. package/src/plugins/routing.ts +14 -12
  65. package/src/routing/codegen.ts +1 -1
  66. package/src/routing/convention-lint.ts +4 -4
  67. package/src/routing/index.ts +5 -3
  68. package/src/routing/interception.ts +1 -1
  69. package/src/routing/scanner.ts +17 -93
  70. package/src/routing/segment-classify.ts +107 -8
  71. package/src/routing/status-file-lint.ts +3 -3
  72. package/src/routing/types.ts +63 -23
  73. package/src/routing/walkers.ts +90 -0
  74. package/src/server/action-handler.ts +6 -0
  75. package/src/server/deny-renderer.ts +5 -5
  76. package/src/server/dev-holding-server.ts +4 -2
  77. package/src/server/fallback-error.ts +1 -1
  78. package/src/server/html-injector-core.ts +403 -0
  79. package/src/server/html-injectors.ts +158 -297
  80. package/src/server/node-stream-transforms.ts +108 -248
  81. package/src/server/pipeline-helpers.ts +180 -0
  82. package/src/server/pipeline-phases.ts +591 -0
  83. package/src/server/pipeline.ts +76 -539
  84. package/src/server/port-resolution.ts +215 -0
  85. package/src/server/route-element-builder.ts +1 -1
  86. package/src/server/route-matcher.ts +28 -60
  87. package/src/server/rsc-entry/api-handler.ts +2 -2
  88. package/src/server/rsc-entry/error-renderer.ts +1 -1
  89. package/src/server/rsc-entry/index.ts +52 -98
  90. package/src/server/rsc-entry/wrap-action-dispatch.ts +156 -0
  91. package/src/server/sitemap-generator.ts +1 -1
  92. package/src/server/slot-resolver.ts +1 -1
  93. package/src/server/status-code-resolver.ts +112 -128
  94. package/src/server/tree-builder.ts +6 -4
  95. package/src/utils/directive-parser.ts +0 -392
  96. package/LICENSE +0 -8
  97. package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
  98. package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
  99. package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
  100. package/dist/server/manifest-status-resolver.d.ts +0 -58
  101. package/dist/server/manifest-status-resolver.d.ts.map +0 -1
  102. package/src/server/manifest-status-resolver.ts +0 -215
@@ -150,4 +150,4 @@ function getMetadataRouteAutoLink(type, href) {
150
150
  //#endregion
151
151
  export { isDynamicMetadataExtension as a, getMetadataRouteServePath as i, classifyMetadataRoute as n, getMetadataRouteAutoLink as r, METADATA_ROUTE_CONVENTIONS as t };
152
152
 
153
- //# sourceMappingURL=metadata-routes-DS3eKNmf.js.map
153
+ //# sourceMappingURL=metadata-routes-BU684ls2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-routes-DS3eKNmf.js","names":[],"sources":["../../src/server/metadata-routes.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'twitter-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /** The URL path this file serves at (relative to segment). */\n servePath: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: [],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n },\n 'twitter-image': {\n type: 'twitter-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'twitter-image',\n },\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n },\n};\n\n// ─── 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\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns An object with tag/attrs for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(\n type: MetadataRouteType,\n href: string\n): { rel: string; href: string; type?: string } | null {\n switch (type) {\n case 'icon':\n return { rel: 'icon', href };\n case 'apple-icon':\n return { rel: 'apple-touch-icon', href };\n case 'manifest':\n return { rel: 'manifest', href };\n default:\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AA6CA,IAAa,6BAWT;CACF,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,YAAY;EACV,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO;EAC1B,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,EAAE;EACrB,WAAW;EACZ;CACD,QAAQ;EACN,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB;GAAC;GAAO;GAAO;GAAM;EACvC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,mBAAmB;EACjB,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,iBAAiB;EACf,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACF;;;;;;;;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;;;;;;;;;;AAWlE,SAAgB,yBACd,MACA,MACqD;AACrD,SAAQ,MAAR;EACE,KAAK,OACH,QAAO;GAAE,KAAK;GAAQ;GAAM;EAC9B,KAAK,aACH,QAAO;GAAE,KAAK;GAAoB;GAAM;EAC1C,KAAK,WACH,QAAO;GAAE,KAAK;GAAY;GAAM;EAClC,QACE,QAAO"}
1
+ {"version":3,"file":"metadata-routes-BU684ls2.js","names":[],"sources":["../../src/server/metadata-routes.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'twitter-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /** The URL path this file serves at (relative to segment). */\n servePath: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: [],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n },\n 'twitter-image': {\n type: 'twitter-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'twitter-image',\n },\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n },\n};\n\n// ─── 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\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns An object with tag/attrs for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(\n type: MetadataRouteType,\n href: string\n): { rel: string; href: string; type?: string } | null {\n switch (type) {\n case 'icon':\n return { rel: 'icon', href };\n case 'apple-icon':\n return { rel: 'apple-touch-icon', href };\n case 'manifest':\n return { rel: 'manifest', href };\n default:\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AA6CA,IAAa,6BAWT;CACF,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,UAAU;EACR,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,YAAY;EACV,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO;EAC1B,mBAAmB,CAAC,KAAK;EACzB,WAAW;EACZ;CACD,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,EAAE;EACrB,WAAW;EACZ;CACD,QAAQ;EACN,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB;GAAC;GAAO;GAAO;GAAM;EACvC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,mBAAmB;EACjB,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,iBAAiB;EACf,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,MAAM;EAChC,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACD,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,MAAM,MAAM;EAChC,WAAW;EACZ;CACF;;;;;;;;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;;;;;;;;;;AAWlE,SAAgB,yBACd,MACA,MACqD;AACrD,SAAQ,MAAR;EACE,KAAK,OACH,QAAO;GAAE,KAAK;GAAQ;GAAM;EAC9B,KAAK,aACH,QAAO;GAAE,KAAK;GAAoB;GAAM;EAC1C,KAAK,WACH,QAAO;GAAE,KAAK;GAAY;GAAM;EAClC,QACE,QAAO"}
@@ -0,0 +1,137 @@
1
+ //#region src/routing/types.ts
2
+ /** All recognized interception markers, ordered longest-first for parsing. */
3
+ var INTERCEPTION_MARKERS = [
4
+ "(..)(..)",
5
+ "(.)",
6
+ "(..)",
7
+ "(...)"
8
+ ];
9
+ /** Default page extensions */
10
+ var DEFAULT_PAGE_EXTENSIONS = [
11
+ "tsx",
12
+ "ts",
13
+ "jsx",
14
+ "js"
15
+ ];
16
+ //#endregion
17
+ //#region src/routing/segment-classify.ts
18
+ /**
19
+ * Classify a URL path segment token.
20
+ *
21
+ * Walks the string left-to-right in one pass:
22
+ * 1. If it doesn't start with '[', it's static.
23
+ * 2. Count opening brackets (1 or 2) to detect optional.
24
+ * 3. Check for '...' to detect catch-all.
25
+ * 4. Read the param name up to the closing bracket.
26
+ * 5. Validate the expected closing sequence (']' or ']]').
27
+ * 6. Reject if there are leftover characters after the close.
28
+ *
29
+ * Any structural violation → static (safe default).
30
+ */
31
+ function classifyUrlSegment(token) {
32
+ const len = token.length;
33
+ if (len === 0 || token[0] !== "[") return {
34
+ kind: "static",
35
+ value: token
36
+ };
37
+ let i = 1;
38
+ const optional = token[i] === "[";
39
+ if (optional) i++;
40
+ const catchAll = i + 2 < len && token[i] === "." && token[i + 1] === "." && token[i + 2] === ".";
41
+ if (catchAll) i += 3;
42
+ const nameStart = i;
43
+ while (i < len && token[i] !== "]") i++;
44
+ if (i >= len || i === nameStart) return {
45
+ kind: "static",
46
+ value: token
47
+ };
48
+ const name = token.slice(nameStart, i);
49
+ i++;
50
+ if (optional) {
51
+ if (i >= len || token[i] !== "]") return {
52
+ kind: "static",
53
+ value: token
54
+ };
55
+ i++;
56
+ }
57
+ if (i !== len) return {
58
+ kind: "static",
59
+ value: token
60
+ };
61
+ if (optional && catchAll) return {
62
+ kind: "optional-catch-all",
63
+ name
64
+ };
65
+ if (catchAll) return {
66
+ kind: "catch-all",
67
+ name
68
+ };
69
+ if (optional) return {
70
+ kind: "static",
71
+ value: token
72
+ };
73
+ return {
74
+ kind: "dynamic",
75
+ name
76
+ };
77
+ }
78
+ /**
79
+ * Classify a directory name into its segment type.
80
+ *
81
+ * Recognizes all timber file-system conventions in priority order:
82
+ * 1. Private folders: `_name` (excluded from routing)
83
+ * 2. Parallel route slots: `@name`
84
+ * 3. Intercepting routes: `(.)name`, `(..)name`, `(...)name`, `(..)(..)name`
85
+ * 4. Route groups: `(name)`
86
+ * 5. Bracket syntax: `[id]`, `[...slug]`, `[[...path]]` (delegated to
87
+ * `classifyUrlSegment`)
88
+ * 6. Static: anything else
89
+ *
90
+ * If you change the bracket syntax, update only `classifyUrlSegment`.
91
+ * If you change the directory-prefix conventions, update this function.
92
+ */
93
+ function classifySegment(dirName) {
94
+ if (dirName.startsWith("_")) return { type: "private" };
95
+ if (dirName.startsWith("@")) return { type: "slot" };
96
+ const interception = parseInterceptionMarker(dirName);
97
+ if (interception) return {
98
+ type: "intercepting",
99
+ interceptionMarker: interception.marker,
100
+ interceptedSegmentName: interception.segmentName
101
+ };
102
+ if (dirName.startsWith("(") && dirName.endsWith(")")) return { type: "group" };
103
+ const urlSeg = classifyUrlSegment(dirName);
104
+ if (urlSeg.kind !== "static") return {
105
+ type: urlSeg.kind,
106
+ paramName: urlSeg.name
107
+ };
108
+ return { type: "static" };
109
+ }
110
+ /**
111
+ * Parse an interception marker from a directory name.
112
+ *
113
+ * Returns the marker and the remaining segment name, or null if not an
114
+ * intercepting route. Markers are checked longest-first to avoid `(..)`
115
+ * matching before `(..)(..)`.
116
+ *
117
+ * Examples:
118
+ * "(.)photo" → { marker: "(.)", segmentName: "photo" }
119
+ * "(..)feed" → { marker: "(..)", segmentName: "feed" }
120
+ * "(...)photos" → { marker: "(...)", segmentName: "photos" }
121
+ * "(..)(..)admin" → { marker: "(..)(..)", segmentName: "admin" }
122
+ * "(marketing)" → null (route group, not interception)
123
+ */
124
+ function parseInterceptionMarker(dirName) {
125
+ for (const marker of INTERCEPTION_MARKERS) if (dirName.startsWith(marker)) {
126
+ const rest = dirName.slice(marker.length);
127
+ if (rest.length > 0 && !rest.endsWith(")")) return {
128
+ marker,
129
+ segmentName: rest
130
+ };
131
+ }
132
+ return null;
133
+ }
134
+ //#endregion
135
+ export { INTERCEPTION_MARKERS as i, classifyUrlSegment as n, DEFAULT_PAGE_EXTENSIONS as r, classifySegment as t };
136
+
137
+ //# sourceMappingURL=segment-classify-BjfuctV2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-classify-BjfuctV2.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/segment-classify.ts"],"sourcesContent":["/**\n * Route tree types for timber.js file-system routing.\n *\n * The route tree is built by scanning the app/ directory and recognizing\n * file conventions (page.*, layout.*, middleware.ts, access.ts, route.ts, etc.).\n *\n * **Single shape, two specializations** (TIM-848):\n *\n * `SegmentNode<TFile>` is the one canonical in-memory shape for the\n * timber route tree. The same interface is used at build time (with\n * `TFile = RouteFile`) and at request time (with `TFile = ManifestFile`,\n * see `server/route-matcher.ts`). Walkers parameterized over `TFile`\n * work on either, eliminating the previous duplication between\n * `SegmentNode` (Map-based) and `ManifestSegmentNode` (object-based).\n *\n * Keyed groups (`slots`, `statusFiles`, `jsonStatusFiles`,\n * `legacyStatusFiles`, `metadataRoutes`) are plain `Record<string, …>`\n * objects rather than `Map`s so that the build-time tree can be\n * serialized into the virtual route manifest with no shape transform.\n *\n * See design/07-routing.md §\"Route Tree Shape\" and design/18-build-system.md\n * §\"Route Manifest Shape\".\n */\n\n/** Segment type classification */\nexport type SegmentType =\n | 'static' // e.g. \"dashboard\"\n | 'dynamic' // e.g. \"[id]\"\n | 'catch-all' // e.g. \"[...slug]\"\n | 'optional-catch-all' // e.g. \"[[...slug]]\"\n | 'group' // e.g. \"(marketing)\"\n | 'slot' // e.g. \"@sidebar\"\n | 'intercepting' // e.g. \"(.)photo\", \"(..)photo\", \"(...)photo\"\n | 'private'; // e.g. \"_components\", \"_lib\" — excluded from routing\n\n/**\n * Intercepting route marker — indicates how many levels up to resolve the\n * intercepted route from the intercepting route's location.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\nexport type InterceptionMarker = '(.)' | '(..)' | '(...)' | '(..)(..)';\n\n/** All recognized interception markers, ordered longest-first for parsing. */\nexport const INTERCEPTION_MARKERS: InterceptionMarker[] = ['(..)(..)', '(.)', '(..)', '(...)'];\n\n/**\n * A single file discovered in a route segment at build time.\n *\n * The runtime equivalent (`ManifestFile`, defined in\n * `server/route-matcher.ts`) replaces `extension` with a lazy `load`\n * function. Walkers that only need `filePath` are parameterized over\n * `TFile` and accept either.\n */\nexport interface RouteFile {\n /** Absolute path to the file */\n filePath: string;\n /** File extension without leading dot (e.g. \"tsx\", \"ts\", \"mdx\") */\n extension: string;\n}\n\n/**\n * A node in the segment tree.\n *\n * Generic over `TFile` so the same interface describes both the\n * build-time tree (`SegmentNode<RouteFile>`, the default) and the\n * runtime manifest tree (`SegmentNode<ManifestFile>`, aliased as\n * `ManifestSegmentNode`). All keyed groups use `Record` (not `Map`)\n * so the build-time tree serializes to the virtual route manifest\n * with no shape transform.\n */\nexport interface SegmentNode<TFile = RouteFile> {\n /** The raw directory name (e.g. \"dashboard\", \"[id]\", \"(auth)\", \"@sidebar\") */\n segmentName: string;\n /** Classified segment type */\n segmentType: SegmentType;\n /** The dynamic param name, if dynamic (e.g. \"id\" for \"[id]\", \"slug\" for \"[...slug]\") */\n paramName?: string;\n /** The URL path prefix at this segment level (e.g. \"/dashboard\") */\n urlPath: string;\n /** For intercepting segments: the marker used, e.g. \"(.)\". */\n interceptionMarker?: InterceptionMarker;\n /**\n * For intercepting segments: the segment name after stripping the marker.\n * E.g., for \"(.)photo\" this is \"photo\".\n */\n interceptedSegmentName?: string;\n\n // --- File conventions ---\n page?: TFile;\n layout?: TFile;\n middleware?: TFile;\n access?: TFile;\n route?: TFile;\n /**\n * params.ts — isomorphic convention file exporting segmentParams and/or searchParams.\n * Discovered by the scanner like middleware.ts and access.ts.\n * See design/07-routing.md §\"params.ts Convention File\"\n */\n params?: TFile;\n error?: TFile;\n default?: TFile;\n /** Status-code files: 4xx.tsx, 5xx.tsx, {status}.tsx (component format) */\n statusFiles?: Record<string, TFile>;\n /** JSON status-code files: 4xx.json, 5xx.json, {status}.json */\n jsonStatusFiles?: Record<string, TFile>;\n /** denied.tsx — slot-only denial rendering */\n denied?: TFile;\n /** Legacy compat: not-found.tsx (maps to 404), forbidden.tsx (403), unauthorized.tsx (401) */\n legacyStatusFiles?: Record<string, TFile>;\n\n /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */\n metadataRoutes?: Record<string, TFile>;\n\n // --- Children ---\n children: SegmentNode<TFile>[];\n /** Parallel route slots (keyed by slot name without @) */\n slots: Record<string, SegmentNode<TFile>>;\n}\n\n/**\n * The full route tree output from the scanner (or the root of the\n * runtime route manifest, when `TFile = ManifestFile`).\n *\n * Generic so the same wrapper carries app-root metadata for both\n * shapes. The runtime manifest extends this with `viteRoot` (see\n * `ManifestRoot` in `server/route-matcher.ts`).\n */\nexport interface RouteTree<TFile = RouteFile> {\n /** The root segment node (representing app/) */\n root: SegmentNode<TFile>;\n /** All discovered proxy.ts files (should be at most one, in app/) */\n proxy?: TFile;\n /**\n * Global error page: app/global-error.{tsx,ts,jsx,js}\n *\n * Rendered as a standalone full-page replacement (no layout wrapping)\n * when no segment-level error file is found. SSR-only render path.\n * Must provide its own <html> and <body>.\n *\n * See design/10-error-handling.md §\"Tier 2 — Global Error Page\"\n */\n globalError?: TFile;\n}\n\n/** Configuration passed to the scanner */\nexport interface ScannerConfig {\n /** Recognized page/layout extensions (without dots). Default: ['tsx', 'ts', 'jsx', 'js'] */\n pageExtensions?: string[];\n}\n\n/** Default page extensions */\nexport const DEFAULT_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];\n","/**\n * Shared segment classifier — both URL tokens and filesystem directory names.\n *\n * `classifyUrlSegment(token)` is a pure single-pass character parser that\n * classifies a route segment token (e.g. \"dashboard\", \"[id]\", \"[...slug]\",\n * \"[[...path]]\") into a typed discriminated union. NO regex, NO Node.js-only\n * APIs — safe to import from browser code (used by `Link` interpolation).\n *\n * `classifySegment(dirName)` is the build-time directory-name classifier\n * used by the scanner. It recognizes timber-only conventions (private\n * `_*`, parallel `@*`, route groups `(name)`, intercepting routes\n * `(.)`/`(..)`/`(...)`/`(..)(..)`) and delegates bracket syntax to\n * `classifyUrlSegment`. It is the **single source of truth** for what\n * counts as a routing segment — there is no separate copy in the\n * scanner. (TIM-848.)\n *\n * Malformed input falls through to `{ kind: 'static' }` — the safe default.\n *\n * If you change the bracket syntax, update ONLY this file. Every\n * consumer imports from here.\n *\n * See design/07-routing.md §\"Route Segments\"\n */\n\nimport type { InterceptionMarker, SegmentType } from './types.js';\nimport { INTERCEPTION_MARKERS } from './types.js';\n\nexport type UrlSegment =\n | { kind: 'static'; value: string }\n | { kind: 'dynamic'; name: string }\n | { kind: 'catch-all'; name: string }\n | { kind: 'optional-catch-all'; name: string };\n\n/**\n * Classify a URL path segment token.\n *\n * Walks the string left-to-right in one pass:\n * 1. If it doesn't start with '[', it's static.\n * 2. Count opening brackets (1 or 2) to detect optional.\n * 3. Check for '...' to detect catch-all.\n * 4. Read the param name up to the closing bracket.\n * 5. Validate the expected closing sequence (']' or ']]').\n * 6. Reject if there are leftover characters after the close.\n *\n * Any structural violation → static (safe default).\n */\nexport function classifyUrlSegment(token: string): UrlSegment {\n const len = token.length;\n\n // Must start with '[' to be dynamic\n if (len === 0 || token[0] !== '[') {\n return { kind: 'static', value: token };\n }\n\n let i = 1;\n\n // Check for optional: '[[...'\n const optional = token[i] === '[';\n if (optional) i++;\n\n // Check for catch-all: '...'\n const catchAll = i + 2 < len && token[i] === '.' && token[i + 1] === '.' && token[i + 2] === '.';\n if (catchAll) i += 3;\n\n // Read param name — everything up to ']'\n const nameStart = i;\n while (i < len && token[i] !== ']') i++;\n\n // Must have found a ']' and name must be non-empty\n if (i >= len || i === nameStart) {\n return { kind: 'static', value: token };\n }\n\n const name = token.slice(nameStart, i);\n i++; // skip first ']'\n\n // Optional requires a second ']'\n if (optional) {\n if (i >= len || token[i] !== ']') {\n return { kind: 'static', value: token };\n }\n i++;\n }\n\n // Must be at end of string — no trailing characters\n if (i !== len) {\n return { kind: 'static', value: token };\n }\n\n if (optional && catchAll) return { kind: 'optional-catch-all', name };\n if (catchAll) return { kind: 'catch-all', name };\n if (optional) {\n // '[[name]]' without '...' is malformed — not a valid segment syntax\n return { kind: 'static', value: token };\n }\n return { kind: 'dynamic', name };\n}\n\n// ─── Directory-name classifier (build-time scanner) ─────────────────────────\n\n/** Result of classifying a filesystem directory name. */\nexport interface SegmentClassification {\n type: SegmentType;\n paramName?: string;\n interceptionMarker?: InterceptionMarker;\n interceptedSegmentName?: string;\n}\n\n/**\n * Classify a directory name into its segment type.\n *\n * Recognizes all timber file-system conventions in priority order:\n * 1. Private folders: `_name` (excluded from routing)\n * 2. Parallel route slots: `@name`\n * 3. Intercepting routes: `(.)name`, `(..)name`, `(...)name`, `(..)(..)name`\n * 4. Route groups: `(name)`\n * 5. Bracket syntax: `[id]`, `[...slug]`, `[[...path]]` (delegated to\n * `classifyUrlSegment`)\n * 6. Static: anything else\n *\n * If you change the bracket syntax, update only `classifyUrlSegment`.\n * If you change the directory-prefix conventions, update this function.\n */\nexport function classifySegment(dirName: string): SegmentClassification {\n // Private folder: _name (excluded from routing)\n if (dirName.startsWith('_')) {\n return { type: 'private' };\n }\n\n // Parallel route slot: @name\n if (dirName.startsWith('@')) {\n return { type: 'slot' };\n }\n\n // Intercepting routes: (.)name, (..)name, (...)name, (..)(..)name\n // Check before route groups since intercepting markers also start with (\n const interception = parseInterceptionMarker(dirName);\n if (interception) {\n return {\n type: 'intercepting',\n interceptionMarker: interception.marker,\n interceptedSegmentName: interception.segmentName,\n };\n }\n\n // Route group: (name)\n if (dirName.startsWith('(') && dirName.endsWith(')')) {\n return { type: 'group' };\n }\n\n // Bracket-syntax segments: [param], [...param], [[...param]]\n const urlSeg = classifyUrlSegment(dirName);\n if (urlSeg.kind !== 'static') {\n return { type: urlSeg.kind, paramName: urlSeg.name };\n }\n\n return { type: 'static' };\n}\n\n/**\n * Parse an interception marker from a directory name.\n *\n * Returns the marker and the remaining segment name, or null if not an\n * intercepting route. Markers are checked longest-first to avoid `(..)`\n * matching before `(..)(..)`.\n *\n * Examples:\n * \"(.)photo\" → { marker: \"(.)\", segmentName: \"photo\" }\n * \"(..)feed\" → { marker: \"(..)\", segmentName: \"feed\" }\n * \"(...)photos\" → { marker: \"(...)\", segmentName: \"photos\" }\n * \"(..)(..)admin\" → { marker: \"(..)(..)\", segmentName: \"admin\" }\n * \"(marketing)\" → null (route group, not interception)\n */\nfunction parseInterceptionMarker(\n dirName: string\n): { marker: InterceptionMarker; segmentName: string } | null {\n for (const marker of INTERCEPTION_MARKERS) {\n if (dirName.startsWith(marker)) {\n const rest = dirName.slice(marker.length);\n // Must have a segment name after the marker, and the rest must not\n // be empty or end with ) (which would be a route group like \"(auth)\")\n if (rest.length > 0 && !rest.endsWith(')')) {\n return { marker, segmentName: rest };\n }\n }\n }\n return null;\n}\n"],"mappings":";;AA4CA,IAAa,uBAA6C;CAAC;CAAY;CAAO;CAAQ;CAAQ;;AA4G9F,IAAa,0BAA0B;CAAC;CAAO;CAAM;CAAO;CAAK;;;;;;;;;;;;;;;;AC1GjE,SAAgB,mBAAmB,OAA2B;CAC5D,MAAM,MAAM,MAAM;AAGlB,KAAI,QAAQ,KAAK,MAAM,OAAO,IAC5B,QAAO;EAAE,MAAM;EAAU,OAAO;EAAO;CAGzC,IAAI,IAAI;CAGR,MAAM,WAAW,MAAM,OAAO;AAC9B,KAAI,SAAU;CAGd,MAAM,WAAW,IAAI,IAAI,OAAO,MAAM,OAAO,OAAO,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI,OAAO;AAC7F,KAAI,SAAU,MAAK;CAGnB,MAAM,YAAY;AAClB,QAAO,IAAI,OAAO,MAAM,OAAO,IAAK;AAGpC,KAAI,KAAK,OAAO,MAAM,UACpB,QAAO;EAAE,MAAM;EAAU,OAAO;EAAO;CAGzC,MAAM,OAAO,MAAM,MAAM,WAAW,EAAE;AACtC;AAGA,KAAI,UAAU;AACZ,MAAI,KAAK,OAAO,MAAM,OAAO,IAC3B,QAAO;GAAE,MAAM;GAAU,OAAO;GAAO;AAEzC;;AAIF,KAAI,MAAM,IACR,QAAO;EAAE,MAAM;EAAU,OAAO;EAAO;AAGzC,KAAI,YAAY,SAAU,QAAO;EAAE,MAAM;EAAsB;EAAM;AACrE,KAAI,SAAU,QAAO;EAAE,MAAM;EAAa;EAAM;AAChD,KAAI,SAEF,QAAO;EAAE,MAAM;EAAU,OAAO;EAAO;AAEzC,QAAO;EAAE,MAAM;EAAW;EAAM;;;;;;;;;;;;;;;;;AA4BlC,SAAgB,gBAAgB,SAAwC;AAEtE,KAAI,QAAQ,WAAW,IAAI,CACzB,QAAO,EAAE,MAAM,WAAW;AAI5B,KAAI,QAAQ,WAAW,IAAI,CACzB,QAAO,EAAE,MAAM,QAAQ;CAKzB,MAAM,eAAe,wBAAwB,QAAQ;AACrD,KAAI,aACF,QAAO;EACL,MAAM;EACN,oBAAoB,aAAa;EACjC,wBAAwB,aAAa;EACtC;AAIH,KAAI,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,CAClD,QAAO,EAAE,MAAM,SAAS;CAI1B,MAAM,SAAS,mBAAmB,QAAQ;AAC1C,KAAI,OAAO,SAAS,SAClB,QAAO;EAAE,MAAM,OAAO;EAAM,WAAW,OAAO;EAAM;AAGtD,QAAO,EAAE,MAAM,UAAU;;;;;;;;;;;;;;;;AAiB3B,SAAS,wBACP,SAC4D;AAC5D,MAAK,MAAM,UAAU,qBACnB,KAAI,QAAQ,WAAW,OAAO,EAAE;EAC9B,MAAM,OAAO,QAAQ,MAAM,OAAO,OAAO;AAGzC,MAAI,KAAK,SAAS,KAAK,CAAC,KAAK,SAAS,IAAI,CACxC,QAAO;GAAE;GAAQ,aAAa;GAAM;;AAI1C,QAAO"}
@@ -1,23 +1,7 @@
1
- import { t as classifyUrlSegment } from "./segment-classify-BDNn6EzD.js";
2
- import { a as isDynamicMetadataExtension, n as classifyMetadataRoute } from "./metadata-routes-DS3eKNmf.js";
1
+ import { r as DEFAULT_PAGE_EXTENSIONS, t as classifySegment } from "./segment-classify-BjfuctV2.js";
2
+ import { a as isDynamicMetadataExtension, n as classifyMetadataRoute } from "./metadata-routes-BU684ls2.js";
3
3
  import { basename, extname, join, posix, relative } from "node:path";
4
4
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
5
- //#region src/routing/types.ts
6
- /** All recognized interception markers, ordered longest-first for parsing. */
7
- var INTERCEPTION_MARKERS = [
8
- "(..)(..)",
9
- "(.)",
10
- "(..)",
11
- "(...)"
12
- ];
13
- /** Default page extensions */
14
- var DEFAULT_PAGE_EXTENSIONS = [
15
- "tsx",
16
- "ts",
17
- "jsx",
18
- "js"
19
- ];
20
- //#endregion
21
5
  //#region src/routing/scanner.ts
22
6
  /**
23
7
  * Route discovery scanner.
@@ -107,54 +91,10 @@ function createSegmentNode(segmentName, segmentType, urlPath, paramName, interce
107
91
  interceptionMarker,
108
92
  interceptedSegmentName,
109
93
  children: [],
110
- slots: /* @__PURE__ */ new Map()
94
+ slots: {}
111
95
  };
112
96
  }
113
97
  /**
114
- * Classify a directory name into its segment type.
115
- */
116
- function classifySegment(dirName) {
117
- if (dirName.startsWith("_")) return { type: "private" };
118
- if (dirName.startsWith("@")) return { type: "slot" };
119
- const interception = parseInterceptionMarker(dirName);
120
- if (interception) return {
121
- type: "intercepting",
122
- interceptionMarker: interception.marker,
123
- interceptedSegmentName: interception.segmentName
124
- };
125
- if (dirName.startsWith("(") && dirName.endsWith(")")) return { type: "group" };
126
- const urlSeg = classifyUrlSegment(dirName);
127
- if (urlSeg.kind !== "static") return {
128
- type: urlSeg.kind,
129
- paramName: urlSeg.name
130
- };
131
- return { type: "static" };
132
- }
133
- /**
134
- * Parse an interception marker from a directory name.
135
- *
136
- * Returns the marker and the remaining segment name, or null if not an
137
- * intercepting route. Markers are checked longest-first to avoid (..)
138
- * matching before (..)(..).
139
- *
140
- * Examples:
141
- * "(.)photo" → { marker: "(.)", segmentName: "photo" }
142
- * "(..)feed" → { marker: "(..)", segmentName: "feed" }
143
- * "(...)photos" → { marker: "(...)", segmentName: "photos" }
144
- * "(..)(..)admin" → { marker: "(..)(..)", segmentName: "admin" }
145
- * "(marketing)" → null (route group, not interception)
146
- */
147
- function parseInterceptionMarker(dirName) {
148
- for (const marker of INTERCEPTION_MARKERS) if (dirName.startsWith(marker)) {
149
- const rest = dirName.slice(marker.length);
150
- if (rest.length > 0 && !rest.endsWith(")")) return {
151
- marker,
152
- segmentName: rest
153
- };
154
- }
155
- return null;
156
- }
157
- /**
158
98
  * Compute the URL path for a child segment given its parent's URL path.
159
99
  * Route groups, slots, and intercepting routes do NOT add URL depth.
160
100
  */
@@ -227,42 +167,42 @@ function scanSegmentFiles(dirPath, node, extSet) {
227
167
  continue;
228
168
  }
229
169
  if (STATUS_CODE_PATTERN.test(name) && ext === "json") {
230
- if (!node.jsonStatusFiles) node.jsonStatusFiles = /* @__PURE__ */ new Map();
231
- node.jsonStatusFiles.set(name, {
170
+ if (!node.jsonStatusFiles) node.jsonStatusFiles = {};
171
+ node.jsonStatusFiles[name] = {
232
172
  filePath: fullPath,
233
173
  extension: ext
234
- });
174
+ };
235
175
  continue;
236
176
  }
237
177
  if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {
238
- if (!node.statusFiles) node.statusFiles = /* @__PURE__ */ new Map();
239
- node.statusFiles.set(name, {
178
+ if (!node.statusFiles) node.statusFiles = {};
179
+ node.statusFiles[name] = {
240
180
  filePath: fullPath,
241
181
  extension: ext
242
- });
182
+ };
243
183
  continue;
244
184
  }
245
185
  if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {
246
- if (!node.legacyStatusFiles) node.legacyStatusFiles = /* @__PURE__ */ new Map();
247
- node.legacyStatusFiles.set(name, {
186
+ if (!node.legacyStatusFiles) node.legacyStatusFiles = {};
187
+ node.legacyStatusFiles[name] = {
248
188
  filePath: fullPath,
249
189
  extension: ext
250
- });
190
+ };
251
191
  continue;
252
192
  }
253
193
  if (classifyMetadataRoute(entry)) {
254
- if (!node.metadataRoutes) node.metadataRoutes = /* @__PURE__ */ new Map();
255
- const existing = node.metadataRoutes.get(name);
194
+ if (!node.metadataRoutes) node.metadataRoutes = {};
195
+ const existing = node.metadataRoutes[name];
256
196
  if (existing) {
257
197
  const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);
258
- if (isDynamicMetadataExtension(name, ext) || !existingIsDynamic) node.metadataRoutes.set(name, {
198
+ if (isDynamicMetadataExtension(name, ext) || !existingIsDynamic) node.metadataRoutes[name] = {
259
199
  filePath: fullPath,
260
200
  extension: ext
261
- });
262
- } else node.metadataRoutes.set(name, {
201
+ };
202
+ } else node.metadataRoutes[name] = {
263
203
  filePath: fullPath,
264
204
  extension: ext
265
- });
205
+ };
266
206
  }
267
207
  }
268
208
  if (node.route && node.page) throw new Error(`Build error: route.ts and page.* cannot coexist in the same segment.\n route.ts: ${node.route.filePath}\n page: ${node.page.filePath}\nA URL is either an API endpoint or a rendered page, not both.`);
@@ -293,7 +233,7 @@ function scanChildren(dirPath, parentNode, extSet) {
293
233
  scanChildren(fullPath, childNode, extSet);
294
234
  if (type === "slot") {
295
235
  const slotName = entry.slice(1);
296
- parentNode.slots.set(slotName, childNode);
236
+ parentNode.slots[slotName] = childNode;
297
237
  } else parentNode.children.push(childNode);
298
238
  }
299
239
  }
@@ -328,7 +268,7 @@ function collectRoutableLeaves(node, seen, segmentPath, insideSlot) {
328
268
  }
329
269
  }
330
270
  for (const child of node.children) collectRoutableLeaves(child, seen, currentPath, insideSlot);
331
- for (const [, slotNode] of node.slots) collectRoutableLeaves(slotNode, seen, currentPath, true);
271
+ for (const slotNode of Object.values(node.slots)) collectRoutableLeaves(slotNode, seen, currentPath, true);
332
272
  }
333
273
  /**
334
274
  * Validate that no route chain contains duplicate dynamic param names.
@@ -356,7 +296,7 @@ function walkForDuplicateParams(node, seen) {
356
296
  seen.set(node.paramName, node.urlPath || "/");
357
297
  }
358
298
  for (const child of node.children) walkForDuplicateParams(child, seen);
359
- for (const [, slotNode] of node.slots) walkForDuplicateParams(slotNode, new Map(seen));
299
+ for (const slotNode of Object.values(node.slots)) walkForDuplicateParams(slotNode, new Map(seen));
360
300
  }
361
301
  /**
362
302
  * Find a fixed-extension file (proxy.ts) in a directory.
@@ -663,7 +603,7 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
663
603
  routes.push(entry);
664
604
  }
665
605
  for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes);
666
- for (const [, slot] of node.slots) collectRoutes(slot, params, nextAncestorFiles, routes);
606
+ for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, routes);
667
607
  }
668
608
  /**
669
609
  * Determine the TypeScript type for a segment's param.
@@ -808,7 +748,7 @@ function collectInterceptionRewrites(root) {
808
748
  function walkForInterceptions(node, ancestors, rewrites) {
809
749
  for (const child of node.children) if (child.segmentType === "intercepting" && child.interceptionMarker) collectFromInterceptingNode(child, ancestors, rewrites);
810
750
  else walkForInterceptions(child, [...ancestors, child], rewrites);
811
- for (const [, slot] of node.slots) walkForInterceptions(slot, ancestors, rewrites);
751
+ for (const slot of Object.values(node.slots)) walkForInterceptions(slot, ancestors, rewrites);
812
752
  }
813
753
  /**
814
754
  * For an intercepting segment, find all leaf pages in its sub-tree and
@@ -866,6 +806,42 @@ function computeInterceptedBase(parentUrlPath, marker) {
866
806
  }
867
807
  }
868
808
  //#endregion
869
- export { DEFAULT_PAGE_EXTENSIONS as a, scanRoutes as i, generateRouteMap as n, INTERCEPTION_MARKERS as o, classifySegment as r, collectInterceptionRewrites as t };
809
+ //#region src/routing/walkers.ts
810
+ /**
811
+ * Walk a segment tree and collect every leaf with a `page` or `route`
812
+ * handler. Generic over `TFile` so it works on both the build-time
813
+ * scanner output and the runtime manifest tree.
814
+ *
815
+ * - Pages and route handlers at the same URL produce two distinct
816
+ * entries (the build report deduplicates by URL afterward).
817
+ * - Parallel slots are skipped unless `includeSlots: true` (slots
818
+ * share their parent's URL and are not addressable on their own).
819
+ * - Result is sorted by `urlPath` for deterministic output.
820
+ */
821
+ function collectLeafRoutes(root, options = {}) {
822
+ const { includeSlots = false } = options;
823
+ const result = [];
824
+ walk(root, [], result, includeSlots);
825
+ result.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
826
+ return result;
827
+ }
828
+ function walk(node, chain, result, includeSlots) {
829
+ const currentChain = [...chain, node];
830
+ const path = node.urlPath || "/";
831
+ if (node.page) result.push({
832
+ urlPath: path,
833
+ segments: currentChain,
834
+ page: node.page
835
+ });
836
+ if (node.route) result.push({
837
+ urlPath: path,
838
+ segments: currentChain,
839
+ route: node.route
840
+ });
841
+ for (const child of node.children) walk(child, currentChain, result, includeSlots);
842
+ if (includeSlots) for (const slotNode of Object.values(node.slots)) walk(slotNode, currentChain, result, includeSlots);
843
+ }
844
+ //#endregion
845
+ export { scanRoutes as i, collectInterceptionRewrites as n, generateRouteMap as r, collectLeafRoutes as t };
870
846
 
871
- //# sourceMappingURL=interception-BbqMCVXa.js.map
847
+ //# sourceMappingURL=walkers-VOXgavMF.js.map