@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.185

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 (61) hide show
  1. package/dist/_chunks/{cli-schema-sync-D3naS3eH.js → cli-schema-sync-BTWEKJXo.js} +1 -21
  2. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +1 -0
  3. package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -1
  4. package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
  5. package/dist/_chunks/segment-classify-Byy425ng.js.map +1 -1
  6. package/dist/_chunks/{walkers-BtTlKviE.js → walkers-_6zKFlch.js} +2 -2
  7. package/dist/_chunks/{walkers-BtTlKviE.js.map → walkers-_6zKFlch.js.map} +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/client/internal.js +36 -28
  10. package/dist/client/internal.js.map +1 -1
  11. package/dist/client/router.d.ts.map +1 -1
  12. package/dist/client/rsc-fetch.d.ts +11 -1
  13. package/dist/client/rsc-fetch.d.ts.map +1 -1
  14. package/dist/index.js +25 -15
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugin-context.d.ts +2 -1
  17. package/dist/plugin-context.d.ts.map +1 -1
  18. package/dist/plugins/static-build.d.ts +19 -6
  19. package/dist/plugins/static-build.d.ts.map +1 -1
  20. package/dist/routing/index.js +2 -2
  21. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  22. package/dist/routing/scanner.d.ts.map +1 -1
  23. package/dist/routing/types.d.ts +1 -3
  24. package/dist/routing/types.d.ts.map +1 -1
  25. package/dist/server/deny-boundary.d.ts +4 -7
  26. package/dist/server/deny-boundary.d.ts.map +1 -1
  27. package/dist/server/internal.js +135 -161
  28. package/dist/server/internal.js.map +1 -1
  29. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  30. package/dist/server/primitives.d.ts +0 -12
  31. package/dist/server/primitives.d.ts.map +1 -1
  32. package/dist/server/sitemap-generator.d.ts.map +1 -1
  33. package/dist/server/static-generator.d.ts.map +1 -1
  34. package/dist/server/status-code-resolver.d.ts +3 -10
  35. package/dist/server/status-code-resolver.d.ts.map +1 -1
  36. package/dist/shims/navigation-client.d.ts +8 -19
  37. package/dist/shims/navigation-client.d.ts.map +1 -1
  38. package/dist/shims/navigation.d.ts +0 -1
  39. package/dist/shims/navigation.d.ts.map +1 -1
  40. package/docs/learn/11-error-handling.mdx +2 -3
  41. package/package.json +1 -1
  42. package/src/client/router.ts +24 -7
  43. package/src/client/rsc-fetch.ts +37 -46
  44. package/src/plugin-context.ts +2 -1
  45. package/src/plugins/routing.ts +1 -1
  46. package/src/plugins/static-build.ts +47 -19
  47. package/src/routing/manifest-codegen.ts +1 -6
  48. package/src/routing/scanner.ts +0 -24
  49. package/src/routing/types.ts +1 -3
  50. package/src/server/deny-boundary.ts +5 -31
  51. package/src/server/deny-renderer.ts +2 -2
  52. package/src/server/pipeline-outcome.ts +1 -1
  53. package/src/server/prebuilt-builder.ts +1 -7
  54. package/src/server/primitives.ts +0 -18
  55. package/src/server/rsc-entry/render-route.ts +3 -3
  56. package/src/server/sitemap-generator.ts +1 -7
  57. package/src/server/static-generator.ts +2 -18
  58. package/src/server/status-code-resolver.ts +4 -47
  59. package/src/shims/navigation-client.ts +8 -26
  60. package/src/shims/navigation.ts +0 -4
  61. package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
@@ -36,7 +36,6 @@ import type { SerializableError } from '../client/error-reconstituter.js';
36
36
  import { requestContextAls } from './als-registry.js';
37
37
  import { DenySignal } from './primitives.js';
38
38
  import { loadModule } from './safe-load.js';
39
- import { LEGACY_FILE_TO_STATUS } from './status-code-resolver.js';
40
39
  import { withSpan } from './tracing.js';
41
40
  import { isMdxFilePath } from './utils/mdx-file.js';
