@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.147

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 (42) hide show
  1. package/dist/_chunks/{cli-schema-sync-DH-M8Poy.js → cli-schema-sync-CBiMEtqa.js} +1 -1
  2. package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-CQaxupHv.js} +47 -54
  3. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +1 -0
  4. package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
  5. package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
  6. package/dist/cli.js +1 -1
  7. package/dist/client/error-boundary.d.ts.map +1 -1
  8. package/dist/client/error-boundary.js +13 -4
  9. package/dist/client/error-boundary.js.map +1 -1
  10. package/dist/client/error-reconstituter.d.ts +8 -1
  11. package/dist/client/error-reconstituter.d.ts.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/routing/codegen-types.d.ts +4 -0
  14. package/dist/routing/codegen-types.d.ts.map +1 -1
  15. package/dist/routing/index.js +2 -2
  16. package/dist/server/deny-boundary.d.ts +27 -2
  17. package/dist/server/deny-boundary.d.ts.map +1 -1
  18. package/dist/server/deny-renderer.d.ts.map +1 -1
  19. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  20. package/dist/server/internal.js +226 -178
  21. package/dist/server/internal.js.map +1 -1
  22. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  23. package/dist/server/status-code-resolver.d.ts +6 -0
  24. package/dist/server/status-code-resolver.d.ts.map +1 -1
  25. package/dist/server/utils/mdx-file.d.ts +15 -0
  26. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  27. package/package.json +2 -2
  28. package/src/client/error-boundary.tsx +14 -0
  29. package/src/client/error-reconstituter.tsx +15 -1
  30. package/src/routing/codegen-types.ts +4 -0
  31. package/src/routing/codegen.ts +63 -6
  32. package/src/routing/scanner.ts +80 -63
  33. package/src/server/deny-boundary.ts +106 -13
  34. package/src/server/deny-renderer.ts +40 -10
  35. package/src/server/error-boundary-wrapper.ts +1 -14
  36. package/src/server/route-matcher.ts +22 -6
  37. package/src/server/rsc-entry/error-renderer.ts +1 -12
  38. package/src/server/status-code-resolver.ts +3 -2
  39. package/src/server/tree-match.ts +15 -5
  40. package/src/server/utils/mdx-file.ts +22 -0
  41. package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
  42. package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
@@ -31,10 +31,14 @@
31
31
 
32
32
  import { createElement } from 'react';
33
33
 
34
+ import { ErrorReconstituter } from '../client/error-reconstituter.js';
35
+ import type { SerializableError } from '../client/error-reconstituter.js';
34
36
  import { requestContextAls } from './als-registry.js';
35
37
  import { DenySignal } from './primitives.js';
36
38
  import { loadModule } from './safe-load.js';
39
+ import { LEGACY_FILE_TO_STATUS } from './status-code-resolver.js';
37
40
  import { withSpan } from './tracing.js';
41
+ import { isMdxFilePath } from './utils/mdx-file.js';
38
42
  import type { ManifestSegmentNode } from './route-matcher.js';
39
43
 
40
44
  // ─── Types ────────────────────────────────────────────────────────────────
@@ -45,6 +49,15 @@ export interface DenyPageEntry {
45
49
  status: number | null;
46
50
  /** The component to render (server or client — both work). */
47
51
  component: (...args: unknown[]) => unknown;
52
+ /**
53
+ * How the entry matched: a status-code file (404.tsx/4xx.tsx), a legacy
54
+ * compat file (not-found.tsx/forbidden.tsx/unauthorized.tsx), or the
55
+ * error.tsx catch-all. error.tsx entries render with their documented
56
+ * { error, digest, reset } contract, not bare deny props. See TIM-1081.
57
+ */
58
+ kind: 'status' | 'legacy' | 'error';
59
+ /** MDX files are server components — rendered with plain props, never ErrorReconstituter. */
60
+ isMdx: boolean;
48
61
  }
49
62
 
50
63
  // ─── Chain Construction ──────────────────────────────────────────────────
