@timber-js/app 0.2.0-alpha.200 → 0.2.0-alpha.201

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 (135) hide show
  1. package/bin/timber.mjs +15 -1
  2. package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
  3. package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
  4. package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
  5. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  6. package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
  7. package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
  8. package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
  9. package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
  11. package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
  15. package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
  17. package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
  18. package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
  19. package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
  20. package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
  21. package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
  22. package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
  23. package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
  24. package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
  25. package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
  26. package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
  27. package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
  28. package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
  29. package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
  30. package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
  31. package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
  32. package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
  33. package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
  34. package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
  35. package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
  36. package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
  37. package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
  38. package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
  39. package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
  40. package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
  41. package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
  42. package/dist/adapters/cloudflare-dev.js +1 -1
  43. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  44. package/dist/adapters/cloudflare.js +1 -1
  45. package/dist/analyze/crawl-entry.js +2 -2
  46. package/dist/analyze/graph-command.js +3 -3
  47. package/dist/cache/index.js +1 -1
  48. package/dist/cli.d.ts +7 -2
  49. package/dist/cli.d.ts.map +1 -1
  50. package/dist/cli.js +44 -7
  51. package/dist/cli.js.map +1 -1
  52. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  53. package/dist/client/browser-entry/action-queue.d.ts +20 -1
  54. package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
  55. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  56. package/dist/client/error-boundary.d.ts +3 -15
  57. package/dist/client/error-boundary.d.ts.map +1 -1
  58. package/dist/client/error-boundary.js +1 -1
  59. package/dist/client/error-reconstituter.d.ts +4 -4
  60. package/dist/client/error-reconstituter.d.ts.map +1 -1
  61. package/dist/client/internal.js +1 -1
  62. package/dist/dev-tools/debug-channel.d.ts +55 -0
  63. package/dist/dev-tools/debug-channel.d.ts.map +1 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +17 -2119
  66. package/dist/index.js.map +1 -1
  67. package/dist/plugins/dev-server.d.ts +7 -0
  68. package/dist/plugins/dev-server.d.ts.map +1 -1
  69. package/dist/plugins/mdx.d.ts.map +1 -1
  70. package/dist/routing/convention-lint.d.ts.map +1 -1
  71. package/dist/routing/index.js +2 -2
  72. package/dist/routing/scanner.d.ts +4 -4
  73. package/dist/routing/scanner.d.ts.map +1 -1
  74. package/dist/server/access-gate.d.ts +2 -2
  75. package/dist/server/deny-boundary.d.ts +3 -5
  76. package/dist/server/deny-boundary.d.ts.map +1 -1
  77. package/dist/server/deny-renderer.d.ts +0 -1
  78. package/dist/server/deny-renderer.d.ts.map +1 -1
  79. package/dist/server/error-boundary-wrapper.d.ts +5 -12
  80. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  81. package/dist/server/index.js +2 -2
  82. package/dist/server/internal.js +20 -195
  83. package/dist/server/internal.js.map +1 -1
  84. package/dist/server/pipeline.d.ts +8 -0
  85. package/dist/server/pipeline.d.ts.map +1 -1
  86. package/dist/server/primitives.d.ts +15 -13
  87. package/dist/server/primitives.d.ts.map +1 -1
  88. package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
  89. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  90. package/dist/server/rsc-entry/helpers.d.ts +6 -27
  91. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  92. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  93. package/dist/server/rsc-entry/render-route.d.ts +1 -0
  94. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  95. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
  96. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
  98. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  99. package/docs/api/30-api-server.mdx +5 -5
  100. package/docs/api/34-api-config.mdx +2 -2
  101. package/docs/api/36-cli.mdx +1 -1
  102. package/docs/learn/12-error-handling.mdx +7 -8
  103. package/package.json +1 -2
  104. package/src/cli.ts +62 -8
  105. package/src/client/browser-entry/action-dispatch.ts +12 -0
  106. package/src/client/browser-entry/action-queue.ts +70 -8
  107. package/src/client/browser-entry/rsc-stream.ts +78 -22
  108. package/src/client/error-boundary.tsx +13 -39
  109. package/src/client/error-reconstituter.tsx +5 -5
  110. package/src/dev-tools/debug-channel.ts +151 -0
  111. package/src/index.ts +8 -12
  112. package/src/plugins/dev-server.ts +58 -7
  113. package/src/plugins/mdx.ts +2 -1
  114. package/src/routing/convention-lint.ts +4 -111
  115. package/src/routing/scanner.ts +33 -22
  116. package/src/server/access-gate.tsx +3 -3
  117. package/src/server/deny-boundary.ts +5 -23
  118. package/src/server/deny-renderer.ts +4 -7
  119. package/src/server/error-boundary-wrapper.ts +11 -35
  120. package/src/server/pipeline.ts +9 -0
  121. package/src/server/primitives.ts +20 -26
  122. package/src/server/rsc-entry/error-renderer.ts +15 -43
  123. package/src/server/rsc-entry/helpers.ts +10 -67
  124. package/src/server/rsc-entry/index.ts +1 -0
  125. package/src/server/rsc-entry/render-route.ts +12 -1
  126. package/src/server/rsc-entry/rsc-stream.ts +27 -24
  127. package/src/server/rsc-entry/ssr-renderer.ts +7 -1
  128. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
  129. package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
  130. package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
  131. package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
  132. package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
  133. package/dist/server/utils/mdx-file.d.ts +0 -17
  134. package/dist/server/utils/mdx-file.d.ts.map +0 -1
  135. package/src/server/utils/mdx-file.ts +0 -22
@@ -1,13 +1,9 @@
1
1
  /**
2
2
  * RSC Error & No-Match Renderers — handles error pages and 404s.
3
3
  *
4
- * All error pages (TSX and MDX) render through the RSC → SSR pipeline:
5
- * - TSX ('use client'): Error is converted to SerializableError, wrapped
6
- * with ErrorReconstituter to reconstitute a real Error on the client.
7
- * - MDX (server component): Receives plain props ({ status }), no wrapper needed.
8
- *
9
- * This unified approach replaces the former SSR-only bypass for TSX error
10
- * pages. See design/spike-TIM-565-unify-error-pages.md for the full analysis.
4
+ * Error pages are 'use client' components rendered through the RSC → SSR
5
+ * pipeline: the Error is converted to SerializableError, wrapped with
6
+ * ErrorReconstituter to reconstitute a real Error on the client.
11
7
  *
12
8
  * Design docs: 10-error-handling.md §"Three-Tier Error Page Rendering"
13
9
  */
@@ -38,7 +34,6 @@ import { ErrorReconstituter } from '../../client/error-reconstituter.tsx';
38
34
  import type { SerializableError } from '../../client/error-reconstituter.tsx';
39
35
  import { loadModule } from '../safe-load.ts';
40
36
  import { sourceMapError } from '../dev-source-map.ts';
41
- import { isMdxFilePath as isMdxFile } from '../utils/mdx-file.ts';
42
37
  import { defaultStatusPageResponse } from '../default-status-page.ts';
43
38
  import { rscErrorEnvelope } from '../rsc-error-envelope.ts';
44
39
 
@@ -60,8 +55,6 @@ interface ErrorFileResolution {
60
55
  component: (...args: unknown[]) => unknown;
61
56
  /** Index of the segment where the file was found */
62
57
  segmentIndex: number;
63
- /** Whether the file is MDX (server component) */
64
- isMdx: boolean;
65
58
  }
66
59
 
67
60
  /**
@@ -95,7 +88,6 @@ async function resolveErrorFile(
95
88
  return {
96
89
  component: mod.default as (...args: unknown[]) => unknown,
97
90
  segmentIndex: i,
98
- isMdx: isMdxFile(specificFile.filePath),
99
91
  };
100
92
  }
101
93
  }
@@ -108,7 +100,6 @@ async function resolveErrorFile(
108
100
  return {
109
101
  component: mod.default as (...args: unknown[]) => unknown,
110
102
  segmentIndex: i,
111
- isMdx: isMdxFile(categoryFile.filePath),
112
103
  };
113
104
  }
114
105
  }
@@ -121,7 +112,6 @@ async function resolveErrorFile(
121
112
  return {
122
113
  component: mod.default as (...args: unknown[]) => unknown,
123
114
  segmentIndex: i,
124
- isMdx: isMdxFile(segment.error.filePath),
125
115
  };
126
116
  }
127
117
  }
@@ -236,7 +226,7 @@ export async function renderErrorPage(
236
226
  return defaultStatusPageResponse(status, responseHeaders);
237
227
  }
238
228
 
239
- const { component: errorComponent, segmentIndex, isMdx } = resolution;
229
+ const { component: errorComponent, segmentIndex } = resolution;
240
230
  const layoutsToWrap = getLayoutsForErrorFile(
241
231
  segments,
242
232
  layoutComponents,
@@ -247,7 +237,6 @@ export async function renderErrorPage(
247
237
  return renderErrorPageViaRsc(
248
238
  error,
249
239
  errorComponent,
250
- isMdx,
251
240
  status,
252
241
  layoutsToWrap,
253
242
  req,
@@ -260,14 +249,12 @@ export async function renderErrorPage(
260
249
  /**
261
250
  * Render an error page through the RSC → SSR pipeline.
262
251
  *
263
- * Handles both TSX and MDX error pages:
264
- * - TSX: Wrapped with ErrorReconstituter to reconstitute Error on client
265
- * - MDX: Receives plain props ({ status }), rendered as server component
252
+ * Error pages are 'use client' components wrapped with ErrorReconstituter
253
+ * to reconstitute a real Error instance on the client.
266
254
  */