42
41
  import type { ManifestSegmentNode } from './route-matcher.js';
@@ -50,12 +49,11 @@ export interface DenyPageEntry {
50
49
  /** The component to render (server or client — both work). */
51
50
  component: (...args: unknown[]) => unknown;
52
51
  /**
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
52
+ * How the entry matched: a status-code file (404.tsx/4xx.tsx) or the
55
53
  * error.tsx catch-all. error.tsx entries render with their documented
56
54
  * { error, digest, reset } contract, not bare deny props. See TIM-1081.
57
55
  */
58
- kind: 'status' | 'legacy' | 'error';
56
+ kind: 'status' | 'error';
59
57
  /** MDX files are server components — rendered with plain props, never ErrorReconstituter. */
60
58
  isMdx: boolean;
61
59
  }
@@ -69,9 +67,7 @@ export interface DenyPageEntry {
69
67
  * status-code file components in fallback order:
70
68
  * 1. Specific status files (403.tsx, 404.tsx) — exact match
71
69
  * 2. Category catch-alls (4xx.tsx) — matches any 4xx
72
- * 3. Legacy compat files (not-found.tsx → 404, forbidden.tsx → 403,
73
- * unauthorized.tsx → 401) — exact match
74
- * 4. error.tsx — catches everything
70
+ * 3. error.tsx — catches everything
75
71
  *
76
72
  * Each segment is checked in this order. The chain is ordered so the
77
73
  * FIRST match wins at catch time. This mirrors resolveStatusFile's 4xx
@@ -127,29 +123,7 @@ export async function buildDenyPageChain(
127
123
  }
128
124
  }
129
125
 
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.
126
+ // Pass 2: error.tsx files — lowest priority catch-all.
153
127
  // Only added AFTER all status and legacy files so they never shadow a
154
128
  // more specific file from an ancestor segment.
