@timber-js/app 0.2.0-alpha.110 → 0.2.0-alpha.112
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-D6gyxlGQ.js → actions-ILhDUzES.js} +46 -10
- package/dist/_chunks/actions-ILhDUzES.js.map +1 -0
- package/dist/_chunks/{build-output-helper-5TK2Emwp.js → build-output-helper-DhxiT18k.js} +1 -1
- package/dist/_chunks/{build-output-helper-5TK2Emwp.js.map → build-output-helper-DhxiT18k.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-BhXdbmgS.js +2 -0
- package/dist/_chunks/cli-schema-sync-DFZmnmqG.js +357 -0
- package/dist/_chunks/cli-schema-sync-DFZmnmqG.js.map +1 -0
- package/dist/_chunks/codec-registry-V7ARyeLN.js +21 -0
- package/dist/_chunks/codec-registry-V7ARyeLN.js.map +1 -0
- package/dist/_chunks/{define-Dn98bPbb.js → define-8zQ8Vm7x.js} +3 -3
- package/dist/_chunks/{define-Dn98bPbb.js.map → define-8zQ8Vm7x.js.map} +1 -1
- package/dist/_chunks/{define-BflNsrwD.js → define-a0CCE6Nx.js} +3 -3
- package/dist/_chunks/{define-BflNsrwD.js.map → define-a0CCE6Nx.js.map} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js → format-CR6v1pWE.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-CR6v1pWE.js.map} +1 -1
- package/dist/_chunks/{handler-store-B_df5Npe.js → handler-store-iqnt2ghN.js} +1 -1
- package/dist/_chunks/{handler-store-B_df5Npe.js.map → handler-store-iqnt2ghN.js.map} +1 -1
- package/dist/_chunks/{merge-search-params-Cm_KIWDX.js → merge-search-params-CwxJzBLS.js} +1 -1
- package/dist/_chunks/{merge-search-params-Cm_KIWDX.js.map → merge-search-params-CwxJzBLS.js.map} +1 -1
- package/dist/_chunks/{navigation-root-2ypaZQDx.js → navigation-root-CY3pRMxF.js} +2 -2
- package/dist/_chunks/{navigation-root-2ypaZQDx.js.map → navigation-root-CY3pRMxF.js.map} +1 -1
- package/dist/_chunks/{registry-DB93kT5c.js → registry-Dp_hJL4s.js} +1 -1
- package/dist/_chunks/{registry-DB93kT5c.js.map → registry-Dp_hJL4s.js.map} +1 -1
- package/dist/_chunks/resolve-schema-DO8B0mO9.js +65 -0
- package/dist/_chunks/resolve-schema-DO8B0mO9.js.map +1 -0
- package/dist/_chunks/{router-ref-TkUdmqtM.js → router-ref-sektkr-t.js} +2 -2
- package/dist/_chunks/{router-ref-TkUdmqtM.js.map → router-ref-sektkr-t.js.map} +1 -1
- package/dist/_chunks/scanner-DmfoB7iK.js +336 -0
- package/dist/_chunks/scanner-DmfoB7iK.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-eL5vQDSA.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-eL5vQDSA.js.map} +1 -1
- package/dist/_chunks/{segment-context-Dc-GbSTe.js → segment-context-EsfQpS--.js} +1 -1
- package/dist/_chunks/{segment-context-Dc-GbSTe.js.map → segment-context-EsfQpS--.js.map} +1 -1
- package/dist/_chunks/{ssr-data-DzuI0bIV.js → ssr-data-BO5OvEvg.js} +1 -1
- package/dist/_chunks/{ssr-data-DzuI0bIV.js.map → ssr-data-BO5OvEvg.js.map} +1 -1
- package/dist/_chunks/{stale-reload-D6FlD3xn.js → stale-reload-BCPRG0Cv.js} +1 -1
- package/dist/_chunks/{stale-reload-D6FlD3xn.js.map → stale-reload-BCPRG0Cv.js.map} +1 -1
- package/dist/_chunks/tracing-DvKLzCFa.js.map +1 -1
- package/dist/_chunks/{use-query-states-B-cLl8mx.js → use-query-states-D9SBlAfp.js} +2 -2
- package/dist/_chunks/{use-query-states-B-cLl8mx.js.map → use-query-states-D9SBlAfp.js.map} +1 -1
- package/dist/_chunks/{use-segment-params-07V1TIxL.js → use-segment-params-BSt_O2cb.js} +2 -2
- package/dist/_chunks/{use-segment-params-07V1TIxL.js.map → use-segment-params-BSt_O2cb.js.map} +1 -1
- package/dist/_chunks/{validation-DfyuMoK2.js → validation-Bm3Je-09.js} +1 -1
- package/dist/_chunks/{validation-DfyuMoK2.js.map → validation-Bm3Je-09.js.map} +1 -1
- package/dist/_chunks/{walkers-DQn5NNrf.js → walkers-DGTmJT0f.js} +109 -353
- package/dist/_chunks/walkers-DGTmJT0f.js.map +1 -0
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/cache/index.js +3 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/sizeof.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +37 -0
- package/dist/cli-schema-sync.d.ts.map +1 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +34 -3
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/error-boundary.js +5 -3
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/index.js +27 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +7 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/index.js +3 -3
- package/dist/dev-tools/browser-logs.d.ts.map +1 -1
- package/dist/dev-tools/error-page.d.ts.map +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -1
- package/dist/dev-tools/overlay.d.ts +1 -1
- package/dist/dev-tools/overlay.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -236
- package/dist/index.js.map +1 -1
- package/dist/params/codec-registry.d.ts +26 -0
- package/dist/params/codec-registry.d.ts.map +1 -0
- package/dist/params/define-schema.d.ts +52 -0
- package/dist/params/define-schema.d.ts.map +1 -0
- package/dist/params/index.d.ts +5 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +33 -0
- package/dist/params/index.js.map +1 -0
- package/dist/params/resolve-schema.d.ts +34 -0
- package/dist/params/resolve-schema.d.ts.map +1 -0
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +8 -0
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +5 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +5 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +2 -1
- package/dist/routing/link-codegen.d.ts +2 -2
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -3
- package/dist/segment-params/index.js +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/coalesce-tick.d.ts +40 -0
- package/dist/server/coalesce-tick.d.ts.map +1 -0
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +30 -4
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +12 -2
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +18 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +17 -3
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +8 -14
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/package.json +11 -6
- package/src/cache/sizeof.ts +3 -1
- package/src/cli-schema-sync.ts +206 -0
- package/src/cli.ts +49 -2
- package/src/client/browser-entry/action-dispatch.ts +15 -5
- package/src/client/browser-entry/index.ts +6 -0
- package/src/client/error-boundary.tsx +2 -2
- package/src/client/link.tsx +25 -1
- package/src/dev-tools/browser-logs.ts +5 -2
- package/src/dev-tools/error-page.ts +3 -6
- package/src/dev-tools/logs.ts +6 -2
- package/src/dev-tools/overlay.ts +18 -5
- package/src/index.ts +5 -0
- package/src/params/codec-registry.ts +34 -0
- package/src/params/define-schema.ts +72 -0
- package/src/params/index.ts +11 -0
- package/src/params/resolve-schema.ts +88 -0
- package/src/plugins/dev-server.ts +24 -16
- package/src/plugins/mdx.ts +5 -1
- package/src/plugins/routing.ts +107 -2
- package/src/routing/codegen-shared.ts +11 -0
- package/src/routing/codegen-types.ts +5 -0
- package/src/routing/codegen.ts +129 -21
- package/src/routing/link-codegen.ts +19 -5
- package/src/server/als-registry.ts +7 -0
- package/src/server/coalesce-tick.ts +75 -0
- package/src/server/fallback-error.ts +3 -3
- package/src/server/html-injectors.ts +11 -22
- package/src/server/node-stream-transforms.ts +6 -16
- package/src/server/param-coercion.ts +55 -2
- package/src/server/pipeline-phases.ts +8 -0
- package/src/server/pipeline.ts +19 -0
- package/src/server/request-context.ts +58 -3
- package/src/server/rsc-entry/helpers.ts +3 -2
- package/src/server/rsc-entry/index.ts +28 -35
- package/LICENSE +0 -8
- package/dist/_chunks/actions-D6gyxlGQ.js.map +0 -1
- package/dist/_chunks/walkers-DQn5NNrf.js.map +0 -1
|
@@ -1,336 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { h as swallow } from "./logger-DDmK5skc.js";
|
|
4
|
-
import { basename, extname, join, posix, relative } from "node:path";
|
|
5
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
-
//#region src/routing/scanner.ts
|
|
7
|
-
/**
|
|
8
|
-
* Route discovery scanner.
|
|
9
|
-
*
|
|
10
|
-
* Pure function: (appDir, config) → RouteTree
|
|
11
|
-
*
|
|
12
|
-
* Scans the app/ directory and builds a segment tree recognizing all
|
|
13
|
-
* timber.js file conventions. Does NOT handle request matching — this
|
|
14
|
-
* is discovery only.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Pattern matching encoded path delimiters that must be rejected during route discovery.
|
|
18
|
-
* %2F / %2f (forward slash) and %5C / %5c (backslash) can cause route collisions
|
|
19
|
-
* when decoded. See design/13-security.md §"Encoded separators rejected".
|
|
20
|
-
*/
|
|
21
|
-
var ENCODED_SEPARATOR_PATTERN = /%(?:2[fF]|5[cC])/;
|
|
22
|
-
/**
|
|
23
|
-
* Pattern matching encoded null bytes (%00) that must be rejected.
|
|
24
|
-
* See design/13-security.md §"Null bytes rejected".
|
|
25
|
-
*/
|
|
26
|
-
var ENCODED_NULL_PATTERN = /%00/;
|
|
27
|
-
/**
|
|
28
|
-
* File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)
|
|
29
|
-
*/
|
|
30
|
-
var PAGE_EXT_CONVENTIONS = new Set([
|
|
31
|
-
"page",
|
|
32
|
-
"layout",
|
|
33
|
-
"error",
|
|
34
|
-
"default",
|
|
35
|
-
"denied"
|
|
36
|
-
]);
|
|
37
|
-
/**
|
|
38
|
-
* Legacy compat status-code files.
|
|
39
|
-
* Maps legacy file name → HTTP status code for the fallback chain.
|
|
40
|
-
* See design/10-error-handling.md §"Fallback Chain".
|
|
41
|
-
*/
|
|
42
|
-
var LEGACY_STATUS_FILES = {
|
|
43
|
-
"not-found": 404,
|
|
44
|
-
"forbidden": 403,
|
|
45
|
-
"unauthorized": 401
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* File convention names that are always .ts/.tsx (never .mdx etc.)
|
|
49
|
-
*/
|
|
50
|
-
var FIXED_CONVENTIONS = new Set([
|
|
51
|
-
"middleware",
|
|
52
|
-
"access",
|
|
53
|
-
"route",
|
|
54
|
-
"params"
|
|
55
|
-
]);
|
|
56
|
-
/**
|
|
57
|
-
* Status-code file patterns:
|
|
58
|
-
* - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx
|
|
59
|
-
* - Category catch-alls: 4xx.tsx, 5xx.tsx
|
|
60
|
-
*/
|
|
61
|
-
var STATUS_CODE_PATTERN = /^(\d{3}|[45]xx)$/;
|
|
62
|
-
/**
|
|
63
|
-
* Scan the app/ directory and build the route tree.
|
|
64
|
-
*
|
|
65
|
-
* @param appDir - Absolute path to the app/ directory
|
|
66
|
-
* @param config - Scanner configuration
|
|
67
|
-
* @returns The complete route tree
|
|
68
|
-
*/
|
|
69
|
-
function scanRoutes(appDir, config = {}) {
|
|
70
|
-
const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;
|
|
71
|
-
const extSet = new Set(pageExtensions);
|
|
72
|
-
const tree = { root: createSegmentNode("", "static", "/") };
|
|
73
|
-
const proxyFile = findFixedFile(appDir, "proxy");
|
|
74
|
-
if (proxyFile) tree.proxy = proxyFile;
|
|
75
|
-
const globalErrorFile = findPageExtFile(appDir, "global-error", extSet);
|
|
76
|
-
if (globalErrorFile) tree.globalError = globalErrorFile;
|
|
77
|
-
scanSegmentFiles(appDir, tree.root, extSet);
|
|
78
|
-
scanChildren(appDir, tree.root, extSet);
|
|
79
|
-
validateRouteGroupCollisions(tree.root);
|
|
80
|
-
validateDuplicateParamNames(tree.root);
|
|
81
|
-
return tree;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Create an empty segment node.
|
|
85
|
-
*/
|
|
86
|
-
function createSegmentNode(segmentName, segmentType, urlPath, paramName, interceptionMarker, interceptedSegmentName) {
|
|
87
|
-
return {
|
|
88
|
-
segmentName,
|
|
89
|
-
segmentType,
|
|
90
|
-
urlPath,
|
|
91
|
-
paramName,
|
|
92
|
-
interceptionMarker,
|
|
93
|
-
interceptedSegmentName,
|
|
94
|
-
children: [],
|
|
95
|
-
slots: {}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Compute the URL path for a child segment given its parent's URL path.
|
|
100
|
-
* Route groups, slots, and intercepting routes do NOT add URL depth.
|
|
101
|
-
*/
|
|
102
|
-
function computeUrlPath(parentUrlPath, dirName, segmentType) {
|
|
103
|
-
if (segmentType === "group" || segmentType === "slot" || segmentType === "intercepting") return parentUrlPath;
|
|
104
|
-
return `${parentUrlPath === "/" ? "" : parentUrlPath}/${dirName}`;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Scan a directory for file conventions and populate the segment node.
|
|
108
|
-
*/
|
|
109
|
-
function scanSegmentFiles(dirPath, node, extSet) {
|
|
110
|
-
let entries;
|
|
111
|
-
try {
|
|
112
|
-
entries = readdirSync(dirPath);
|
|
113
|
-
} catch (err) {
|
|
114
|
-
swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: "warn" });
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
for (const entry of entries) {
|
|
118
|
-
const fullPath = join(dirPath, entry);
|
|
119
|
-
try {
|
|
120
|
-
if (statSync(fullPath).isDirectory()) continue;
|
|
121
|
-
} catch {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const ext = extname(entry).slice(1);
|
|
125
|
-
const name = basename(entry, `.${ext}`);
|
|
126
|
-
if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {
|
|
127
|
-
const file = {
|
|
128
|
-
filePath: fullPath,
|
|
129
|
-
extension: ext
|
|
130
|
-
};
|
|
131
|
-
switch (name) {
|
|
132
|
-
case "page":
|
|
133
|
-
node.page = file;
|
|
134
|
-
break;
|
|
135
|
-
case "layout":
|
|
136
|
-
node.layout = file;
|
|
137
|
-
break;
|
|
138
|
-
case "error":
|
|
139
|
-
node.error = file;
|
|
140
|
-
break;
|
|
141
|
-
case "default":
|
|
142
|
-
node.default = file;
|
|
143
|
-
break;
|
|
144
|
-
case "denied":
|
|
145
|
-
node.denied = file;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (FIXED_CONVENTIONS.has(name) && /\.?[jt]sx?$/.test(ext)) {
|
|
151
|
-
const file = {
|
|
152
|
-
filePath: fullPath,
|
|
153
|
-
extension: ext
|
|
154
|
-
};
|
|
155
|
-
switch (name) {
|
|
156
|
-
case "middleware":
|
|
157
|
-
node.middleware = file;
|
|
158
|
-
break;
|
|
159
|
-
case "access":
|
|
160
|
-
node.access = file;
|
|
161
|
-
break;
|
|
162
|
-
case "route":
|
|
163
|
-
node.route = file;
|
|
164
|
-
break;
|
|
165
|
-
case "params":
|
|
166
|
-
node.params = file;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (STATUS_CODE_PATTERN.test(name) && ext === "json") {
|
|
172
|
-
if (!node.jsonStatusFiles) node.jsonStatusFiles = {};
|
|
173
|
-
node.jsonStatusFiles[name] = {
|
|
174
|
-
filePath: fullPath,
|
|
175
|
-
extension: ext
|
|
176
|
-
};
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {
|
|
180
|
-
if (!node.statusFiles) node.statusFiles = {};
|
|
181
|
-
node.statusFiles[name] = {
|
|
182
|
-
filePath: fullPath,
|
|
183
|
-
extension: ext
|
|
184
|
-
};
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {
|
|
188
|
-
if (!node.legacyStatusFiles) node.legacyStatusFiles = {};
|
|
189
|
-
node.legacyStatusFiles[name] = {
|
|
190
|
-
filePath: fullPath,
|
|
191
|
-
extension: ext
|
|
192
|
-
};
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (classifyMetadataRoute(entry)) {
|
|
196
|
-
if (!node.metadataRoutes) node.metadataRoutes = {};
|
|
197
|
-
const existing = node.metadataRoutes[name];
|
|
198
|
-
if (existing) {
|
|
199
|
-
const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);
|
|
200
|
-
if (isDynamicMetadataExtension(name, ext) || !existingIsDynamic) node.metadataRoutes[name] = {
|
|
201
|
-
filePath: fullPath,
|
|
202
|
-
extension: ext
|
|
203
|
-
};
|
|
204
|
-
} else node.metadataRoutes[name] = {
|
|
205
|
-
filePath: fullPath,
|
|
206
|
-
extension: ext
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (node.route && node.page) throw new Error(`Build error: route.ts and page.* cannot coexist in the same segment.\n route.ts: ${node.route.filePath}\n page: ${node.page.filePath}\nA URL is either an API endpoint or a rendered page, not both.`);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Recursively scan child directories and build the segment tree.
|
|
214
|
-
*/
|
|
215
|
-
function scanChildren(dirPath, parentNode, extSet) {
|
|
216
|
-
let entries;
|
|
217
|
-
try {
|
|
218
|
-
entries = readdirSync(dirPath);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: "warn" });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
for (const entry of entries) {
|
|
224
|
-
const fullPath = join(dirPath, entry);
|
|
225
|
-
try {
|
|
226
|
-
if (!statSync(fullPath).isDirectory()) continue;
|
|
227
|
-
} catch {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (ENCODED_SEPARATOR_PATTERN.test(entry)) throw new Error(`Build error: directory name contains an encoded path delimiter (%%2F or %%5C).\n Directory: ${fullPath}\nEncoded separators in directory names cause route collisions when decoded. Rename the directory to remove the encoded delimiter.`);
|
|
231
|
-
if (ENCODED_NULL_PATTERN.test(entry)) throw new Error(`Build error: directory name contains an encoded null byte (%%00).\n Directory: ${fullPath}\nEncoded null bytes in directory names are not allowed. Rename the directory to remove the null byte encoding.`);
|
|
232
|
-
const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);
|
|
233
|
-
if (type === "private") continue;
|
|
234
|
-
const childNode = createSegmentNode(entry, type, computeUrlPath(parentNode.urlPath, entry, type), paramName, interceptionMarker, interceptedSegmentName);
|
|
235
|
-
scanSegmentFiles(fullPath, childNode, extSet);
|
|
236
|
-
scanChildren(fullPath, childNode, extSet);
|
|
237
|
-
if (type === "slot") {
|
|
238
|
-
const slotName = entry.slice(1);
|
|
239
|
-
parentNode.slots[slotName] = childNode;
|
|
240
|
-
} else parentNode.children.push(childNode);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Validate that route groups don't produce conflicting pages/routes at the same URL path.
|
|
245
|
-
*
|
|
246
|
-
* Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
|
|
247
|
-
* /login — the scanner must detect and reject this at build time.
|
|
248
|
-
*
|
|
249
|
-
* Parallel slots are excluded from collision detection — they intentionally coexist at
|
|
250
|
-
* the same URL path as their parent (that's the whole point of parallel routes).
|
|
251
|
-
*/
|
|
252
|
-
function validateRouteGroupCollisions(root) {
|
|
253
|
-
collectRoutableLeaves(root, /* @__PURE__ */ new Map(), "", false);
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Walk the segment tree and collect all routable leaves (page or route files),
|
|
257
|
-
* throwing on collision. Slots are tracked in their own collision space since
|
|
258
|
-
* they are parallel routes that intentionally share URL paths with their parent.
|
|
259
|
-
*/
|
|
260
|
-
function collectRoutableLeaves(node, seen, segmentPath, insideSlot) {
|
|
261
|
-
const currentPath = segmentPath ? `${segmentPath}/${node.segmentName}` : node.segmentName || "(root)";
|
|
262
|
-
if (!insideSlot) {
|
|
263
|
-
const routableFile = node.page ?? node.route;
|
|
264
|
-
if (routableFile) {
|
|
265
|
-
const existing = seen.get(node.urlPath);
|
|
266
|
-
if (existing) throw new Error(`Build error: route collision — multiple route groups produce a page/route at the same URL path.\n URL path: ${node.urlPath}\n File 1: ${existing.filePath} (via ${existing.segmentPath})\n File 2: ${routableFile.filePath} (via ${currentPath})\nEach URL path must map to exactly one page or route handler. Rename or move one of the conflicting files.`);
|
|
267
|
-
seen.set(node.urlPath, {
|
|
268
|
-
filePath: routableFile.filePath,
|
|
269
|
-
segmentPath: currentPath
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
for (const child of node.children) collectRoutableLeaves(child, seen, currentPath, insideSlot);
|
|
274
|
-
for (const slotNode of Object.values(node.slots)) collectRoutableLeaves(slotNode, seen, currentPath, true);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Validate that no route chain contains duplicate dynamic param names.
|
|
278
|
-
*
|
|
279
|
-
* Example violation:
|
|
280
|
-
* app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.
|
|
281
|
-
*
|
|
282
|
-
* Route groups are transparent — params accumulate through them.
|
|
283
|
-
* Slots are independent — duplicate detection does NOT cross slot boundaries.
|
|
284
|
-
*
|
|
285
|
-
* See design/07-routing.md §"Duplicate Param Name Detection"
|
|
286
|
-
*/
|
|
287
|
-
function validateDuplicateParamNames(root) {
|
|
288
|
-
walkForDuplicateParams(root, /* @__PURE__ */ new Map());
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Recursively walk the segment tree, tracking seen param names → segment paths.
|
|
292
|
-
* Throws on the first duplicate found.
|
|
293
|
-
*/
|
|
294
|
-
function walkForDuplicateParams(node, seen) {
|
|
295
|
-
if (node.paramName) {
|
|
296
|
-
const existing = seen.get(node.paramName);
|
|
297
|
-
if (existing) throw new Error(`[timber] Duplicate param name '${node.paramName}' in route chain.\n First defined at: ${existing}\n Duplicate at: ${node.urlPath || "/"}\n Rename one of the segments to avoid ambiguity.`);
|
|
298
|
-
seen = new Map(seen);
|
|
299
|
-
seen.set(node.paramName, node.urlPath || "/");
|
|
300
|
-
}
|
|
301
|
-
for (const child of node.children) walkForDuplicateParams(child, seen);
|
|
302
|
-
for (const slotNode of Object.values(node.slots)) walkForDuplicateParams(slotNode, new Map(seen));
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Find a fixed-extension file (proxy.ts) in a directory.
|
|
306
|
-
*/
|
|
307
|
-
function findFixedFile(dirPath, name) {
|
|
308
|
-
for (const ext of ["ts", "tsx"]) {
|
|
309
|
-
const fullPath = join(dirPath, `${name}.${ext}`);
|
|
310
|
-
try {
|
|
311
|
-
if (statSync(fullPath).isFile()) return {
|
|
312
|
-
filePath: fullPath,
|
|
313
|
-
extension: ext
|
|
314
|
-
};
|
|
315
|
-
} catch {}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).
|
|
320
|
-
* Used for app-root conventions like global-error that aren't per-segment.
|
|
321
|
-
*/
|
|
322
|
-
function findPageExtFile(dirPath, name, extSet) {
|
|
323
|
-
for (const ext of extSet) {
|
|
324
|
-
const fullPath = join(dirPath, `${name}.${ext}`);
|
|
325
|
-
try {
|
|
326
|
-
if (statSync(fullPath).isFile()) return {
|
|
327
|
-
filePath: fullPath,
|
|
328
|
-
extension: ext
|
|
329
|
-
};
|
|
330
|
-
} catch {}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
//#endregion
|
|
1
|
+
import { join, posix, relative } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
334
3
|
//#region src/routing/codegen-shared.ts
|
|
335
4
|
/**
|
|
336
5
|
* Shared codegen helpers — import-path computation, codec chain type
|
|
@@ -395,6 +64,16 @@ function buildCodecChainType(p, importBase, fallback) {
|
|
|
395
64
|
return inner;
|
|
396
65
|
}
|
|
397
66
|
/**
|
|
67
|
+
* Build a TypeScript type expression for a single param using the global
|
|
68
|
+
* schema's InferCodecType. Falls back to `fallback` when schema doesn't
|
|
69
|
+
* declare this param.
|
|
70
|
+
*
|
|
71
|
+
* Generates: `InferCodecType<typeof schema.segmentParams['[paramName]']>`
|
|
72
|
+
*/
|
|
73
|
+
function buildSchemaParamType(p) {
|
|
74
|
+
return `InferCodecType<typeof schema.segmentParams[${JSON.stringify(p.bracketName)}]>`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
398
77
|
* Format the searchParams type for a route entry.
|
|
399
78
|
*
|
|
400
79
|
* When a page.tsx (or params.ts) exports searchParams, we reference its
|
|
@@ -435,10 +114,11 @@ function buildResolvedPattern(route) {
|
|
|
435
114
|
* When the segment's params chain (TIM-834) declares a typed codec, the
|
|
436
115
|
* inferred type from the codec wins via a nested conditional.
|
|
437
116
|
*/
|
|
438
|
-
function formatLinkParamsType(params, importBase) {
|
|
117
|
+
function formatLinkParamsType(params, importBase, hasSchema) {
|
|
439
118
|
if (params.length === 0) return "{}";
|
|
440
119
|
return `{ ${params.map((p) => {
|
|
441
|
-
const
|
|
120
|
+
const fallback = p.type === "string" ? "string | number" : p.type;
|
|
121
|
+
const codecType = hasSchema ? buildSchemaParamType(p) : buildCodecChainType(p, importBase, fallback);
|
|
442
122
|
return `${p.name}: ${codecType}`;
|
|
443
123
|
}).join("; ")} }`;
|
|
444
124
|
}
|
|
@@ -505,13 +185,13 @@ function formatLinkCatchAllOverloads() {
|
|
|
505
185
|
* so the resolved-template-vs-pattern ordering reduces to placing the
|
|
506
186
|
* pattern variant before the resolved-template variant inside the union.
|
|
507
187
|
*/
|
|
508
|
-
function formatTypedLinkOverloads(routes, importBase) {
|
|
188
|
+
function formatTypedLinkOverloads(routes, importBase, hasSchema) {
|
|
509
189
|
const lines = [];
|
|
510
190
|
const baseProps = LINK_BASE_PROPS_TYPE;
|
|
511
191
|
const variants = [];
|
|
512
192
|
for (const route of routes) {
|
|
513
193
|
const hasDynamicParams = route.params.length > 0;
|
|
514
|
-
const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase)}` : "segmentParams?: Record<string, unknown>";
|
|
194
|
+
const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase, hasSchema)}` : "segmentParams?: Record<string, unknown>";
|
|
515
195
|
const searchParamsType = route.hasSearchParams ? formatSearchParamsType(route, importBase) : null;
|
|
516
196
|
const patternSearchParamsProp = searchParamsType ? `searchParams?: Partial<${searchParamsType}>` : "searchParams?: Record<string, unknown>";
|
|
517
197
|
variants.push(`${baseProps} & { href: '${route.urlPath}'; ${paramsProp}; ${patternSearchParamsProp} }`);
|
|
@@ -553,10 +233,25 @@ function formatTypedLinkOverloads(routes, importBase) {
|
|
|
553
233
|
* interface that maps every route path to its params and searchParams shape.
|
|
554
234
|
*/
|
|
555
235
|
function generateRouteMap(tree, options = {}) {
|
|
236
|
+
const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
|
|
556
237
|
const routes = [];
|
|
557
|
-
collectRoutes(tree.root, [], [], routes);
|
|
238
|
+
collectRoutes(tree.root, [], [], routes, hasSchema);
|
|
558
239
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
559
|
-
return formatDeclarationFile(routes, options.outputDir ?? options.appDir);
|
|
240
|
+
return formatDeclarationFile(routes, options.outputDir ?? options.appDir, hasSchema);
|
|
241
|
+
}
|
|
242
|
+
/** Schema file extensions to check, in priority order. */
|
|
243
|
+
var SCHEMA_EXTENSIONS = [
|
|
244
|
+
".ts",
|
|
245
|
+
".tsx",
|
|
246
|
+
".js",
|
|
247
|
+
".jsx"
|
|
248
|
+
];
|
|
249
|
+
/**
|
|
250
|
+
* Detect whether app/schema.{ts,tsx,js,jsx} exists.
|
|
251
|
+
*/
|
|
252
|
+
function detectSchema(appDir) {
|
|
253
|
+
if (!appDir) return false;
|
|
254
|
+
return SCHEMA_EXTENSIONS.some((ext) => existsSync(join(appDir, `schema${ext}`)));
|
|
560
255
|
}
|
|
561
256
|
/**
|
|
562
257
|
* Recursively walk the segment tree and collect route entries.
|
|
@@ -564,13 +259,14 @@ function generateRouteMap(tree, options = {}) {
|
|
|
564
259
|
* A route entry is created for any segment that has a `page` or `route` file.
|
|
565
260
|
* Params accumulate from ancestor dynamic segments.
|
|
566
261
|
*/
|
|
567
|
-
function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
|
|
262
|
+
function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes, hasSchema) {
|
|
568
263
|
const ownParamsFile = node.params && fileHasExport(node.params.filePath, "segmentParams") ? node.params.filePath : void 0;
|
|
569
264
|
const params = [...ancestorParams];
|
|
570
265
|
if (node.paramName) {
|
|
571
266
|
const legacyFallback = ownParamsFile ? void 0 : findLegacyParamsExport(node);
|
|
572
267
|
params.push({
|
|
573
268
|
name: node.paramName,
|
|
269
|
+
bracketName: node.segmentName,
|
|
574
270
|
type: paramTypeForSegment(node.segmentType),
|
|
575
271
|
legacyCodecFilePath: legacyFallback
|
|
576
272
|
});
|
|
@@ -584,6 +280,7 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
|
|
|
584
280
|
const codecFilePaths = leafFirstChain.length > 0 ? leafFirstChain : p.legacyCodecFilePath ? [p.legacyCodecFilePath] : void 0;
|
|
585
281
|
return {
|
|
586
282
|
name: p.name,
|
|
283
|
+
bracketName: p.bracketName,
|
|
587
284
|
type: p.type,
|
|
588
285
|
codecFilePaths
|
|
589
286
|
};
|
|
@@ -592,7 +289,8 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
|
|
|
592
289
|
urlPath: node.urlPath,
|
|
593
290
|
params: resolvedParams,
|
|
594
291
|
hasSearchParams: false,
|
|
595
|
-
isApiRoute
|
|
292
|
+
isApiRoute,
|
|
293
|
+
hasSchema
|
|
596
294
|
};
|
|
597
295
|
if (isPage) {
|
|
598
296
|
if (node.params && fileHasExport(node.params.filePath, "searchParams")) {
|
|
@@ -605,8 +303,8 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, routes) {
|
|
|
605
303
|
}
|
|
606
304
|
routes.push(entry);
|
|
607
305
|
}
|
|
608
|
-
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes);
|
|
609
|
-
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, routes);
|
|
306
|
+
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, routes, hasSchema);
|
|
307
|
+
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, routes, hasSchema);
|
|
610
308
|
}
|
|
611
309
|
/**
|
|
612
310
|
* Determine the TypeScript type for a segment's param.
|
|
@@ -646,21 +344,69 @@ function findLegacyParamsExport(node) {
|
|
|
646
344
|
if (node.page && fileHasExport(node.page.filePath, "segmentParams")) return node.page.filePath;
|
|
647
345
|
}
|
|
648
346
|
/**
|
|
347
|
+
* Emit the schema-based type exports: TimberSegmentParams, TimberRoute,
|
|
348
|
+
* AllSegmentParams, RoutesWithParams.
|
|
349
|
+
*
|
|
350
|
+
* These types use InferCodecType references so they stay in sync with
|
|
351
|
+
* the schema without hardcoding resolved types.
|
|
352
|
+
*
|
|
353
|
+
* Design doc: design/41-global-params.md §Codegen Output
|
|
354
|
+
*/
|
|
355
|
+
function formatSchemaTypes(routes) {
|
|
356
|
+
const lines = [];
|
|
357
|
+
lines.push("/** Union of all valid route patterns. */");
|
|
358
|
+
lines.push("export type TimberRoute = keyof TimberSegmentParams;");
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push("/** Maps each route pattern to its accumulated segment params. */");
|
|
361
|
+
lines.push("export interface TimberSegmentParams {");
|
|
362
|
+
for (const route of routes) if (route.params.length === 0) lines.push(` '${route.urlPath}': {};`);
|
|
363
|
+
else {
|
|
364
|
+
lines.push(` '${route.urlPath}': {`);
|
|
365
|
+
for (const p of route.params) lines.push(` ${p.name}: ${buildSchemaParamType(p)};`);
|
|
366
|
+
lines.push(" };");
|
|
367
|
+
}
|
|
368
|
+
lines.push("}");
|
|
369
|
+
lines.push("");
|
|
370
|
+
lines.push("/** All segment params (union of all route params). */");
|
|
371
|
+
lines.push("export type AllSegmentParams = {");
|
|
372
|
+
lines.push(" [K in keyof TimberSegmentParams as TimberSegmentParams[K] extends Record<string, never>");
|
|
373
|
+
lines.push(" ? never");
|
|
374
|
+
lines.push(" : K]: TimberSegmentParams[K];");
|
|
375
|
+
lines.push("} extends infer U");
|
|
376
|
+
lines.push(" ? { [K in keyof U[keyof U]]: U[keyof U][K] }");
|
|
377
|
+
lines.push(" : never;");
|
|
378
|
+
lines.push("");
|
|
379
|
+
lines.push("/** Filters TimberRoute to routes whose accumulated params include all listed keys. */");
|
|
380
|
+
lines.push("export type RoutesWithParams<K extends keyof AllSegmentParams> = {");
|
|
381
|
+
lines.push(" [R in TimberRoute]: K extends keyof TimberSegmentParams[R] ? R : never;");
|
|
382
|
+
lines.push("}[TimberRoute];");
|
|
383
|
+
return lines;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
649
386
|
* Format the collected routes into a TypeScript declaration file.
|
|
650
387
|
*/
|
|
651
|
-
function formatDeclarationFile(routes, importBase) {
|
|
388
|
+
function formatDeclarationFile(routes, importBase, hasSchema) {
|
|
652
389
|
const lines = [];
|
|
653
390
|
lines.push("// This file is auto-generated by timber.js route map codegen.");
|
|
654
391
|
lines.push("// Do not edit manually. Regenerated on build and in dev mode.");
|
|
655
392
|
lines.push("");
|
|
656
393
|
lines.push("export {};");
|
|
657
394
|
lines.push("");
|
|
658
|
-
|
|
659
|
-
|
|
395
|
+
if (hasSchema) {
|
|
396
|
+
lines.push("import type schema from '../app/schema';");
|
|
397
|
+
lines.push("import type { InferCodecType } from '@timber-js/app/params';");
|
|
398
|
+
lines.push("");
|
|
399
|
+
lines.push(...formatSchemaTypes(routes));
|
|
400
|
+
lines.push("");
|
|
401
|
+
}
|
|
402
|
+
if (!hasSchema) {
|
|
403
|
+
lines.push(emitResolveSegmentFieldHelper());
|
|
404
|
+
lines.push("");
|
|
405
|
+
}
|
|
660
406
|
lines.push("declare module '@timber-js/app' {");
|
|
661
407
|
lines.push(" interface Routes {");
|
|
662
408
|
for (const route of routes) {
|
|
663
|
-
const paramsType = formatParamsType(route.params, importBase);
|
|
409
|
+
const paramsType = formatParamsType(route.params, importBase, hasSchema);
|
|
664
410
|
const searchParamsType = formatSearchParamsType(route, importBase);
|
|
665
411
|
lines.push(` '${route.urlPath}': {`);
|
|
666
412
|
lines.push(` segmentParams: ${paramsType}`);
|
|
@@ -672,13 +418,23 @@ function formatDeclarationFile(routes, importBase) {
|
|
|
672
418
|
lines.push("");
|
|
673
419
|
const pageRoutes = routes.filter((r) => !r.isApiRoute);
|
|
674
420
|
const dynamicRoutes = routes.filter((r) => r.params.length > 0);
|
|
421
|
+
if (dynamicRoutes.length > 0) {
|
|
422
|
+
lines.push("declare module '@timber-js/app/server' {");
|
|
423
|
+
for (const route of dynamicRoutes) {
|
|
424
|
+
const paramsType = formatParamsType(route.params, importBase, hasSchema);
|
|
425
|
+
lines.push(` export function getSegmentParams(route: '${route.urlPath}'): ${paramsType}`);
|
|
426
|
+
}
|
|
427
|
+
lines.push(" export function getSegmentParams(): Record<string, string | string[]>");
|
|
428
|
+
lines.push("}");
|
|
429
|
+
lines.push("");
|
|
430
|
+
}
|
|
675
431
|
if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {
|
|
676
432
|
lines.push("declare module '@timber-js/app/client' {");
|
|
677
433
|
lines.push(" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'");
|
|
678
434
|
lines.push("");
|
|
679
435
|
if (dynamicRoutes.length > 0) {
|
|
680
436
|
for (const route of dynamicRoutes) {
|
|
681
|
-
const paramsType = formatParamsType(route.params, importBase);
|
|
437
|
+
const paramsType = formatParamsType(route.params, importBase, hasSchema);
|
|
682
438
|
lines.push(` export function useSegmentParams(route: '${route.urlPath}'): ${paramsType}`);
|
|
683
439
|
}
|
|
684
440
|
lines.push(" export function useSegmentParams(): Record<string, string | string[]>");
|
|
@@ -690,7 +446,7 @@ function formatDeclarationFile(routes, importBase) {
|
|
|
690
446
|
}
|
|
691
447
|
if (pageRoutes.length > 0) {
|
|
692
448
|
lines.push(" // Typed Link overloads — per-route (block 1 / emitted first)");
|
|
693
|
-
lines.push(...formatTypedLinkOverloads(pageRoutes, importBase));
|
|
449
|
+
lines.push(...formatTypedLinkOverloads(pageRoutes, importBase, hasSchema));
|
|
694
450
|
lines.push("");
|
|
695
451
|
}
|
|
696
452
|
lines.push("}");
|
|
@@ -707,10 +463,10 @@ function formatDeclarationFile(routes, importBase) {
|
|
|
707
463
|
/**
|
|
708
464
|
* Format the params type for a route entry.
|
|
709
465
|
*/
|
|
710
|
-
function formatParamsType(params, importBase) {
|
|
466
|
+
function formatParamsType(params, importBase, hasSchema) {
|
|
711
467
|
if (params.length === 0) return "{}";
|
|
712
468
|
return `{ ${params.map((p) => {
|
|
713
|
-
const codecType = buildCodecChainType(p, importBase, p.type);
|
|
469
|
+
const codecType = hasSchema ? buildSchemaParamType(p) : buildCodecChainType(p, importBase, p.type);
|
|
714
470
|
return `${p.name}: ${codecType}`;
|
|
715
471
|
}).join("; ")} }`;
|
|
716
472
|
}
|
|
@@ -845,6 +601,6 @@ function walk(node, chain, result, includeSlots) {
|
|
|
845
601
|
if (includeSlots) for (const slotNode of Object.values(node.slots)) walk(slotNode, currentChain, result, includeSlots);
|
|
846
602
|
}
|
|
847
603
|
//#endregion
|
|
848
|
-
export {
|
|
604
|
+
export { collectInterceptionRewrites as n, generateRouteMap as r, collectLeafRoutes as t };
|
|
849
605
|
|
|
850
|
-
//# sourceMappingURL=walkers-
|
|
606
|
+
//# sourceMappingURL=walkers-DGTmJT0f.js.map
|