267
255
  async function renderErrorPageViaRsc(
268
256
  error: unknown,
269
257
  errorComponent: (...args: unknown[]) => unknown,
270
- isMdx: boolean,
271
258
  status: number,
272
259
  layoutsToWrap: LayoutEntry[],
273
260
  req: Request,
@@ -278,26 +265,15 @@ async function renderErrorPageViaRsc(
278
265
  const h = createElement as (...args: unknown[]) => React.ReactElement;
279
266
  const url = new URL(req.url);
280
267
 
281
- let element: React.ReactElement;
282
-
283
- if (isMdx) {
284
- // MDX error pages receive plain props — no Error serialization issue
285
- element = h(errorComponent, { status });
286
- } else {
287
- // TSX error pages: wrap with ErrorReconstituter for Error reconstitution.
288
- // The ErrorReconstituter is a 'use client' component that receives
289
- // SerializableError (plain object) and reconstitutes a real Error
290
- // before passing to the user's error component.
291
- const serializedError = toSerializableError(error);
292
- const digest = isRenderError(error) ? { code: error.code, data: error.digest.data } : null;
293
-
294
- element = h(ErrorReconstituter, {
295
- error: serializedError,
296
- digest,
297
- reset: undefined,
298
- component: errorComponent,
299
- });
300
- }
268
+ const serializedError = toSerializableError(error);
269
+ const digest = isRenderError(error) ? { code: error.code, data: error.digest.data } : null;
270
+
271
+ let element: React.ReactElement = h(ErrorReconstituter, {
272
+ error: serializedError,
273
+ digest,
274
+ reset: undefined,
275
+ component: errorComponent,
276
+ });
301
277
 
302
278
  // Wrap in layouts
303
279
  for (let i = layoutsToWrap.length - 1; i >= 0; i--) {
@@ -358,7 +334,6 @@ async function renderErrorPageViaRsc(
358
334
  * Tier 2 — Render global-error.tsx as a standalone full-page replacement.
359
335
  *
360
336
  * No layout wrapping. The component must provide its own <html> and <body>.
361
- * All formats (TSX and MDX) go through RSC → SSR.
362
337
  *
363
338
  * See design/10-error-handling.md §"Tier 2 — Global Error Page"
364
339
  */
@@ -380,13 +355,10 @@ async function renderGlobalErrorPage(
380
355
  }
381
356
 
382
357
  const component = mod.default as (...args: unknown[]) => unknown;
383
- const isMdx = isMdxFile(globalError.filePath);
384
358
 
385
- // Reuse the unified RSC path with no layouts
386
359
  return renderErrorPageViaRsc(
387
360
  error,
388
361
  component,
389
- isMdx,
390
362
  status,
391
363
  [], // No layouts — global-error is standalone
392
364
  req,
@@ -15,22 +15,21 @@ import { isStaticRequestDependent } from '../request-dep.ts';
15
15
  /**
16
16
  * Create a debug channel sink that discards all debug data.
17
17
  *
18
- * React Flight's dev mode serializes server component source code as `$E`
19
- * entries for DevTools. Without a separate debugChannel, this data is
20
- * embedded inline in the main RSC stream — leaking source code to the
21
- * browser. By providing a debug channel, debug data goes to a separate
22
- * stream that we drain and discard.
18
+ * Used in production to divert debug rows ($E source entries, component
19
+ * info) away from the main Flight stream. In dev mode, the debug channel
20
+ * is omitted entirely so debug rows (owner stacks) ride the main stream
21
+ * and reach the browser for console warning resolution.
23
22
  *
24
- * See design/13-security.md §"Server component source leak"
23
+ * Only provides `writable` — omitting `readable` avoids Flight's
24
+ * keepDebugAlive mode which holds deferred debug refs open per request.
25
25
  */
26
- export function createDebugChannelSink(): { readable: ReadableStream; writable: WritableStream } {
26
+ export function createDebugChannelSink(): { writable: WritableStream } {
27
27
  const sink = new TransformStream();
28
28
  // Drain the readable side so the writable never back-pressures.
29
29
  sink.readable.pipeTo(new WritableStream()).catch(() => {});
30
- return {
31
- readable: new ReadableStream(), // no commands to send to Flight
32
- writable: sink.writable,
33
- };
30
+ // Only writable — omitting `readable` avoids Flight's keepDebugAlive
31
+ // mode which holds deferred debug refs open per request.
32
+ return { writable: sink.writable };
34
33
  }
35
34
 
36
35
  // ─── Debug Channel Collector (dev mode only) ────────────────────────────
@@ -49,62 +48,6 @@ export interface DebugComponentEntry {
49
48
  stack: unknown[] | null;
50
49
  }
51
50
 
52
- /**
53
- * A debug channel that collects Flight debug rows instead of discarding them.
54
- *
55
- * Used in dev mode to capture server component tree information for the
56
- * Vite error overlay. The collector provides the same `{ readable, writable }`
57
- * shape as the discard sink, plus methods to retrieve collected data.
58
- *
59
- * Security: only component names, environments, and stack frames are
60
- * extracted — props and source code are stripped. In production builds,
61
- * use `createDebugChannelSink()` instead (this function is never called).
62
- */
63
- export interface DebugChannelCollector {
64
- readable: ReadableStream;
65
- writable: WritableStream;
66
- /** Get the raw collected text from the debug channel. */
67
- getCollectedText(): string;
68
- /** Get parsed component entries (names, stacks — no props or source). */
69
- getComponents(): DebugComponentEntry[];
70
- }
71
-
72
- export function createDebugChannelCollector(): DebugChannelCollector {
73
- const chunks: string[] = [];
74
- const decoder = new TextDecoder();
75
-
76
- const sink = new TransformStream();
77
-
78
- // Collect chunks from the readable side instead of discarding them.
79
- sink.readable
80
- .pipeTo(
81
- new WritableStream({
82
- write(chunk: Uint8Array) {
83
- chunks.push(decoder.decode(chunk, { stream: true }));
84
- },
85
- close() {
86
- // Flush any remaining bytes in the decoder
87
- const remaining = decoder.decode();
88
- if (remaining) chunks.push(remaining);
89
- },
90
- })
91
- )
92
- .catch(() => {
93
- // Stream abort — request cancelled. Not an error.
94
- });
95
-
96
- return {
97
- readable: new ReadableStream(), // no commands to send to Flight
98
- writable: sink.writable,
99
- getCollectedText() {
100
- return chunks.join('');
101
- },
102
- getComponents() {
103
- return parseDebugRows(chunks.join(''));
104
- },
105
- };
106
- }
107
-
108
51
  /**
109
52
  * Parse React Flight debug rows into component entries.
110
53
  *
@@ -299,6 +299,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
299
299
  rootSegment: manifest.root,
300
300
  buildManifest: typedBuildManifest,
301
301
  globalError: manifest.globalError,
302
+ devDebugRegistry: pipelineConfig.devDebugRegistry,
302
303
  },
303
304
  interception
304
305
  );
@@ -60,6 +60,7 @@ export interface RenderRouteDeps {
60
60
  rootSegment: ManifestSegmentNode;
61
61
  buildManifest: BuildManifest;
62
62
  globalError?: { load: () => Promise<unknown>; filePath: string };
63
+ devDebugRegistry?: import('../../dev-tools/debug-channel.ts').DebugChannelRegistry;
63
64
  }
64
65
 
65
66
  /**
@@ -198,7 +199,16 @@ export async function renderRoute(
198
199
 
199
200
  // Render to RSC Flight stream with signal tracking.
200
201
  const _rscStart = performance.now();
201
- const { rscStream, signals, getDebugComponents } = renderRscStream(payload, req, leafDenyChain);
202
+ // Only wire the HMR debug transport for HTML requests. RSC payload
203
+ // requests (client navigations) don't serve HTML with __timber_debug_id,
204
+ // so the browser can't subscribe — skip to avoid orphaned sessions.
205
+ const debugRegistry = isRscRequest(req) || clientJsDisabled ? undefined : deps.devDebugRegistry;
206
+ const { rscStream, signals, getDebugComponents, debugRequestId } = renderRscStream(
207
+ payload,
208
+ req,
209
+ leafDenyChain,
210
+ debugRegistry
211
+ );
202
212
 
203
213
  // Store the debug components getter in ALS so onPipelineError can
204
214
  // include component tree context for render-phase errors (dev mode only).
@@ -300,5 +310,6 @@ export async function renderRoute(
300
310
  deferSuspenseFor,
301
311
  globalError,
302
312
  slotSkipInfo,
313
+ debugRequestId,
303
314
  });
304
315
  }
@@ -27,12 +27,8 @@ import {
27
27
  } from '../primitives.ts';
28
28
  import { checkAndWarnRscPropError } from '../rsc-prop-warnings.ts';
29
29
 
30
- import {
31
- createDebugChannelSink,
32
- createDebugChannelCollector,
33
- isAbortError,
34
- type DebugComponentEntry,
35
- } from './helpers.ts';
30
+ import { createDebugChannelSink, isAbortError, type DebugComponentEntry } from './helpers.ts';
31
+ import { createDevDebugChannel, type DebugChannelRegistry } from '../../dev-tools/debug-channel.ts';
36
32
  import { isDebug } from '../debug.ts';
37
33
  import { isDevMode } from '../debug.ts';
38
34
 
@@ -84,6 +80,8 @@ export interface RscStreamResult {
84
80
  signals: RenderSignals;
85
81
  /** Dev-only: server component debug info from the Flight debug channel. */
86
82
  getDebugComponents?: () => DebugComponentEntry[];
83
+ /** Dev-only: request ID for the debug channel HMR transport. */
84
+ debugRequestId?: string;
87
85
  }
88
86
 
89
87
  /**
@@ -100,7 +98,8 @@ export interface RscStreamResult {
100
98
  export function renderRscStream(
101
99
  payload: RscPayloadRoot,
102
100
  req: Request,
103
- leafDenyChain?: DenyPageEntry[]
101
+ leafDenyChain?: DenyPageEntry[],
102
+ devDebugRegistry?: DebugChannelRegistry
104
103
  ): RscStreamResult {
105
104
  const signals: RenderSignals = {
106
105
  denySignal: null,
@@ -112,9 +111,18 @@ export function renderRscStream(
112
111
 
113
112
  let rscStream: ReadableStream<Uint8Array> | undefined;
114
113
 
115
- // In dev mode, collect debug channel data for the error overlay.
116
- // In production, use the discard sink (no overhead).
117
- const debugChannel = isDevMode() ? createDebugChannelCollector() : createDebugChannelSink();
114
+ // Always provide a debugChannel to prevent $E source entries (which
115
+ // may contain bound-arg secrets) from leaking into the main stream.
116
+ //
117
+ // In dev mode with an HMR transport: use createDevDebugChannel which
118
+ // pipes debug data to the browser via WebSocket for owner stack
119
+ // resolution. In production (or dev without transport): use a sink
120
+ // that discards everything.
121
+ const debugChannel = devDebugRegistry
122
+ ? createDevDebugChannel(devDebugRegistry)
123
+ : createDebugChannelSink();
124
+ const debugRequestId =
125
+ 'requestId' in debugChannel ? (debugChannel as { requestId: string }).requestId : undefined;
118
126
 
119
127
  try {
120
128
  rscStream = renderToReadableStream(
@@ -128,20 +136,13 @@ export function renderRscStream(
128
136
  if (isDenySignal(error)) {
129
137
  // Late addressing (TIM-1450): a nested-component deny escapes
130
138
  // all in-tree catch sites and arrives here without owners.
131
- // Resolve it against the leaf deny chain — when the first match
132
- // is non-MDX, stamp the full candidate list so the client can
133
- // find the first reachable owner via its ancestry context.
139
+ // Resolve it against the leaf deny chain and stamp the full
140
+ // candidate list so the client can find the first reachable
141
+ // owner via its ancestry context.
134
142
  if (!error.owners && leafDenyChain) {
135
143
  const entries = matchDenyEntries(leafDenyChain, error.status);
136
- if (entries.length > 0 && !entries[0].isMdx) {
137
- // Only non-MDX entries are addressable — MDX boundaries hold
138
- // pre-rendered fallbackElements without runtime props. TIM-1451
139
- // will widen exact-code MDX; until then they stay on the
140
- // re-render fallback path.
141
- const nonMdx = entries.filter((e) => !e.isMdx);
142
- if (nonMdx.length > 0) {
143
- error.owners = nonMdx.map((e) => e.ownerKey);
144
- }
144
+ if (entries.length > 0) {
145
+ error.owners = entries.map((e) => e.ownerKey);
145
146
  }
146
147
  }
147
148
  // First-wins: two concurrent denies from separately serialized
@@ -279,11 +280,13 @@ export function renderRscStream(
279
280
  return {
280
281
  rscStream,
281
282
  signals,
282
- // Expose the debug channel collector's getComponents in dev mode.
283
- // The caller can retrieve component tree info when handling errors.
283
+ // In production, the debug channel collector's getComponents is available.
284
+ // In dev mode, debug rows ride the main stream (no separate channel), so
285
+ // this is undefined — the error overlay uses stack traces directly.
284
286
  getDebugComponents:
285
287
  'getComponents' in debugChannel
286
288
  ? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
287
289
  : undefined,
290
+ debugRequestId,
288
291
  };
289
292
  }
@@ -105,6 +105,7 @@ interface SsrRenderOptions {
105
105
  /** Tier 2 global-error.tsx file, if present in app/. */
106
106
  globalError?: GlobalErrorFile;
107
107
  slotSkipInfo?: SlotSkipEntry[];
108
+ debugRequestId?: string;
108
109
  }
109
110
 
110
111
  /**
@@ -197,7 +198,12 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
197
198
  clientBootstrap.preloadLinks +
198
199
  // Initialize __timber_f in <head> so it exists before any streaming
199
200
  // chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
200
- (clientJsDisabled ? '' : flightInitScript(nonce)),
201
+ (clientJsDisabled ? '' : flightInitScript(nonce)) +
202
+ // Embed the debug channel request ID so the browser can subscribe
203
+ // to the correct HMR debug stream for owner stack resolution.
204
+ (opts.debugRequestId && !clientJsDisabled
205
+ ? `<script${nonceAttr(nonce)}>self.__timber_debug_id=${htmlEscapeJsonString(JSON.stringify(opts.debugRequestId))}</script>`
206
+ : ''),
201
207
  getHeadSegmentScript: clientJsDisabled
202
208
  ? undefined
203
209
  : () =>
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-manifest-DTmSGLRz.js","names":[],"sources":["../../src/server/csp-nonce.ts","../../src/server/build-manifest.ts"],"sourcesContent":["/**\n * CSP nonce — per-request, generated on demand.\n *\n * timber emits several inline `<script>` tags per HTML response — React's\n * bootstrap script and Fizz runtime, the Flight init/chunk scripts, the\n * segment-tree script, and the post-flush noindex script. Under a strict\n * `Content-Security-Policy` (`script-src 'nonce-…' 'strict-dynamic'`) every\n * one of them needs the request's nonce or the browser refuses to run it.\n *\n * The app opts in by reading `ctx.nonce` in `middleware.ts` and putting it\n * in the CSP header it sets through `ctx.headers`. The first read generates\n * the value and stores it on the request context; the render pipeline reads\n * it back with `getCspNonce()` and stamps it on every inline script. A\n * request whose middleware never reads `ctx.nonce` has no nonce, and its\n * markup is byte-identical to a render without this feature. timber does\n * not parse the CSP header — the app owns the policy, timber owns the\n * markup, and `ctx.nonce` is the one value both are built from.\n *\n * `proxy.ts` cannot participate: it only sees the finished response from\n * `next()`, after the markup exists.\n *\n * See design/13-security.md §\"Content Security Policy\" (TIM-1414).\n */\n\nimport { requestContextAls } from './als-registry.ts';\n\n/**\n * Generate a CSP nonce: 128 random bits, base64. Matches CSP's\n * `base64-value` grammar, so it is valid in the header verbatim and safe\n * inside a double-quoted HTML attribute without escaping.\n */\nfunction generateNonce(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n let binary = '';\n for (const b of bytes) binary += String.fromCharCode(b);\n return btoa(binary);\n}\n\n/**\n * The request's CSP nonce, creating it on first call. Backs the\n * `ctx.nonce` getter on `MiddlewareContext`. Outside a request context\n * (tests, build-time rendering) a fresh value is returned but not\n * retained, since there is no request for it to belong to.\n */\nexport function requestCspNonce(): string {\n const store = requestContextAls.getStore();\n if (!store) return generateNonce();\n store.cspNonce ??= generateNonce();\n return store.cspNonce;\n}\n\n/**\n * The request's CSP nonce if middleware asked for one, else `undefined` —\n * in which case the pipeline emits bare `<script>` tags.\n */\nexport function getCspNonce(): string | undefined {\n return requestContextAls.getStore()?.cspNonce;\n}\n\n/**\n * The ` nonce=\"…\"` attribute fragment for a timber-generated `<script>` tag,\n * or `''` when the request has no nonce. Callers interpolate it directly\n * after the tag name: `` `<script${nonceAttr(nonce)}>` ``.\n *\n * The value comes from `generateNonce` and is base64, so no attribute\n * escaping is required.\n */\nexport function nonceAttr(nonce: string | undefined): string {\n return nonce ? ` nonce=\"${nonce}\"` : '';\n}\n","/**\n * Build manifest types and utilities for CSS and JS asset tracking.\n *\n * The build manifest maps route segment file paths to their output\n * chunks from Vite's client build. This enables:\n * - <link rel=\"stylesheet\"> injection in HTML <head>\n * - <script type=\"module\"> with hashed URLs in production\n * - <link rel=\"modulepreload\"> for client chunk dependencies\n * - Link preload headers for Early Hints (103)\n *\n * In dev mode, Vite's HMR client handles CSS/JS injection, so the build\n * manifest is empty. In production, it's populated from Vite's\n * .vite/manifest.json after the client build.\n *\n * Design docs: 18-build-system.md §\"Build Manifest\", 02-rendering-pipeline.md §\"Early Hints\"\n */\n\n/** A font asset entry in the build manifest. */\nexport interface ManifestFontEntry {\n /** URL path to the font file (e.g. `/_timber/fonts/inter-latin-400-abc123.woff2`). */\n href: string;\n /** Font format (e.g. `woff2`). */\n format: string;\n /** Crossorigin attribute — always `anonymous` for fonts. */\n crossOrigin: string;\n}\n\n/** Build manifest mapping input file paths to output asset URLs. */\nexport interface BuildManifest {\n /** Map from input file path (relative to project root) to output CSS URLs. */\n css: Record<string, string[]>;\n /** Map from input file path to output JS chunk URL (hashed filename). */\n js: Record<string, string>;\n /** Map from input file path to transitive JS dependency URLs for modulepreload. */\n modulepreload: Record<string, string[]>;\n /** Map from input file path to font assets used by that module. */\n fonts: Record<string, ManifestFontEntry[]>;\n /**\n * Cache-busting hashes for metadata route files (opengraph-image, etc.).\n * Map from file path (relative to project root) to short content hash.\n * In dev mode this is empty — a startup nonce is used instead.\n */\n metadataRouteHashes?: Record<string, string>;\n /**\n * Static request-dependency map from the Vite plugin's module-graph analysis.\n * Map from segment entry file path to whether it's request-dependent.\n * Used by buildSegmentInfo() for X-Timber-Segments in production.\n */\n requestDep?: Record<string, boolean>;\n}\n\n/**\n * Empty build manifest used in dev mode, and the single source of truth for\n * what an empty manifest contains.\n *\n * `virtual:timber-build-manifest` serializes this constant into its generated\n * source rather than restating it — one literal for the dev module, one for the\n * production module's `globalThis.__TIMBER_BUILD_MANIFEST__ ?? …` fallback.\n * They were three hand-written objects that had already drifted: this one and\n * the dev literal were missing `requestDep` while the production fallback had\n * it, so a reader who grepped the built bundle for `requestDep` found the one\n * copy that mentioned it and read the fallback as the shipped map (TIM-1300).\n * A field added here now reaches every copy.\n */\nexport const EMPTY_BUILD_MANIFEST: BuildManifest = {\n css: {},\n js: {},\n modulepreload: {},\n fonts: {},\n metadataRouteHashes: {},\n requestDep: {},\n};\n\n/** Segment shape expected by collectRouteCss (matches ManifestSegmentNode). */\ninterface SegmentWithFiles {\n layout?: { filePath: string };\n page?: { filePath: string };\n}\n\n/**\n * Collect all CSS files needed for a matched route's segment chain.\n *\n * Walks segments root → leaf, collecting CSS for each layout and page.\n * Deduplicates while preserving order (root layout CSS first).\n */\nexport function collectRouteCss(segments: SegmentWithFiles[], manifest: BuildManifest): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n\n for (const segment of segments) {\n for (const file of [segment.layout, segment.page]) {\n if (!file) continue;\n const cssFiles = manifest.css[file.filePath];\n if (!cssFiles) continue;\n for (const url of cssFiles) {\n if (!seen.has(url)) {\n seen.add(url);\n result.push(url);\n }\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generate <link rel=\"stylesheet\"> tags for CSS URLs.\n *\n * Returns an HTML string to prepend to headHtml for injection\n * via injectHead() before </head>.\n */\nexport function buildCssLinkTags(cssUrls: string[]): string {\n // Emit only <link rel=\"stylesheet\"> — no preload tags. CSS preloading\n // is handled by 103 Early Hints (Link header) which fires before the\n // HTML stream. Float also emits `<link rel=\"stylesheet\" data-precedence>`\n // at the top of <head> during Fizz rendering — the browser discovers\n // CSS from that tag. A <link rel=\"preload\"> here would arrive *after*\n // Float's stylesheet tag and trigger \"Preload was ignored\" warnings.\n return cssUrls.map((url) => `<link rel=\"stylesheet\" href=\"${url}\">`).join('');\n}\n\n// ─── Font utilities ──────────────────────────────────────────────────────\n\n/**\n * Collect all font entries needed for a matched route's segment chain.\n *\n * Walks segments root → leaf, collecting fonts for each layout and page.\n * Deduplicates by href while preserving order.\n */\nexport function collectRouteFonts(\n segments: SegmentWithFiles[],\n manifest: BuildManifest\n): ManifestFontEntry[] {\n const seen = new Set<string>();\n const result: ManifestFontEntry[] = [];\n\n for (const segment of segments) {\n for (const file of [segment.layout, segment.page]) {\n if (!file) continue;\n const fonts = manifest.fonts[file.filePath];\n if (!fonts) continue;\n for (const entry of fonts) {\n if (!seen.has(entry.href)) {\n seen.add(entry.href);\n result.push(entry);\n }\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generate <link rel=\"preload\"> tags for font assets.\n *\n * Font preloads use `as=font` and always include `crossorigin` (required\n * for font preloads even for same-origin resources per the spec).\n */\nexport function buildFontPreloadTags(fonts: ManifestFontEntry[]): string {\n return fonts\n .map(\n (f) =>\n `<link rel=\"preload\" href=\"${f.href}\" as=\"font\" type=\"font/${f.format}\" crossorigin=\"${f.crossOrigin}\">`\n )\n .join('');\n}\n\n// ─── JS chunk utilities ──────────────────────────────────────────────────\n\n/**\n * Collect modulepreload URLs for a matched route's segment chain.\n *\n * Walks segments root → leaf, collecting transitive JS dependencies\n * for each layout and page. Deduplicates across segments.\n */\nexport function collectRouteModulepreloads(\n segments: SegmentWithFiles[],\n manifest: BuildManifest\n): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n\n for (const segment of segments) {\n for (const file of [segment.layout, segment.page]) {\n if (!file) continue;\n const preloads = manifest.modulepreload[file.filePath];\n if (!preloads) continue;\n for (const url of preloads) {\n if (!seen.has(url)) {\n seen.add(url);\n result.push(url);\n }\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generate <link rel=\"modulepreload\"> tags for JS dependency URLs.\n *\n * Modulepreload hints tell the browser to fetch and parse JS modules\n * before they're needed, reducing waterfall latency for dynamic imports.\n */\nexport function buildModulepreloadTags(urls: string[]): string {\n return urls.map((url) => `<link rel=\"modulepreload\" href=\"${url}\">`).join('');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAS,gBAAwB;CAC/B,MAAM,QAAQ,OAAO,gCAAgB,IAAI,WAAW,EAAE,CAAC;CACvD,IAAI,SAAS;CACb,KAAK,MAAM,KAAK,OAAO,UAAU,OAAO,aAAa,CAAC;CACtD,OAAO,KAAK,MAAM;AACpB;;;;;;;AAQA,SAAgB,kBAA0B;CACxC,MAAM,QAAQ,kBAAkB,SAAS;CACzC,IAAI,CAAC,OAAO,OAAO,cAAc;CACjC,MAAM,aAAa,cAAc;CACjC,OAAO,MAAM;AACf;;;;;;;;;;;;;;;;ACeA,IAAa,uBAAsC;CACjD,KAAK,CAAC;CACN,IAAI,CAAC;CACL,eAAe,CAAC;CAChB,OAAO,CAAC;CACR,qBAAqB,CAAC;CACtB,YAAY,CAAC;AACf;;;;;;;AAcA,SAAgB,gBAAgB,UAA8B,UAAmC;CAC/F,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,IAAI,GAAG;EACjD,IAAI,CAAC,MAAM;EACX,MAAM,WAAW,SAAS,IAAI,KAAK;EACnC,IAAI,CAAC,UAAU;EACf,KAAK,MAAM,OAAO,UAChB,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG;GAClB,KAAK,IAAI,GAAG;GACZ,OAAO,KAAK,GAAG;EACjB;CAEJ;CAGF,OAAO;AACT;;;;;;;AA0BA,SAAgB,kBACd,UACA,UACqB;CACrB,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAA8B,CAAC;CAErC,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,IAAI,GAAG;EACjD,IAAI,CAAC,MAAM;EACX,MAAM,QAAQ,SAAS,MAAM,KAAK;EAClC,IAAI,CAAC,OAAO;EACZ,KAAK,MAAM,SAAS,OAClB,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,GAAG;GACzB,KAAK,IAAI,MAAM,IAAI;GACnB,OAAO,KAAK,KAAK;EACnB;CAEJ;CAGF,OAAO;AACT;;;;;;;AAyBA,SAAgB,2BACd,UACA,UACU;CACV,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,WAAW,UACpB,KAAK,MAAM,QAAQ,CAAC,QAAQ,QAAQ,QAAQ,IAAI,GAAG;EACjD,IAAI,CAAC,MAAM;EACX,MAAM,WAAW,SAAS,cAAc,KAAK;EAC7C,IAAI,CAAC,UAAU;EACf,KAAK,MAAM,OAAO,UAChB,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG;GAClB,KAAK,IAAI,GAAG;GACZ,OAAO,KAAK,GAAG;EACjB;CAEJ;CAGF,OAAO;AACT"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"convention-lint-fRkwVwEH.js","names":[],"sources":["../../src/server/utils/mdx-file.ts","../../src/routing/segment-codegen.ts","../../src/routing/codegen-write.ts","../../src/routing/export-detect.ts","../../src/routing/convention-lint.ts"],"sourcesContent":["/**\n * MDX file-path detection shared by error/deny rendering paths.\n *\n * MDX status and error files are server components — they cannot be passed\n * as function props across the RSC→client boundary (e.g. to\n * TimberErrorBoundary or ErrorReconstituter) and must be pre-rendered as\n * elements with plain props instead.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\".\n */\n\n/** MDX/markdown extensions (dotted) — the canonical list, also used by plugins/mdx.ts. */\nexport const MDX_EXTENSIONS = new Set(['.mdx', '.md']);\n\n/**\n * Check if a file path ends with an MDX/markdown extension.\n */\nexport function isMdxFilePath(filePath: string): boolean {\n const dotIndex = filePath.lastIndexOf('.');\n if (dotIndex === -1) return false;\n return MDX_EXTENSIONS.has(filePath.slice(dotIndex));\n}\n","/**\n * $segment type generation and directory-to-route mapping.\n *\n * Generates per-route $segment.d.ts files in .timber/types/ that mirror\n * the app/ directory structure. TypeScript's rootDirs feature makes these\n * importable as './$segment' from route files.\n *\n * Design doc: design/41-global-params.md §The $segment Module\n */\n\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { RouteTree, SegmentNode } from './types.ts';\n\n/** Info carried per directory for type generation. */\nexport interface SegmentInfo {\n /** The URL route pattern (groups and slots stripped). e.g. '/[artistSlug]/[year]' */\n route: string;\n /** The full tree path including groups and slots — uniquely identifies a segment.\n * e.g. '/(browse)/[artistSlug]/@shows/[year]' */\n segmentPath: string;\n}\n\n/**\n * Build a map of absolute filesystem directory → segment info.\n * Used by resolveId to map a $segment import's importer to the correct route,\n * and by writeSegmentTypes to generate per-route $segment.d.ts files.\n *\n * Walks every segment (not just leaves) so layouts and middleware\n * at any depth can import $segment.\n */\nexport function buildDirToRouteMap(tree: RouteTree, appDir: string): Map<string, SegmentInfo> {\n const map = new Map<string, SegmentInfo>();\n\n function walk(node: SegmentNode, fsPath: string, treePath: string): void {\n map.set(fsPath, {\n route: node.urlPath || '/',\n segmentPath: treePath || '/',\n });\n\n for (const child of node.children) {\n const childDir = join(fsPath, child.segmentName);\n const childTreePath = `${treePath}/${child.segmentName}`;\n walk(child, childDir, childTreePath);\n }\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n const slotDir = join(fsPath, `@${slotName}`);\n const slotTreePath = `${treePath}/@${slotName}`;\n walk(slotNode, slotDir, slotTreePath);\n }\n }\n\n walk(tree.root, appDir, '');\n return map;\n}\n\n/**\n * Generate the full $segment.d.ts content for a segment.\n */\nexport function generateSegmentDts(info: SegmentInfo): string {\n return [\n `export const ROUTE: '${info.route}';`,\n `export const SEGMENT_PATH: '${info.segmentPath}';`,\n '',\n ].join('\\n');\n}\n\n/**\n * Generate per-route $segment.d.ts files in .timber/types/ mirroring\n * the app/ directory structure. These files are importable via TypeScript's\n * rootDirs feature.\n */\nexport async function writeSegmentTypes(\n routeTree: RouteTree,\n appDir: string,\n root: string\n): Promise<void> {\n // Compute the relative prefix from project root to appDir.\n // e.g. if appDir is /project/src/app, relative is 'src/app'\n const relative = appDir.slice(root.length + 1);\n const typesBase = join(root, '.timber', 'types', relative);\n const map = buildDirToRouteMap(routeTree, appDir);\n\n const writes: Promise<void>[] = [];\n for (const [fsDir, info] of map) {\n // Mirror the app/ structure under .timber/types/\n const relDir = fsDir.slice(appDir.length); // e.g. '/[artistSlug]/[year]'\n const segmentDir = join(typesBase, relDir);\n const segmentFile = join(segmentDir, '$segment.d.ts');\n const content = generateSegmentDts(info);\n\n writes.push(\n mkdir(segmentDir, { recursive: true }).then(() => writeFile(segmentFile, content, 'utf-8'))\n );\n }\n\n await Promise.all(writes);\n}\n","/**\n * Route-map codegen writer.\n *\n * Materializes the generated route types on disk: `.timber/timber-routes.d.ts`,\n * `.timber/segment-types.d.ts` (via `writeSegmentTypes`), and the root\n * `timber-env.d.ts` that references them.\n *\n * Lives outside `plugins/routing.ts` because it has two callers with two\n * different failure policies: the dev/build plugin starts the write off the hot\n * path and logs failures, while `timber check` awaits it and treats a failure as\n * fatal — a type check run against absent or stale route types reports errors\n * about code that is fine. One implementation, two policies, so the bytes on\n * disk cannot drift between the two commands.\n *\n * Design docs: 09-typescript.md §\"Typed Routes\", 18-build-system.md\n */\n\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { generateRouteMap } from './codegen.ts';\nimport { writeSegmentTypes } from './segment-codegen.ts';\nimport type { RouteTree } from './types.ts';\n\n/**\n * Path (relative to the project root) of the generated route map declaration\n * file, and the name of the root file that `/// <reference>`s it.\n *\n * Exported because `timber check` has to answer whether the user's tsconfig\n * pulls these into the program — a question about these exact paths. It derives\n * them from here rather than restating them, so a rename cannot leave the check\n * looking for a file nothing writes.\n */\nexport const CODEGEN_OUTPUT = '.timber/timber-routes.d.ts';\n\n/** Root-level file that references the generated declarations. */\nexport const ENV_DTS_FILENAME = 'timber-env.d.ts';\n\n/**\n * Content of the timber-env.d.ts file written to the project root.\n *\n * Uses a triple-slash reference so TypeScript auto-discovers the generated\n * route map without the user needing to edit tsconfig.json — same pattern\n * as Next.js's next-env.d.ts.\n *\n * Also references `vite/client` so side-effect imports like\n * `import './globals.css'` type-check without ts(2882), and so\n * `import.meta.env`, `import.meta.hot`, `import.meta.glob`, asset\n * imports (`*.png`, `*.svg`, …), CSS modules, and `?url`/`?raw`/\n * `?inline` query variants all resolve without the user editing\n * tsconfig.json themselves. Next.js's next-env.d.ts plays the same\n * role via `next/types/global`.\n *\n * `vite` is a peer dep of `@timber-js/app`, so the reference always\n * resolves in a correctly-installed project.\n */\nconst TIMBER_ENV_DTS = [\n '// This file is auto-generated by timber.js. Do not edit.',\n '// It ensures TypeScript picks up the generated route types and',\n '// Vite-provided ambient types (CSS, assets, import.meta.env, HMR).',\n '/* eslint-disable @typescript-eslint/triple-slash-reference */',\n '/// <reference path=\".timber/timber-routes.d.ts\" />',\n '/// <reference types=\"vite/client\" />',\n '/// <reference types=\"react/canary\" />',\n '/// <reference types=\"react-dom/canary\" />',\n '',\n].join('\\n');\n\n/**\n * Codegen writes that have been started but have not yet landed on disk.\n *\n * `writeRouteCodegen` is deliberately unawaited on the dev/build hot path,\n * which leaves that caller no way to know the files exist yet. Anything that\n * tears down the project root — a build process exiting, a test removing its\n * temp root — then races the writes: `rmSync` empties a directory that a\n * pending `mkdir` immediately recreates, and the removal fails with ENOTEMPTY.\n *\n * Every started write registers here and deregisters when it settles, so\n * `flushCodegenWrites()` can await them instead of guessing at a delay.\n */\nconst pendingCodegenWrites = new Set<Promise<void>>();\n\n/**\n * Resolve once every codegen write started so far has landed on disk.\n *\n * Safe to call at any time and from anywhere — writes are tracked per process,\n * not per plugin instance, so a caller does not need a handle on the plugin\n * that started them. Never rejects: the tracked copy of each write has its\n * rejection swallowed, so a failing write cannot turn a teardown barrier into\n * an unhandled rejection. Callers that care about failure await the promise\n * `writeRouteCodegen` returned instead.\n */\nexport async function flushCodegenWrites(): Promise<void> {\n // Loop rather than a single Promise.all: a write started while we were\n // awaiting would otherwise be missed, and the set would still be non-empty\n // on return.\n //\n // Each round drops what it awaited rather than trusting the per-write\n // `finally` to have run, so the two removal paths are independent — a\n // deregistration that stopped firing would leak memory in dev, but it can\n // never turn this into a spin.\n while (pendingCodegenWrites.size > 0) {\n const inFlight = [...pendingCodegenWrites];\n await Promise.all(inFlight);\n for (const write of inFlight) pendingCodegenWrites.delete(write);\n }\n}\n\n/**\n * Write the generated route map, segment types, and the timber-env.d.ts\n * reference file for `tree`.\n *\n * Returns a promise that **rejects** if any write fails. The dev/build plugin\n * does not await it (types are a developer convenience there, not a runtime\n * dependency) and logs instead; `timber check` awaits it and fails the command.\n * Either way the write is registered with `flushCodegenWrites()`.\n */\nexport function writeRouteCodegen(tree: RouteTree, appDir: string, root: string): Promise<void> {\n const timberDir = join(root, '.timber');\n const content = generateRouteMap(tree, { appDir, outputDir: timberDir });\n const routesPath = join(root, CODEGEN_OUTPUT);\n const envPath = join(root, ENV_DTS_FILENAME);\n\n const done = mkdir(timberDir, { recursive: true })\n .then(() =>\n Promise.all([\n writeFile(routesPath, content, 'utf-8'),\n writeFile(envPath, TIMBER_ENV_DTS, 'utf-8'),\n writeSegmentTypes(tree, appDir, root),\n ])\n )\n .then(() => undefined);\n\n // The tracked copy swallows rejection so `flushCodegenWrites` — a teardown\n // barrier — never rejects and never leaves an unhandled rejection behind.\n // The returned promise keeps its rejection for callers that want it.\n const tracked = done.catch(() => undefined);\n pendingCodegenWrites.add(tracked);\n void tracked.finally(() => pendingCodegenWrites.delete(tracked));\n\n return done;\n}\n","/**\n * AST-based export detection for route files.\n *\n * Uses Vite's `parseAst` (backed by oxc) to precisely detect named and\n * default exports, replacing the regex-based approach that was fragile\n * with TypeScript syntax (type exports, `as` aliases, comments, etc.).\n */\n\nimport { existsSync } from 'node:fs';\nimport { parseAst } from 'vite';\nimport { readFileCached } from './file-cache.ts';\n\ninterface AstNode {\n type: string;\n exportKind?: string;\n declaration?: AstNode;\n specifiers?: Array<{\n exported?: { name: string };\n local?: { name: string };\n exportKind?: string;\n }>;\n id?: { name: string };\n declarations?: Array<{ id?: { name: string }; init?: AstNode }>;\n /** Set on re-exports: `export { X } from './y'`. Absent on local exports. */\n source?: { value?: string } | null;\n /** Set on runtime-transparent wrappers: `X as T`, `X satisfies T`, `(X)`, `X!`. */\n expression?: AstNode;\n /** Set on `Identifier` nodes. */\n name?: string;\n}\n\ninterface ProgramNode {\n body: AstNode[];\n}\n\nfunction tryParse(source: string): ProgramNode | null {\n try {\n return parseAst(source, { lang: 'tsx' }) as unknown as ProgramNode;\n } catch {\n return null;\n }\n}\n\n/**\n * Collect all named export identifiers from a parsed program.\n * Handles: `export function X`, `export const X`, `export { X }`,\n * `export { X } from '...'`, `export { Y as X }`.\n */\nfunction collectNamedExports(program: ProgramNode): Set<string> {\n const names = new Set<string>();\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n // Skip `export type ...` declarations — they're erased at runtime\n if (stmt.exportKind === 'type') continue;\n\n if (stmt.declaration) {\n if (stmt.declaration.id?.name) {\n names.add(stmt.declaration.id.name);\n }\n if (stmt.declaration.declarations) {\n for (const decl of stmt.declaration.declarations) {\n if (decl.id?.name) names.add(decl.id.name);\n }\n }\n }\n\n if (stmt.specifiers) {\n for (const spec of stmt.specifiers) {\n // Skip `export { type X }` — per-specifier type exports\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name && spec.exported.name !== 'default') {\n names.add(spec.exported.name);\n }\n }\n }\n }\n return names;\n}\n\n/**\n * The specifier exporting a *runtime* default from this statement, if any.\n *\n * Type-only exports erase, so they are not default exports at runtime — and\n * TypeScript spells that two ways, on two different nodes: `export type { X as\n * default }` marks the **statement**, `export { type X as default }` marks the\n * **specifier**. Both have to be checked, or a type alias reads as a component:\n * a status file whose only default is a type has nothing for the error boundary\n * to receive (the renderer's `mod?.default` skips it), so treating it as one\n * would fail a build over a file that cannot break anything.\n *\n * One implementation because two callers must agree on what \"has a default\n * export\" means: `hasDefaultExport` decides whether to warn that a page has\n * none, and `fileDefaultExportIsLocalValue` decides whether to fail the build.\n */\nfunction defaultSpecifier(stmt: AstNode): NonNullable<AstNode['specifiers']>[number] | null {\n if (stmt.type !== 'ExportNamedDeclaration' || !stmt.specifiers) return null;\n if (stmt.exportKind === 'type') return null;\n for (const spec of stmt.specifiers) {\n if (spec.exportKind === 'type') continue;\n if (spec.exported?.name === 'default') return spec;\n }\n return null;\n}\n\n/**\n * Check whether a program has a default export.\n * Handles: `export default ...`, `export { X as default }`,\n * `export { default } from '...'`. Type-only defaults do not count —\n * see `defaultSpecifier`.\n */\nfunction hasDefaultExport(program: ProgramNode): boolean {\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') return true;\n if (defaultSpecifier(stmt)) return true;\n }\n return false;\n}\n\n/**\n * Check whether a program has any bare `export * from '...'` declarations.\n * Excludes `export * as X from '...'` (namespace re-exports) since those\n * create a namespace object, not individual top-level exports.\n */\nfunction hasStarExport(program: ProgramNode): boolean {\n return program.body.some(\n (stmt) =>\n stmt.type === 'ExportAllDeclaration' && !(stmt as any).exported && stmt.exportKind !== 'type'\n );\n}\n\n/**\n * Check if a file exports a specific named export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasExport(filePath: string, exportName: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return collectNamedExports(program).has(exportName);\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any of the given named exports.\n * Returns the set of matching export names.\n */\nexport function fileHasAnyExport(filePath: string, exportNames: readonly string[]): Set<string> {\n const matches = new Set<string>();\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return matches;\n const exports = collectNamedExports(program);\n for (const name of exportNames) {\n if (exports.has(name)) matches.add(name);\n }\n } catch {\n // Graceful fallback: return empty set\n }\n return matches;\n}\n\n/**\n * Check if a file has a default export.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasDefaultExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasDefaultExport(program);\n } catch {\n return false;\n }\n}\n\n/** Node types whose value is unambiguously constructed by this module. */\nconst FUNCTION_LIKE = new Set([\n 'FunctionDeclaration',\n 'FunctionExpression',\n 'ArrowFunctionExpression',\n 'ClassDeclaration',\n 'ClassExpression',\n]);\n\n/**\n * Expression wrappers that erase at runtime and so cannot change where a value\n * was constructed: `X as T`, `X satisfies T`, `<T>X`, `X!`, `X<T>`, `(X)`.\n */\nconst TRANSPARENT_WRAPPERS = new Set([\n 'TSAsExpression',\n 'TSSatisfiesExpression',\n 'TSTypeAssertion',\n 'TSNonNullExpression',\n 'TSInstantiationExpression',\n 'ParenthesizedExpression',\n]);\n\n/**\n * Strip runtime-transparent wrappers so the node underneath can be classified.\n *\n * Without this, `(() => <h1 />) satisfies React.FC` reads as a\n * `TSSatisfiesExpression` rather than the arrow function it compiles to, and a\n * provably-local component is misread as one whose origin cannot be\n * determined — turning a build error into a warning.\n */\nfunction unwrapTransparent(node: AstNode): AstNode {\n let current = node;\n while (TRANSPARENT_WRAPPERS.has(current.type) && current.expression) {\n current = current.expression;\n }\n return current;\n}\n\n/**\n * Whether `name` is bound at module top level to a function or class written\n * in this file. An imported binding can never match: imports are\n * `ImportDeclaration` specifiers, not function/class/variable declarations,\n * and a module-level name cannot be both.\n */\nfunction bindsLocalFunction(program: ProgramNode, name: string): boolean {\n for (const stmt of program.body) {\n // `export function X() {}` binds X locally too — unwrap the export.\n const decl = stmt.type === 'ExportNamedDeclaration' ? stmt.declaration : stmt;\n if (!decl) continue;\n if (\n (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&\n decl.id?.name === name\n ) {\n return true;\n }\n if (decl.type === 'VariableDeclaration') {\n for (const d of decl.declarations ?? []) {\n if (d.id?.name === name && d.init && FUNCTION_LIKE.has(unwrapTransparent(d.init).type)) {\n return true;\n }\n }\n }\n }\n return false;\n}\n\n/**\n * Whether the default export is *provably* a value constructed by this module:\n * `export default function/class/() => {}`, or a name bound to a function or\n * class declared here — whether it leaves via `export default Name` or\n * `export { Name as default }`. Both spellings are the same fact about where\n * the value was built, so both must answer the same way.\n *\n * This is the sound half of \"is this a server component?\". A file with no\n * `'use client'` directive whose default export is built here is a server\n * component, full stop. Everything else — `export { default } from\n * './client-thing'`, `export default Imported`, `export default memo(X)`,\n * an unparseable file — returns false, because the exported value may have\n * been minted in a module that *does* carry the directive. Callers must not\n * infer \"server component\" from a missing directive alone.\n */\nexport function fileDefaultExportIsLocalValue(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type === 'ExportDefaultDeclaration') {\n const decl = stmt.declaration && unwrapTransparent(stmt.declaration);\n if (!decl) return false;\n if (FUNCTION_LIKE.has(decl.type)) return true;\n if (decl.type === 'Identifier' && decl.name) {\n return bindsLocalFunction(program, decl.name);\n }\n return false;\n }\n\n // `export { Name as default }` — the same default export by another\n // spelling. `hasDefaultExport` already counts it, so an analysis that\n // skipped it here would let the exact defect this powers slip through\n // as a warning.\n const spec = defaultSpecifier(stmt);\n if (spec) {\n // `export { X as default } from './client-thing'` re-exports another\n // module's value — the directive governing it lives over there.\n if (stmt.source) return false;\n return spec.local?.name ? bindsLocalFunction(program, spec.local.name) : false;\n }\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if a file has any `export * from '...'` declarations.\n * Returns false if the file doesn't exist or can't be parsed.\n */\nexport function fileHasStarExport(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n return hasStarExport(program);\n } catch {\n return false;\n }\n}\n\n/**\n * Parsed value of `export const prerender` from a route file.\n *\n * - `true` / `false` — literal boolean\n * - `{ ttl?, tags? }` — ISR options object (prerender: true is implied)\n * - `undefined` — no `export const prerender` found\n *\n * See design/45-cache-lifetimes.md §\"Route-Level Static\".\n */\nexport type PrerenderExportValue = boolean | { ttl?: number; tags?: string[] };\n\nfunction extractLiteralValue(node: AstNode): unknown {\n if (!node) return undefined;\n const n = node as any;\n if (n.type === 'Literal') return n.value;\n if (n.type === 'UnaryExpression' && n.operator === '-' && n.argument?.type === 'Literal') {\n return -(n.argument.value as number);\n }\n if (n.type === 'ArrayExpression') {\n const elements = n.elements as AstNode[] | undefined;\n if (!elements) return undefined;\n const result: unknown[] = [];\n for (const el of elements) {\n const v = extractLiteralValue(el);\n if (v === undefined) return undefined;\n result.push(v);\n }\n return result;\n }\n return undefined;\n}\n\nfunction extractObjectLiteral(node: AstNode): Record<string, unknown> | undefined {\n if ((node as any).type !== 'ObjectExpression') return undefined;\n const props = (node as any).properties as AstNode[] | undefined;\n if (!props) return undefined;\n const result: Record<string, unknown> = {};\n for (const prop of props) {\n const p = prop as any;\n if (p.type === 'SpreadElement') return undefined;\n if (p.type !== 'Property' || p.computed) return undefined;\n const key =\n p.key?.type === 'Identifier'\n ? p.key.name\n : p.key?.type === 'Literal'\n ? String(p.key.value)\n : undefined;\n if (!key) return undefined;\n const value = extractLiteralValue(p.value);\n if (value === undefined) return undefined;\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Extract the value of `export const prerender` from a file.\n * Returns undefined if the file doesn't export `prerender` or can't be parsed.\n *\n * Supports:\n * export const prerender = true;\n * export const prerender = false;\n * export const prerender = { ttl: 3600, tags: ['docs'] };\n */\nexport function getPrerenderExport(filePath: string): PrerenderExportValue | undefined {\n if (!existsSync(filePath)) return undefined;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return undefined;\n\n for (const stmt of program.body) {\n if (stmt.type !== 'ExportNamedDeclaration') continue;\n if (stmt.exportKind === 'type') continue;\n if (!stmt.declaration) continue;\n const decl = stmt.declaration;\n if (decl.type !== 'VariableDeclaration') continue;\n const declarations = decl.declarations as\n | Array<{\n id?: { name?: string };\n init?: AstNode;\n }>\n | undefined;\n if (!declarations) continue;\n for (const d of declarations) {\n if (d.id?.name !== 'prerender' || !d.init) continue;\n const init = d.init as any;\n if (init.type === 'Literal' && typeof init.value === 'boolean') {\n return init.value;\n }\n if (init.type === 'ObjectExpression') {\n const obj = extractObjectLiteral(init);\n if (!obj) return undefined;\n const result: { ttl?: number; tags?: string[] } = {};\n if ('ttl' in obj && typeof obj.ttl === 'number') result.ttl = obj.ttl;\n if (\n 'tags' in obj &&\n Array.isArray(obj.tags) &&\n obj.tags.every((t: unknown) => typeof t === 'string')\n ) {\n result.tags = obj.tags as string[];\n }\n return result;\n }\n return undefined;\n }\n }\n return undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check if a file starts with a specific directive (e.g. \"use client\").\n * Directives are string literal expression statements at the top of the file.\n */\nexport function fileHasDirective(filePath: string, directive: string): boolean {\n if (!existsSync(filePath)) return false;\n try {\n const source = readFileCached(filePath);\n const program = tryParse(source);\n if (!program) return false;\n for (const stmt of program.body) {\n if (stmt.type !== 'ExpressionStatement') break;\n const expr = (stmt as any).expression;\n if (expr?.type !== 'Literal' || typeof expr.value !== 'string') break;\n if (expr.value === directive) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n","/**\n * Convention linter — validates common misconfigurations in the route tree.\n *\n * Runs at scan time (build and dev startup). Each check produces a warning\n * with the file path, what's wrong, and what to do about it.\n *\n * Most checks are warnings — they don't block the build. The goal is to catch\n * issues that would otherwise produce cryptic runtime behavior (silent 404s,\n * empty pages, confusing React errors).\n *\n * A check emits `level: 'error'` only when the misconfiguration is *proven*\n * (never inferred from an absence the analysis cannot see through) and fatal\n * to rendering. `plugins/routing.ts` turns those into a thrown build error,\n * so a false positive there refuses a working app — see\n * `classifyClientBoundaryFile` for how that bar is met.\n *\n * Design doc: 07-routing.md, 10-error-handling.md\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport type { RouteFile, RouteTree, SegmentNode } from './types.ts';\nimport { swallow } from '../server/logger.ts';\nimport { isMdxFilePath } from '../server/utils/mdx-file.ts';\nimport {\n fileDefaultExportIsLocalValue,\n fileHasAnyExport,\n fileHasDefaultExport,\n fileHasDirective,\n fileHasStarExport,\n} from './export-detect.ts';\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface ConventionWarning {\n /** Warning ID for deduplication and filtering. */\n id: string;\n /** Human-readable single-line summary. */\n summary: string;\n /** Multi-line details with file path and fix suggestion. */\n details: string;\n /** Severity: 'warn' for potential issues, 'error' for definite misconfigurations. */\n level: 'warn' | 'error';\n}\n\n// ─── Lint Rules ─────────────────────────────────────────────────────────────\n\n/**\n * Run all convention lint checks on a route tree.\n *\n * Returns an array of warnings. Empty array means everything looks good.\n */\nexport function lintConventions(tree: RouteTree, appDir: string): ConventionWarning[] {\n const warnings: ConventionWarning[] = [];\n\n // Check 1: app/ directory exists (caller should check before scanning,\n // but we validate the root has at least one routable file)\n checkEmptyApp(tree.root, appDir, warnings);\n\n // Check 2: route.ts without recognized HTTP method exports\n checkRouteExports(tree.root, warnings);\n\n // Check 3: Segments with layout but no page/route anywhere in subtree\n // (not a misconfiguration per se — layouts without pages are valid for\n // route groups. Skip this check.)\n\n // Check 4: Root segment has no layout.tsx (no HTML shell)\n checkRootLayout(tree.root, warnings);\n\n // Check 5: page.tsx / layout.tsx without default export\n checkDefaultExports(tree.root, warnings);\n\n // Check 6: Client layouts cannot suppress server children before Flight\n // serializes them. They remain valid UI boundaries, but not authorization\n // or data-hiding boundaries.\n checkClientLayouts(tree.root, warnings);\n\n // Check 7: error.tsx / 404.tsx / 4xx.tsx / … without 'use client' directive\n checkClientBoundaryFiles(tree.root, warnings);\n if (tree.globalError) {\n checkFileUseClientDirective(tree.globalError, 'global-error', warnings);\n }\n\n // Check 8: Wildcard MDX status files (4xx.mdx, 5xx.mdx, error.mdx) that\n // reference `props` — props resolve on deny and throw paths but are empty\n // on the boundary path. See design/10-error-handling.md §\"MDX Props by Path\".\n checkWildcardMdxProps(tree.root, warnings);\n\n return warnings;\n}\n\nfunction checkClientLayouts(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.layout && isScriptExtension(node.layout.extension)) {\n try {\n const isClient = fileHasDirective(node.layout.filePath, 'use client');\n const isProvablyServer = fileDefaultExportIsLocalValue(node.layout.filePath);\n if (isClient || !isProvablyServer) {\n const certainty = isClient\n ? `This layout is a client component.`\n : `Timber cannot prove this layout is a server component because its default export comes from another module or an opaque expression.`;\n warnings.push({\n id: `CLIENT_LAYOUT_NOT_AUTH_BOUNDARY:${node.layout.filePath}`,\n summary: isClient\n ? `Client layout cannot securely suppress server children: ${node.layout.filePath}`\n : `Layout may resolve to a client component and cannot be trusted as an auth boundary: ${node.layout.filePath}`,\n details:\n ` File: ${node.layout.filePath}\\n\\n` +\n ` ${certainty}\\n` +\n ` React Flight resolves server content passed to a client layout before the layout runs in the browser.\\n` +\n ` Conditionally omitting children here does not prevent descendant server components from executing or being serialized.\\n\\n` +\n ` To fix: Put authorization in access.ts or a provably server layout with a locally defined default component, and keep client components inside that boundary.\\n`,\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable layout file ${node.layout.filePath}`);\n }\n }\n for (const child of node.children) checkClientLayouts(child, warnings);\n for (const slot of Object.values(node.slots)) checkClientLayouts(slot, warnings);\n}\n\n// ─── Check: Empty App ───────────────────────────────────────────────────────\n\n/**\n * Warn when the app/ directory has no routable files at all.\n *\n * This catches the \"I created app/ but didn't add any pages\" case where\n * every request silently 404s with no guidance.\n */\nfunction checkEmptyApp(root: SegmentNode, appDir: string, warnings: ConventionWarning[]): void {\n if (hasAnyRoutable(root)) return;\n\n warnings.push({\n id: 'EMPTY_APP',\n summary: 'No pages or route handlers found in app/',\n details:\n ` Directory: ${appDir}\\n\\n` +\n ' Your app/ directory has no page.tsx or route.ts files.\\n' +\n ' Every request will return 404.\\n\\n' +\n ' To fix: Create app/page.tsx with a default export:\\n\\n' +\n ' export default function Home() {\\n return <h1>Hello</h1>;\\n }\\n',\n level: 'warn',\n });\n}\n\n/**\n * Check if a segment tree has any routable files (page or route) anywhere.\n */\nfunction hasAnyRoutable(node: SegmentNode): boolean {\n if (node.page || node.route) return true;\n for (const child of node.children) {\n if (hasAnyRoutable(child)) return true;\n }\n for (const slot of Object.values(node.slots)) {\n if (hasAnyRoutable(slot)) return true;\n }\n return false;\n}\n\n/**\n * Check if a segment tree has any page files (not just route handlers).\n * Used by checkRootLayout to avoid false positives for API-only apps (TIM-794).\n * API-only apps (just route.ts handlers) don't need a root layout.\n */\nfunction hasAnyPage(node: SegmentNode): boolean {\n if (node.page) return true;\n for (const child of node.children) {\n if (hasAnyPage(child)) return true;\n }\n for (const slot of Object.values(node.slots)) {\n if (hasAnyPage(slot)) return true;\n }\n return false;\n}\n\n// ─── Check: Route Exports ───────────────────────────────────────────────────\n\n/** HTTP methods that route.ts can export. */\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const;\n\n/**\n * Warn when a route.ts file doesn't appear to export any recognized HTTP methods.\n *\n * Uses AST-based detection via parseAst for precise matching.\n * Falls back silently when the file can't be parsed (syntax errors produce\n * no spurious warning — the TS compiler will catch those).\n */\nfunction checkRouteExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.route) {\n const filePath = node.route.filePath;\n try {\n const methods = fileHasAnyExport(filePath, HTTP_METHODS);\n if (methods.size === 0 && !fileHasStarExport(filePath)) {\n const hasDefault = fileHasDefaultExport(filePath);\n const hint = hasDefault\n ? ' It looks like you have a default export. route.ts uses named exports\\n' +\n ' for HTTP methods (GET, POST, etc.), not a default export.\\n'\n : '';\n\n warnings.push({\n id: 'ROUTE_NO_METHODS',\n summary: `route.ts has no HTTP method exports: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ' route.ts files must export named HTTP method handlers.\\n' +\n ' Without them, the route matches but returns 405 Method Not Allowed.\\n\\n' +\n hint +\n ' Example:\\n\\n' +\n ' export function GET(ctx: RouteContext) {\\n' +\n \" return Response.json({ hello: 'world' });\\n\" +\n ' }\\n',\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable route.ts ${filePath}`);\n }\n }\n\n // Recurse\n for (const child of node.children) {\n checkRouteExports(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkRouteExports(slot, warnings);\n }\n}\n\n// ─── Check: Root Layout ─────────────────────────────────────────────────────\n\n/**\n * Warn when the root segment has no layout.tsx.\n *\n * Without a root layout, there's no HTML shell (<html>, <body>).\n * The page will render but may have hydration issues or no proper document structure.\n */\nfunction checkRootLayout(root: SegmentNode, warnings: ConventionWarning[]): void {\n if (root.layout) return;\n\n // Only warn if there are pages — API-only apps (just route.ts handlers)\n // don't need a root layout (TIM-794).\n if (!hasAnyPage(root)) return;\n\n warnings.push({\n id: 'NO_ROOT_LAYOUT',\n summary: 'No root layout.tsx found',\n details:\n ' Your app has pages but no root layout.\\n' +\n ' Without app/layout.tsx, pages have no <html> or <body> wrapper.\\n\\n' +\n ' To fix: Create app/layout.tsx:\\n\\n' +\n ' export default function RootLayout({ children }: { children: React.ReactNode }) {\\n' +\n ' return (\\n' +\n ' <html lang=\"en\">\\n' +\n ' <body>{children}</body>\\n' +\n ' </html>\\n' +\n ' );\\n' +\n ' }\\n',\n level: 'warn',\n });\n}\n\n// ─── Check: Default Exports ───────────────────────────────────────────\n\n/**\n * Warn when page.tsx or layout.tsx files don't have a default export.\n *\n * Without a default export:\n * - page.tsx: the page renders nothing (empty content)\n * - layout.tsx: the layout module loads but has no component to render\n *\n * Uses AST-based detection via parseAst for precise matching. Falls back\n * silently when the file can't be parsed (no spurious warning on syntax errors).\n */\nfunction checkDefaultExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n // Check page files (tsx/ts/jsx/js only — not mdx, which is default-exported by the mdx compiler)\n if (node.page && isScriptExtension(node.page.extension)) {\n checkFileDefaultExport(node.page.filePath, 'page', warnings);\n }\n\n // Check layout files\n if (node.layout && isScriptExtension(node.layout.extension)) {\n checkFileDefaultExport(node.layout.filePath, 'layout', warnings);\n }\n\n // Recurse\n for (const child of node.children) {\n checkDefaultExports(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkDefaultExports(slot, warnings);\n }\n}\n\nfunction isScriptExtension(ext: string): boolean {\n return ext === 'tsx' || ext === 'ts' || ext === 'jsx' || ext === 'js';\n}\n\nfunction checkFileDefaultExport(\n filePath: string,\n fileType: string,\n warnings: ConventionWarning[]\n): void {\n try {\n if (!fileHasDefaultExport(filePath)) {\n warnings.push({\n id: `NO_DEFAULT_EXPORT:${filePath}`,\n summary: `${fileType}.tsx has no default export: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` ${fileType}.tsx files must export a default React component.\\n` +\n ` Without a default export, the ${fileType === 'page' ? 'page renders nothing' : 'layout has no component to wrap children'}.\\n\\n` +\n ` To fix: Add a default export:\\n\\n` +\n ` export default function My${fileType === 'page' ? 'Page' : 'Layout'}(${fileType === 'layout' ? '{ children }' : ''}) {\\n` +\n ` return ${fileType === 'layout' ? '<div>{children}</div>' : '<h1>Hello</h1>'};\\n` +\n ` }\\n`,\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);\n }\n}\n\n// ─── Check: Client Boundary Directive ───────────────────────────────────────\n\n/**\n * Check every file the renderer hands to `TimberErrorBoundary` as a component\n * prop: `error.tsx`, `global-error.tsx`, and the status-code family\n * (`404.tsx`, `4xx.tsx`, `503.tsx`, …).\n *\n * `TimberErrorBoundary` is a client component, so a server component passed as\n * `fallbackComponent` is a function crossing the RSC → client boundary. React\n * Flight refuses to serialize it, and because the boundary wraps the segment's\n * *children*, the failure hits every page under that segment — not just the\n * error path. The raw message (\"Functions cannot be passed directly to Client\n * Components\") names neither the file nor the fix (TIM-1329).\n *\n * MDX status files are exempt: they are server components by design and the\n * renderer pre-renders them into `fallbackElement` instead. See TIM-503 and\n * `server/error-boundary-wrapper.ts`.\n *\n * `.json` status files never reach React at all, and are not in `statusFiles`.\n */\nfunction checkClientBoundaryFiles(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.error) {\n checkFileUseClientDirective(node.error, 'error', warnings);\n }\n\n for (const [key, file] of Object.entries(node.statusFiles ?? {})) {\n checkFileUseClientDirective(file, key, warnings);\n }\n\n for (const child of node.children) {\n checkClientBoundaryFiles(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkClientBoundaryFiles(slot, warnings);\n }\n}\n\n/**\n * `'client'` — carries the directive.\n * `'server'` — proven server component: no directive, and the default export is\n * a function or class built by this very module, so no other file's directive\n * can be governing it.\n * `'unknown'` — no directive, but the exported value may come from elsewhere\n * (`export { default } from './client-thing'`), or the file could not be\n * read or parsed. Never escalated to a build error.\n */\ntype ClientBoundaryVerdict = 'client' | 'server' | 'unknown';\n\nfunction classifyClientBoundaryFile(filePath: string): ClientBoundaryVerdict {\n if (fileHasDirective(filePath, 'use client')) return 'client';\n if (fileDefaultExportIsLocalValue(filePath)) return 'server';\n return 'unknown';\n}\n\n/** Example component name for the fix snippet, keyed by convention name. */\nfunction exampleComponentName(convention: string): string {\n if (convention === 'global-error') return 'GlobalError';\n if (convention === 'error') return 'ErrorBoundary';\n if (convention === '404') return 'NotFound';\n return 'StatusPage';\n}\n\n/**\n * @param file the route file (its extension decides whether the check applies)\n * @param convention the convention name — `error`, `global-error`, `404`, `4xx`, …\n */\nfunction checkFileUseClientDirective(\n file: RouteFile,\n convention: string,\n warnings: ConventionWarning[]\n): void {\n // MDX/markdown status and error files are server components by design — the\n // renderer pre-renders them into `fallbackElement`, so they never cross the\n // boundary as a function.\n //\n // The exemption is the *complement* of the renderer's own test, using the\n // renderer's own helper: everything `error-boundary-wrapper.ts` and\n // `error-renderer.ts` do not treat as MDX, they pass as a component. An\n // independent \"is this a script extension?\" list here would be a second\n // expression of that fact, free to drift into a gap where a file is a\n // component to the renderer and exempt to the linter.\n if (isMdxFilePath(file.filePath)) return;\n\n const filePath = file.filePath;\n const label = `${convention}.${file.extension}`;\n let verdict: ClientBoundaryVerdict;\n try {\n verdict = classifyClientBoundaryFile(filePath);\n } catch (err) {\n swallow(err, `convention-lint: unreadable ${label} file ${filePath}`);\n return;\n }\n if (verdict === 'client') return;\n\n const isError = verdict === 'server';\n const name = exampleComponentName(convention);\n const props =\n convention === 'error' || convention === 'global-error' ? '{ error, reset }' : '{ status }';\n\n warnings.push({\n id: `NEEDS_USE_CLIENT:${filePath}`,\n summary: isError\n ? `${label} is a server component and must be a client component: ${filePath}`\n : `${label} may be missing 'use client': ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` ${label} is handed to timber's error boundary — a client\\n` +\n ' component — as a prop. A server component cannot cross that boundary:\\n' +\n ' React fails with \"Functions cannot be passed directly to Client\\n' +\n ' Components\", and every page under this segment fails to render.\\n\\n' +\n (isError\n ? \" To fix: Add 'use client' as the first line of the file:\\n\\n\" +\n \" 'use client';\\n\\n\" +\n ` export default function ${name}(${props}) {\\n` +\n ' return <h1>…</h1>;\\n' +\n ' }\\n'\n : // The default export comes from somewhere this analysis cannot follow\n // — a re-export, an imported binding, a wrapper call. It may well\n // already be a client component, so say what was checked rather than\n // assert a break.\n \" This file has no 'use client' directive, and its default export\\n\" +\n ' comes from elsewhere, so timber cannot tell which it is. If the\\n' +\n \" module it re-exports starts with 'use client', nothing is wrong\\n\" +\n ' and you can ignore this. Otherwise add the directive there.\\n'),\n level: isError ? 'error' : 'warn',\n });\n}\n\n// ─── Check: Wildcard MDX Props ──────────────────────────────────────────────\n\n/**\n * Wildcard status file keys in `statusFiles` whose props differ by path.\n * `error` is not here — the scanner stores it in `node.error`, not\n * `node.statusFiles`, and the check handles it separately below.\n */\nconst WILDCARD_MDX_STATUS_KEYS = new Set(['4xx', '5xx']);\n\n/**\n * Warn when a wildcard MDX status file (`4xx.mdx`, `5xx.mdx`, `error.mdx`)\n * appears to reference `props` in its source.\n *\n * These files are pre-rendered into `fallbackElement` on the error-boundary\n * path, before the error happens, so they receive empty or partial props.\n * On the deny and error-renderer paths they receive the real props. An author\n * writing `{props.status}` gets the number on some paths and `undefined` on\n * others — same source, different output.\n *\n * A text scan for `props` is acceptable here because the check is `level:\n * 'warn'` — a false positive costs a printed line, not a refused build. A\n * sound check would need the MDX compiler, which is optional, async, and\n * scoped to the RSC environment. See TIM-1333.\n *\n * Exact-code MDX files (e.g. `503.mdx`) are unaffected — all four paths\n * agree on their status.\n */\nfunction checkWildcardMdxProps(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.statusFiles) {\n for (const [key, file] of Object.entries(node.statusFiles)) {\n if (WILDCARD_MDX_STATUS_KEYS.has(key) && isMdxFilePath(file.filePath)) {\n checkMdxFileForProps(file.filePath, key, warnings);\n }\n }\n }\n if (node.error && isMdxFilePath(node.error.filePath)) {\n checkMdxFileForProps(node.error.filePath, 'error', warnings);\n }\n\n for (const child of node.children) {\n checkWildcardMdxProps(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkWildcardMdxProps(slot, warnings);\n }\n}\n\n/**\n * Check if an MDX file's source appears to reference `props`.\n *\n * MDX passes a `props` object to the default export function. A reference to\n * `props.status` or `props.dangerouslyPassData` in a wildcard file works on\n * the deny/throw paths but renders `undefined` on the boundary path.\n */\nfunction checkMdxFileForProps(\n filePath: string,\n convention: string,\n warnings: ConventionWarning[]\n): void {\n let source: string;\n try {\n source = readFileSync(filePath, 'utf-8');\n } catch {\n return;\n }\n\n // MDX exposes props via the default export: export default function(props) { ... }\n // and via JSX expressions: {props.status}. A text scan is intentionally\n // broad — false positives are cheap at warn level.\n if (!/\\bprops\\b/.test(source)) return;\n\n const label = `${convention}.${filePath.endsWith('.md') ? 'md' : 'mdx'}`;\n warnings.push({\n id: `MDX_WILDCARD_PROPS:${filePath}`,\n summary: `${label} references props, which are empty on the error boundary path: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` Wildcard MDX status files (${convention}.mdx) are pre-rendered into\\n` +\n ' fallbackElement before the error occurs. On that path, props are\\n' +\n ` empty${convention === 'error' ? '' : ' (no status, no dangerouslyPassData)'}. On the deny() and error-renderer\\n` +\n ' paths, props.status resolves correctly.\\n\\n' +\n ' If this file uses only static content and \"props\" appears in prose,\\n' +\n ' this warning is a false positive — ignore it.\\n\\n' +\n ' To fix: Use only static content in wildcard MDX status files, or\\n' +\n ' use an exact-code file (e.g. 404.mdx) where all paths agree.\\n',\n level: 'warn',\n });\n}\n\n// ─── Check: App Directory Exists ────────────────────────────────────────────\n\n/**\n * Check if the app/ directory exists. Called before scanning.\n * Returns a warning if missing, or null if the directory exists.\n */\nexport function checkAppDirExists(appDir: string): ConventionWarning | null {\n if (existsSync(appDir)) return null;\n\n return {\n id: 'NO_APP_DIR',\n summary: 'No app/ directory found',\n details:\n ` Expected: ${appDir}\\n\\n` +\n ' timber.js requires an app/ directory for file-system routing.\\n' +\n ' Every request will return 404 until you create it.\\n\\n' +\n ' To fix: Create the app/ directory with a page:\\n\\n' +\n ' mkdir -p app\\n' +\n ' # Create app/layout.tsx and app/page.tsx\\n',\n level: 'error',\n };\n}\n\n// ─── Formatting ─────────────────────────────────────────────────────────────\n\nimport type { InspectColor } from 'node:util';\nimport { styleText } from 'node:util';\n\ntype Format = InspectColor | readonly InspectColor[];\nconst noValidate = { validateStream: false } as const;\n\nexport interface FormatOptions {\n /**\n * Emit ANSI escapes. Off when the text becomes an `Error` message —\n * whoever prints it decides its formatting, and colored escapes inside\n * an exception leak into logs and stack traces.\n */\n color?: boolean;\n}\n\n/**\n * Format warnings for terminal output.\n *\n * Groups by severity, uses colors, and includes fix suggestions.\n */\nexport function formatConventionWarnings(\n warnings: ConventionWarning[],\n options: FormatOptions = {}\n): string {\n const { color = true } = options;\n const style = (fmt: Format, text: string) => (color ? styleText(fmt, text, noValidate) : text);\n if (warnings.length === 0) return '';\n\n const errors = warnings.filter((w) => w.level === 'error');\n const warns = warnings.filter((w) => w.level === 'warn');\n\n const lines: string[] = [];\n\n if (errors.length > 0) {\n lines.push(\n `${style(['red', 'bold'], '[timber]')} ${style('red', `${errors.length} configuration error${errors.length !== 1 ? 's' : ''}:`)}`\n );\n for (const e of errors) {\n lines.push('');\n lines.push(` ${style('red', '✗')} ${e.summary}`);\n lines.push(style('dim', e.details));\n }\n }\n\n if (warns.length > 0) {\n if (errors.length > 0) lines.push('');\n lines.push(\n `${style(['yellow', 'bold'], '[timber]')} ${style('yellow', `${warns.length} configuration warning${warns.length !== 1 ? 's' : ''}:`)}`\n );\n for (const w of warns) {\n lines.push('');\n lines.push(` ${style('yellow', '⚠')} ${w.summary}`);\n lines.push(style('dim', w.details));\n }\n }\n\n return lines.join('\\n');\n}\n"],"mappings":"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"}