155
129
  for (let i = startIndex; i >= 0; i--) {
@@ -202,7 +176,7 @@ export function renderMatchingDenyPage(
202
176
  * Build the element for a matched deny chain entry with the props contract
203
177
  * the file's convention documents:
204
178
  *
205
- * - Status-code and legacy files: { status, dangerouslyPassData }
179
+ * - Status-code files: { status, dangerouslyPassData }
206
180
  * - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
207
181
  * { status, dangerouslyPassData } for dual-shape implementations. Without
208
182
  * the Error prop, any error.tsx written per the docs (reading
@@ -76,13 +76,13 @@ function isApiRoute(segments: ReadonlyArray<ManifestSegmentNode>): boolean {
76
76
  function buildDenyElement(
77
77
  deny: DenySignal,
78
78
  component: (...args: unknown[]) => unknown,
79
- resolutionKind: 'exact' | 'category' | 'legacy' | 'error',
79
+ resolutionKind: 'exact' | 'category' | 'error',
80
80
  filePath: string
81
81
  ): React.ReactElement {
82
82
  const entry: DenyPageEntry = {
83
83
  status: deny.status,
84
84
  component,
85
- kind: resolutionKind === 'error' ? 'error' : resolutionKind === 'legacy' ? 'legacy' : 'status',
85
+ kind: resolutionKind === 'error' ? 'error' : 'status',
86
86
  isMdx: isMdxFilePath(filePath),
87
87
  };
88
88
  return renderDenyEntry(entry, deny.status, deny.data);
@@ -132,7 +132,7 @@ export async function outcomeToResponse(
132
132
  `[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — ` +
133
133
  `no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n` +
134
134
  ` Request: ${ctx.method} ${ctx.path}\n` +
135
- ` Add a not-found.tsx or error.tsx to render a custom deny page.`
135
+ ` Add a status-code file (e.g. 404.tsx) or error.tsx to render a custom deny page.`
136
136
  );
137
137
  }
138
138
  return new Response(null, { status: outcome.signal.status, headers });
@@ -235,13 +235,7 @@ async function enumerateCombos(
235
235
  ): Promise<ParamCombo[] | null> {
236
236
  if (!hasDynamicSegments(route.urlPath)) return [{}];
237
237
 
238
- let generateFn = pageModule.generateStaticSegmentParams;
239
- if (typeof generateFn !== 'function' && typeof pageModule.generateStaticParams === 'function') {
240
- generateFn = pageModule.generateStaticParams;
241
- log(
242
- `[timber] ${route.page.filePath}: generateStaticParams is deprecated — rename to generateStaticSegmentParams`
243
- );
244
- }
238
+ const generateFn = pageModule.generateStaticSegmentParams;
245
239
  if (typeof generateFn !== 'function') {
246
240
  log(
247
241
  `[timber] cache.component: route ${route.urlPath} has prebuilt components but no ` +
@@ -201,15 +201,6 @@ export function deny(statusOrOptions?: number | DenyOptions, data?: JsonSerializ
201
201
  throw new DenySignal(status, resolvedData);
202
202
  }
203
203
 
204
- /**
205
- * @deprecated Use `deny(404)` instead.
206
- * Kept for internal use by the Next.js shim layer.
207
- * @internal
208
- */
209
- export function notFound(): never {
210
- deny(404);
211
- }
212
-
213
204
  // Single source of truth shared with the client next/navigation shim —
214
205
  // see shared/redirect-type.ts.
215
206
  export { RedirectType } from '../shared/redirect-type.js';
@@ -334,15 +325,6 @@ export function redirect(path: string, statusOrOptions?: number | RedirectOption
334
325
  throw new RedirectSignal(resolvedPath, status);
335
326
  }
336
327
 
337
- /**
338
- * @deprecated Use `redirect(path, { permanent: true })` instead.
339
- * Kept for internal use by the Next.js shim layer.
340
- * @internal
341
- */
342
- export function permanentRedirect(path: string, options?: Omit<RedirectOptions, 'status'>): never {
343
- redirect(path, { permanent: true, ...options });
344
- }
345
-
346
328
  /**
347
329
  * Redirect to an external URL. The origin must be in the provided allow-list.
348
330
  *
@@ -115,10 +115,10 @@ export async function renderRoute(
115
115
  buildManifest.metadataRouteHashes
116
116
  );
117
117
  } catch (error) {
118
- // Param coercion failed — render the custom 404 page (status files / not-found).
118
+ // Param coercion failed — render the custom 404 page via status files.
119
119
  // Previously returned a bare Response(null, { status: 404 }) which bypassed
120
- // custom not-found pages. Now routes through renderNoMatchPage so apps with
121
- // 404.tsx / not-found status files render their custom page.
120
+ // custom 404 pages. Now routes through renderNoMatchPage so apps with
121
+ // 404.tsx render their custom page.
122
122
  if (error instanceof ParamCoercionError) {
123
123
  return renderNoMatchPage(req, rootSegment, responseHeaders, clientBootstrap);
124
124
  }
@@ -187,13 +187,7 @@ export async function resolveSitemapUrls(
187
187
  // Dynamic route — try to call generateStaticSegmentParams
188
188
  try {
189
189
  const mod = await loadModule(route.pageLoader);
190
- let generateFn = mod.generateStaticSegmentParams;
191
- if (typeof generateFn !== 'function' && typeof mod.generateStaticParams === 'function') {
192
- generateFn = mod.generateStaticParams;
193
- console.warn(
194
- `[timber] ${route.pageLoader.filePath}: generateStaticParams is deprecated — rename to generateStaticSegmentParams`
195
- );
196
- }
190
+ const generateFn = mod.generateStaticSegmentParams;
197
191
  if (typeof generateFn !== 'function') {
198
192
  // No generateStaticSegmentParams export — skip this dynamic route
199
193
  continue;
@@ -227,15 +227,7 @@ async function expandDynamicRoute(
227
227
  ): Promise<string[] | null> {
228
228
  const mod = await loadModuleWithTimeout(page, timeoutMs);
229
229
 
230
- let generateFn = mod.generateStaticSegmentParams;
231
- if (typeof generateFn !== 'function' && typeof mod.generateStaticParams === 'function') {
232
- generateFn = mod.generateStaticParams;
233
- log(
234
- `[timber] ${page.filePath}: generateStaticParams is deprecated — ` +
235
- `rename to generateStaticSegmentParams`
236
- );
237
- }
238
-
230
+ const generateFn = mod.generateStaticSegmentParams;
239
231
  if (typeof generateFn !== 'function') return null;
240
232
 
241
233
  const combos = (await (generateFn as () => Promise<unknown>)()) as Record<
@@ -284,15 +276,7 @@ async function expandDynamicApiRoute(
284
276
  ): Promise<string[] | null> {
285
277
  const mod = await loadModuleWithTimeout(route, timeoutMs);
286
278
 
287
- let generateFn = mod.generateStaticSegmentParams;
288
- if (typeof generateFn !== 'function' && typeof mod.generateStaticParams === 'function') {
289
- generateFn = mod.generateStaticParams;
290
- log(
291
- `[timber] ${route.filePath}: generateStaticParams is deprecated — ` +
292
- `rename to generateStaticSegmentParams`
293
- );
294
- }
295
-
279
+ const generateFn = mod.generateStaticSegmentParams;
296
280
  if (typeof generateFn !== 'function') return null;
297
281
 
298
282
  const combos = (await (generateFn as () => Promise<unknown>)()) as Record<
@@ -19,9 +19,8 @@
19
19
  *
20
20
  * **Component chain (4xx):**
21
21
  * Pass 1 — status files (leaf → root): {status}.tsx → 4xx.tsx
22
- * Pass 2 — legacy compat (leaf → root): not-found.tsx / forbidden.tsx / unauthorized.tsx
23
- * Pass 3 — error.tsx (leaf → root)
24
- * Pass 4 — framework default (returns null)
22
+ * Pass 2 — error.tsx (leaf → root)
23
+ * Pass 3 — framework default (returns null)
25
24
  *
26
25
  * **JSON chain (4xx and 5xx):**
27
26
  * Pass 1 — json status files (leaf → root): {status}.json → {category}.json
@@ -40,7 +39,6 @@ import type { SegmentNode } from '../routing/types.js';
40
39
  export type StatusFileKind =
41
40
  | 'exact' // e.g. 403.tsx matched status 403
42
41
  | 'category' // e.g. 4xx.tsx matched status 403
43
- | 'legacy' // e.g. not-found.tsx matched status 404
44
42
  | 'error'; // error.tsx as last resort
45
43
 
46
44
  /** Response format family for status-code resolution. */
@@ -71,24 +69,6 @@ export interface SlotDeniedResolution<TFile> {
71
69
  kind: SlotDeniedKind;
72
70
  }
73
71
 
74
- // ─── Legacy Compat Mapping ───────────────────────────────────────────────────
75
-
76
- /**
77
- * Maps legacy file convention names to their corresponding HTTP status codes.
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.
80
- */
81
- export const LEGACY_FILE_TO_STATUS: Record<string, number> = {
82
- 'not-found': 404,
83
- 'forbidden': 403,
84
- 'unauthorized': 401,
85
- };
86
-
87
- /** Reverse index: status code → legacy file name. Built once at module load. */
88
- const STATUS_TO_LEGACY_FILE: Record<number, string> = Object.fromEntries(
89
- Object.entries(LEGACY_FILE_TO_STATUS).map(([name, status]) => [status, name])
90
- );
91
-
92
72
  // ─── Lookup Helpers ──────────────────────────────────────────────────────
93
73
 
94
74
  /**
@@ -113,23 +93,6 @@ function lookupInGroup<TFile>(
113
93
  return null;
114
94
  }
115
95
 
116
- /**
117
- * Look up the legacy convention file (`not-found.tsx` / `forbidden.tsx` /
118
- * `unauthorized.tsx`) for `status` on a single segment. Returns null if
119
- * `status` has no legacy mapping or the file isn't present.
120
- */
121
- function lookupLegacy<TFile>(
122
- group: Record<string, TFile> | undefined,
123
- status: number,
124
- segmentIndex: number
125
- ): StatusFileResolution<TFile> | null {
126
- if (!group) return null;
127
- const name = STATUS_TO_LEGACY_FILE[status];
128
- if (!name) return null;
129
- const file = group[name];
130
- return file ? { file, status, kind: 'legacy', segmentIndex } : null;
131
- }
132
-
133
96
  // ─── Resolver ────────────────────────────────────────────────────────────────
134
97
 
135
98
  /**
@@ -155,7 +118,7 @@ export function resolveStatusFile<TFile>(
155
118
  }
156
119
 
157
120
  /**
158
- * 4xx component fallback chain — three separate full passes leaf→root.
121
+ * 4xx component fallback chain — two separate full passes leaf→root.
159
122
  *
160
123
  * The passes must be separate (not interleaved per-segment) so that a
161
124
  * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
@@ -164,8 +127,7 @@ export function resolveStatusFile<TFile>(
164
127
  * separately.
165
128
  *
166
129
  * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
167
- * Pass 2 — not-found / forbidden / unauthorized (legacyStatusFiles)
168
- * Pass 3 — error.tsx (error)
130
+ * Pass 2 — error.tsx (error)
169
131
  */
170
132
  function resolve4xx<TFile>(
171
133
  status: number,
@@ -178,11 +140,6 @@ function resolve4xx<TFile>(
178
140
  if (r) return r;
179
141
  }
180
142
 
181
- for (let i = segments.length - 1; i >= 0; i--) {
182
- const r = lookupLegacy(segments[i].legacyStatusFiles, status, i);
183
- if (r) return r;
184
- }
185
-
186
143
  for (let i = segments.length - 1; i >= 0; i--) {
187
144
  const errorFile = segments[i].error;
188
145
  if (errorFile) {
@@ -2,15 +2,15 @@
2
2
  * Shim: next/navigation (client environment only)
3
3
  *
4
4
  * Re-exports the client-side hooks from timber's client API, and implements
5
- * client-render `redirect()` / `permanentRedirect()` / `notFound()` without
6
- * pulling server/primitives.ts into the browser bundle.
5
+ * client-render `redirect()` without pulling server/primitives.ts into the
6
+ * browser bundle.
7
7
  *
8
- * Next.js supports calling redirect()/notFound() during client component
9
- * render. timber matches that by throwing a plain Error carrying the same
10
- * JSON digest the server pipeline attaches to RedirectSignal/DenySignal.
11
- * TimberErrorBoundary recognizes the digest and converts it to an SPA
12
- * navigation (redirect) or a status-code fallback (notFound), and
13
- * browser-dev's isFrameworkSignalError() keeps it out of the dev overlay.
8
+ * Next.js supports calling redirect() during client component render.
9
+ * timber matches that by throwing a plain Error carrying the same JSON
10
+ * digest the server pipeline attaches to RedirectSignal. TimberErrorBoundary
11
+ * recognizes the digest and converts it to an SPA navigation (replace
12
+ * semantics), and browser-dev's isFrameworkSignalError() keeps it out of
13
+ * the dev overlay.
14
14
  *
15
15
  * The full shim (navigation.ts) is still used in the RSC and SSR environments
16
16
  * where the real server primitives produce proper HTTP responses.
@@ -59,21 +59,3 @@ export function redirect(path: string, _type?: RedirectTypeValue): never {
59
59
  assertRelativeRedirectPath(path);
60
60
  throwSignal(`Redirect to ${path}`, { type: 'redirect', location: path, status: 302 });
61
61
  }
62
-
63
- /**
64
- * Client-render permanent redirect. On the client there is no HTTP status —
65
- * this behaves identically to redirect() apart from the digest's status.
66
- */
67
- export function permanentRedirect(path: string, _type?: RedirectTypeValue): never {
68
- assertRelativeRedirectPath(path);
69
- throwSignal(`Redirect to ${path}`, { type: 'redirect', location: path, status: 308 });
70
- }
71
-
72
- /**
73
- * Client-render notFound. Throws a deny-digest error (status 404) that the
74
- * nearest matching TimberErrorBoundary renders as the 404 fallback. The
75
- * message matches DenySignal's so digest-less fallback parsing still works.
76
- */
77
- export function notFound(): never {
78
- throwSignal('Access denied with status 404', { type: 'deny', status: 404, data: null });
79
- }
@@ -21,7 +21,3 @@ export {
21
21
 
22
22
  // Functions (server-side)
23
23
  export { redirect, redirectExternal, RedirectType } from '@timber-js/app/server';
24
-
25
- // Next.js compat aliases — these are not part of timber's primary API.
26
- // They exist so that libraries importing from 'next/navigation' still work.
27
- export { permanentRedirect, notFound } from '../server/primitives.js';
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli-schema-sync-D3naS3eH.js","names":[],"sources":["../../src/routing/scanner.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';\nimport { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.js';\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 // Validate: detect affixed dynamic siblings with equal specificity\n // e.g., a-[id] and [slug]-b both have prefix+suffix length = 2\n validateAffixedSiblingConflicts(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 paramPrefix?: string,\n paramSuffix?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n paramPrefix,\n paramSuffix,\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 key = name as 'page' | 'layout' | 'error' | 'default' | 'denied';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Fixed conventions (middleware, access, route, params) — only\n // .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is\n // anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)\n // are NOT recognized. See design/07-routing.md §\"Page Extensions\".\n if (FIXED_CONVENTIONS.has(name) && /^[jt]sx?$/.test(ext)) {\n const key = name as 'middleware' | 'access' | 'route' | 'params';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\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 const existing = node.statusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\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 const existing = node.legacyStatusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\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 // Two files of the SAME kind (static+static or dynamic+dynamic) are a\n // build error — picking one by readdir order is platform-dependent.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!metaInfo.nestable && node.segmentName !== '') {\n throw new Error(\n `Build error: '${name}' is a root-only metadata convention and must be in the app root directory.\\n` +\n ` File: ${fullPath}\\n` +\n ` Move this file to the app root (not a route group or nested segment).`\n );\n }\n if (!node.metadataRoutes) {\n node.metadataRoutes = {};\n }\n const existing = node.metadataRoutes[name];\n if (existing) {\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (existingIsDynamic === newIsDynamic) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n // Dynamic > static precedence: overwrite only when the new file is dynamic.\n if (newIsDynamic) {\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 * Throw a build error for two files claiming the same convention slot in\n * one segment (e.g. page.tsx + page.jsx). Which file readdir returns first\n * is platform-dependent (APFS sorts entries, ext4 does not) — silently\n * picking one would let macOS dev and Linux CI serve different\n * implementations of the same route.\n */\nfunction throwDuplicateConvention(\n convention: string,\n firstPath: string,\n secondPath: string\n): never {\n throw new Error(\n `Build error: duplicate '${convention}' files in the same route segment.\\n` +\n ` File 1: ${firstPath}\\n` +\n ` File 2: ${secondPath}\\n` +\n `Each segment may define '${convention}' at most once. Remove one of the files.`\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_RE.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 (NULL_BYTE_RE.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 {\n type,\n paramName,\n paramPrefix,\n paramSuffix,\n interceptionMarker,\n interceptedSegmentName,\n } = 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 paramPrefix,\n paramSuffix\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 * Each parallel slot is validated as its OWN collision space: a slot's pages\n * intentionally share URL paths with the parent tree (that's the point of\n * parallel routes), but two route groups INSIDE one slot claiming the same\n * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.\n */\nfunction validateRouteGroupCollisions(root: SegmentNode): void {\n // Map from urlPath → { filePath, source } for the first page/route seen at that path\n collectRoutableLeaves(root, new Map(), '');\n}\n\n/**\n * Walk the segment tree and collect all routable leaves (page or route files),\n * throwing on collision. Each slot recurses with a FRESH collision map —\n * slots are parallel routes that intentionally share URL paths with their\n * parent tree, but collisions within a single slot are real errors.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string\n): void {\n // Intercepting routes are resolved by the slot resolver, not URL matching,\n // and their computed urlPaths intentionally mirror other routes (e.g. two\n // interceptors in one slot both produce /gallery/[id]). Skip them entirely.\n if (node.segmentType === 'intercepting') return;\n\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\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 // Recurse into children\n for (const child of node.children) {\n collectRoutableLeaves(child, seen, currentPath);\n }\n\n // Recurse into slots — each slot gets a fresh map (its own collision space)\n for (const slotNode of Object.values(node.slots)) {\n collectRoutableLeaves(slotNode, new Map(), currentPath);\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 * Validate that no sibling affixed dynamic segments have equal specificity.\n *\n * Among affixed siblings, longer combined prefix+suffix length wins. When two\n * siblings tie (same total affix length), the router would fall back to\n * filesystem scan order — which is platform-dependent. This is a build error.\n *\n * Route groups are URL-transparent, so affixed segments inside different groups\n * at the same level still conflict with each other.\n *\n * See design/07-routing.md §\"Priority\"\n */\nfunction validateAffixedSiblingConflicts(node: SegmentNode): void {\n const affixed = collectEffectiveAffixed(node);\n\n if (affixed.length >= 2) {\n const byLength = new Map<number, SegmentNode[]>();\n for (const child of affixed) {\n const len = (child.paramPrefix?.length ?? 0) + (child.paramSuffix?.length ?? 0);\n const group = byLength.get(len);\n if (group) {\n group.push(child);\n } else {\n byLength.set(len, [child]);\n }\n }\n\n for (const [, group] of byLength) {\n if (group.length >= 2) {\n throw new Error(\n `Build error: ambiguous affixed dynamic siblings with equal specificity.\\n` +\n ` Parent: ${node.urlPath || '/'}\\n` +\n ` Segments: ${group.map((s) => s.segmentName).join(', ')}\\n` +\n ` Combined prefix+suffix length: ${(group[0]!.paramPrefix?.length ?? 0) + (group[0]!.paramSuffix?.length ?? 0)}\\n` +\n `The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\\n` +\n `Rename one segment to give it a longer or shorter combined affix.`\n );\n }\n }\n }\n\n for (const child of node.children) {\n validateAffixedSiblingConflicts(child);\n }\n for (const slotNode of Object.values(node.slots)) {\n validateAffixedSiblingConflicts(slotNode);\n }\n}\n\n/**\n * Collect affixed dynamic segments that are effective children of a node.\n * Route groups are URL-transparent, so we look through them recursively.\n */\nfunction collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {\n const result: SegmentNode[] = [];\n for (const child of node.children) {\n if (child.segmentType === 'dynamic' && (child.paramPrefix || child.paramSuffix)) {\n result.push(child);\n } else if (child.segmentType === 'group') {\n result.push(...collectEffectiveAffixed(child));\n }\n }\n return result;\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 * `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\nfunction toBracketKey(node: SegmentNode): string {\n if (node.paramPrefix || node.paramSuffix) return `[${node.paramName}]`;\n switch (node.segmentType) {\n case 'catch-all':\n return `[...${node.paramName}]`;\n case 'optional-catch-all':\n return `[[...${node.paramName}]]`;\n default:\n return `[${node.paramName}]`;\n }\n}\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 // Use bare bracket form for schema keys — affixed segments like\n // `img-[id].png` register as `[id]`, matching runtime codec lookup.\n segments.add(toBracketKey(node));\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 * Replace comment bodies (and optionally string contents) with spaces,\n * preserving length and newlines so every index maps 1:1 to the original.\n * Lets the structural scanning below ignore `segmentParams` mentions and\n * braces inside comments/strings without a TS parser (fine pre-alpha,\n * see TIM-1066).\n *\n * With `maskStrings`, string contents are blanked too — except literal\n * 'segmentParams' strings, kept so a quoted property key stays findable.\n */\nfunction maskSource(content: string, maskStrings: boolean): string {\n const out = content.split('');\n const n = content.length;\n let i = 0;\n\n const blank = (from: number, to: number): void => {\n for (let j = from; j < to; j++) {\n if (content[j] !== '\\n') out[j] = ' ';\n }\n };\n\n while (i < n) {\n const ch = content[i];\n const next = content[i + 1];\n if (ch === '/' && next === '/') {\n const start = i;\n while (i < n && content[i] !== '\\n') i++;\n blank(start, i);\n } else if (ch === '/' && next === '*') {\n const start = i;\n i += 2;\n while (i < n && !(content[i] === '*' && content[i + 1] === '/')) i++;\n i = Math.min(i + 2, n);\n blank(start, i);\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n const start = i;\n i++;\n while (i < n && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n const end = i; // at closing quote (or n if unterminated)\n if (maskStrings && content.slice(start + 1, end) !== 'segmentParams') {\n blank(start + 1, end);\n }\n i++;\n } else {\n i++;\n }\n }\n return out.join('');\n}\n\n/**\n * Locate the `segmentParams: { ... }` block in a schema file.\n * Returns the indices of the opening and matching closing brace, or null\n * if no segmentParams property is found. Comments and strings are masked\n * first, so a JSDoc example mentioning `segmentParams: { ... }` above the\n * real defineSchema call is not mistaken for the block, and braces inside\n * comments/strings don't skew the depth count. The search is anchored to\n * the `defineSchema(` call when present, so helper objects with their own\n * segmentParams property earlier in the file are skipped.\n */\nexport function findSegmentParamsBlock(\n content: string\n): { openBrace: number; closeBrace: number } | null {\n const masked = maskSource(content, true);\n\n const defineCall = /\\bdefineSchema\\s*\\(/.exec(masked);\n const keyRe = /(['\"]?)segmentParams\\1\\s*:\\s*\\{/g;\n keyRe.lastIndex = defineCall ? defineCall.index + defineCall[0].length : 0;\n const keyMatch = keyRe.exec(masked);\n if (!keyMatch) return null;\n\n const openBrace = keyMatch.index + keyMatch[0].length - 1;\n let depth = 1;\n let i = openBrace + 1;\n while (i < masked.length && depth > 0) {\n if (masked[i] === '{') depth++;\n else if (masked[i] === '}') depth--;\n i++;\n }\n if (depth > 0) return null;\n // i now points to just after the closing brace\n return { openBrace, closeBrace: i - 1 };\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Scoped to the segmentParams: { ... } block — bracket keys elsewhere in\n * the file (unrelated objects, comments above defineSchema) are ignored.\n * Comments inside the block are masked, so a commented-out entry does not\n * count as registered.\n *\n * The key charset matches the route classifier (segment-classify.ts):\n * any character except `]`, so hyphenated params like '[my-param]' parse.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const block = findSegmentParamsBlock(content);\n if (!block) return [];\n\n const blockContent = maskSource(content, false).slice(block.openBrace, block.closeBrace);\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?[^\\]]+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(blockContent)) !== 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 const block = findSegmentParamsBlock(content);\n if (!block) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n const { openBrace, closeBrace } = block;\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 entry including trailing comma/whitespace. Scoped to\n * the segmentParams block — identical keys in unrelated objects elsewhere\n * in the file are left untouched.\n */\nfunction removeFromSchema(content: string, staleSegments: string[]): string {\n const block = findSegmentParamsBlock(content);\n if (!block) return content;\n\n let blockContent = content.slice(block.openBrace, block.closeBrace);\n // Key matching and the entry walk run on a comment-masked copy (same\n // length, indices align) so commented-out entries are never removed.\n // Both copies are spliced in sync to keep indices valid.\n let blockMasked = maskSource(blockContent, false);\n\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(blockMasked)) !== 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 < blockMasked.length) {\n const ch = blockMasked[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 < blockMasked.length && blockMasked[i] === '\\n') i++;\n\n blockContent = blockContent.slice(0, start) + blockContent.slice(i);\n blockMasked = blockMasked.slice(0, start) + blockMasked.slice(i);\n keyRe.lastIndex = start;\n }\n }\n\n return content.slice(0, block.openBrace) + blockContent + content.slice(block.closeBrace);\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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"}