@timber-js/app 0.2.0-alpha.114 → 0.2.0-alpha.116

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 (43) hide show
  1. package/dist/_chunks/{actions-ILhDUzES.js → actions-DAVm0FQe.js} +17 -17
  2. package/dist/_chunks/actions-DAVm0FQe.js.map +1 -0
  3. package/dist/_chunks/{define-a0CCE6Nx.js → define-zQX6e1tN.js} +2 -2
  4. package/dist/_chunks/{define-a0CCE6Nx.js.map → define-zQX6e1tN.js.map} +1 -1
  5. package/dist/_chunks/{navigation-root-CY3pRMxF.js → navigation-root-DKsJ5Di7.js} +2 -2
  6. package/dist/_chunks/{navigation-root-CY3pRMxF.js.map → navigation-root-DKsJ5Di7.js.map} +1 -1
  7. package/dist/_chunks/tracing-DvKLzCFa.js.map +1 -1
  8. package/dist/_chunks/{use-segment-params-BSt_O2cb.js → use-segment-params-CskiK-59.js} +2 -2
  9. package/dist/_chunks/{use-segment-params-BSt_O2cb.js.map → use-segment-params-CskiK-59.js.map} +1 -1
  10. package/dist/_chunks/{walkers-DGV9dQRq.js → walkers-DgOXkTts.js} +12 -10
  11. package/dist/_chunks/walkers-DgOXkTts.js.map +1 -0
  12. package/dist/client/index.js +3 -3
  13. package/dist/client/internal.js +2 -2
  14. package/dist/client/use-segment-params.d.ts +3 -3
  15. package/dist/client/use-segment-params.d.ts.map +1 -1
  16. package/dist/index.js +43 -26
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/routing.d.ts.map +1 -1
  19. package/dist/routing/codegen-types.d.ts +3 -0
  20. package/dist/routing/codegen-types.d.ts.map +1 -1
  21. package/dist/routing/index.js +1 -1
  22. package/dist/routing/segment-codegen.d.ts +22 -9
  23. package/dist/routing/segment-codegen.d.ts.map +1 -1
  24. package/dist/segment-params/index.js +1 -1
  25. package/dist/server/als-registry.d.ts +5 -4
  26. package/dist/server/als-registry.d.ts.map +1 -1
  27. package/dist/server/index.js +1 -1
  28. package/dist/server/internal.js +3 -3
  29. package/dist/server/internal.js.map +1 -1
  30. package/dist/server/pipeline-phases.d.ts.map +1 -1
  31. package/dist/server/request-context.d.ts +9 -9
  32. package/dist/server/request-context.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/client/use-segment-params.ts +4 -4
  35. package/src/plugins/routing.ts +26 -17
  36. package/src/routing/codegen-types.ts +3 -0
  37. package/src/routing/codegen.ts +18 -9
  38. package/src/routing/segment-codegen.ts +46 -21
  39. package/src/server/als-registry.ts +5 -4
  40. package/src/server/pipeline-phases.ts +9 -7
  41. package/src/server/request-context.ts +17 -17
  42. package/dist/_chunks/actions-ILhDUzES.js.map +0 -1
  43. package/dist/_chunks/walkers-DGV9dQRq.js.map +0 -1
@@ -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;AAID;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAavB;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;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CA0PnB"}
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;AAID;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAavB;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;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CA4PnB"}
@@ -40,16 +40,16 @@ export declare function getSearchParams(): URLSearchParams;
40
40
  /**
41
41
  * Returns the current request's coerced segment params.
42
42
  *
43
- * The optional `_route` argument exists only for TypeScript narrowing —
44
- * it does not affect the runtime return value. Pass a route pattern
45
- * (e.g. `'/[artistSlug]/[year]'`) to narrow the return type to the
46
- * exact params shape for that route.
43
+ * The optional `segmentPath` argument exists only for TypeScript narrowing —
44
+ * it does not affect the runtime return value. Pass a segment path
45
+ * (e.g. `'/(browse)/[artistSlug]/[year]'`) to narrow the return type to the
46
+ * exact params shape for that segment.
47
47
  *
48
- * Without a route argument, returns all coerced params with optional types.
48
+ * Without an argument, returns all coerced params with optional types.
49
49
  *
50
50
  * See design/41-global-params.md §Function Signatures
51
51
  */