@@ -56,10 +69,14 @@ export interface DenyPageEntry {
56
69
  * status-code file components in fallback order:
57
70
  * 1. Specific status files (403.tsx, 404.tsx) — exact match
58
71
  * 2. Category catch-alls (4xx.tsx) — matches any 4xx
59
- * 3. error.tsx — catches everything
72
+ * 3. Legacy compat files (not-found.tsx → 404, forbidden.tsx → 403,
73
+ * unauthorized.tsx → 401) — exact match
74
+ * 4. error.tsx — catches everything
60
75
  *
61
76
  * Each segment is checked in this order. The chain is ordered so the
62
- * FIRST match wins at catch time.
77
+ * FIRST match wins at catch time. This mirrors resolveStatusFile's 4xx
78
+ * component chain (status-code-resolver.ts) — the two walks must stay in
79
+ * sync or in-tree and re-render deny paths resolve different files.
63
80
  */
64
81
  export async function buildDenyPageChain(
65
82
  segments: ManifestSegmentNode[],
@@ -82,7 +99,12 @@ export async function buildDenyPageChain(
82
99
  if (!isNaN(status)) {
83
100
  const mod = await loadModule(file).catch(() => null);
84
101
  if (mod?.default) {
85
- chain.push({ status, component: mod.default as (...args: unknown[]) => unknown });
102
+ chain.push({
103
+ status,
104
+ component: mod.default as (...args: unknown[]) => unknown,
105
+ kind: 'status',
106
+ isMdx: isMdxFilePath(file.filePath),
107
+ });
86
108
  }
87
109
  }
88
110
  }
@@ -97,21 +119,50 @@ export async function buildDenyPageChain(
97
119
  chain.push({
98
120
  status: categoryStatus,
99
121
  component: mod.default as (...args: unknown[]) => unknown,
122
+ kind: 'status',
123
+ isMdx: isMdxFilePath(file.filePath),
100
124
  });
101
125
  }
102
126
  }
103
127
  }
104
128
  }
105
129
 
