@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.
- package/dist/_chunks/{actions-ILhDUzES.js → actions-DAVm0FQe.js} +17 -17
- package/dist/_chunks/actions-DAVm0FQe.js.map +1 -0
- package/dist/_chunks/{define-a0CCE6Nx.js → define-zQX6e1tN.js} +2 -2
- package/dist/_chunks/{define-a0CCE6Nx.js.map → define-zQX6e1tN.js.map} +1 -1
- package/dist/_chunks/{navigation-root-CY3pRMxF.js → navigation-root-DKsJ5Di7.js} +2 -2
- package/dist/_chunks/{navigation-root-CY3pRMxF.js.map → navigation-root-DKsJ5Di7.js.map} +1 -1
- package/dist/_chunks/tracing-DvKLzCFa.js.map +1 -1
- package/dist/_chunks/{use-segment-params-BSt_O2cb.js → use-segment-params-CskiK-59.js} +2 -2
- package/dist/_chunks/{use-segment-params-BSt_O2cb.js.map → use-segment-params-CskiK-59.js.map} +1 -1
- package/dist/_chunks/{walkers-DGV9dQRq.js → walkers-DgOXkTts.js} +12 -10
- package/dist/_chunks/walkers-DgOXkTts.js.map +1 -0
- package/dist/client/index.js +3 -3
- package/dist/client/internal.js +2 -2
- package/dist/client/use-segment-params.d.ts +3 -3
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/index.js +43 -26
- package/dist/index.js.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +3 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/segment-codegen.d.ts +22 -9
- package/dist/routing/segment-codegen.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/als-registry.d.ts +5 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +3 -3
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +9 -9
- package/dist/server/request-context.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/use-segment-params.ts +4 -4
- package/src/plugins/routing.ts +26 -17
- package/src/routing/codegen-types.ts +3 -0
- package/src/routing/codegen.ts +18 -9
- package/src/routing/segment-codegen.ts +46 -21
- package/src/server/als-registry.ts +5 -4
- package/src/server/pipeline-phases.ts +9 -7
- package/src/server/request-context.ts +17 -17
- package/dist/_chunks/actions-ILhDUzES.js.map +0 -1
- 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,
|
|
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 `
|
|
44
|
-
* it does not affect the runtime return value. Pass a
|
|
45
|
-
* (e.g. `'/[artistSlug]/[year]'`) to narrow the return type to the
|
|
46
|
-
* exact params shape for that
|
|
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
|
|
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(
|
|
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
|
|
62
|
-
* dev-mode
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
123
|
+
segmentPath: R
|
|
124
124
|
): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;
|
|
125
|
-
export function useSegmentParams(
|
|
126
|
-
export function useSegmentParams(
|
|
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.
|
package/src/plugins/routing.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
//
|
|
63
|
-
// `\
|
|
64
|
-
const SEGMENT_PREFIX = '\
|
|
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 →
|
|
126
|
-
let dirToRoute: Map<string,
|
|
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:
|
|
208
|
-
// Resolves `import { route } from '
|
|
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 === '
|
|
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
|
|
218
|
-
if (
|
|
219
|
-
return `${SEGMENT_PREFIX}${
|
|
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:
|
|
257
|
+
// TIM-935: $segment virtual module — exports route and segmentPath.
|
|
253
258
|
if (id.startsWith(SEGMENT_PREFIX)) {
|
|
254
|
-
const
|
|
255
|
-
return
|
|
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
|
|
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 */
|
package/src/routing/codegen.ts
CHANGED
|
@@ -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
|
|
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.
|
|
258
|
+
lines.push(` '${route.segmentPath}': {};`);
|
|
254
259
|
} else {
|
|
255
|
-
lines.push(` '${route.
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
*
|
|
2
|
+
* $segment type generation and directory-to-route mapping.
|
|
3
3
|
*
|
|
4
|
-
* Generates per-route
|
|
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 '
|
|
6
|
+
* importable as './$segment' from route files.
|
|
7
7
|
*
|
|
8
|
-
* Design doc: design/41-global-params.md §The
|
|
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 →
|
|
17
|
-
* Used by resolveId to map a
|
|
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
|
|
30
|
+
* at any depth can import $segment.
|
|
21
31
|
*/
|
|
22
|
-
export function buildDirToRouteMap(tree: RouteTree, appDir: string): Map<string,
|
|
23
|
-
const map = new Map<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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
65
|
-
const content =
|
|
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(
|
|
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
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
423
|
-
//
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
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 `
|
|
93
|
-
* it does not affect the runtime return value. Pass a
|
|
94
|
-
* (e.g. `'/[artistSlug]/[year]'`) to narrow the return type to the
|
|
95
|
-
* exact params shape for that
|
|
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
|
|
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(
|
|
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
|
|
117
|
-
// segments that don't exist on the actual matched
|
|
118
|
-
// bugs like passing a child
|
|
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 (
|
|
121
|
-
const expected = extractDynamicSegments(
|
|
122
|
-
const actual = extractDynamicSegments(store.
|
|
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('${
|
|
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
|
|
153
|
-
* dev-mode
|
|
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
|
|
157
|
+
export function setMatchedSegmentPath(segmentPath: string): void {
|
|
158
158
|
const store = requestContextAls.getStore();
|
|
159
159
|
if (!store) return; // non-throwing — optional diagnostic
|
|
160
|
-
store.
|
|
160
|
+
store.matchedSegmentPath = segmentPath;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|