52
- export declare function getSegmentParams(route?: string): Record<string, string | string[]>;
52
+ export declare function getSegmentParams(segmentPath?: string): Record<string, string | string[]>;
53
53
  /**
54
54
  * Set the segment params on the current request context.
55
55
  * Called by the pipeline after route matching and param coercion.
@@ -58,12 +58,12 @@ export declare function getSegmentParams(route?: string): Record<string, string
58
58
  */
59
59
  export declare function setSegmentParams(params: Record<string, string | string[]>): void;
60
60
  /**
61
- * Store the matched route pattern (e.g. '/[artistSlug]/[year]') for
62
- * dev-mode route mismatch validation in getSegmentParams().
61
+ * Store the matched segment path (e.g. '/(browse)/[artistSlug]/[year]') for
62
+ * dev-mode validation in getSegmentParams().
63
63
  *
64
64
  * @internal — framework use only
65
65
  */
66
- export declare function setMatchedRoutePattern(pattern: string): void;
66
+ export declare function setMatchedSegmentPath(segmentPath: string): void;
67
67
  /**
68
68
  * Returns the raw search string from the current request URL (e.g. "?foo=bar").
69
69
  * Synchronous — safe for use in `redirect()` which throws synchronously.
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,iBAAiB,EAA4B,MAAM,mBAAmB,CAAC;AAOhF,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAQ7B;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,eAAe,CAS5C;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,eAAe,CASjD;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAkClF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAMhF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI5D;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C;AAID;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,OAAO,EACP,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,MAAM,CAAC,QAAQ,CACnF,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAmBrE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmBhE"}
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,iBAAiB,EAA4B,MAAM,mBAAmB,CAAC;AAOhF,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAQ7B;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,eAAe,CAS5C;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,eAAe,CASjD;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAkCxF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,IAAI,CAMhF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAI/D;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C;AAID;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,OAAO,EACP,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,MAAM,CAAC,QAAQ,CACnF,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAmBrE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmBhE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.114",
3
+ "version": "0.2.0-alpha.116",
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",
@@ -115,15 +115,15 @@ export function notifyParamsListeners(): void {
115
115
  * When called outside a React component (e.g., in test assertions),
116
116
  * falls back to the module-level snapshot.
117
117
  *
118
- * @overload Typed — when a known route path is passed, returns the
118
+ * @overload Typed — when a known segment path is passed, returns the
119
119
  * exact params shape from the generated Routes interface.
120
120
  * @overload Fallback — returns the generic params record.
121
121
  */
122
122
  export function useSegmentParams<R extends keyof Routes>(
123
- route: R
123
+ segmentPath: R
124
124
  ): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;