106
- // Pass 2: error.tsx files — lowest priority catch-all.
107
- // Only added AFTER all status files so they never shadow a specific
108
- // or category status file from an ancestor segment.
130
+ // Pass 2: legacy compat files (not-found.tsx → 404, forbidden.tsx → 403,
131
+ // unauthorized.tsx → 401). Lower priority than status files (a root
132
+ // 4xx.tsx beats a leaf not-found.tsx) but higher than error.tsx — the
133
+ // same ordering as resolveStatusFile's resolve4xx. See TIM-1081.
134
+ for (let i = startIndex; i >= 0; i--) {
135
+ const segment = segments[i];
136
+ if (!segment.legacyStatusFiles) continue;
137
+ for (const [name, status] of Object.entries(LEGACY_FILE_TO_STATUS)) {
138
+ const file = segment.legacyStatusFiles[name];
139
+ if (!file) continue;
140
+ const mod = await loadModule(file).catch(() => null);
141
+ if (mod?.default) {
142
+ chain.push({
143
+ status,
144
+ component: mod.default as (...args: unknown[]) => unknown,
145
+ kind: 'legacy',
146
+ isMdx: isMdxFilePath(file.filePath),
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ // Pass 3: error.tsx files — lowest priority catch-all.
153
+ // Only added AFTER all status and legacy files so they never shadow a
154
+ // more specific file from an ancestor segment.
109
155
  for (let i = startIndex; i >= 0; i--) {
110
156
  const segment = segments[i];
111
157
  if (segment.error) {
112
158
  const mod = await loadModule(segment.error).catch(() => null);
113
159
  if (mod?.default) {
114
- chain.push({ status: null, component: mod.default as (...args: unknown[]) => unknown });
160
+ chain.push({
161
+ status: null,
162
+ component: mod.default as (...args: unknown[]) => unknown,
163
+ kind: 'error',
164
+ isMdx: isMdxFilePath(segment.error.filePath),
165
+ });
115
166
  }
116
167
  }
117
168
  }
@@ -130,25 +181,67 @@ export function renderMatchingDenyPage(
130
181
  status: number,
131
182
  data: unknown
132
183
  ): React.ReactElement | null {
133
- const h = createElement as (...args: unknown[]) => React.ReactElement;
134
-
135
184
  for (const entry of chain) {
136
185
  if (entry.status === status) {
137
- return h(entry.component, { status, dangerouslyPassData: data });
186
+ return renderDenyEntry(entry, status, data);
138
187
  }
139
188
  if (entry.status === 400 && status >= 400 && status <= 499) {
140
- return h(entry.component, { status, dangerouslyPassData: data });
189
+ return renderDenyEntry(entry, status, data);
141
190
  }
142
191
  if (entry.status === 500 && status >= 500 && status <= 599) {
143
- return h(entry.component, { status, dangerouslyPassData: data });
192
+ return renderDenyEntry(entry, status, data);
144
193
  }
145
194
  if (entry.status === null) {
146
- return h(entry.component, { status, dangerouslyPassData: data });
195
+ return renderDenyEntry(entry, status, data);
147
196
  }
148
197
  }
149
198
  return null;
150
199
  }
151
200
 
201
+ /**
202
+ * Build the element for a matched deny chain entry with the props contract
203
+ * the file's convention documents:
204
+ *
205
+ * - Status-code and legacy files: { status, dangerouslyPassData }
206
+ * - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
207
+ * { status, dangerouslyPassData } for dual-shape implementations. Without
208
+ * the Error prop, any error.tsx written per the docs (reading
209
+ * error.message) crashes — turning a clean deny(404) into a 500. TIM-1081.
210
+ * - error.mdx: plain { status } — server component, static content.
211
+ */
212
+ export function renderDenyEntry(
213
+ entry: DenyPageEntry,
214
+ status: number,
215
+ data: unknown
216
+ ): React.ReactElement {
217
+ const h = createElement as (...args: unknown[]) => React.ReactElement;
218
+
219
+ if (entry.kind !== 'error') {
220
+ return h(entry.component, { status, dangerouslyPassData: data });
221
+ }
222
+
223
+ // MDX error pages receive plain props — no Error serialization possible.
224
+ if (entry.isMdx) {
225
+ return h(entry.component, { status });
226
+ }
227
+
228
+ // The message is derived solely from the status code (already on the
229
+ // wire) — safe to cross the RSC→client boundary in production. No stack,
230
+ // no user data. See design/13-security.md §"Errors don't leak".
231
+ const serializableDeny: SerializableError = {
232
+ message: `Access denied with status ${status}`,
233
+ name: 'DenySignal',
234
+ };
235
+ return h(ErrorReconstituter, {
236
+ error: serializableDeny,
237
+ digest: null,
238
+ reset: undefined,
239
+ component: entry.component,
240
+ status,
241
+ dangerouslyPassData: data,
242
+ });
243
+ }
244
+
152
245
  // ─── Page Boundary ────────────────────────────────────────────────────────
153
246
 
154
247
  /**
@@ -19,8 +19,10 @@ import { createElement } from 'react';
19
19
  import { renderToReadableStream } from '../rsc-runtime/rsc.js';
20
20
 
21
21
  import { DenySignal } from './primitives.js';
22
+ import { renderDenyEntry, type DenyPageEntry } from './deny-boundary.js';
22
23
  import { logRenderError } from './logger.js';
23
24
  import { loadModule } from './safe-load.js';
25
+ import { isMdxFilePath } from './utils/mdx-file.js';
24
26
  import { isDebug } from './debug.js';
25
27
  import { escapeHtml } from './utils/escape-html.js';
26
28
  import { resolveMetadata, renderMetadataToElements } from './metadata.js';
@@ -62,6 +64,29 @@ function isApiRoute(segments: ReadonlyArray<ManifestSegmentNode>): boolean {
62
64
  return !!leaf?.route;
63
65
  }
64
66
 
67
+ /**
68
+ * Build the deny page element for a resolved status file with the correct
69
+ * props contract for the file's convention. Delegates to renderDenyEntry
70
+ * (deny-boundary.ts) so the in-tree and re-render deny paths render
71
+ * identical elements — in particular, error.tsx serving a deny gets
72
+ * { error, digest, reset } via ErrorReconstituter, not bare deny props.
73
+ * See TIM-1081.
74
+ */
75
+ function buildDenyElement(
76
+ deny: DenySignal,
77
+ component: (...args: unknown[]) => unknown,
78
+ resolutionKind: 'exact' | 'category' | 'legacy' | 'error',
79
+ filePath: string
80
+ ): React.ReactElement {
81
+ const entry: DenyPageEntry = {
82
+ status: deny.status,
83
+ component,
84
+ kind: resolutionKind === 'error' ? 'error' : resolutionKind === 'legacy' ? 'legacy' : 'status',
85
+ isMdx: isMdxFilePath(filePath),
86
+ };
87
+ return renderDenyEntry(entry, deny.status, deny.data);
88
+ }
89
+
65
90
  /**
66
91
  * Render a status-code error page for a DenySignal.
67
92
  *
@@ -120,12 +145,15 @@ export async function renderDenyPage(
120
145
  const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
121
146
  const h = createElement as (...args: unknown[]) => React.ReactElement;
122
147
 
123
- // 4xx status-code pages receive { status, dangerouslyPassData }
124
- // per design/10-error-handling.md §"Status-Code File Props"
125
- let element = h(ErrorPageComponent, {
126
- status: deny.status,
127
- dangerouslyPassData: deny.data,
128
- });
148
+ // 4xx status-code pages receive { status, dangerouslyPassData }; an
149
+ // error.tsx match gets { error, digest, reset } via ErrorReconstituter.
150
+ // See design/10-error-handling.md §"Status-Code File Props" and TIM-1081.
151
+ let element = buildDenyElement(
152
+ deny,
153
+ ErrorPageComponent,
154
+ resolution.kind,
155
+ resolution.file.filePath
156
+ );
129
157
 
130
158
  // Always wrap in layout shell.
131
159
  // NOTE: Shell opt-out (export const shell = false) is a future feature
@@ -207,10 +235,12 @@ export async function renderDenyPageAsRsc(
207
235
  const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
208
236
  const h = createElement as (...args: unknown[]) => React.ReactElement;
209
237
 
210
- let element = h(ErrorPageComponent, {
211
- status: deny.status,
212
- dangerouslyPassData: deny.data,
213
- });
238
+ let element = buildDenyElement(
239
+ deny,
240
+ ErrorPageComponent,
241
+ resolution.kind,
242
+ resolution.file.filePath
243
+ );
214
244
 
215
245
  // Always wrap in layout shell.
216
246
  const resolvedSegments = new Set(segments.slice(0, resolution.segmentIndex + 1));
@@ -8,20 +8,7 @@
8
8
  import { TimberErrorBoundary } from '../client/error-boundary.js';
9
9
  import type { ManifestSegmentNode } from './route-matcher.js';
10
10
  import { loadModule } from './safe-load.js';
11
-
12
- /** MDX/markdown extensions — server components that cannot be passed as function props. */
13
- const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
14
-
15
- /**
16
- * Check if a manifest file path ends with an MDX/markdown extension.
17
- * MDX components are server components and cannot cross the RSC→client
18
- * boundary as function props to TimberErrorBoundary.
19
- */
20
- function isMdxFilePath(filePath: string): boolean {
21
- const dotIndex = filePath.lastIndexOf('.');
22
- if (dotIndex === -1) return false;
23
- return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
24
- }
11
+ import { isMdxFilePath } from './utils/mdx-file.js';
25
12
 
26
13
  /**
27
14
  * Wrap an element in error boundaries defined by a route segment.
@@ -185,7 +185,7 @@ function matchMetadataPathname(
185
185
  // Non-nestable routes (robots, manifest, favicon) only serve from root
186
186
  if (!convention.nestable && segment !== root) return null;
187
187
 
188
- const file = findMetadataRouteInSegment(segment, baseName);
188
+ const file = findMetadataRouteInSegment(segment, baseName, convention.nestable);
189
189
  if (!file) return null;
190
190
 
191
191
  const ext = file.filePath.slice(file.filePath.lastIndexOf('.') + 1);
@@ -205,19 +205,35 @@ function matchMetadataPathname(
205
205
  }
206
206
 
207
207
  /**
208
- * Find a metadata route file in a segment, including transparent group children.
209
- * Groups don't add URL depth, so their metadata routes are reachable at the parent's path.
208
+ * Find a metadata route file in a segment, including children that match
209
+ * zero URL segments at the segment's own path:
210
+ *
211
+ * - Group children are transparent — they don't add URL depth.
212
+ * - Optional catch-all children ([[...slug]]) match zero segments, so
213
+ * `app/docs/[[...path]]/icon.tsx` serves `/docs/icon`. The non-zero
214
+ * case is handled by the tree walker (requireTerminal:false), but the
215
+ * zero-segment case never reaches the walker — the parent node accepts
216
+ * the fully-consumed prefix first, so the fallback lives here.
217
+ *
218
+ * Optional catch-all descent applies to NESTABLE conventions only. A
219
+ * non-nestable file (robots, manifest, favicon) inside `[[...slug]]` is
220
+ * not at the root — serving it there would bypass the root-only rule.
221
+ * Group descent is unconditional: groups are transparent and a group
222
+ * child of the root IS at the root.
223
+ *
224
+ * The segment's own metadata file wins over a zero-segment child's.
210
225
  */
211
226
  function findMetadataRouteInSegment(
212
227
  node: ManifestSegmentNode,
213
- baseName: string
228
+ baseName: string,
229
+ nestable: boolean
214
230
  ): ManifestFile | null {
215
231
  if (node.metadataRoutes?.[baseName]) {
216
232
  return node.metadataRoutes[baseName];
217
233
  }
218
234
  for (const child of node.children) {
219
- if (child.segmentType === 'group') {
220
- const found = findMetadataRouteInSegment(child, baseName);
235
+ if (child.segmentType === 'group' || (nestable && child.segmentType === 'optional-catch-all')) {
236
+ const found = findMetadataRouteInSegment(child, baseName, nestable);
221
237
  if (found) return found;
222
238
  }
223
239
  }
@@ -33,6 +33,7 @@ import { ErrorReconstituter } from '../../client/error-reconstituter.js';
33
33
  import type { SerializableError } from '../../client/error-reconstituter.js';
34
34
  import { loadModule } from '../safe-load.js';
35
35
  import { sourceMapError } from '../dev-source-map.js';
36
+ import { isMdxFilePath as isMdxFile } from '../utils/mdx-file.js';
36
37
 
37
38
  /**
38
39
  * A manifest file reference with lazy import and path.
@@ -44,18 +45,6 @@ export interface GlobalErrorFile {
44
45
  filePath: string;
45
46
  }
46
47
 
47
- /** MDX/markdown extensions — server components that need RSC rendering. */
48
- const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
49
-
50
- /**
51
- * Check if a file path ends with an MDX/markdown extension.
52
- */
53
- function isMdxFile(filePath: string): boolean {
54
- const dotIndex = filePath.lastIndexOf('.');
55
- if (dotIndex === -1) return false;
56
- return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
57
- }
58
-
59
48
  /**
60
49
  * Result of walking the segment chain for an error file.
61
50
  */
@@ -75,9 +75,10 @@ export interface SlotDeniedResolution<TFile> {
75
75
 
76
76
  /**
77
77
  * Maps legacy file convention names to their corresponding HTTP status codes.
78
- * Only used in the 4xx component fallback chain.
78
+ * Only used in the 4xx component fallback chain. Exported so the in-tree
79
+ * deny chain (deny-boundary.ts) uses the same mapping — see TIM-1081.
79
80
  */
80
- const LEGACY_FILE_TO_STATUS: Record<string, number> = {
81
+ export const LEGACY_FILE_TO_STATUS: Record<string, number> = {
81
82
  'not-found': 404,
82
83
  'forbidden': 403,
83
84
  'unauthorized': 401,
@@ -142,7 +142,7 @@ function matchNode<TFile>(
142
142
  // with no path segments.
143
143
  const effective = collectEffectiveChildren(node);
144
144
  for (const { child, groupChain } of effective) {
145
- if (child.segmentType === 'optional-catch-all' && isTerminal(child)) {
145
+ if (child.segmentType === 'optional-catch-all' && (!requireTerminal || isTerminal(child))) {
146
146
  chain.push(...groupChain, child);
147
147
  return true;
148
148
  }
@@ -187,18 +187,28 @@ function matchNode<TFile>(
187
187
  }
188
188
  }
189
189
 
190
- // 3. Catch-all segments ([...param]) — must be terminal
190
+ // 3. Catch-all segments ([...param]) — must be terminal unless the
191
+ // caller passed requireTerminal:false (metadata route matching needs to
192
+ // find the owning segment even when it has no page).
191
193
  for (const { child, groupChain } of effective) {
192
- if (child.segmentType === 'catch-all' && child.paramName && isTerminal(child)) {
194
+ if (
195
+ child.segmentType === 'catch-all' &&
196
+ child.paramName &&
197
+ (!requireTerminal || isTerminal(child))
198
+ ) {
193
199
  chain.push(...groupChain, child);
194
200
  params[child.paramName] = parts.slice(index);
195
201
  return true;
196
202
  }
197
203
  }
198
204
 
199
- // 4. Optional catch-all segments ([[...param]]) — must be terminal
205
+ // 4. Optional catch-all segments ([[...param]]) — same terminal rule
200
206
  for (const { child, groupChain } of effective) {
201
- if (child.segmentType === 'optional-catch-all' && child.paramName && isTerminal(child)) {
207
+ if (
208
+ child.segmentType === 'optional-catch-all' &&
209
+ child.paramName &&
210
+ (!requireTerminal || isTerminal(child))
211
+ ) {
202
212
  chain.push(...groupChain, child);
203
213
  params[child.paramName] = parts.slice(index);
204
214
  return true;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MDX file-path detection shared by error/deny rendering paths.
3
+ *
4
+ * MDX status and error files are server components — they cannot be passed
5
+ * as function props across the RSC→client boundary (e.g. to
6
+ * TimberErrorBoundary or ErrorReconstituter) and must be pre-rendered as
7
+ * elements with plain props instead.
8
+ *
9
+ * See design/10-error-handling.md §"Status-Code File Variants".
10
+ */
11
+
12
+ /** MDX/markdown extensions — server components by default. */
13
+ const MDX_EXTENSIONS = new Set(['.mdx', '.md']);
14
+
15
+ /**
16
+ * Check if a file path ends with an MDX/markdown extension.
17
+ */
18
+ export function isMdxFilePath(filePath: string): boolean {
19
+ const dotIndex = filePath.lastIndexOf('.');
20
+ if (dotIndex === -1) return false;
21
+ return MDX_EXTENSIONS.has(filePath.slice(dotIndex));
22
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli-schema-sync-D2IpXicg.js","names":[],"sources":["../../src/routing/scanner.ts","../../src/utils/startup-timer.ts","../../src/plugin-context.ts","../../src/cli-schema-sync.ts"],"sourcesContent":["/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.js';\nimport { classifySegment } from './segment-classify.js';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.js';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';\nimport { swallow } from '../server/logger.js';\n\n/**\n * Pattern matching encoded path delimiters that must be rejected during route discovery.\n * %2F / %2f (forward slash) and %5C / %5c (backslash) can cause route collisions\n * when decoded. See design/13-security.md §\"Encoded separators rejected\".\n */\nconst ENCODED_SEPARATOR_PATTERN = /%(?:2[fF]|5[cC])/;\n\n/**\n * Pattern matching encoded null bytes (%00) that must be rejected.\n * See design/13-security.md §\"Null bytes rejected\".\n */\nconst ENCODED_NULL_PATTERN = /%00/;\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);\n\n/**\n * Legacy compat status-code files.\n * Maps legacy file name → HTTP status code for the fallback chain.\n * See design/10-error-handling.md §\"Fallback Chain\".\n */\nconst LEGACY_STATUS_FILES: Record<string, number> = {\n 'not-found': 404,\n 'forbidden': 403,\n 'unauthorized': 401,\n};\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route', 'params']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Check for global-error.{tsx,ts,jsx,js} at app root.\n // Tier 2 error page — renders standalone (no layouts) when no segment-level\n // error file is found. See design/10-error-handling.md §\"Tier 2\".\n const globalErrorFile = findPageExtFile(appDir, 'global-error', extSet);\n if (globalErrorFile) {\n tree.globalError = globalErrorFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n // Validate: detect duplicate param names in nested dynamic segments\n // e.g., /[id]/items/[id] — same param name in ancestor and descendant\n validateDuplicateParamNames(tree.root);\n\n // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: {},\n };\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, error, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const file: RouteFile = { filePath: fullPath, extension: ext };\n switch (name) {\n case 'page':\n node.page = file;\n break;\n case 'layout':\n node.layout = file;\n break;\n case 'error':\n node.error = file;\n break;\n case 'default':\n node.default = file;\n break;\n case 'denied':\n node.denied = file;\n break;\n }\n continue;\n }\n\n // Fixed conventions (middleware, access, route) — always .ts or .tsx\n if (FIXED_CONVENTIONS.has(name) && /\\.?[jt]sx?$/.test(ext)) {\n const file: RouteFile = { filePath: fullPath, extension: ext };\n switch (name) {\n case 'middleware':\n node.middleware = file;\n break;\n case 'access':\n node.access = file;\n break;\n case 'route':\n node.route = file;\n break;\n case 'params':\n node.params = file;\n break;\n }\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = {};\n }\n node.jsonStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx)\n if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = {};\n }\n node.statusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)\n if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {\n if (!node.legacyStatusFiles) {\n node.legacyStatusFiles = {};\n }\n node.legacyStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.\n // When both exist for the same base name, dynamic takes precedence.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!node.metadataRoutes) {\n node.metadataRoutes = {};\n }\n const existing = node.metadataRoutes[name];\n if (existing) {\n // Dynamic > static precedence: only overwrite if the new file is dynamic\n // or the existing file is static (dynamic always wins).\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (newIsDynamic || !existingIsDynamic) {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_PATTERN.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%%2F or %%5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (ENCODED_NULL_PATTERN.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[slotName] = childNode;\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that route groups don't produce conflicting pages/routes at the same URL path.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Parallel slots are excluded from collision detection — they intentionally coexist at\n * the same URL path as their parent (that's the whole point of parallel routes).\n */\nfunction validateRouteGroupCollisions(root: SegmentNode): void {\n // Map from urlPath → { filePath, source } for the first page/route seen at that path\n const seen = new Map<string, { filePath: string; segmentPath: string }>();\n collectRoutableLeaves(root, seen, '', false);\n}\n\n/**\n * Walk the segment tree and collect all routable leaves (page or route files),\n * throwing on collision. Slots are tracked in their own collision space since\n * they are parallel routes that intentionally share URL paths with their parent.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string,\n insideSlot: boolean\n): void {\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n // Only check collisions for non-slot pages — slots intentionally share URL paths\n if (!insideSlot) {\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${node.urlPath}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n }\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutableLeaves(child, seen, currentPath, insideSlot);\n }\n\n // Recurse into slots — each slot is its own parallel route space\n for (const slotNode of Object.values(node.slots)) {\n collectRoutableLeaves(slotNode, seen, currentPath, true);\n }\n}\n\n/**\n * Validate that no route chain contains duplicate dynamic param names.\n *\n * Example violation:\n * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.\n *\n * Route groups are transparent — params accumulate through them.\n * Slots are independent — duplicate detection does NOT cross slot boundaries.\n *\n * See design/07-routing.md §\"Duplicate Param Name Detection\"\n */\nfunction validateDuplicateParamNames(root: SegmentNode): void {\n walkForDuplicateParams(root, new Map());\n}\n\n/**\n * Recursively walk the segment tree, tracking seen param names → segment paths.\n * Throws on the first duplicate found.\n */\nfunction walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {\n // If this node introduces a param name, check for duplicates\n if (node.paramName) {\n const existing = seen.get(node.paramName);\n if (existing) {\n throw new Error(\n `[timber] Duplicate param name '${node.paramName}' in route chain.\\n` +\n ` First defined at: ${existing}\\n` +\n ` Duplicate at: ${node.urlPath || '/'}\\n` +\n ` Rename one of the segments to avoid ambiguity.`\n );\n }\n // Add to seen for descendants (use a new Map to avoid polluting siblings)\n seen = new Map(seen);\n seen.set(node.paramName, node.urlPath || '/');\n }\n\n // Recurse into children (they inherit the accumulated params)\n for (const child of node.children) {\n walkForDuplicateParams(child, seen);\n }\n\n // Slots are independent parallel routes — start fresh param tracking\n // (a slot's params don't conflict with the main route's params)\n for (const slotNode of Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {\n const existing = forms.get(node.paramName);\n if (existing && existing.segmentType !== node.segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${node.paramName}]`,\n 'catch-all': `[...${node.paramName}]`,\n 'optional-catch-all': `[[...${node.paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${node.paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${node.paramName}] → [${node.paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(node.paramName, {\n segmentType: node.segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\n }\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n\n/**\n * Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).\n * Used for app-root conventions like global-error that aren't per-segment.\n */\nfunction findPageExtFile(\n dirPath: string,\n name: string,\n extSet: Set<string>\n): RouteFile | undefined {\n for (const ext of extSet) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n","/**\n * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createRequire } from 'node:module';\nimport type { RouteTree } from './routing/types';\nimport type { BuildManifest } from './server/build-manifest';\nimport type { StartupTimer } from './utils/startup-timer';\nimport { createStartupTimer } from './utils/startup-timer';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\nimport type { HoldingServer } from './dev-tools/holding-server.js';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /** CSS build manifest (populated by adapter after client build, null in dev) */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * Used by adapter-build and cli preview.\n */\n buildDir: string;\n /**\n * Per-route client reference keys captured during the RSC build.\n * Maps absolute route file paths to the set of client reference\n * hash keys they transitively import. Used by adapter-build to\n * populate per-route modulepreload entries from clientReferenceDeps.\n */\n routeClientRefKeys?: Map<string, Set<string>>;\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return join(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return join(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n *\n * Uses require() which works for ESM modules on Node 22.12+.\n * This keeps timber() synchronous — no async config loading needed.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. The `output` key is excluded because it defaults to 'server'\n * in createPluginContext and would always appear as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Merge file-based config into ctx.config. Inline config (already in ctx.config)\n * takes precedence — file config only fills in missing fields.\n */\nexport function mergeFileConfig(ctx: PluginContext, fileConfig: TimberUserConfig): void {\n const inline = ctx.config;\n warnConfigConflicts(inline, fileConfig);\n\n ctx.config = {\n ...fileConfig,\n ...inline,\n ...(fileConfig.limits && inline.limits\n ? { limits: { ...fileConfig.limits, ...inline.limits } }\n : {}),\n ...(fileConfig.dev && inline.dev ? { dev: { ...fileConfig.dev, ...inline.dev } } : {}),\n ...(fileConfig.mdx && inline.mdx ? { mdx: { ...fileConfig.mdx, ...inline.mdx } } : {}),\n };\n}\n","/**\n * `timber schema sync` — scan filesystem for dynamic segments, append\n * missing entries with `codec.string` defaults, and remove stale entries\n * whose segments no longer exist in the filesystem.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.js';\nimport type { SegmentNode } from './routing/types.js';\nimport { resolveAppDir } from './plugin-context.js';\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Reconstruct bracket key from segmentName (already has brackets)\n segments.add(node.segmentName);\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Uses regex to find keys in segmentParams: { '[key]': ... } blocks.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?\\w+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\nexport function defaultCodecForSegment(seg: string): string {\n if (seg.startsWith('[[...')) return 'codec.optionalCatchAll(codec.string)';\n if (seg.startsWith('[...')) return 'codec.stringArray';\n return 'codec.string';\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': ${defaultCodecForSegment(seg)},`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nconst CODEC_IMPORT = \"import { codec } from '@timber-js/app/codec';\";\n\nfunction hasCodecImport(content: string): boolean {\n return /import\\s+\\{[^}]*\\bcodec\\b[^}]*\\}\\s+from\\s+['\"]@timber-js\\/app\\/codec['\"]/.test(content);\n}\n\nfunction ensureCodecImport(content: string): string {\n if (hasCodecImport(content)) return content;\n\n // Insert after the last existing import statement\n const lines = content.split('\\n');\n let lastImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*import\\s/.test(lines[i])) lastImportIndex = i;\n }\n\n if (lastImportIndex === -1) {\n return CODEC_IMPORT + '\\n' + content;\n }\n\n lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);\n return lines.join('\\n');\n}\n\nfunction appendToSchema(content: string, newSegments: string[]): string {\n content = ensureCodecImport(content);\n\n // Find \"segmentParams\" key position\n const spIndex = content.indexOf('segmentParams');\n if (spIndex === -1) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n\n // Find the opening brace after segmentParams\n const openBrace = content.indexOf('{', spIndex + 'segmentParams'.length);\n if (openBrace === -1) {\n throw new Error('[timber] Could not find opening brace for segmentParams.');\n }\n\n // Count braces to find the matching close\n let depth = 1;\n let i = openBrace + 1;\n while (i < content.length && depth > 0) {\n if (content[i] === '{') depth++;\n else if (content[i] === '}') depth--;\n i++;\n }\n // i now points to just after the closing brace\n const closeBrace = i - 1;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments\n .map((seg) => `${indent}'${seg}': ${defaultCodecForSegment(seg)},`)\n .join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\n/**\n * Remove entries from the segmentParams block whose keys match `staleSegments`.\n * Removes the entire line including trailing comma/whitespace.\n */\nfunction removeFromSchema(content: string, staleSegments: string[]): string {\n for (const seg of staleSegments) {\n const escaped = seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const keyRe = new RegExp(`^[ \\t]*['\"]${escaped}['\"][ \\t]*:`, 'gm');\n let m: RegExpExecArray | null;\n while ((m = keyRe.exec(content)) !== null) {\n const start = m.index;\n let i = m.index + m[0].length;\n let depth = 0;\n let inString: string | null = null;\n\n while (i < content.length) {\n const ch = content[i];\n if (inString) {\n if (ch === '\\\\') {\n i++;\n } else if (ch === inString) {\n inString = null;\n }\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch;\n } else if (ch === '(' || ch === '{' || ch === '[') {\n depth++;\n } else if (ch === ')' || ch === '}' || ch === ']') {\n depth--;\n } else if (depth === 0 && ch === ',') {\n i++;\n break;\n } else if (depth === 0 && (ch === '\\n' || ch === '}')) {\n break;\n }\n i++;\n }\n\n // Consume trailing newline\n if (i < content.length && content[i] === '\\n') i++;\n\n content = content.slice(0, start) + content.slice(i);\n keyRe.lastIndex = start;\n }\n }\n return content;\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Segments that were removed (no longer in filesystem). */\n removedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n const schemaPath = findSchemaFile(appDir);\n\n if (filesystemSegments.length === 0 && !schemaPath) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n removedSegments: [],\n created: true,\n };\n }\n\n // Read existing schema and diff\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n const stale = existingSegments.filter((seg) => !filesystemSegments.includes(seg));\n\n if (missing.length === 0 && stale.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n let updated = content;\n if (stale.length > 0) {\n updated = removeFromSchema(updated, stale);\n }\n if (missing.length > 0) {\n updated = appendToSchema(updated, missing);\n }\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n removedSegments: stale,\n created: false,\n };\n}\n"],"mappings":"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"}