@timber-js/app 0.2.0-alpha.169 → 0.2.0-alpha.170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/dist/_chunks/{canonicalize-P41GR6tY.js → canonicalize-Du3o_ptW.js} +30 -12
- package/dist/_chunks/canonicalize-Du3o_ptW.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-B73L6pMq.js → cli-schema-sync-B5FDplGI.js} +55 -7
- package/dist/_chunks/cli-schema-sync-B5FDplGI.js.map +1 -0
- package/dist/_chunks/{segment-classify-CDDRVKs7.js → segment-classify-Byy425ng.js} +57 -18
- package/dist/_chunks/segment-classify-Byy425ng.js.map +1 -0
- package/dist/_chunks/{walkers-CoOC8Hga.js → walkers-DCoE-LJf.js} +23 -9
- package/dist/_chunks/walkers-DCoE-LJf.js.map +1 -0
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/index.js +5 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/schema-validation.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +8 -2
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/types.d.ts +4 -0
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/server/internal.js +13 -4
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-routes.d.ts +24 -4
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/docs/more/01-advanced-routing.mdx +35 -1
- package/docs/more/04-metadata-and-fonts.mdx +2 -2
- package/package.json +7 -8
- package/src/cli-schema-sync.ts +15 -2
- package/src/cli.ts +0 -0
- package/src/client/link.tsx +4 -1
- package/src/routing/codegen.ts +13 -1
- package/src/routing/link-codegen.ts +15 -12
- package/src/routing/manifest-codegen.ts +6 -0
- package/src/routing/scanner.ts +85 -3
- package/src/routing/schema-validation.ts +7 -1
- package/src/routing/segment-classify.ts +49 -14
- package/src/routing/types.ts +4 -0
- package/src/server/metadata-routes.ts +71 -10
- package/src/server/pipeline-interception.ts +14 -1
- package/src/server/pipeline-phases.ts +7 -1
- package/src/server/route-element-builder.ts +12 -3
- package/src/server/route-matcher.ts +16 -2
- package/src/server/sitemap-generator.ts +5 -4
- package/src/server/slot-resolver.ts +7 -1
- package/src/server/tree-match.ts +46 -4
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +0 -1
- package/dist/_chunks/segment-classify-CDDRVKs7.js.map +0 -1
- package/dist/_chunks/walkers-CoOC8Hga.js.map +0 -1
|
@@ -34,8 +34,18 @@ export declare const METADATA_ROUTE_CONVENTIONS: Record<string, {
|
|
|
34
34
|
nestable: boolean;
|
|
35
35
|
staticExtensions: string[];
|
|
36
36
|
dynamicExtensions: string[];
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The URL path basename this file serves at (relative to segment).
|
|
39
|
+
* For image routes, the full serve path includes an extension via
|
|
40
|
+
* `resolveServePathForFile()`.
|
|
41
|
+
*/
|
|
38
42
|
servePath: string;
|
|
43
|
+
/**
|
|
44
|
+
* When set, image routes append `.{serveExtension}` to the serve path.
|
|
45
|
+
* Dynamic handlers (`.ts`/`.tsx`) use this as the default. Static files
|
|
46
|
+
* use their own extension instead. Non-image routes leave this undefined.
|
|
47
|
+
*/
|
|
48
|
+
serveExtension?: string;
|
|
39
49
|
}>;
|
|
40
50
|
/**
|
|
41
51
|
* Resolve the concrete MIME type for a static metadata route file.
|
|
@@ -72,10 +82,20 @@ export declare function isDynamicMetadataExtension(baseName: string, extension:
|
|
|
72
82
|
*/
|
|
73
83
|
export declare function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null;
|
|
74
84
|
/**
|
|
75
|
-
*
|
|
85
|
+
* Resolve the serve path for a metadata route file.
|
|
76
86
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
87
|
+
* For image routes (icon, opengraph-image, apple-icon), the serve path includes
|
|
88
|
+
* a file extension so CDNs cache correctly:
|
|
89
|
+
* - Dynamic handlers (.ts/.tsx) use the convention's `serveExtension` (default: .png)
|
|
90
|
+
* - Static files use their own extension (e.g., icon.svg → icon.svg)
|
|
91
|
+
*
|
|
92
|
+
* Non-image routes return the convention's `servePath` as-is (already includes
|
|
93
|
+
* extension: sitemap.xml, robots.txt, etc.).
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveServePathForFile(baseName: string, filePath: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* Get the default serve path for a metadata route type (using default extension
|
|
98
|
+
* for image routes). Used for auto-link generation when only the type is known.
|
|
79
99
|
*/
|
|
80
100
|
export declare function getMetadataRouteServePath(type: MetadataRouteType): string;
|
|
81
101
|
export declare function isMetadataRouteServePath(pathname: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-routes.d.ts","sourceRoot":"","sources":["../../src/server/metadata-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,+BAA+B;IAC/B,IAAI,EAAE,iBAAiB,CAAC;IACxB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,QAAQ,GACR,UAAU,GACV,SAAS,GACT,MAAM,GACN,iBAAiB,GACjB,YAAY,CAAC;AAIjB;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAC7C,MAAM,EACN;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B
|
|
1
|
+
{"version":3,"file":"metadata-routes.d.ts","sourceRoot":"","sources":["../../src/server/metadata-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,+BAA+B;IAC/B,IAAI,EAAE,iBAAiB,CAAC;IACxB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,QAAQ,GACR,UAAU,GACV,SAAS,GACT,MAAM,GACN,iBAAiB,GACjB,YAAY,CAAC;AAIjB;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAC7C,MAAM,EACN;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CA8DF,CAAC;AAoBF;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,qBAAqB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKjG;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAItF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAIvF;AAID;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAoBhF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWlF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAUzE;AAiBD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAalE;AAED,8BAA8B;AAC9B,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8BAA8B;AAC9B,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAgB7F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-interception.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-interception.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAcD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,4BAA4B,EAAE,mBAAmB,EAAE,GACnE,uBAAuB,GAAG,IAAI,CAahC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"pipeline-interception.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-interception.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAcD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,4BAA4B,EAAE,mBAAmB,EAAE,GACnE,uBAAuB,GAAG,IAAI,CAahC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAsCjF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GAC7B,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GAC7B,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,CAmTnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"route-element-builder.d.ts","sourceRoot":"","sources":["../../src/server/route-element-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAiC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC,YAAY,CAW/E;AAwCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAID,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;IAC3C,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC;IAC5B,wDAAwD;IACxD,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,qCAAqC;IACrC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AA8DD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,CAAC,EAAE,mBAAmB,EAClC,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3C,OAAO,CAAC,kBAAkB,CAAC,CAujB7B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAY9B,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,YAAY,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AAsCD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAClD;AAcD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAEjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap-generator.d.ts","sourceRoot":"","sources":["../../src/server/sitemap-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,OAAO,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,SAAS,EAAE,OAAO,CAAC;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE;IAClC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvC,oCAAoC;AACpC,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAIpE,8DAA8D;AAC9D,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAIvC;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,YAAY,EAAE,CAI7F;AAwDD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,aAAa,EACrB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,YAAY,EAAE,CAAC,CAqDzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"sitemap-generator.d.ts","sourceRoot":"","sources":["../../src/server/sitemap-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,OAAO,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,SAAS,EAAE,OAAO,CAAC;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE;IAClC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvC,oCAAoC;AACpC,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAIpE,8DAA8D;AAC9D,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAIvC;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,YAAY,EAAE,CAI7F;AAwDD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,aAAa,EACrB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,YAAY,EAAE,CAAC,CAqDzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,MAAM,GAAG,IAAI,CA2Cf;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,aAAa,EACrB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,aAAa,CAAC,CA+BxB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAGzD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: 'Advanced Routing'
|
|
3
|
-
description: 'Route groups, parallel routes (slots), intercepting routes, catch-all segments, and API routes.'
|
|
3
|
+
description: 'Route groups, parallel routes (slots), intercepting routes, catch-all segments, affixed dynamic segments, and API routes.'
|
|
4
4
|
slug: 'advanced-routing'
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -45,6 +45,40 @@ app/blog/page.tsx # /blog (index)
|
|
|
45
45
|
app/blog/[year]/page.tsx # /blog/2024 (filtered)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## Affixed Dynamic Segments
|
|
49
|
+
|
|
50
|
+
Dynamic segments can have literal prefixes and/or suffixes:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
app/
|
|
54
|
+
images/
|
|
55
|
+
[id].png/
|
|
56
|
+
route.ts # /images/123.png → id = "123"
|
|
57
|
+
user-[name]/
|
|
58
|
+
page.tsx # /user-alice → name = "alice"
|
|
59
|
+
img-[id].webp/
|
|
60
|
+
page.tsx # /img-photo.webp → id = "photo"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This is useful for CDN-cacheable file-extension routes (e.g. image generation endpoints) and prefixed slugs. The literal parts must match exactly; the dynamic middle is extracted as the param value.
|
|
64
|
+
|
|
65
|
+
**Rules:**
|
|
66
|
+
- The param must be non-empty — `img-[id].png` does NOT match `img-.png`
|
|
67
|
+
- One param per segment — `[foo]-[bar]` is not supported
|
|
68
|
+
- Affixes cannot be used with catch-all (`[...slug].png`) or optional catch-all segments
|
|
69
|
+
- Matching is greedy: `[id].png` matching `a.png.png` yields `id = "a.png"`
|
|
70
|
+
- `params.ts` codecs use the bare param name (`id`), not the full segment pattern
|
|
71
|
+
|
|
72
|
+
**Priority:** static → affixed dynamic → bare dynamic → catch-all → optional catch-all. A static `foo.png` always wins over `[id].png`.
|
|
73
|
+
|
|
74
|
+
`<Link>` interpolates affixed segments automatically:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<Link href="/images/[id].png" segmentParams={{ id: 'photo' }}>
|
|
78
|
+
{/* renders href="/images/photo.png" */}
|
|
79
|
+
</Link>
|
|
80
|
+
```
|
|
81
|
+
|
|
48
82
|
## Parallel Routes (Slots)
|
|
49
83
|
|
|
50
84
|
Directories prefixed with `@` are slots — they render simultaneously within a parent layout:
|
|
@@ -81,9 +81,9 @@ Special files in your route tree generate metadata endpoints:
|
|
|
81
81
|
| `sitemap.ts` | `/sitemap.xml` |
|
|
82
82
|
| `robots.ts` | `/robots.txt` |
|
|
83
83
|
| `manifest.ts` | `/manifest.json` |
|
|
84
|
-
| `opengraph-image.tsx` | `/opengraph-image` + auto `og:image` and `twitter:image` meta tags |
|
|
84
|
+
| `opengraph-image.tsx` | `/opengraph-image.png` + auto `og:image` and `twitter:image` meta tags |
|
|
85
85
|
| `favicon.ico` / `favicon.tsx` | `/favicon.ico` |
|
|
86
|
-
| `icon.tsx` | `/icon` + auto `<link rel="icon">` |
|
|
86
|
+
| `icon.tsx` | `/icon.png` + auto `<link rel="icon">` |
|
|
87
87
|
|
|
88
88
|
When `opengraph-image.tsx` exists in a segment, timber automatically emits both `<meta property="og:image">` and `<meta name="twitter:image">` in `<head>` — no need to declare them in your `metadata` export. The auto-linked URL includes a cache-busting hash so social platforms pick up changes on redeploy.
|
|
89
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.170",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -150,12 +150,6 @@
|
|
|
150
150
|
"publishConfig": {
|
|
151
151
|
"access": "public"
|
|
152
152
|
},
|
|
153
|
-
"scripts": {
|
|
154
|
-
"copy-docs": "node scripts/copy-docs.js",
|
|
155
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
|
|
156
|
-
"typecheck": "tsgo --noEmit",
|
|
157
|
-
"prepublishOnly": "pnpm run build"
|
|
158
|
-
},
|
|
159
153
|
"dependencies": {
|
|
160
154
|
"@opentelemetry/api": "^1.9.1",
|
|
161
155
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -202,5 +196,10 @@
|
|
|
202
196
|
},
|
|
203
197
|
"engines": {
|
|
204
198
|
"node": ">=22.18.0"
|
|
199
|
+
},
|
|
200
|
+
"scripts": {
|
|
201
|
+
"copy-docs": "node scripts/copy-docs.js",
|
|
202
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
|
|
203
|
+
"typecheck": "tsgo --noEmit"
|
|
205
204
|
}
|
|
206
|
-
}
|
|
205
|
+
}
|
package/src/cli-schema-sync.ts
CHANGED
|
@@ -12,6 +12,18 @@ import { scanRoutes } from './routing/scanner.js';
|
|
|
12
12
|
import type { SegmentNode } from './routing/types.js';
|
|
13
13
|
import { resolveAppDir } from './plugin-context.js';
|
|
14
14
|
|
|
15
|
+
function toBracketKey(node: SegmentNode): string {
|
|
16
|
+
if (node.paramPrefix || node.paramSuffix) return `[${node.paramName}]`;
|
|
17
|
+
switch (node.segmentType) {
|
|
18
|
+
case 'catch-all':
|
|
19
|
+
return `[...${node.paramName}]`;
|
|
20
|
+
case 'optional-catch-all':
|
|
21
|
+
return `[[...${node.paramName}]]`;
|
|
22
|
+
default:
|
|
23
|
+
return `[${node.paramName}]`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
/**
|
|
16
28
|
* Collect all unique bracket-keyed dynamic segment names from the route tree.
|
|
17
29
|
* Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.
|
|
@@ -25,8 +37,9 @@ export function collectDynamicSegments(appDir: string): string[] {
|
|
|
25
37
|
|
|
26
38
|
function walkSegments(node: SegmentNode, segments: Set<string>): void {
|
|
27
39
|
if (node.paramName) {
|
|
28
|
-
//
|
|
29
|
-
|
|
40
|
+
// Use bare bracket form for schema keys — affixed segments like
|
|
41
|
+
// `img-[id].png` register as `[id]`, matching runtime codec lookup.
|
|
42
|
+
segments.add(toBracketKey(node));
|
|
30
43
|
}
|
|
31
44
|
for (const child of node.children) {
|
|
32
45
|
walkSegments(child, segments);
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/client/link.tsx
CHANGED
|
@@ -264,7 +264,10 @@ function resolveSegment(
|
|
|
264
264
|
}
|
|
265
265
|
const codec = getLinkCodec(`[${seg.name}]`);
|
|
266
266
|
const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);
|
|
267
|
-
|
|
267
|
+
const encoded = encodeURIComponent(str);
|
|
268
|
+
const prefix = seg.prefix ?? '';
|
|
269
|
+
const suffix = seg.suffix ?? '';
|
|
270
|
+
return prefix + encoded + suffix;
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
}
|
package/src/routing/codegen.ts
CHANGED
|
@@ -111,7 +111,7 @@ function collectRoutes(
|
|
|
111
111
|
const legacyFallback = ownParamsFile ? undefined : findLegacyParamsExport(node);
|
|
112
112
|
params.push({
|
|
113
113
|
name: node.paramName,
|
|
114
|
-
bracketName: node
|
|
114
|
+
bracketName: bracketNameForSegment(node),
|
|
115
115
|
type: paramTypeForSegment(node.segmentType),
|
|
116
116
|
// Codec chain populated at leaf time. We carry the per-segment
|
|
117
117
|
// legacy fallback (if any) so leaf-time resolution can fall back
|
|
@@ -238,6 +238,18 @@ function dedupeRoutesByUrlPath(routes: RouteEntry[]): RouteEntry[] {
|
|
|
238
238
|
/**
|
|
239
239
|
* Determine the TypeScript type for a segment's param.
|
|
240
240
|
*/
|
|
241
|
+
/**
|
|
242
|
+
* Derive the bracket key for schema/codegen lookup.
|
|
243
|
+
* For bare segments like `[id]` this is the segmentName itself.
|
|
244
|
+
* For affixed segments like `img-[id].png` this is `[id]` — the bare bracket form.
|
|
245
|
+
*/
|
|
246
|
+
function bracketNameForSegment(node: SegmentNode): string {
|
|
247
|
+
if (node.paramPrefix || node.paramSuffix) {
|
|
248
|
+
return `[${node.paramName}]`;
|
|
249
|
+
}
|
|
250
|
+
return node.segmentName;
|
|
251
|
+
}
|
|
252
|
+
|
|
241
253
|
function paramTypeForSegment(segmentType: string): ParamEntry['type'] {
|
|
242
254
|
switch (segmentType) {
|
|
243
255
|
case 'catch-all':
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
buildSchemaParamType,
|
|
30
30
|
formatSearchParamsType,
|
|
31
31
|
} from './codegen-shared.js';
|
|
32
|
+
import { classifyUrlSegment } from './segment-classify.js';
|
|
32
33
|
|
|
33
34
|
/** Shared Link base-props type literal used in every emitted call signature. */
|
|
34
35
|
export const LINK_BASE_PROPS_TYPE =
|
|
@@ -44,19 +45,21 @@ export const LINK_BASE_PROPS_TYPE =
|
|
|
44
45
|
export function buildResolvedPattern(route: RouteEntry): string | null {
|
|
45
46
|
const parts = route.urlPath.split('/');
|
|
46
47
|
const templateParts = parts.map((part) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
const seg = classifyUrlSegment(part);
|
|
49
|
+
switch (seg.kind) {
|
|
50
|
+
case 'catch-all':
|
|
51
|
+
case 'optional-catch-all':
|
|
52
|
+
// eslint-disable-next-line no-template-curly-in-string -- codegen output
|
|
53
|
+
return '${string}';
|
|
54
|
+
case 'dynamic': {
|
|
55
|
+
const prefix = seg.prefix ?? '';
|
|
56
|
+
const suffix = seg.suffix ?? '';
|
|
57
|
+
// eslint-disable-next-line no-template-curly-in-string -- codegen output
|
|
58
|
+
return `${prefix}\${string}${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
return part;
|
|
58
62
|
}
|
|
59
|
-
return part;
|
|
60
63
|
});
|
|
61
64
|
return templateParts.join('/');
|
|
62
65
|
}
|
|
@@ -88,6 +88,12 @@ export function generateManifestModule(tree: RouteTree, viteRoot: string): strin
|
|
|
88
88
|
if (node.paramName) {
|
|
89
89
|
parts.push(`${nextIndent}paramName: ${JSON.stringify(node.paramName)},`);
|
|
90
90
|
}
|
|
91
|
+
if (node.paramPrefix) {
|
|
92
|
+
parts.push(`${nextIndent}paramPrefix: ${JSON.stringify(node.paramPrefix)},`);
|
|
93
|
+
}
|
|
94
|
+
if (node.paramSuffix) {
|
|
95
|
+
parts.push(`${nextIndent}paramSuffix: ${JSON.stringify(node.paramSuffix)},`);
|
|
96
|
+
}
|
|
91
97
|
|
|
92
98
|
if (node.interceptionMarker) {
|
|
93
99
|
parts.push(`${nextIndent}interceptionMarker: ${JSON.stringify(node.interceptionMarker)},`);
|
package/src/routing/scanner.ts
CHANGED
|
@@ -98,6 +98,10 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
|
|
|
98
98
|
// e.g., [id] in one route and [...id] in another
|
|
99
99
|
validateUniqueBracketForms(tree.root);
|
|
100
100
|
|
|
101
|
+
// Validate: detect affixed dynamic siblings with equal specificity
|
|
102
|
+
// e.g., a-[id] and [slug]-b both have prefix+suffix length = 2
|
|
103
|
+
validateAffixedSiblingConflicts(tree.root);
|
|
104
|
+
|
|
101
105
|
return tree;
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -110,13 +114,17 @@ function createSegmentNode(
|
|
|
110
114
|
urlPath: string,
|
|
111
115
|
paramName?: string,
|
|
112
116
|
interceptionMarker?: InterceptionMarker,
|
|
113
|
-
interceptedSegmentName?: string
|
|
117
|
+
interceptedSegmentName?: string,
|
|
118
|
+
paramPrefix?: string,
|
|
119
|
+
paramSuffix?: string
|
|
114
120
|
): SegmentNode {
|
|
115
121
|
return {
|
|
116
122
|
segmentName,
|
|
117
123
|
segmentType,
|
|
118
124
|
urlPath,
|
|
119
125
|
paramName,
|
|
126
|
+
paramPrefix,
|
|
127
|
+
paramSuffix,
|
|
120
128
|
interceptionMarker,
|
|
121
129
|
interceptedSegmentName,
|
|
122
130
|
children: [],
|
|
@@ -331,7 +339,14 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
|
|
|
331
339
|
);
|
|
332
340
|
}
|
|
333
341
|
|
|
334
|
-
const {
|
|
342
|
+
const {
|
|
343
|
+
type,
|
|
344
|
+
paramName,
|
|
345
|
+
paramPrefix,
|
|
346
|
+
paramSuffix,
|
|
347
|
+
interceptionMarker,
|
|
348
|
+
interceptedSegmentName,
|
|
349
|
+
} = classifySegment(entry);
|
|
335
350
|
|
|
336
351
|
// Skip private folders — underscore-prefixed dirs are excluded from routing
|
|
337
352
|
if (type === 'private') continue;
|
|
@@ -343,7 +358,9 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
|
|
|
343
358
|
urlPath,
|
|
344
359
|
paramName,
|
|
345
360
|
interceptionMarker,
|
|
346
|
-
interceptedSegmentName
|
|
361
|
+
interceptedSegmentName,
|
|
362
|
+
paramPrefix,
|
|
363
|
+
paramSuffix
|
|
347
364
|
);
|
|
348
365
|
|
|
349
366
|
// Scan this segment's files
|
|
@@ -527,6 +544,71 @@ function collectBracketForms(
|
|
|
527
544
|
}
|
|
528
545
|
}
|
|
529
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Validate that no sibling affixed dynamic segments have equal specificity.
|
|
549
|
+
*
|
|
550
|
+
* Among affixed siblings, longer combined prefix+suffix length wins. When two
|
|
551
|
+
* siblings tie (same total affix length), the router would fall back to
|
|
552
|
+
* filesystem scan order — which is platform-dependent. This is a build error.
|
|
553
|
+
*
|
|
554
|
+
* Route groups are URL-transparent, so affixed segments inside different groups
|
|
555
|
+
* at the same level still conflict with each other.
|
|
556
|
+
*
|
|
557
|
+
* See design/07-routing.md §"Priority"
|
|
558
|
+
*/
|
|
559
|
+
function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
560
|
+
const affixed = collectEffectiveAffixed(node);
|
|
561
|
+
|
|
562
|
+
if (affixed.length >= 2) {
|
|
563
|
+
const byLength = new Map<number, SegmentNode[]>();
|
|
564
|
+
for (const child of affixed) {
|
|
565
|
+
const len = (child.paramPrefix?.length ?? 0) + (child.paramSuffix?.length ?? 0);
|
|
566
|
+
const group = byLength.get(len);
|
|
567
|
+
if (group) {
|
|
568
|
+
group.push(child);
|
|
569
|
+
} else {
|
|
570
|
+
byLength.set(len, [child]);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const [, group] of byLength) {
|
|
575
|
+
if (group.length >= 2) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Build error: ambiguous affixed dynamic siblings with equal specificity.\n` +
|
|
578
|
+
` Parent: ${node.urlPath || '/'}\n` +
|
|
579
|
+
` Segments: ${group.map((s) => s.segmentName).join(', ')}\n` +
|
|
580
|
+
` Combined prefix+suffix length: ${(group[0]!.paramPrefix?.length ?? 0) + (group[0]!.paramSuffix?.length ?? 0)}\n` +
|
|
581
|
+
`The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\n` +
|
|
582
|
+
`Rename one segment to give it a longer or shorter combined affix.`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
for (const child of node.children) {
|
|
589
|
+
validateAffixedSiblingConflicts(child);
|
|
590
|
+
}
|
|
591
|
+
for (const slotNode of Object.values(node.slots)) {
|
|
592
|
+
validateAffixedSiblingConflicts(slotNode);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Collect affixed dynamic segments that are effective children of a node.
|
|
598
|
+
* Route groups are URL-transparent, so we look through them recursively.
|
|
599
|
+
*/
|
|
600
|
+
function collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {
|
|
601
|
+
const result: SegmentNode[] = [];
|
|
602
|
+
for (const child of node.children) {
|
|
603
|
+
if (child.segmentType === 'dynamic' && (child.paramPrefix || child.paramSuffix)) {
|
|
604
|
+
result.push(child);
|
|
605
|
+
} else if (child.segmentType === 'group') {
|
|
606
|
+
result.push(...collectEffectiveAffixed(child));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
530
612
|
/**
|
|
531
613
|
* Find a fixed-extension file (proxy.ts) in a directory.
|
|
532
614
|
*/
|
|
@@ -33,7 +33,13 @@ function walkTree(node: SegmentNode, segments: Set<string>): void {
|
|
|
33
33
|
if (node.paramName && node.segmentName) {
|
|
34
34
|
const classified = classifyUrlSegment(node.segmentName);
|
|
35
35
|
if (classified.kind !== 'static') {
|
|
36
|
-
segments
|
|
36
|
+
// Use bare bracket form for schema keys — affixed segments like
|
|
37
|
+
// `img-[id].png` register as `[id]`, matching runtime codec lookup.
|
|
38
|
+
if (classified.kind === 'dynamic' && (classified.prefix || classified.suffix)) {
|
|
39
|
+
segments.add(`[${classified.name}]`);
|
|
40
|
+
} else {
|
|
41
|
+
segments.add(node.segmentName);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
for (const child of node.children) {
|
|
@@ -27,7 +27,7 @@ import { INTERCEPTION_MARKERS } from './types.js';
|
|
|
27
27
|
|
|
28
28
|
export type UrlSegment =
|
|
29
29
|
| { kind: 'static'; value: string }
|
|
30
|
-
| { kind: 'dynamic'; name: string }
|
|
30
|
+
| { kind: 'dynamic'; name: string; prefix?: string; suffix?: string }
|
|
31
31
|
| { kind: 'catch-all'; name: string }
|
|
32
32
|
| { kind: 'optional-catch-all'; name: string };
|
|
33
33
|
|
|
@@ -35,27 +35,31 @@ export type UrlSegment =
|
|
|
35
35
|
* Classify a URL path segment token.
|
|
36
36
|
*
|
|
37
37
|
* Walks the string left-to-right in one pass:
|
|
38
|
-
* 1.
|
|
38
|
+
* 1. Find the first '[' — characters before it are the prefix.
|
|
39
39
|
* 2. Count opening brackets (1 or 2) to detect optional.
|
|
40
40
|
* 3. Check for '...' to detect catch-all.
|
|
41
41
|
* 4. Read the param name up to the closing bracket.
|
|
42
42
|
* 5. Validate the expected closing sequence (']' or ']]').
|
|
43
|
-
* 6.
|
|
43
|
+
* 6. Characters after the close are the suffix.
|
|
44
|
+
* 7. Reject affixes on catch-all/optional-catch-all.
|
|
45
|
+
* 8. Reject affixes that contain '[' or ']' (no multi-param segments).
|
|
44
46
|
*
|
|
45
47
|
* Any structural violation → static (safe default).
|
|
46
48
|
*/
|
|
47
49
|
export function classifyUrlSegment(token: string): UrlSegment {
|
|
48
50
|
const len = token.length;
|
|
51
|
+
if (len === 0) return { kind: 'static', value: token };
|
|
49
52
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
53
|
+
// Find first '[' — characters before it are the prefix
|
|
54
|
+
const bracketStart = token.indexOf('[');
|
|
55
|
+
if (bracketStart === -1) return { kind: 'static', value: token };
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
const prefix = token.slice(0, bracketStart);
|
|
58
|
+
|
|
59
|
+
let i = bracketStart + 1;
|
|
56
60
|
|
|
57
61
|
// Check for optional: '[[...'
|
|
58
|
-
const optional = token[i] === '[';
|
|
62
|
+
const optional = i < len && token[i] === '[';
|
|
59
63
|
if (optional) i++;
|
|
60
64
|
|
|
61
65
|
// Check for catch-all: '...'
|
|
@@ -82,17 +86,41 @@ export function classifyUrlSegment(token: string): UrlSegment {
|
|
|
82
86
|
i++;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
const suffix = token.slice(i);
|
|
90
|
+
|
|
91
|
+
// Reject affixes containing brackets (no multi-param segments like [foo]-[bar])
|
|
92
|
+
if (suffix.includes('[') || suffix.includes(']')) {
|
|
87
93
|
return { kind: 'static', value: token };
|
|
88
94
|
}
|
|
95
|
+
if (prefix.includes(']')) {
|
|
96
|
+
return { kind: 'static', value: token };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hasAffixes = prefix.length > 0 || suffix.length > 0;
|
|
89
100
|
|
|
90
|
-
if (optional && catchAll)
|
|
91
|
-
|
|
101
|
+
if (optional && catchAll) {
|
|
102
|
+
// No affixes on optional catch-all
|
|
103
|
+
if (hasAffixes) return { kind: 'static', value: token };
|
|
104
|
+
return { kind: 'optional-catch-all', name };
|
|
105
|
+
}
|
|
106
|
+
if (catchAll) {
|
|
107
|
+
// No affixes on catch-all
|
|
108
|
+
if (hasAffixes) return { kind: 'static', value: token };
|
|
109
|
+
return { kind: 'catch-all', name };
|
|
110
|
+
}
|
|
92
111
|
if (optional) {
|
|
93
112
|
// '[[name]]' without '...' is malformed — not a valid segment syntax
|
|
94
113
|
return { kind: 'static', value: token };
|
|
95
114
|
}
|
|
115
|
+
|
|
116
|
+
if (hasAffixes) {
|
|
117
|
+
return {
|
|
118
|
+
kind: 'dynamic',
|
|
119
|
+
name,
|
|
120
|
+
prefix: prefix || undefined,
|
|
121
|
+
suffix: suffix || undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
96
124
|
return { kind: 'dynamic', name };
|
|
97
125
|
}
|
|
98
126
|
|
|
@@ -102,6 +130,8 @@ export function classifyUrlSegment(token: string): UrlSegment {
|
|
|
102
130
|
export interface SegmentClassification {
|
|
103
131
|
type: SegmentType;
|
|
104
132
|
paramName?: string;
|
|
133
|
+
paramPrefix?: string;
|
|
134
|
+
paramSuffix?: string;
|
|
105
135
|
interceptionMarker?: InterceptionMarker;
|
|
106
136
|
interceptedSegmentName?: string;
|
|
107
137
|
}
|
|
@@ -151,7 +181,12 @@ export function classifySegment(dirName: string): SegmentClassification {
|
|
|
151
181
|
// Bracket-syntax segments: [param], [...param], [[...param]]
|
|
152
182
|
const urlSeg = classifyUrlSegment(dirName);
|
|
153
183
|
if (urlSeg.kind !== 'static') {
|
|
154
|
-
|
|
184
|
+
const result: SegmentClassification = { type: urlSeg.kind, paramName: urlSeg.name };
|
|
185
|
+
if (urlSeg.kind === 'dynamic') {
|
|
186
|
+
if (urlSeg.prefix) result.paramPrefix = urlSeg.prefix;
|
|
187
|
+
if (urlSeg.suffix) result.paramSuffix = urlSeg.suffix;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
155
190
|
}
|
|
156
191
|
|
|
157
192
|
return { type: 'static' };
|
package/src/routing/types.ts
CHANGED
|
@@ -76,6 +76,10 @@ export interface SegmentNode<TFile = RouteFile> {
|
|
|
76
76
|
segmentType: SegmentType;
|
|
77
77
|
/** The dynamic param name, if dynamic (e.g. "id" for "[id]", "slug" for "[...slug]") */
|
|
78
78
|
paramName?: string;
|
|
79
|
+
/** Literal prefix before the dynamic bracket (e.g. "img-" for "img-[id].png") */
|
|
80
|
+
paramPrefix?: string;
|
|
81
|
+
/** Literal suffix after the dynamic bracket (e.g. ".png" for "img-[id].png") */
|
|
82
|
+
paramSuffix?: string;
|
|
79
83
|
/** The URL path prefix at this segment level (e.g. "/dashboard") */
|
|
80
84
|
urlPath: string;
|
|
81
85
|
/** For intercepting segments: the marker used, e.g. "(.)". */
|