125
- export function useSegmentParams(route?: string): Record<string, string | string[]>;
126
- export function useSegmentParams(_route?: string): Record<string, string | string[]> {
125
+ export function useSegmentParams(segmentPath?: string): Record<string, string | string[]>;
126
+ export function useSegmentParams(_segmentPath?: string): Record<string, string | string[]> {
127
127
  // Try reading from NavigationContext (client-side, inside React tree).
128
128
  // During SSR, no NavigationProvider is mounted, so this returns null.
129
129
  // When called outside a React component, useContext throws — caught below.
@@ -15,7 +15,11 @@ import { join } from 'node:path';
15
15
  import { scanRoutes } from '../routing/scanner.js';
16
16
  import { generateRouteMap } from '../routing/codegen.js';
17
17
  import { collectInterceptionRewrites } from '../routing/interception.js';
18
- import { buildDirToRouteMap, writeSegmentTypes } from '../routing/segment-codegen.js';
18
+ import {
19
+ buildDirToRouteMap,
20
+ writeSegmentTypes,
21
+ type SegmentInfo,
22
+ } from '../routing/segment-codegen.js';
19
23
  import {
20
24
  collectDynamicSegments,
21
25
  findSchemaFile,
@@ -56,12 +60,13 @@ const RESOLVED_SEARCH_PARAMS_REGISTRY_ID = `\0${SEARCH_PARAMS_REGISTRY_ID}`;
56
60
  const SCHEMA_ID = 'virtual:timber-schema';
57
61
  const RESOLVED_SCHEMA_ID = `\0${SCHEMA_ID}`;
58
62
 
59
- // TIM-935: _segment virtual module prefix.
63
+ // TIM-935: $segment virtual module prefix.
64
+ // Resolved ID encodes segment info as JSON after the prefix.
60
65
  //
61
66
  // Each route directory gets a virtual module that exports the route
62
- // pattern string. `import { route } from './_segment'` resolves to
63
- // `\0_segment:<route-pattern>` which returns `export const route = '...'`.
64
- const SEGMENT_PREFIX = '\0_segment:';
67
+ // and segmentPath. `import { route, segmentPath } from './$segment'`
68
+ // resolves to `\0$segment:<json>`.
69
+ const SEGMENT_PREFIX = '\0$segment:';
65
70
 
66
71
  /**
67
72
  * File convention names we track for changes that require manifest regeneration.
@@ -122,8 +127,8 @@ export function timberRouting(ctx: PluginContext): Plugin {
122
127
  /** Track warned files to avoid repeating warnings on rescan. */
123
128
  const warnedFiles = new Set<string>();
124
129
 
125
- /** Maps absolute filesystem directory → route pattern for _segment resolution. */
126
- let dirToRoute: Map<string, string> = new Map();
130
+ /** Maps absolute filesystem directory → segment info for $segment resolution. */
131
+ let dirToRoute: Map<string, SegmentInfo> = new Map();
127
132
 
128
133
  function rescan(): void {
129
134
  // Check app/ directory exists before scanning
@@ -204,19 +209,19 @@ export function timberRouting(ctx: PluginContext): Plugin {
204
209
  return RESOLVED_SCHEMA_ID;
205
210
  }
206
211
 
207
- // TIM-935: _segment virtual module.
208
- // Resolves `import { route } from './_segment'` based on the
212
+ // TIM-935: $segment virtual module.
213
+ // Resolves `import { route } from './$segment'` based on the
209
214
  // importing file's directory in the app/ tree.
210
215
  if (
211
216
  importer &&
212
- (cleanId === './_segment' || cleanId === './_segment.js' || cleanId === './_segment.ts')
217
+ (cleanId === './$segment' || cleanId === './$segment.js' || cleanId === './$segment.ts')
213
218
  ) {
214
219
  // Strip \0 prefix from importer if present
215
220
  const cleanImporter = importer.startsWith('\0') ? importer.slice(1) : importer;
216
221
  const importerDir = cleanImporter.substring(0, cleanImporter.lastIndexOf('/'));
217
- const routePattern = dirToRoute.get(importerDir);
218
- if (routePattern !== undefined) {
219
- return `${SEGMENT_PREFIX}${routePattern}`;
222
+ const segmentInfo = dirToRoute.get(importerDir);
223
+ if (segmentInfo !== undefined) {
224
+ return `${SEGMENT_PREFIX}${JSON.stringify(segmentInfo)}`;
220
225
  }
221
226
  }
222
227
 
@@ -249,10 +254,14 @@ export function timberRouting(ctx: PluginContext): Plugin {
249
254
  return generateSchemaModule(ctx.appDir);
250
255
  }
251
256
 
252
- // TIM-935: _segment virtual module — exports the route pattern string.
257
+ // TIM-935: $segment virtual module — exports route and segmentPath.
253
258
  if (id.startsWith(SEGMENT_PREFIX)) {
254
- const routePattern = id.slice(SEGMENT_PREFIX.length);
255
- return `export const route = ${JSON.stringify(routePattern)};\n`;
259
+ const info: SegmentInfo = JSON.parse(id.slice(SEGMENT_PREFIX.length));
260
+ return [
261
+ `export const ROUTE = ${JSON.stringify(info.route)};`,
262
+ `export const SEGMENT_PATH = ${JSON.stringify(info.segmentPath)};`,
263
+ '',
264
+ ].join('\n');
256
265
  }
257
266
 
258
267
  return null;
@@ -383,7 +392,7 @@ function invalidateManifest(server: ViteDevServer): void {
383
392
  }
384
393
  }
385
394
 
386
- // TIM-935: Invalidate all _segment virtual modules (prefixed \0_segment:)
395
+ // TIM-935: Invalidate all $segment virtual modules (prefixed \0$segment:)
387
396
  for (const mod of env.moduleGraph.idToModuleMap.values()) {
388
397
  if (mod.id?.startsWith(SEGMENT_PREFIX)) {
389
398
  env.moduleGraph.invalidateModule(mod);
@@ -10,6 +10,9 @@
10
10
  export interface RouteEntry {
11
11
  /** URL path pattern (e.g. "/products/[id]") */
12
12
  urlPath: string;
13
+ /** Full tree path including groups and slots (e.g. "/(browse)/[artistSlug]/@shows/[year]").
14
+ * Uniquely identifies a segment — unlike urlPath which strips groups/slots. */
15
+ segmentPath: string;
13
16
  /** Accumulated params from all ancestor dynamic segments */
14
17
  params: ParamEntry[];
15
18
  /** Whether the page.tsx exports searchParams */
@@ -48,7 +48,7 @@ export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}):
48
48
  const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
49
49
 
50
50
  const routes: RouteEntry[] = [];
51
- collectRoutes(tree.root, [], [], routes, hasSchema);
51
+ collectRoutes(tree.root, [], [], '', routes, hasSchema);
52
52
 
53
53
  // Sort routes alphabetically for deterministic output
54
54
  routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
@@ -80,9 +80,13 @@ function collectRoutes(
80
80
  node: SegmentNode,
81
81
  ancestorParams: ParamEntry[],
82
82
  ancestorParamsFiles: string[],
83
+ parentTreePath: string,
83
84
  routes: RouteEntry[],
84
85
  hasSchema: boolean
85
86
  ): void {
87
+ // Build the tree path (includes groups, slots — uniquely identifies this segment).
88
+ // Root node has empty segmentName, producing treePath = ''.
89
+ const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;
86
90
  // TIM-834: Identify this segment's own params.ts (if it has a
87
91
  // segmentParams export). The full chain of params.ts files in the
88
92
  // route ancestry is threaded down via `ancestorParamsFiles`; codec
@@ -148,6 +152,7 @@ function collectRoutes(
148
152
 
149
153
  const entry: RouteEntry = {
150
154
  urlPath: node.urlPath,
155
+ segmentPath: treePath || '/',
151
156
  params: resolvedParams,
152
157
  hasSearchParams: false,
153
158
  isApiRoute,
@@ -170,12 +175,12 @@ function collectRoutes(
170
175
 
171
176
  // Recurse into children
172
177
  for (const child of node.children) {
173
- collectRoutes(child, params, nextAncestorFiles, routes, hasSchema);
178
+ collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema);
174
179
  }
175
180
 
176
181
  // Recurse into slots (they share the parent's URL path, but may have their own pages)
177
182
  for (const slot of Object.values(node.slots)) {
178
- collectRoutes(slot, params, nextAncestorFiles, routes, hasSchema);
183
+ collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema);
179
184
  }
180
185
  }
181
186
 
@@ -245,14 +250,14 @@ function formatSchemaTypes(routes: RouteEntry[]): string[] {
245
250
  lines.push('/** Union of all valid route patterns. */');
246
251
  lines.push('export type TimberRoute = keyof TimberSegmentParams;');
247
252
  lines.push('');
248
- lines.push('/** Maps each route pattern to its accumulated segment params. */');
253
+ lines.push('/** Maps each segment path to its accumulated segment params. */');
249
254
  lines.push('export interface TimberSegmentParams {');
250
255
 
251
256
  for (const route of routes) {
252
257
  if (route.params.length === 0) {
253
- lines.push(` '${route.urlPath}': {};`);
258
+ lines.push(` '${route.segmentPath}': {};`);
254
259
  } else {
255
- lines.push(` '${route.urlPath}': {`);
260
+ lines.push(` '${route.segmentPath}': {`);
256
261
  for (const p of route.params) {
257
262
  lines.push(` ${p.name}: ${buildSchemaParamType(p)};`);
258
263
  }
@@ -356,7 +361,9 @@ function formatDeclarationFile(
356
361
  lines.push("declare module '@timber-js/app/server' {");
357
362
  for (const route of dynamicRoutes) {
358
363
  const paramsType = formatParamsType(route.params, importBase, hasSchema);
359
- lines.push(` export function getSegmentParams(route: '${route.urlPath}'): ${paramsType}`);
364
+ lines.push(
365
+ ` export function getSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`
366
+ );
360
367
  }
361
368
  lines.push(' export function getSegmentParams(): Record<string, string | string[]>');
362
369
  lines.push('}');
@@ -372,11 +379,13 @@ function formatDeclarationFile(
372
379
  );
373
380
  lines.push('');
374
381
 
375
- // useParams overloads
382
+ // useSegmentParams overloads
376
383
  if (dynamicRoutes.length > 0) {
377
384
  for (const route of dynamicRoutes) {
378
385
  const paramsType = formatParamsType(route.params, importBase, hasSchema);
379
- lines.push(` export function useSegmentParams(route: '${route.urlPath}'): ${paramsType}`);
386
+ lines.push(
387
+ ` export function useSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`
388
+ );
380
389
  }
381
390
  lines.push(' export function useSegmentParams(): Record<string, string | string[]>');
382
391
  lines.push('');
@@ -1,47 +1,72 @@
1
1
  /**
2
- * _segment type generation and directory-to-route mapping.
2
+ * $segment type generation and directory-to-route mapping.
3
3
  *
4
- * Generates per-route _segment.d.ts files in .timber/types/ that mirror
4
+ * Generates per-route $segment.d.ts files in .timber/types/ that mirror
5
5
  * the app/ directory structure. TypeScript's rootDirs feature makes these
6
- * importable as './_segment' from route files.
6
+ * importable as './$segment' from route files.
7
7
  *
8
- * Design doc: design/41-global-params.md §The _segment Module
8
+ * Design doc: design/41-global-params.md §The $segment Module
9
9
  */
10
10
 
11
11
  import { writeFile, mkdir } from 'node:fs/promises';
12
12
  import { join } from 'node:path';
13
13
  import type { RouteTree, SegmentNode } from './types.js';
14
14
 
15
+ /** Info carried per directory for type generation. */
16
+ export interface SegmentInfo {
17
+ /** The URL route pattern (groups and slots stripped). e.g. '/[artistSlug]/[year]' */
18
+ route: string;
19
+ /** The full tree path including groups and slots — uniquely identifies a segment.
20
+ * e.g. '/(browse)/[artistSlug]/@shows/[year]' */
21
+ segmentPath: string;
22
+ }
23
+
15
24
  /**
16
- * Build a map of absolute filesystem directory → route pattern.
17
- * Used by resolveId to map a _segment import's importer to the correct route.
25
+ * Build a map of absolute filesystem directory → segment info.
26
+ * Used by resolveId to map a $segment import's importer to the correct route,
27
+ * and by writeSegmentTypes to generate per-route $segment.d.ts files.
18
28
  *
19
29
  * Walks every segment (not just leaves) so layouts and middleware
20
- * at any depth can import _segment.
30
+ * at any depth can import $segment.
21
31
  */
22
- export function buildDirToRouteMap(tree: RouteTree, appDir: string): Map<string, string> {
23
- const map = new Map<string, string>();
32
+ export function buildDirToRouteMap(tree: RouteTree, appDir: string): Map<string, SegmentInfo> {
33
+ const map = new Map<string, SegmentInfo>();
24
34
 
25
- function walk(node: SegmentNode, fsPath: string): void {
26
- const routePattern = node.urlPath || '/';
27
- map.set(fsPath, routePattern);
35
+ function walk(node: SegmentNode, fsPath: string, treePath: string): void {
36
+ map.set(fsPath, {
37
+ route: node.urlPath || '/',
38
+ segmentPath: treePath || '/',
39
+ });
28
40
 
29
41
  for (const child of node.children) {
30
42
  const childDir = join(fsPath, child.segmentName);
31
- walk(child, childDir);
43
+ const childTreePath = `${treePath}/${child.segmentName}`;
44
+ walk(child, childDir, childTreePath);
32
45
  }
33
46
  for (const [slotName, slotNode] of Object.entries(node.slots)) {
34
- const slotDir = join(fsPath, slotName);
35
- walk(slotNode, slotDir);
47
+ const slotDir = join(fsPath, `@${slotName}`);
48
+ const slotTreePath = `${treePath}/@${slotName}`;
49
+ walk(slotNode, slotDir, slotTreePath);
36
50
  }
37
51
  }
38
52
 
39
- walk(tree.root, appDir);
53
+ walk(tree.root, appDir, '');
40
54
  return map;
41
55
  }
42
56
 
43
57
  /**
44
- * Generate per-route _segment.d.ts files in .timber/types/ mirroring
58
+ * Generate the full $segment.d.ts content for a segment.
59
+ */
60
+ export function generateSegmentDts(info: SegmentInfo): string {
61
+ return [
62
+ `export const ROUTE: '${info.route}';`,
63
+ `export const SEGMENT_PATH: '${info.segmentPath}';`,
64
+ '',
65
+ ].join('\n');
66
+ }
67
+
68
+ /**
69
+ * Generate per-route $segment.d.ts files in .timber/types/ mirroring
45
70
  * the app/ directory structure. These files are importable via TypeScript's
46
71
  * rootDirs feature.
47
72
  */
@@ -57,15 +82,15 @@ export async function writeSegmentTypes(
57
82
  const map = buildDirToRouteMap(routeTree, appDir);
58
83
 
59
84
  const writes: Promise<void>[] = [];
60
- for (const [fsDir, routePattern] of map) {
85
+ for (const [fsDir, info] of map) {
61
86
  // Mirror the app/ structure under .timber/types/
62
87
  const relDir = fsDir.slice(appDir.length); // e.g. '/[artistSlug]/[year]'
63
88
  const segmentDir = join(typesBase, relDir);
64
- const segmentPath = join(segmentDir, '_segment.d.ts');
65
- const content = `export const route: '${routePattern}';\n`;
89
+ const segmentFile = join(segmentDir, '$segment.d.ts');
90
+ const content = generateSegmentDts(info);
66
91
 
67
92
  writes.push(
68
- mkdir(segmentDir, { recursive: true }).then(() => writeFile(segmentPath, content, 'utf-8'))
93
+ mkdir(segmentDir, { recursive: true }).then(() => writeFile(segmentFile, content, 'utf-8'))
69
94
  );
70
95
  }
71
96
 
@@ -60,12 +60,13 @@ export interface RequestContextStore {
60
60
  */
61
61
  segmentParams?: Record<string, string | string[]>;
62
62
  /**
63
- * The matched route pattern (e.g. '/[artistSlug]/[year]').
64
- * Set by the pipeline alongside segmentParams. Used by
65
- * getSegmentParams() for dev-mode route mismatch validation.
63
+ * The matched segment path (e.g. '/(browse)/[artistSlug]/[year]').
64
+ * Includes route groups and parallel slots — uniquely identifies the
65
+ * segment in the route tree. Set by the pipeline alongside segmentParams.
66
+ * Used by getSegmentParams() for dev-mode validation.
66
67
  * See design/41-global-params.md §Runtime Validation
67
68
  */
68
- matchedRoutePattern?: string;
69
+ matchedSegmentPath?: string;
69
70
  /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */
70
71
  cookieJar: Map<string, CookieEntry>;
71
72
  /** Whether the response has flushed (headers committed). */
@@ -20,7 +20,7 @@ import {
20
20
  applyRequestHeaderOverlay,
21
21
  setMutableCookieContext,
22
22
  setSegmentParams,
23
- setMatchedRoutePattern,
23
+ setMatchedSegmentPath,
24
24
  } from './request-context.js';
25
25
  import { withSpan } from './tracing.js';
26
26
  import { logRenderError } from './logger.js';
@@ -419,12 +419,14 @@ export async function handleRequest(
419
419
  // See design/07-routing.md §"params.ts — Convention File for Typed Params"
420
420
  setSegmentParams(match.segmentParams);
421
421
 
422
- // Store the matched route pattern for dev-mode validation in getSegmentParams().
423
- // The leaf segment's urlPath is the accumulated route pattern.
424
- const leaf = match.segments[match.segments.length - 1];
425
- if (leaf) {
426
- setMatchedRoutePattern(leaf.urlPath);
427
- }
422
+ // Store the matched segment path for dev-mode validation in getSegmentParams().
423
+ // Build the tree path from the segment chain (includes groups and slots).
424
+ const segmentPath =
425
+ match.segments
426
+ .map((s) => s.segmentName)
427
+ .filter(Boolean)
428
+ .join('/') || '/';
429
+ setMatchedSegmentPath(segmentPath.startsWith('/') ? segmentPath : `/${segmentPath}`);
428
430
 
429
431
  // Bypass middleware for synthetic re-render requests built by the
430
432
  // action-dispatch wrapper after a no-JS form validation failure.
@@ -89,16 +89,16 @@ _setGetSegmentParamsFn(getSegmentParams);
89
89
  /**
90
90
  * Returns the current request's coerced segment params.
91
91
  *
92
- * The optional `_route` argument exists only for TypeScript narrowing —
93
- * it does not affect the runtime return value. Pass a route pattern
94
- * (e.g. `'/[artistSlug]/[year]'`) to narrow the return type to the
95
- * exact params shape for that route.
92
+ * The optional `segmentPath` argument exists only for TypeScript narrowing —
93
+ * it does not affect the runtime return value. Pass a segment path
94
+ * (e.g. `'/(browse)/[artistSlug]/[year]'`) to narrow the return type to the
95
+ * exact params shape for that segment.
96
96
  *
97
- * Without a route argument, returns all coerced params with optional types.
97
+ * Without an argument, returns all coerced params with optional types.
98
98
  *
99
99
  * See design/41-global-params.md §Function Signatures
100
100
  */
101
- export function getSegmentParams(route?: string): Record<string, string | string[]> {
101
+ export function getSegmentParams(segmentPath?: string): Record<string, string | string[]> {
102
102
  const store = requestContextAls.getStore();
103
103
  if (!store) {
104
104
  throw new Error(
@@ -113,18 +113,18 @@ export function getSegmentParams(route?: string): Record<string, string | string
113
113
  );
114
114
  }
115
115
 
116
- // Dev-mode validation: warn when the route argument expects dynamic
117
- // segments that don't exist on the actual matched route. This catches
118
- // bugs like passing a child route's _segment to a parent layout.
116
+ // Dev-mode validation: warn when the segment path expects dynamic
117
+ // segments that don't exist on the actual matched segment. This catches
118
+ // bugs like passing a child segment's $segment to a parent layout.
119
119
  // See design/41-global-params.md §Runtime Validation
120
- if (route && isDebug() && store.matchedRoutePattern) {
121
- const expected = extractDynamicSegments(route);
122
- const actual = extractDynamicSegments(store.matchedRoutePattern);
120
+ if (segmentPath && isDebug() && store.matchedSegmentPath) {
121
+ const expected = extractDynamicSegments(segmentPath);
122
+ const actual = extractDynamicSegments(store.matchedSegmentPath);
123
123
  const missing = expected.filter((s) => !actual.includes(s));
124
124
 
125
125
  if (missing.length > 0) {
126
126
  console.warn(
127
- `[timber] getSegmentParams('${route}') called but current route is '${store.matchedRoutePattern}'\n` +
127
+ `[timber] getSegmentParams('${segmentPath}') called but current segment is '${store.matchedSegmentPath}'\n` +
128
128
  ` Missing params: ${missing.join(', ')}\n` +
129
129
  ` These will be undefined at runtime despite the type annotation.`
130
130
  );
@@ -149,15 +149,15 @@ export function setSegmentParams(params: Record<string, string | string[]>): voi
149
149
  }
150
150
 
151
151
  /**
152
- * Store the matched route pattern (e.g. '/[artistSlug]/[year]') for
153
- * dev-mode route mismatch validation in getSegmentParams().
152
+ * Store the matched segment path (e.g. '/(browse)/[artistSlug]/[year]') for
153
+ * dev-mode validation in getSegmentParams().
154
154
  *
155
155
  * @internal — framework use only
156
156
  */
157
- export function setMatchedRoutePattern(pattern: string): void {
157
+ export function setMatchedSegmentPath(segmentPath: string): void {
158
158
  const store = requestContextAls.getStore();
159
159
  if (!store) return; // non-throwing — optional diagnostic
160
- store.matchedRoutePattern = pattern;
160
+ store.matchedSegmentPath = segmentPath;
161
161
  }
162
162
 
163
163
  /**