@timber-js/app 0.2.0-alpha.96 → 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.
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
- package/dist/_chunks/{interception-BsLCA9gk.js → walkers-VOXgavMF.js} +66 -92
- package/dist/_chunks/walkers-VOXgavMF.js.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +55 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -62
- package/dist/index.js.map +1 -1
- package/dist/plugins/build-report.d.ts +6 -4
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/dev-404-page.d.ts +8 -18
- package/dist/plugins/dev-404-page.d.ts.map +1 -1
- package/dist/routing/index.d.ts +5 -3
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts +1 -10
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +37 -8
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/types.d.ts +63 -23
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +51 -0
- package/dist/routing/walkers.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/dev-holding-server.d.ts +4 -2
- package/dist/server/dev-holding-server.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +212 -0
- package/dist/server/html-injector-core.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +59 -59
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/internal.js +710 -563
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +46 -49
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +88 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +97 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +53 -32
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +117 -0
- package/dist/server/port-resolution.d.ts.map +1 -0
- package/dist/server/route-matcher.d.ts +20 -47
- 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/wrap-action-dispatch.d.ts +74 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +16 -11
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +0 -45
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/adapters/nitro.ts +55 -5
- package/src/cli.ts +0 -0
- package/src/index.ts +84 -31
- package/src/plugins/build-report.ts +13 -22
- package/src/plugins/dev-404-page.ts +15 -41
- package/src/plugins/routing.ts +14 -12
- package/src/routing/codegen.ts +1 -1
- package/src/routing/convention-lint.ts +4 -4
- package/src/routing/index.ts +5 -3
- package/src/routing/interception.ts +1 -1
- package/src/routing/link-codegen.ts +25 -13
- package/src/routing/scanner.ts +17 -93
- package/src/routing/segment-classify.ts +107 -8
- package/src/routing/status-file-lint.ts +3 -3
- package/src/routing/types.ts +63 -23
- package/src/routing/walkers.ts +90 -0
- package/src/server/action-handler.ts +6 -0
- package/src/server/deny-renderer.ts +5 -5
- package/src/server/dev-holding-server.ts +4 -2
- package/src/server/fallback-error.ts +1 -1
- package/src/server/html-injector-core.ts +403 -0
- package/src/server/html-injectors.ts +158 -297
- package/src/server/node-stream-transforms.ts +108 -248
- package/src/server/pipeline-helpers.ts +180 -0
- package/src/server/pipeline-phases.ts +591 -0
- package/src/server/pipeline.ts +76 -539
- package/src/server/port-resolution.ts +215 -0
- package/src/server/route-element-builder.ts +1 -1
- package/src/server/route-matcher.ts +28 -60
- package/src/server/rsc-entry/api-handler.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +1 -1
- package/src/server/rsc-entry/index.ts +52 -98
- package/src/server/rsc-entry/wrap-action-dispatch.ts +156 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/status-code-resolver.ts +112 -128
- package/src/server/tree-builder.ts +6 -4
- package/src/utils/directive-parser.ts +0 -392
- package/LICENSE +0 -8
- package/dist/_chunks/interception-BsLCA9gk.js.map +0 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
- package/dist/server/manifest-status-resolver.d.ts +0 -58
- package/dist/server/manifest-status-resolver.d.ts.map +0 -1
- 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-
|
|
153
|
+
//# sourceMappingURL=metadata-routes-BU684ls2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-routes-
|
|
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
|
|
2
|
-
import { a as isDynamicMetadataExtension, n as classifyMetadataRoute } from "./metadata-routes-
|
|
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:
|
|
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 =
|
|
231
|
-
node.jsonStatusFiles
|
|
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 =
|
|
239
|
-
node.statusFiles
|
|
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 =
|
|
247
|
-
node.legacyStatusFiles
|
|
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 =
|
|
255
|
-
const existing = node.metadataRoutes
|
|
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
|
|
198
|
+
if (isDynamicMetadataExtension(name, ext) || !existingIsDynamic) node.metadataRoutes[name] = {
|
|
259
199
|
filePath: fullPath,
|
|
260
200
|
extension: ext
|
|
261
|
-
}
|
|
262
|
-
} else node.metadataRoutes
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
@@ -524,13 +464,11 @@ function formatLinkCatchAllOverloads() {
|
|
|
524
464
|
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
525
465
|
lines.push(` }): import('react').JSX.Element`);
|
|
526
466
|
lines.push(` <H extends string>(`);
|
|
527
|
-
lines.push(` props:
|
|
528
|
-
lines.push(` ?
|
|
529
|
-
lines.push(`
|
|
530
|
-
lines.push(`
|
|
531
|
-
lines.push(`
|
|
532
|
-
lines.push(` }`);
|
|
533
|
-
lines.push(` : never`);
|
|
467
|
+
lines.push(` props: ${baseProps} & {`);
|
|
468
|
+
lines.push(` href: string extends H ? H : never`);
|
|
469
|
+
lines.push(` segmentParams?: Record<string, string | number | string[]>`);
|
|
470
|
+
lines.push(` searchParams?: ${catchAllSearchParams}`);
|
|
471
|
+
lines.push(` }`);
|
|
534
472
|
lines.push(` ): import('react').JSX.Element`);
|
|
535
473
|
lines.push(" }");
|
|
536
474
|
return lines;
|
|
@@ -665,7 +603,7 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
|
|
|
665
603
|
routes.push(entry);
|
|
666
604
|
}
|
|
667
605
|
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes);
|
|
668
|
-
for (const
|
|
606
|
+
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, routes);
|
|
669
607
|
}
|
|
670
608
|
/**
|
|
671
609
|
* Determine the TypeScript type for a segment's param.
|
|
@@ -810,7 +748,7 @@ function collectInterceptionRewrites(root) {
|
|
|
810
748
|
function walkForInterceptions(node, ancestors, rewrites) {
|
|
811
749
|
for (const child of node.children) if (child.segmentType === "intercepting" && child.interceptionMarker) collectFromInterceptingNode(child, ancestors, rewrites);
|
|
812
750
|
else walkForInterceptions(child, [...ancestors, child], rewrites);
|
|
813
|
-
for (const
|
|
751
|
+
for (const slot of Object.values(node.slots)) walkForInterceptions(slot, ancestors, rewrites);
|
|
814
752
|
}
|
|
815
753
|
/**
|
|
816
754
|
* For an intercepting segment, find all leaf pages in its sub-tree and
|
|
@@ -868,6 +806,42 @@ function computeInterceptedBase(parentUrlPath, marker) {
|
|
|
868
806
|
}
|
|
869
807
|
}
|
|
870
808
|
//#endregion
|
|
871
|
-
|
|
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 };
|
|
872
846
|
|
|
873
|
-
//# sourceMappingURL=
|
|
847
|
+
//# sourceMappingURL=walkers-VOXgavMF.js.map
|