@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
@@ -0,0 +1,2288 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { S as isSsrStreamError, h as swallow } from "./logger-DqJ2VoAY.js";
3
+ import "./csp-nonce-hOGniaG4.js";
4
+ import { t as RSC_CONTENT_TYPE } from "./rsc-media-type-DDc7duTD.js";
5
+ import { l as registerDevDiscovery } from "./graph-cache-Cv3njEH8.js";
6
+ import { n as isScannableSourceFile, t as buildSourceExtensions } from "./scan-BoP17W3w.js";
7
+ import { o as isMetadataRouteServePath } from "./metadata-routes-DSDjM_hJ.js";
8
+ import { r as canonicalize } from "./canonicalize-CgHoscYO.js";
9
+ import { t as matchUrlParts } from "./tree-match-CdbvYTBz.js";
10
+ import { normalizePath } from "vite";
11
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
12
+ import { createRequire } from "node:module";
13
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
14
+ import "node:http";
15
+ import { Readable, pipeline } from "node:stream";
16
+ import { pipeline as pipeline$1 } from "node:stream/promises";
17
+ import "node:https";
18
+ import "node:http2";
19
+ import { pathToFileURL } from "node:url";
20
+ import { styleText } from "node:util";
21
+ import { randomUUID } from "node:crypto";
22
+ import { realpath, stat } from "node:fs/promises";
23
+ import { isIP } from "node:net";
24
+ import { constants, createGzip } from "node:zlib";
25
+ //#region ../../node_modules/.pnpm/srvx@1.0.5/node_modules/srvx/dist/adapters/node.mjs
26
+ function sendNodeResponse(nodeRes, webRes) {
27
+ try {
28
+ return _sendNodeResponse(nodeRes, webRes, false) || Promise.resolve();
29
+ } catch (error) {
30
+ return Promise.reject(error);
31
+ }
32
+ }
33
+ function _sendNodeResponse(nodeRes, webRes, detached) {
34
+ if (!webRes) {
35
+ nodeRes.statusCode = 500;
36
+ return endNodeResponse(nodeRes, detached);
37
+ }
38
+ if (webRes._toNodeResponse) {
39
+ const res = webRes._toNodeResponse();
40
+ if (res.body) {
41
+ if (res.body instanceof ReadableStream) {
42
+ writeHead(nodeRes, res.status, res.statusText, res.headers);
43
+ return streamBody(res.body, nodeRes);
44
+ } else if (typeof res.body?.pipe === "function") return pipeBody(res.body, nodeRes, res.status, res.statusText, res.headers);
45
+ writeHead(nodeRes, res.status, res.statusText, res.headers);
46
+ nodeRes.write(res.body);
47
+ } else writeHead(nodeRes, res.status, res.statusText, res.headers);
48
+ return endNodeResponse(nodeRes, detached);
49
+ }
50
+ const rawHeaders = [];
51
+ for (const [key, value] of webRes.headers) rawHeaders.push(key, value);
52
+ writeHead(nodeRes, webRes.status, webRes.statusText, rawHeaders);
53
+ return webRes.body ? streamBody(webRes.body, nodeRes) : endNodeResponse(nodeRes, detached);
54
+ }
55
+ function writeHead(nodeRes, status, statusText, rawHeaders) {
56
+ if (!nodeRes.headersSent) {
57
+ if (nodeRes.req?.httpVersion === "2.0") nodeRes.writeHead(status, rawHeaders);
58
+ else nodeRes.writeHead(status, safeStatusText(statusText), rawHeaders);
59
+ }
60
+ }
61
+ var INVALID_REASON_PHRASE_RE = /[^\t\u0020-\u007E\u0080-\u00FF]/g;
62
+ function safeStatusText(statusText) {
63
+ return typeof statusText === "string" && statusText ? statusText.replace(INVALID_REASON_PHRASE_RE, "") : statusText;
64
+ }
65
+ function endNodeResponse(nodeRes, detached) {
66
+ if (detached) {
67
+ nodeRes.end();
68
+ return;
69
+ }
70
+ return new Promise((resolve) => nodeRes.end(resolve));
71
+ }
72
+ function pipeBody(stream, nodeRes, status, statusText, headers) {
73
+ if (nodeRes.destroyed) {
74
+ stream.destroy?.();
75
+ return;
76
+ }
77
+ if (nodeRes.req?.method === "HEAD") {
78
+ if (typeof stream.destroy === "function") stream.destroy();
79
+ else stream.abort?.();
80
+ writeHead(nodeRes, status, statusText, headers);
81
+ return endNodeResponse(nodeRes);
82
+ }
83
+ if (typeof stream.on !== "function" || typeof stream.destroy !== "function") {
84
+ writeHead(nodeRes, status, statusText, headers);
85
+ stream.pipe(nodeRes);
86
+ return new Promise((resolve) => nodeRes.on("close", resolve));
87
+ }
88
+ if (stream.destroyed) {
89
+ writeHead(nodeRes, 500, "Internal Server Error", []);
90
+ return endNodeResponse(nodeRes);
91
+ }
92
+ return new Promise((resolve) => {
93
+ function cleanup() {
94
+ stream.off("error", onEarlyError);
95
+ stream.off("readable", onReadable);
96
+ nodeRes.off("close", onResClose);
97
+ }
98
+ function onEarlyError() {
99
+ cleanup();
100
+ stream.destroy();
101
+ writeHead(nodeRes, 500, "Internal Server Error", []);
102
+ endNodeResponse(nodeRes).then(resolve);
103
+ }
104
+ function onReadable() {
105
+ cleanup();
106
+ if (nodeRes.destroyed) {
107
+ stream.destroy();
108
+ return resolve();
109
+ }
110
+ writeHead(nodeRes, status, statusText, headers);
111
+ pipeline$1(stream, nodeRes).catch(() => {}).then(() => resolve());
112
+ }
113
+ function onResClose() {
114
+ cleanup();
115
+ stream.destroy();
116
+ resolve();
117
+ }
118
+ stream.once("error", onEarlyError);
119
+ stream.once("readable", onReadable);
120
+ nodeRes.once("close", onResClose);
121
+ });
122
+ }
123
+ function streamBody(stream, nodeRes) {
124
+ if (nodeRes.destroyed) {
125
+ stream.cancel().catch(() => {});
126
+ return;
127
+ }
128
+ if (nodeRes.req?.method === "HEAD") {
129
+ stream.cancel().catch(() => {});
130
+ return endNodeResponse(nodeRes);
131
+ }
132
+ const reader = stream.getReader();
133
+ function streamCancel(error) {
134
+ reader.cancel(error).catch(() => {});
135
+ if (error) nodeRes.destroy(error);
136
+ }
137
+ function streamHandle({ done, value }) {
138
+ try {
139
+ if (done) nodeRes.end();
140
+ else if (nodeRes.write(value)) reader.read().then(streamHandle, streamCancel);
141
+ else nodeRes.once("drain", () => reader.read().then(streamHandle, streamCancel));
142
+ } catch (error) {
143
+ streamCancel(error instanceof Error ? error : void 0);
144
+ }
145
+ }
146
+ nodeRes.on("close", streamCancel);
147
+ nodeRes.on("error", streamCancel);
148
+ reader.read().then(streamHandle, streamCancel);
149
+ return reader.closed.catch(streamCancel).finally(() => {
150
+ nodeRes.off("close", streamCancel);
151
+ nodeRes.off("error", streamCancel);
152
+ });
153
+ }
154
+ //#endregion
155
+ //#region src/dev-tools/stack-classifier.ts
156
+ /** Parse file/line/col from a stack frame line. */
157
+ function parseFrame(frameLine) {
158
+ const parenMatch = /\(([^)]+):(\d+):(\d+)\)/.exec(frameLine);
159
+ if (parenMatch) return {
160
+ file: parenMatch[1],
161
+ line: Number(parenMatch[2]),
162
+ col: Number(parenMatch[3])
163
+ };
164
+ const bareMatch = /at (\/[^:]+):(\d+):(\d+)/.exec(frameLine);
165
+ if (bareMatch) return {
166
+ file: bareMatch[1],
167
+ line: Number(bareMatch[2]),
168
+ col: Number(bareMatch[3])
169
+ };
170
+ return {};
171
+ }
172
+ /**
173
+ * Classify a single stack frame line by origin.
174
+ *
175
+ * - 'app': user application code (in project root, not node_modules)
176
+ * - 'framework': timber-app internal code
177
+ * - 'internal': node_modules, Node.js internals
178
+ */
179
+ function classifyFrame(frameLine, projectRoot) {
180
+ const trimmed = frameLine.trim();
181
+ if (trimmed.includes("packages/timber-app/")) return "framework";
182
+ if (trimmed.includes("node_modules/")) return "internal";
183
+ if (trimmed.startsWith("at node:") || trimmed.includes("(node:")) return "internal";
184
+ if (trimmed.includes(projectRoot)) return "app";
185
+ return "internal";
186
+ }
187
+ /**
188
+ * Classify all frames in a full stack trace string.
189
+ *
190
+ * Parses the stack, skips the first line (error message), filters to
191
+ * lines starting with "at ", and returns classified frames with optional
192
+ * file/line/col metadata.
193
+ */
194
+ function classifyStack(stack, projectRoot) {
195
+ return stack.split("\n").slice(1).filter((line) => line.trim().startsWith("at ")).map((raw) => {
196
+ const type = classifyFrame(raw, projectRoot);
197
+ const { file, line, col } = parseFrame(raw);
198
+ return {
199
+ raw,
200
+ type,
201
+ file,
202
+ line,
203
+ col
204
+ };
205
+ });
206
+ }
207
+ //#endregion
208
+ //#region src/dev-tools/terminal.ts
209
+ /**
210
+ * Terminal error formatting — boxed, color-coded error output for dev mode.
211
+ *
212
+ * Produces a visually scannable error block with:
213
+ * - Unicode box-drawing border around the error
214
+ * - Phase badge and error message
215
+ * - First app frame highlighted as the primary action item
216
+ * - OSC 8 clickable file:line links (VSCode terminal, iTerm2, etc.)
217
+ * - Internal/framework frames collapsed with a count
218
+ * - Component stack (for React render errors)
219
+ *
220
+ * Dev-only: this module is only imported by dev-error-overlay.ts.
221
+ *
222
+ * Design doc: 21-dev-server.md §"Error Overlay"
223
+ */
224
+ var noValidate = { validateStream: false };
225
+ var style = (fmt, text) => styleText(fmt, text, noValidate);
226
+ /**
227
+ * Wrap text in an OSC 8 hyperlink escape sequence.
228
+ *
229
+ * Terminals that support OSC 8 (VSCode, iTerm2, Windows Terminal, etc.)
230
+ * render this as a clickable link. Others ignore the escape sequences
231
+ * and show the text normally.
232
+ *
233
+ * Format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
234
+ */
235
+ function hyperlink(text, url) {
236
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
237
+ }
238
+ /**
239
+ * Format a file:line:col reference as a clickable terminal link.
240
+ *
241
+ * Uses file:// URLs so terminals open the file in the configured editor.
242
+ * The link text is styled with cyan + underline for visibility.
243
+ */
244
+ function fileLink(filePath, line, col) {
245
+ const display = line ? `${filePath}:${line}${col ? `:${col}` : ""}` : filePath;
246
+ const base = pathToFileURL(filePath).href;
247
+ return style(["cyan", "underline"], hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base));
248
+ }
249
+ var BOX = {
250
+ topLeft: "╭",
251
+ topRight: "╮",
252
+ bottomLeft: "╰",
253
+ bottomRight: "╯",
254
+ horizontal: "─",
255
+ vertical: "│"
256
+ };
257
+ /**
258
+ * Wrap lines of text in a Unicode box with a colored left border.
259
+ *
260
+ * @param lines - Content lines (no ANSI length calculation — keeps it simple)
261
+ * @param width - Box width (characters). Lines longer than this are not truncated.
262
+ */
263
+ function box(lines, borderFormat, width = 80) {
264
+ const bar = BOX.horizontal.repeat(width - 2);
265
+ const output = [];
266
+ output.push(style(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
267
+ for (const line of lines) output.push(`${style(borderFormat, BOX.vertical)} ${line}`);
268
+ output.push(style(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
269
+ return output.join("\n");
270
+ }
271
+ /**
272
+ * Format an error for terminal output with a boxed layout.
273
+ *
274
+ * The output is designed to be scannable at a glance:
275
+ * 1. Red box with phase badge and error message
276
+ * 2. First app frame as a clickable link (the primary action item)
277
+ * 3. App frames listed normally
278
+ * 4. Internal/framework frames collapsed with count
279
+ * 5. Component stack (if present)
280
+ */
281
+ function formatTerminalError$1(error, phase, projectRoot) {
282
+ const sections = [];
283
+ const componentStack = extractComponentStack(error);
284
+ const loc = parseFirstAppFrame(error.stack ?? "", projectRoot);
285
+ const frames = error.stack ? classifyStack(error.stack, projectRoot) : [];
286
+ const appFrames = frames.filter((f) => f.type === "app");
287
+ const internalCount = frames.filter((f) => f.type !== "app").length;
288
+ const boxLines = [];
289
+ boxLines.push(style(["red", "bold"], `${PHASE_LABELS$1[phase]} Error`));
290
+ boxLines.push("");
291
+ for (const msgLine of error.message.split("\n")) boxLines.push(style("red", msgLine));
292
+ if (loc) {
293
+ boxLines.push("");
294
+ const relPath = loc.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc.file;
295
+ boxLines.push(`${style("bold", "→")} ${fileLink(loc.file, loc.line, loc.column)} ${style("dim", `(${relPath})`)}`);
296
+ }
297
+ sections.push(box(boxLines, "red"));
298
+ if (componentStack) {
299
+ sections.push("");
300
+ sections.push(` ${style("bold", "Component Stack:")}`);
301
+ for (const csLine of componentStack.trim().split("\n")) sections.push(` ${style("dim", csLine.trim())}`);
302
+ }
303
+ if (appFrames.length > 0) {
304
+ sections.push("");
305
+ sections.push(` ${style("bold", "Application Frames:")}`);
306
+ for (let i = 0; i < appFrames.length; i++) {
307
+ const f = appFrames[i];
308
+ if (f.file && f.line) {
309
+ const prefix = i === 0 ? style("bold", "▸") : " ";
310
+ sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style("dim", extractFnName(f.raw))}`);
311
+ } else sections.push(` ${f.raw}`);
312
+ }
313
+ }
314
+ if (internalCount > 0) sections.push(` ${style("dim", `… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden`)}`);
315
+ sections.push("");
316
+ return sections.join("\n");
317
+ }
318
+ /** Extract the function name from a stack frame line like " at fnName (/path:1:2)". */
319
+ function extractFnName(frameLine) {
320
+ const match = /at\s+(\S+)\s+\(/.exec(frameLine.trim());
321
+ return match ? match[1] : "";
322
+ }
323
+ //#endregion
324
+ //#region src/dev-tools/overlay.ts
325
+ var _traceMapping = null;
326
+ /**
327
+ * Lazy-load @jridgewell/trace-mapping from Vite's dependency tree.
328
+ * Vite bundles it internally; we resolve from Vite's package to avoid
329
+ * adding a direct dependency.
330
+ */
331
+ function getTraceMapping() {
332
+ if (_traceMapping) return _traceMapping;
333
+ const vitePath = createRequire(import.meta.url).resolve("vite");
334
+ _traceMapping = createRequire(vitePath)("@jridgewell/trace-mapping");
335
+ return _traceMapping;
336
+ }
337
+ /** Labels for terminal output. */
338
+ var PHASE_LABELS$1 = {
339
+ "module-transform": "Module Transform",
340
+ "proxy": "Proxy",
341
+ "middleware": "Middleware",
342
+ "access": "Access Check",
343
+ "render": "RSC Render",
344
+ "handler": "Route Handler"
345
+ };
346
+ /**
347
+ * Extract the React component stack from an error, if present.
348
+ * React attaches this as `componentStack` during renderToReadableStream errors.
349
+ */
350
+ function extractComponentStack(error) {
351
+ if (error && typeof error === "object" && "componentStack" in error && typeof error.componentStack === "string") return error.componentStack;
352
+ return null;
353
+ }
354
+ /**
355
+ * Parse the first application frame from a stack trace.
356
+ * Returns file/line/column for the overlay's `loc` field.
357
+ */
358
+ function parseFirstAppFrame(stack, projectRoot) {
359
+ const lines = stack.split("\n");
360
+ const parenRegex = /\(([^)]+):(\d+):(\d+)\)/;
361
+ const bareRegex = /at (\/[^:]+):(\d+):(\d+)/;
362
+ for (const line of lines) {
363
+ if (classifyFrame(line, projectRoot) !== "app") continue;
364
+ const match = parenRegex.exec(line) ?? bareRegex.exec(line);
365
+ if (!match) continue;
366
+ const [, file, lineNum, col] = match;
367
+ if (file && lineNum && col) return {
368
+ file,
369
+ line: parseInt(lineNum, 10),
370
+ column: parseInt(col, 10)
371
+ };
372
+ }
373
+ return null;
374
+ }
375
+ /**
376
+ * Classify the error phase by inspecting the error's stack trace.
377
+ * Falls back to 'render' if no specific phase can be determined.
378
+ */
379
+ function classifyErrorPhase(error, projectRoot) {
380
+ const stack = error.stack ?? "";
381
+ if (extractComponentStack(error)) return "render";
382
+ const appRoot = projectRoot.replace(/\/$/, "");
383
+ if (stack.includes(`${appRoot}/app/`) || stack.includes("/app/")) {
384
+ if (stack.includes("/middleware.ts") || stack.includes("/middleware.js")) return "middleware";
385
+ if (stack.includes("/access.ts") || stack.includes("/access.js")) return "access";
386
+ if (stack.includes("/route.ts") || stack.includes("/route.js")) return "handler";
387
+ }
388
+ return "render";
389
+ }
390
+ var formatTerminalError = formatTerminalError$1;
391
+ /**
392
+ * Format RSC debug component info into a readable string for the overlay.
393
+ *
394
+ * Renders the server component tree that was active when an error occurred,
395
+ * including component names and source locations from stack frames. This
396
+ * gives developers visibility into which server components were rendering
397
+ * without exposing source code.
398
+ *
399
+ * Returns an empty string if no components are provided.
400
+ */
401
+ function formatRscDebugContext(components) {
402
+ if (!components || components.length === 0) return "";
403
+ const seen = /* @__PURE__ */ new Set();
404
+ const unique = [];
405
+ for (const c of components) if (!seen.has(c.name)) {
406
+ seen.add(c.name);
407
+ unique.push(c);
408
+ }
409
+ const lines = ["Server Component Tree:"];
410
+ for (let i = 0; i < unique.length; i++) {
411
+ const c = unique[i];
412
+ const indent = " ".repeat(i + 1);
413
+ const envLabel = c.env ? ` [${c.env}]` : "";
414
+ let locStr = "";
415
+ if (c.stack && c.stack.length > 0) {
416
+ const frame = c.stack[0];
417
+ if (Array.isArray(frame) && frame.length >= 3) locStr = ` (${frame[1]}:${frame[2]})`;
418
+ }
419
+ lines.push(`${indent}${c.name}${envLabel}${locStr}`);
420
+ }
421
+ return lines.join("\n");
422
+ }
423
+ /**
424
+ * Dynamically compute the line offset that Vite's module runner adds
425
+ * when wrapping modules in an async function.
426
+ *
427
+ * Vite's `calculateOffsetOnce()` uses the same technique: create a new
428
+ * AsyncFunction, throw from line 1, and check where the engine reports
429
+ * the error. The difference between the reported line and 1 is the offset.
430
+ *
431
+ * This is engine-dependent (currently 2 on Node 18-22) and could change
432
+ * in future Node.js or V8 versions. Computing it at runtime ensures we
433
+ * always match the actual behavior.
434
+ */
435
+ var _cachedOffset = null;
436
+ function calculateModuleRunnerOffset() {
437
+ if (_cachedOffset !== null) return _cachedOffset;
438
+ try {
439
+ const AsyncFunction = async function() {}.constructor;
440
+ const src = new AsyncFunction("BODY").toString();
441
+ const bodyIndex = src.indexOf("BODY");
442
+ if (bodyIndex === -1) {
443
+ _cachedOffset = 2;
444
+ return _cachedOffset;
445
+ }
446
+ _cachedOffset = (src.slice(0, bodyIndex).match(/\n/g) || []).length;
447
+ } catch (e) {
448
+ console.debug("[timber] overlay offset calculation failed:", e instanceof Error ? e.message : e);
449
+ _cachedOffset = 2;
450
+ }
451
+ return _cachedOffset;
452
+ }
453
+ /**
454
+ * Phases where the error originated in the RSC environment.
455
+ * These use `server.environments.rsc.moduleGraph` for source-mapping.
456
+ */
457
+ var RSC_PHASES = /* @__PURE__ */ new Set([
458
+ "render",
459
+ "access",
460
+ "middleware",
461
+ "handler"
462
+ ]);
463
+ /**
464
+ * Rewrite an error's stack trace using the correct Vite environment module graph.
465
+ *
466
+ * `server.ssrFixStacktrace()` hardcodes `server.environments.ssr.moduleGraph`,
467
+ * but RSC errors have stack frames pointing to modules loaded in the RSC
468
+ * environment — a separate Vite module graph with separate transform results
469
+ * and source maps. Using the SSR module graph silently fails to find the
470
+ * modules, leaving transpiled/bundled line numbers in the stack trace.
471
+ *
472
+ * This function picks the RSC module graph for render-phase errors and falls
473
+ * back to SSR for module-transform/proxy errors. If the first pass doesn't
474
+ * rewrite any frames (e.g., mixed RSC+SSR stack), it tries the other graph.
475
+ */
476
+ function fixStacktraceForEnvironment(server, error, phase) {
477
+ if (!error.stack) return;
478
+ const primaryEnvName = RSC_PHASES.has(phase) ? "rsc" : "ssr";
479
+ const fallbackEnvName = primaryEnvName === "rsc" ? "ssr" : "rsc";
480
+ const primaryEnv = server.environments[primaryEnvName];
481
+ const fallbackEnv = server.environments[fallbackEnvName];
482
+ if (primaryEnv?.moduleGraph) {
483
+ const rewritten = rewriteStacktrace(error.stack, primaryEnv.moduleGraph);
484
+ if (rewritten.changed) {
485
+ error.stack = rewritten.stack;
486
+ return;
487
+ }
488
+ }
489
+ if (fallbackEnv?.moduleGraph) {
490
+ const rewritten = rewriteStacktrace(error.stack, fallbackEnv.moduleGraph);
491
+ if (rewritten.changed) {
492
+ error.stack = rewritten.stack;
493
+ return;
494
+ }
495
+ }
496
+ }
497
+ /**
498
+ * Rewrite stack trace frames using source maps from an environment's module graph.
499
+ *
500
+ * Mirrors Vite's internal `ssrRewriteStacktrace` logic but works with any
501
+ * `EnvironmentModuleGraph`, not just the SSR one.
502
+ *
503
+ * Returns the rewritten stack and whether any frames were actually changed.
504
+ */
505
+ function rewriteStacktrace(stack, moduleGraph) {
506
+ let changed = false;
507
+ return {
508
+ stack: stack.split("\n").map((line) => {
509
+ return line.replace(/^ {4}at (?:(\S.*?)\s\()?(.+?):(\d+)(?::(\d+))?\)?/, (input, varName, id, lineStr, colStr) => {
510
+ if (!id) return input;
511
+ const rawSourceMap = moduleGraph.getModuleById(id)?.transformResult?.map;
512
+ if (!rawSourceMap) return input;
513
+ const OFFSET = calculateModuleRunnerOffset();
514
+ const origLine = Number(lineStr) - OFFSET;
515
+ const origCol = Number(colStr) - 1;
516
+ if (origLine <= 0 || origCol < 0) return input;
517
+ let pos;
518
+ try {
519
+ const { TraceMap: TM, originalPositionFor: opf } = getTraceMapping();
520
+ pos = opf(new TM(rawSourceMap), {
521
+ line: origLine,
522
+ column: origCol
523
+ });
524
+ } catch (e) {
525
+ console.debug("[timber] source map tracing failed:", e instanceof Error ? e.message : e);
526
+ return input;
527
+ }
528
+ if (!pos.source || pos.line == null) return input;
529
+ changed = true;
530
+ const source = `${resolve(dirname(id), pos.source)}:${pos.line}:${(pos.column ?? 0) + 1}`;
531
+ const trimmedVarName = varName?.trim();
532
+ if (!trimmedVarName || trimmedVarName === "eval") return ` at ${source}`;
533
+ return ` at ${trimmedVarName} (${source})`;
534
+ });
535
+ }).join("\n"),
536
+ changed
537
+ };
538
+ }
539
+ /**
540
+ * Source-map an error's stack trace using the Vite dev server's module graph.
541
+ *
542
+ * Exported so that the fallback error renderer (fallback-error.ts) can
543
+ * source-map errors before rendering the dev error page. Without this,
544
+ * the dev error page shows transpiled positions (e.g. page.tsx:1:1 instead
545
+ * of page.tsx:4:9).
546
+ *
547
+ * See TIM-811.
548
+ */
549
+ function fixErrorStacktrace(server, error, phase) {
550
+ fixStacktraceForEnvironment(server, error, phase ?? classifyErrorPhase(error, server.config.root));
551
+ }
552
+ /**
553
+ * Send an error to Vite's browser overlay and log it to stderr.
554
+ *
555
+ * Uses `server.ssrFixStacktrace()` to map stack traces back to source,
556
+ * then sends the error via `server.hot.send()` for the browser overlay.
557
+ *
558
+ * When `rscDebugComponents` is provided (dev mode only), the server
559
+ * component tree context is appended to the error message. This helps
560
+ * developers identify which server component caused the error without
561
+ * exposing source code.
562
+ *
563
+ * The dev server remains running — errors are handled, not fatal.
564
+ */
565
+ function sendErrorToOverlay(server, error, phase, projectRoot, rscDebugComponents) {
566
+ fixStacktraceForEnvironment(server, error, phase);
567
+ const formatted = formatTerminalError(error, phase, projectRoot);
568
+ process.stderr.write(`${formatted}\n`);
569
+ const loc = parseFirstAppFrame(error.stack ?? "", projectRoot);
570
+ const componentStack = extractComponentStack(error);
571
+ let message = error.message;
572
+ if (componentStack) message = `${error.message}\n\nComponent Stack:\n${componentStack.trim()}`;
573
+ const debugContext = formatRscDebugContext(rscDebugComponents ?? []);
574
+ if (debugContext) message = `${message}\n\n${debugContext}`;
575
+ try {
576
+ server.hot.send({
577
+ type: "error",
578
+ err: {
579
+ message,
580
+ stack: error.stack ?? "",
581
+ id: loc?.file,
582
+ plugin: `timber (${PHASE_LABELS$1[phase]})`,
583
+ loc: loc ? {
584
+ file: loc.file,
585
+ line: loc.line,
586
+ column: loc.column
587
+ } : void 0
588
+ }
589
+ });
590
+ } catch (e) {
591
+ console.debug("[timber] overlay error send failed:", e instanceof Error ? e.message : e);
592
+ }
593
+ }
594
+ //#endregion
595
+ //#region src/server/utils/escape-html.ts
596
+ /**
597
+ * Shared HTML escaping utility.
598
+ *
599
+ * Used by both dev-only paths (error pages, 404 page) and production
600
+ * paths (deny-renderer, RSC helpers). Lives in server/utils/ because
601
+ * it's not dev-only.
602
+ */
603
+ /**
604
+ * Escape a string for safe embedding in HTML content or attributes.
605
+ *
606
+ * Replaces `&`, `<`, `>`, and `"` with their HTML entity equivalents.
607
+ */
608
+ function escapeHtml(str) {
609
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
610
+ }
611
+ //#endregion
612
+ //#region src/dev-tools/dev-page-shell.ts
613
+ /**
614
+ * Shared scaffold for dev-mode HTML pages (dev error page, dev 404 page).
615
+ *
616
+ * Both pages are self-contained HTML documents served when the normal
617
+ * render pipeline can't produce a page. They share:
618
+ * - The document scaffold (head, container, footer)
619
+ * - Base CSS (layout, light/dark variables, common components)
620
+ * - The HMR auto-reload script that watches Vite's WebSocket and reloads
621
+ * when the developer fixes the error / adds the missing route
622
+ *
623
+ * Dev-only: this module is only imported by dev-mode code paths. It is
624
+ * never included in production builds.
625
+ *
626
+ * Design doc: 21-dev-server.md §"Error Overlay", §"Dev 404 Page"
627
+ */
628
+ /**
629
+ * Extract HMR connection options from a Vite resolved config.
630
+ * Used by the dev server to pass HMR config to the dev page generators
631
+ * so the auto-reload WebSocket connects to the correct endpoint.
632
+ */
633
+ function extractHmrOptions(config) {
634
+ const hmr = config.server?.hmr;
635
+ if (hmr === false) return void 0;
636
+ const hmrOpts = typeof hmr === "object" ? hmr : {};
637
+ const opts = {
638
+ protocol: hmrOpts.protocol,
639
+ host: hmrOpts.host,
640
+ port: hmrOpts.clientPort || hmrOpts.port,
641
+ path: hmrOpts.path,
642
+ token: config.webSocketToken
643
+ };
644
+ if (!opts.protocol && !opts.host && !opts.port && !opts.path && !opts.token) return;
645
+ return opts;
646
+ }
647
+ /**
648
+ * Build the inline auto-reload script for dev pages.
649
+ *
650
+ * The page is static HTML, not a Vite-managed module, so the normal HMR
651
+ * update flow can't reach it. Instead we connect to Vite's HMR WebSocket
652
+ * directly and reload on any update event.
653
+ *
654
+ * URL construction mirrors Vite's own client.mjs: explicitly configured
655
+ * values win, and the browser's location fills any gaps at runtime. This
656
+ * matters because Vite 8 requires a `?token=` on browser WS upgrades in
657
+ * every config — a config with only a token still needs the host and port
658
+ * from `location` (TIM-1067).
659
+ */
660
+ function buildReloadScript(hmrOptions) {
661
+ return ` (function() {
662
+ try {
663
+ var o = ${JSON.stringify(hmrOptions ?? {}).replace(/</g, "\\u003c")};
664
+ var protocol = o.protocol || (location.protocol === 'https:' ? 'wss' : 'ws');
665
+ var host = o.host || location.hostname;
666
+ var port = o.port || location.port;
667
+ var url = protocol + '://' + host + (port ? ':' + port : '') + (o.path || '')
668
+ + (o.token ? '?token=' + encodeURIComponent(o.token) : '');
669
+ var ws = new WebSocket(url, 'vite-hmr');
670
+ ws.addEventListener('message', function(e) {
671
+ try {
672
+ var data = JSON.parse(e.data);
673
+ if (data.type === 'full-reload' || data.type === 'update') {
674
+ location.reload();
675
+ }
676
+ } catch (ex) {}
677
+ });
678
+ } catch (ex) {}
679
+ })();`;
680
+ }
681
+ /**
682
+ * Render a self-contained dev page: scaffold + shared CSS + footer +
683
+ * HMR auto-reload script.
684
+ */
685
+ function renderDevPageShell(opts) {
686
+ return `<!DOCTYPE html>
687
+ <html lang="en">
688
+ <head>
689
+ <meta charset="utf-8">
690
+ <meta name="viewport" content="width=device-width, initial-scale=1">
691
+ <title>${escapeHtml(opts.title)}</title>
692
+ <style>${SHARED_CSS}${opts.extraCss ?? ""}</style>
693
+ </head>
694
+ <body>
695
+ <div class="container">
696
+ ${opts.bodyHtml}
697
+
698
+ <footer class="footer">
699
+ <span class="timber-logo">🪵 timber.js</span>
700
+ <span class="footer-hint">${opts.footerHint}</span>
701
+ </footer>
702
+ </div>
703
+
704
+ <script>
705
+ ${opts.extraScript ?? ""}
706
+ // Auto-reload when Vite HMR signals an update (file fixed, route added).
707
+ ${buildReloadScript(opts.hmrOptions)}
708
+ <\/script>
709
+ </body>
710
+ </html>`;
711
+ }
712
+ /**
713
+ * Base CSS shared by all dev pages. Pages override the --badge-* variables
714
+ * (and add their own component styles) via `extraCss`.
715
+ */
716
+ var SHARED_CSS = `
717
+ :root {
718
+ --bg: #fff;
719
+ --fg: #1a1a1a;
720
+ --fg-dim: #6b7280;
721
+ --border: #e5e7eb;
722
+ --source-bg: #fafafa;
723
+ --link: #2563eb;
724
+ --code-bg: #f3f4f6;
725
+ --btn-bg: #f3f4f6;
726
+ --btn-fg: #374151;
727
+ --btn-border: #d1d5db;
728
+ --code-font: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
729
+ }
730
+
731
+ @media (prefers-color-scheme: dark) {
732
+ :root {
733
+ --bg: #0a0a0a;
734
+ --fg: #f5f5f5;
735
+ --fg-dim: #9ca3af;
736
+ --border: #27272a;
737
+ --source-bg: #18181b;
738
+ --link: #60a5fa;
739
+ --code-bg: #27272a;
740
+ --btn-bg: #27272a;
741
+ --btn-fg: #e5e7eb;
742
+ --btn-border: #3f3f46;
743
+ }
744
+ }
745
+
746
+ * { margin: 0; padding: 0; box-sizing: border-box; }
747
+
748
+ body {
749
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
750
+ background: var(--bg);
751
+ color: var(--fg);
752
+ line-height: 1.6;
753
+ padding: 2rem;
754
+ }
755
+
756
+ .container { max-width: 56rem; margin: 0 auto; }
757
+ .header { margin-bottom: 1.5rem; }
758
+
759
+ .badge {
760
+ display: inline-block;
761
+ font-size: 0.75rem;
762
+ font-weight: 600;
763
+ text-transform: uppercase;
764
+ letter-spacing: 0.05em;
765
+ padding: 0.25rem 0.625rem;
766
+ border-radius: 0.375rem;
767
+ background: var(--badge-bg);
768
+ color: var(--badge-fg);
769
+ border: 1px solid var(--badge-border);
770
+ margin-bottom: 0.75rem;
771
+ }
772
+
773
+ .message {
774
+ font-size: 1.375rem;
775
+ font-weight: 600;
776
+ line-height: 1.3;
777
+ word-break: break-word;
778
+ }
779
+
780
+ .hint {
781
+ margin-top: 0.5rem;
782
+ color: var(--fg-dim);
783
+ font-size: 0.875rem;
784
+ }
785
+
786
+ .section {
787
+ margin-bottom: 1rem;
788
+ border: 1px solid var(--border);
789
+ border-radius: 0.5rem;
790
+ overflow: hidden;
791
+ }
792
+
793
+ .section-header {
794
+ font-size: 0.75rem;
795
+ font-weight: 600;
796
+ text-transform: uppercase;
797
+ letter-spacing: 0.05em;
798
+ padding: 0.5rem 0.75rem;
799
+ background: var(--source-bg);
800
+ border-bottom: 1px solid var(--border);
801
+ color: var(--fg-dim);
802
+ }
803
+
804
+ .footer {
805
+ display: flex;
806
+ align-items: center;
807
+ gap: 0.75rem;
808
+ padding-top: 1rem;
809
+ border-top: 1px solid var(--border);
810
+ font-size: 0.75rem;
811
+ color: var(--fg-dim);
812
+ }
813
+
814
+ .timber-logo { font-weight: 600; white-space: nowrap; }
815
+ `;
816
+ //#endregion
817
+ //#region src/dev-tools/error-page.ts
818
+ /**
819
+ * Dev error page — self-contained HTML error page for dev server 500s.
820
+ *
821
+ * Generates a styled, self-contained HTML page when the RSC pipeline fails
822
+ * and the Vite error overlay can't fire (e.g., first page load before HMR
823
+ * WebSocket connects, RSC entry module crash, early pipeline errors).
824
+ *
825
+ * This is NOT a replacement for Vite's error overlay — it's the fallback
826
+ * for when the overlay's transport (WebSocket) isn't available yet.
827
+ *
828
+ * Dev-only: this module is only imported by dev-server.ts (apply: 'serve')
829
+ * and via dynamic import from fallback-error.ts in dev mode. It is never
830
+ * included in production builds.
831
+ *
832
+ * Design doc: 21-dev-server.md §"Error Overlay"
833
+ */
834
+ var PHASE_LABELS = {
835
+ "module-transform": "Module Transform",
836
+ "proxy": "Proxy",
837
+ "middleware": "Middleware",
838
+ "access": "Access Check",
839
+ "render": "RSC Render",
840
+ "handler": "Route Handler"
841
+ };
842
+ var PHASE_HINTS = {
843
+ "module-transform": "This error occurred while Vite was transforming a module. Check for syntax errors or missing imports.",
844
+ "proxy": "This error occurred in proxy.ts. Check your proxy configuration.",
845
+ "middleware": "This error occurred in a middleware.ts file. Check the middleware function for unhandled exceptions.",
846
+ "access": "This error occurred in an access.ts file. Check your access control logic.",
847
+ "render": "This error occurred while rendering a server component. Check the component for runtime errors.",
848
+ "handler": "This error occurred in a route handler (route.ts). Check the handler function."
849
+ };
850
+ /**
851
+ * Try to read a few lines of source code around the error location.
852
+ * Returns null if the file can't be read (e.g., virtual modules).
853
+ *
854
+ * Uses a top-level `node:fs` import — this module is dev-server-only and
855
+ * Node-only. A bare `require()` here would throw in the ESM build and the
856
+ * code frame would silently never render (TIM-1067, B48).
857
+ */
858
+ function readSourceContext(filePath, line, contextLines = 3) {
859
+ try {
860
+ const allLines = readFileSync(filePath, "utf-8").split("\n");
861
+ const start = Math.max(0, line - 1 - contextLines);
862
+ const end = Math.min(allLines.length, line + contextLines);
863
+ return {
864
+ startLine: start + 1,
865
+ lines: allLines.slice(start, end).map((text, i) => ({
866
+ num: start + 1 + i,
867
+ text,
868
+ highlight: start + 1 + i === line
869
+ }))
870
+ };
871
+ } catch (e) {
872
+ console.debug("[timber] source context extraction failed:", e instanceof Error ? e.message : e);
873
+ return null;
874
+ }
875
+ }
876
+ var esc = escapeHtml;
877
+ /**
878
+ * Escape a JSON string for safe embedding in an HTML `<script>` block.
879
+ *
880
+ * `JSON.stringify` does not escape `<\/script>`, so error text containing
881
+ * that sequence breaks out of the script block — an XSS vector.
882
+ * We replace all `<` with the equivalent unicode escape, which is valid in
883
+ * JSON strings and prevents the HTML parser from seeing a closing
884
+ * `<\/script>` tag.
885
+ *
886
+ * Security: TIM-788
887
+ */
888
+ function escJsonForScript(json) {
889
+ return json.replace(/</g, "\\u003c");
890
+ }
891
+ /**
892
+ * Generate a self-contained HTML error page for dev server 500 responses.
893
+ *
894
+ * The page includes:
895
+ * - Error message and phase label
896
+ * - Source code context around the first app frame (if readable)
897
+ * - Component stack (for React render errors)
898
+ * - Classified stack trace (app frames highlighted, internals collapsed)
899
+ * - Copy button for the full error
900
+ * - Dark/light mode via prefers-color-scheme
901
+ * - Auto-reconnect script that watches for Vite HMR and reloads
902
+ */
903
+ function generateDevErrorPage(error, phase, projectRoot, hmrOptions) {
904
+ const message = error.message || "Unknown error";
905
+ const phaseLabel = PHASE_LABELS[phase];
906
+ const phaseHint = PHASE_HINTS[phase];
907
+ const componentStack = extractComponentStack(error);
908
+ const loc = parseFirstAppFrame(error.stack ?? "", projectRoot);
909
+ const frames = error.stack ? classifyStack(error.stack, projectRoot) : [];
910
+ const appFrames = frames.filter((f) => f.type === "app");
911
+ const internalFrameCount = frames.filter((f) => f.type !== "app").length;
912
+ let sourceContext = null;
913
+ if (loc) sourceContext = readSourceContext(loc.file, loc.line);
914
+ const relPath = loc?.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc?.file;
915
+ const bodyHtml = `<header class="header">
916
+ <div class="badge">${esc(phaseLabel)} Error</div>
917
+ <h1 class="message">${esc(message)}</h1>
918
+ ${phaseHint ? `<p class="hint">${esc(phaseHint)}</p>` : ""}
919
+ </header>
920
+
921
+ ${relPath ? `<div class="location">${esc(relPath)}${loc ? `:${loc.line}:${loc.column}` : ""}</div>` : ""}
922
+
923
+ ${sourceContext ? `<div class="source-context">
924
+ <div class="section-header">Source</div>
925
+ <pre class="source-code"><code>${sourceContext.lines.map((l) => `<span class="source-line${l.highlight ? " source-line-highlight" : ""}"><span class="line-num">${l.num}</span>${esc(l.text)}</span>`).join("\n")}</code></pre>
926
+ </div>` : ""}
927
+
928
+ ${componentStack ? `<div class="section">
929
+ <div class="section-header">Component Stack</div>
930
+ <pre class="component-stack">${esc(componentStack.trim())}</pre>
931
+ </div>` : ""}
932
+
933
+ ${appFrames.length > 0 ? `<div class="section">
934
+ <div class="section-header">Application Frames</div>
935
+ <pre class="stack">${appFrames.map((f) => esc(f.raw)).join("\n")}</pre>
936
+ </div>` : ""}
937
+
938
+ ${internalFrameCount > 0 ? `<details class="section internal-frames">
939
+ <summary class="section-header clickable">${internalFrameCount} internal frame${internalFrameCount !== 1 ? "s" : ""}</summary>
940
+ <pre class="stack dimmed">${frames.filter((f) => f.type !== "app").map((f) => esc(f.raw)).join("\n")}</pre>
941
+ </details>` : ""}
942
+
943
+ <div class="actions">
944
+ <button class="btn" onclick="copyError()">Copy Error</button>
945
+ </div>`;
946
+ const copyScript = ` function copyError() {
947
+ var text = ${escJsonForScript(JSON.stringify(`${phaseLabel} Error: ${message}\n\n` + (relPath ? `File: ${relPath}${loc ? `:${loc.line}:${loc.column}` : ""}\n\n` : "") + (componentStack ? `Component Stack:\n${componentStack.trim()}\n\n` : "") + `Stack Trace:\n${error.stack ?? ""}`))};
948
+ navigator.clipboard.writeText(text).then(function() {
949
+ var btn = document.querySelector('.btn');
950
+ if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy Error'; }, 1500); }
951
+ });
952
+ }
953
+ `;
954
+ return renderDevPageShell({
955
+ title: `Error — ${phaseLabel} | timber.js`,
956
+ extraCss: ERROR_CSS,
957
+ bodyHtml,
958
+ footerHint: "Fix the error and save — the page will reload automatically via HMR.",
959
+ extraScript: copyScript,
960
+ hmrOptions
961
+ });
962
+ }
963
+ var ERROR_CSS = `
964
+ :root {
965
+ --badge-bg: #fef2f2;
966
+ --badge-fg: #991b1b;
967
+ --badge-border: #fecaca;
968
+ --highlight-bg: #fef2f2;
969
+ --highlight-border: #ef4444;
970
+ --line-num: #9ca3af;
971
+ }
972
+
973
+ @media (prefers-color-scheme: dark) {
974
+ :root {
975
+ --badge-bg: #450a0a;
976
+ --badge-fg: #fca5a5;
977
+ --badge-border: #7f1d1d;
978
+ --highlight-bg: #450a0a;
979
+ --highlight-border: #dc2626;
980
+ --line-num: #6b7280;
981
+ }
982
+ }
983
+
984
+ .location {
985
+ font-family: var(--code-font);
986
+ font-size: 0.8125rem;
987
+ color: var(--link);
988
+ margin-bottom: 1rem;
989
+ padding: 0.5rem 0.75rem;
990
+ background: var(--source-bg);
991
+ border-radius: 0.375rem;
992
+ border: 1px solid var(--border);
993
+ }
994
+
995
+ .source-context {
996
+ margin-bottom: 1rem;
997
+ border: 1px solid var(--border);
998
+ border-radius: 0.5rem;
999
+ overflow: hidden;
1000
+ }
1001
+
1002
+ .source-code {
1003
+ overflow-x: auto;
1004
+ font-family: var(--code-font);
1005
+ font-size: 0.8125rem;
1006
+ line-height: 1.7;
1007
+ padding: 0;
1008
+ margin: 0;
1009
+ }
1010
+
1011
+ .source-code code {
1012
+ display: block;
1013
+ }
1014
+
1015
+ .source-line {
1016
+ display: block;
1017
+ padding: 0 0.75rem;
1018
+ border-left: 3px solid transparent;
1019
+ }
1020
+
1021
+ .source-line-highlight {
1022
+ background: var(--highlight-bg);
1023
+ border-left-color: var(--highlight-border);
1024
+ }
1025
+
1026
+ .line-num {
1027
+ display: inline-block;
1028
+ width: 3rem;
1029
+ text-align: right;
1030
+ margin-right: 1rem;
1031
+ color: var(--line-num);
1032
+ user-select: none;
1033
+ }
1034
+
1035
+ .clickable { cursor: pointer; }
1036
+ .clickable:hover { background: var(--btn-bg); }
1037
+
1038
+ .component-stack, .stack {
1039
+ font-family: var(--code-font);
1040
+ font-size: 0.8125rem;
1041
+ line-height: 1.7;
1042
+ padding: 0.75rem;
1043
+ overflow-x: auto;
1044
+ white-space: pre;
1045
+ margin: 0;
1046
+ }
1047
+
1048
+ .dimmed { color: var(--fg-dim); }
1049
+
1050
+ .actions {
1051
+ margin: 1.5rem 0;
1052
+ }
1053
+
1054
+ .btn {
1055
+ font-family: inherit;
1056
+ font-size: 0.8125rem;
1057
+ font-weight: 500;
1058
+ padding: 0.5rem 1rem;
1059
+ border-radius: 0.375rem;
1060
+ border: 1px solid var(--btn-border);
1061
+ background: var(--btn-bg);
1062
+ color: var(--btn-fg);
1063
+ cursor: pointer;
1064
+ transition: background 0.15s;
1065
+ }
1066
+
1067
+ .btn:hover { opacity: 0.85; }
1068
+ `;
1069
+ //#endregion
1070
+ //#region src/config-validation.ts
1071
+ /**
1072
+ * Config validation — validates timber.config.ts at startup.
1073
+ *
1074
+ * Runs in the plugin's configResolved hook (once, at startup/build).
1075
+ * Each check produces a clear error message with the invalid value,
1076
+ * what's expected, and how to fix it.
1077
+ *
1078
+ * Design doc: 18-build-system.md
1079
+ */
1080
+ var knownKeys = new Set(Object.keys({
1081
+ output: true,
1082
+ debug: true,
1083
+ clientJavascript: true,
1084
+ adapter: true,
1085
+ allowedOrigins: true,
1086
+ csrf: true,
1087
+ limits: true,
1088
+ forms: true,
1089
+ pageExtensions: true,
1090
+ slowRequestMs: true,
1091
+ renderTimeoutMs: true,
1092
+ devBrowserLogs: true,
1093
+ dev: true,
1094
+ serverTiming: true,
1095
+ appDir: true,
1096
+ mdx: true,
1097
+ actionEncryption: true,
1098
+ reactCompiler: true,
1099
+ sitemap: true,
1100
+ buildDir: true,
1101
+ cache: true,
1102
+ clientSegmentCache: true,
1103
+ topLoader: true,
1104
+ budget: true
1105
+ }));
1106
+ /**
1107
+ * Validate a TimberUserConfig object.
1108
+ *
1109
+ * Returns an array of errors. Empty array means the config is valid.
1110
+ * Does not throw — the caller decides how to surface errors.
1111
+ */
1112
+ function validateConfig(config) {
1113
+ const errors = [];
1114
+ if (config.output !== void 0 && config.output !== "server" && config.output !== "static") errors.push({
1115
+ field: "output",
1116
+ message: `Invalid output mode: "${String(config.output)}". Must be "server" or "static".`,
1117
+ value: config.output,
1118
+ suggestion: "Use output: \"server\" (default) or output: \"static\" for static site generation."
1119
+ });
1120
+ if (config.pageExtensions !== void 0) {
1121
+ if (!Array.isArray(config.pageExtensions)) errors.push({
1122
+ field: "pageExtensions",
1123
+ message: "pageExtensions must be an array of strings.",
1124
+ value: config.pageExtensions,
1125
+ suggestion: "Example: pageExtensions: [\"tsx\", \"ts\", \"jsx\", \"js\", \"mdx\"]"
1126
+ });
1127
+ else for (const ext of config.pageExtensions) {
1128
+ if (typeof ext !== "string") {
1129
+ errors.push({
1130
+ field: "pageExtensions",
1131
+ message: `pageExtensions contains a non-string value: ${JSON.stringify(ext)}`,
1132
+ value: ext
1133
+ });
1134
+ break;
1135
+ }
1136
+ if (ext.startsWith(".")) {
1137
+ errors.push({
1138
+ field: "pageExtensions",
1139
+ message: `pageExtensions should not include the leading dot: "${ext}"`,
1140
+ value: ext,
1141
+ suggestion: `Use "${ext.slice(1)}" instead of "${ext}".`
1142
+ });
1143
+ break;
1144
+ }
1145
+ }
1146
+ }
1147
+ if (config.slowRequestMs !== void 0) {
1148
+ if (typeof config.slowRequestMs !== "number" || config.slowRequestMs < 0) errors.push({
1149
+ field: "slowRequestMs",
1150
+ message: `slowRequestMs must be a non-negative number (got ${JSON.stringify(config.slowRequestMs)}).`,
1151
+ value: config.slowRequestMs,
1152
+ suggestion: "Use slowRequestMs: 3000 (default) or slowRequestMs: 0 to disable."
1153
+ });
1154
+ }
1155
+ if (config.renderTimeoutMs !== void 0) {
1156
+ if (typeof config.renderTimeoutMs !== "number" || config.renderTimeoutMs < 0) errors.push({
1157
+ field: "renderTimeoutMs",
1158
+ message: `renderTimeoutMs must be a non-negative number (got ${JSON.stringify(config.renderTimeoutMs)}).`,
1159
+ value: config.renderTimeoutMs,
1160
+ suggestion: "Use renderTimeoutMs: 30000 (default). Setting 0 uses a 120s safety ceiling."
1161
+ });
1162
+ }
1163
+ if (config.serverTiming !== void 0) {
1164
+ if (config.serverTiming !== "detailed" && config.serverTiming !== "total" && config.serverTiming !== false) errors.push({
1165
+ field: "serverTiming",
1166
+ message: `Invalid serverTiming value: ${JSON.stringify(config.serverTiming)}.`,
1167
+ value: config.serverTiming,
1168
+ suggestion: "Use \"detailed\", \"total\", or false."
1169
+ });
1170
+ }
1171
+ if (config.devBrowserLogs !== void 0) {
1172
+ const valid = [
1173
+ "error",
1174
+ "warn",
1175
+ "info",
1176
+ "none"
1177
+ ];
1178
+ if (!valid.includes(config.devBrowserLogs)) errors.push({
1179
+ field: "devBrowserLogs",
1180
+ message: `Invalid devBrowserLogs value: ${JSON.stringify(config.devBrowserLogs)}.`,
1181
+ value: config.devBrowserLogs,
1182
+ suggestion: `Use one of: ${valid.map((v) => `"${v}"`).join(", ")}.`
1183
+ });
1184
+ }
1185
+ if (config.sitemap != null && typeof config.sitemap === "object") {
1186
+ if (config.sitemap.enabled && !config.sitemap.baseUrl) errors.push({
1187
+ field: "sitemap.baseUrl",
1188
+ message: "sitemap.baseUrl is required when sitemap is enabled.",
1189
+ suggestion: "Add sitemap: { enabled: true, baseUrl: \"https://example.com\" }."
1190
+ });
1191
+ if (config.sitemap.defaultPriority !== void 0 && (config.sitemap.defaultPriority < 0 || config.sitemap.defaultPriority > 1)) errors.push({
1192
+ field: "sitemap.defaultPriority",
1193
+ message: `sitemap.defaultPriority must be between 0.0 and 1.0 (got ${config.sitemap.defaultPriority}).`,
1194
+ value: config.sitemap.defaultPriority
1195
+ });
1196
+ }
1197
+ if (config.budget != null && typeof config.budget === "object") {
1198
+ if (config.budget.firstLoadJs !== void 0 && (typeof config.budget.firstLoadJs !== "number" || config.budget.firstLoadJs <= 0)) errors.push({
1199
+ field: "budget.firstLoadJs",
1200
+ message: `budget.firstLoadJs must be a positive number in bytes (got ${JSON.stringify(config.budget.firstLoadJs)}).`,
1201
+ value: config.budget.firstLoadJs,
1202
+ suggestion: "Example: budget: { firstLoadJs: 150_000 } for a ~150 kB limit."
1203
+ });
1204
+ }
1205
+ if (config.cache != null && typeof config.cache === "object") {
1206
+ if (config.cache.strictKeyDiscipline !== void 0 && typeof config.cache.strictKeyDiscipline !== "boolean") errors.push({
1207
+ field: "cache.strictKeyDiscipline",
1208
+ message: `cache.strictKeyDiscipline must be a boolean (got ${JSON.stringify(config.cache.strictKeyDiscipline)}).`,
1209
+ value: config.cache.strictKeyDiscipline,
1210
+ suggestion: "Use cache: { strictKeyDiscipline: true } to promote key-discipline warnings to errors."
1211
+ });
1212
+ }
1213
+ const MOVED_TO_CACHE_FILE = /* @__PURE__ */ new Set(["cacheHandler", "cdnPurge"]);
1214
+ for (const key of Object.keys(config)) if (!knownKeys.has(key)) errors.push({
1215
+ field: key,
1216
+ message: `Unknown config option: "${key}".`,
1217
+ suggestion: MOVED_TO_CACHE_FILE.has(key) ? `"${key}" has moved to timber.cache.ts. Export cacheHandler as default, cdnPurge as a named export.` : `Check for typos. Known options: ${[...knownKeys].sort().join(", ")}.`
1218
+ });
1219
+ return errors;
1220
+ }
1221
+ var RED = "\x1B[31m";
1222
+ var BOLD = "\x1B[1m";
1223
+ var DIM = "\x1B[2m";
1224
+ var RESET = "\x1B[0m";
1225
+ /**
1226
+ * Format config errors for terminal output.
1227
+ */
1228
+ function formatConfigErrors(errors) {
1229
+ if (errors.length === 0) return "";
1230
+ const lines = [];
1231
+ lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}${errors.length} config error${errors.length !== 1 ? "s" : ""} in timber.config.ts:${RESET}`);
1232
+ for (const err of errors) {
1233
+ lines.push("");
1234
+ lines.push(` ${RED}✗${RESET} ${BOLD}${err.field}${RESET}: ${err.message}`);
1235
+ if (err.suggestion) lines.push(` ${DIM}${err.suggestion}${RESET}`);
1236
+ }
1237
+ return lines.join("\n");
1238
+ }
1239
+ var VIRTUAL_MODULE_NAMES = {
1240
+ "virtual:timber-rsc-entry": "RSC entry (server component handler)",
1241
+ "virtual:timber-ssr-entry": "SSR entry (HTML renderer)",
1242
+ "virtual:timber-browser-entry": "Browser entry (client hydration)",
1243
+ "virtual:timber-config": "Runtime config (timber.config.ts)",
1244
+ "virtual:timber-route-manifest": "Route manifest (app/ file tree)",
1245
+ "virtual:timber-instrumentation": "Instrumentation (instrumentation.ts)",
1246
+ "virtual:timber-cache-handler": "Cache handler (timber.cache.ts)",
1247
+ "virtual:timber-build-manifest": "Build manifest (asset mapping)"
1248
+ };
1249
+ /**
1250
+ * Add timber-specific context to an error message that references virtual modules.
1251
+ *
1252
+ * If the error message contains a `virtual:timber-*` ID, appends a
1253
+ * human-readable explanation. Does not replace the original message.
1254
+ */
1255
+ function addVirtualModuleContext(errorMessage) {
1256
+ for (const [id, name] of Object.entries(VIRTUAL_MODULE_NAMES)) if (errorMessage.includes(id)) return `${errorMessage}\n\n [timber] This error references "${id}" — timber's ${name}.\n This is an internal module. The issue is likely in your app code or timber configuration.`;
1257
+ return errorMessage;
1258
+ }
1259
+ /**
1260
+ * Required (non-optional) peer dependencies that must be installed.
1261
+ */
1262
+ var REQUIRED_PEERS = [
1263
+ "react",
1264
+ "react-dom",
1265
+ "@vitejs/plugin-react",
1266
+ "@vitejs/plugin-rsc"
1267
+ ];
1268
+ /**
1269
+ * Check that required peer dependencies are installed.
1270
+ *
1271
+ * Uses require.resolve with a try/catch — no fs scanning.
1272
+ * Returns only the missing packages.
1273
+ */
1274
+ function checkPeerDependencies(projectRoot) {
1275
+ const results = [];
1276
+ const userRequire = createRequire(`${projectRoot}/package.json`);
1277
+ for (const name of REQUIRED_PEERS) try {
1278
+ userRequire.resolve(name);
1279
+ results.push({
1280
+ name,
1281
+ status: "ok"
1282
+ });
1283
+ } catch {
1284
+ results.push({
1285
+ name,
1286
+ status: "missing"
1287
+ });
1288
+ }
1289
+ return results;
1290
+ }
1291
+ /**
1292
+ * Format missing peer dependencies as an actionable warning.
1293
+ */
1294
+ function formatMissingPeers(results) {
1295
+ const missing = results.filter((r) => r.status === "missing");
1296
+ if (missing.length === 0) return "";
1297
+ const names = missing.map((r) => r.name);
1298
+ const lines = [];
1299
+ lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}Missing required dependencies:${RESET}`);
1300
+ lines.push("");
1301
+ for (const name of names) lines.push(` ${RED}✗${RESET} ${name}`);
1302
+ lines.push("");
1303
+ lines.push(` ${DIM}Install with:${RESET}`);
1304
+ lines.push(` ${BOLD}pnpm add ${names.join(" ")}${RESET}`);
1305
+ return lines.join("\n");
1306
+ }
1307
+ //#endregion
1308
+ //#region src/server/rsc-entry/helpers.ts
1309
+ /**
1310
+ * Parse React Flight debug rows into component entries.
1311
+ *
1312
+ * The Flight debug channel writes rows in `hexId:json\n` format. Each row
1313
+ * with a JSON object containing a `name` field is a component debug info
1314
+ * entry. Rows without `name` (timing rows, reference rows like `D"$id"`)
1315
+ * are skipped.
1316
+ *
1317
+ * Security: `props` are explicitly stripped from parsed entries — they may
1318
+ * contain rendered output or user data. Only `name`, `env`, `key`, and
1319
+ * `stack` are retained.
1320
+ */
1321
+ function parseDebugRows(text) {
1322
+ if (!text) return [];
1323
+ const entries = [];
1324
+ const lines = text.split("\n");
1325
+ for (const line of lines) {
1326
+ if (!line) continue;
1327
+ const colonIdx = line.indexOf(":");
1328
+ if (colonIdx === -1) continue;
1329
+ let payload = line.slice(colonIdx + 1);
1330
+ if (payload.length > 0 && payload[0] !== "{" && payload[0] !== "[" && payload[0] !== "\"") payload = payload.slice(1);
1331
+ if (!payload.startsWith("{")) continue;
1332
+ try {
1333
+ const parsed = JSON.parse(payload);
1334
+ if (typeof parsed !== "object" || parsed === null) continue;
1335
+ if (typeof parsed.name !== "string") continue;
1336
+ entries.push({
1337
+ name: parsed.name,
1338
+ env: parsed.env ?? null,
1339
+ key: parsed.key ?? null,
1340
+ stack: Array.isArray(parsed.stack) ? parsed.stack : null
1341
+ });
1342
+ } catch (e) {
1343
+ swallow(e, "malformed JSON in RSC client reference row");
1344
+ }
1345
+ }
1346
+ return entries;
1347
+ }
1348
+ //#endregion
1349
+ //#region src/dev-tools/debug-channel.ts
1350
+ /**
1351
+ * Dev-mode RSC debug channel transport — pipes Flight debug data to
1352
+ * the browser via Vite's HMR WebSocket.
1353
+ *
1354
+ * React Flight writes debug rows (component info, owner stacks, $E
1355
+ * source entries) to a debug channel writable. This module reads
1356
+ * from the paired readable and buffers chunks until the browser
1357
+ * subscribes via 'timber:debug-subscribe'. Chunks are then replayed
1358
+ * and live-forwarded to that specific client (never broadcast).
1359
+ *
1360
+ * This matches the approach used by Next.js and Waku — debug data
1361
+ * travels out-of-band via WebSocket, not inline in the HTML response.
1362
+ *
1363
+ * Design ref: 13-security.md §7, TIM-1507
1364
+ */
1365
+ var DEBUG_CHANNEL_EVENT = "timber:debug-data";
1366
+ var DEBUG_SUBSCRIBE_EVENT = "timber:debug-subscribe";
1367
+ var SESSION_TTL_MS = 6e4;
1368
+ /**
1369
+ * Registry of active debug channel sessions. The dev-server plugin
1370
+ * creates one instance and wires it into the HMR handlers.
1371
+ */
1372
+ var DebugChannelRegistry = class {
1373
+ sessions = /* @__PURE__ */ new Map();
1374
+ /** Called by pipeToTransport when a chunk arrives from the RSC render. */
1375
+ push(id, chunk) {
1376
+ const session = this.sessions.get(id);
1377
+ if (!session) return;
1378
+ session.chunks.push(chunk);
1379
+ if (session.client) session.client(DEBUG_CHANNEL_EVENT, {
1380
+ id,
1381
+ chunk
1382
+ });
1383
+ }
1384
+ /** Called by pipeToTransport when the debug channel stream ends. */
1385
+ finish(id) {
1386
+ const session = this.sessions.get(id);
1387
+ if (!session) return;
1388
+ session.done = true;
1389
+ if (session.client) {
1390
+ session.client(DEBUG_CHANNEL_EVENT, {
1391
+ id,
1392
+ done: true
1393
+ });
1394
+ this.evict(id);
1395
+ }
1396
+ }
1397
+ /** Called by the HMR subscribe handler when the browser requests its debug data. */
1398
+ subscribe(id, clientSend) {
1399
+ const session = this.sessions.get(id);
1400
+ if (!session) {
1401
+ clientSend(DEBUG_CHANNEL_EVENT, {
1402
+ id,
1403
+ done: true
1404
+ });
1405
+ return;
1406
+ }
1407
+ session.client = clientSend;
1408
+ for (const chunk of session.chunks) clientSend(DEBUG_CHANNEL_EVENT, {
1409
+ id,
1410
+ chunk
1411
+ });
1412
+ if (session.done) {
1413
+ clientSend(DEBUG_CHANNEL_EVENT, {
1414
+ id,
1415
+ done: true
1416
+ });
1417
+ this.evict(id);
1418
+ }
1419
+ }
1420
+ /** Parse buffered debug rows into component entries for the error overlay. */
1421
+ getComponents(id) {
1422
+ const session = this.sessions.get(id);
1423
+ if (!session) return [];
1424
+ return parseDebugRows(session.chunks.map((b64) => Buffer.from(b64, "base64").toString("utf-8")).join(""));
1425
+ }
1426
+ /** Create a new session for a render request. Returns the requestId. */
1427
+ create() {
1428
+ const id = randomUUID();
1429
+ const timer = setTimeout(() => this.evict(id), SESSION_TTL_MS);
1430
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
1431
+ this.sessions.set(id, {
1432
+ chunks: [],
1433
+ done: false,
1434
+ timer
1435
+ });
1436
+ return id;
1437
+ }
1438
+ evict(id) {
1439
+ const session = this.sessions.get(id);
1440
+ if (session) {
1441
+ if (session.client && !session.done) session.client(DEBUG_CHANNEL_EVENT, {
1442
+ id,
1443
+ done: true
1444
+ });
1445
+ clearTimeout(session.timer);
1446
+ this.sessions.delete(id);
1447
+ }
1448
+ }
1449
+ };
1450
+ /** HMR custom event name for graph-affecting file changes. */
1451
+ var GRAPH_UPDATE_EVENT = "timber:graph-update";
1452
+ /**
1453
+ * Host-header allowlist check, ported branch-for-branch from Vite's
1454
+ * host-validation middleware (`isHostAllowedWithoutCache`) so this
1455
+ * endpoint's reachability matches the rest of the dev server:
1456
+ *
1457
+ * - `allowedHosts: true` allows everything
1458
+ * - a MISSING Host header is allowed (Vite parity — HTTP/1.0 probes)
1459
+ * - any IP-literal host is allowed (`vite --host` LAN access sends
1460
+ * `Host: 192.168.x.x:port`; rejecting it would 403 this endpoint
1461
+ * while every other dev route works)
1462
+ * - `localhost` and `*.localhost` are always allowed
1463
+ * - allowedHosts entries: exact match, or `.suffix` subdomain entries
1464
+ */
1465
+ function isAllowedGraphHost(hostHeader, allowedHosts) {
1466
+ if (allowedHosts === true) return true;
1467
+ if (hostHeader === void 0) return true;
1468
+ let host = hostHeader;
1469
+ const bracket = host.match(/^\[([^\]]+)\](?::\d+)?$/);
1470
+ if (bracket) return isIP(bracket[1]) === 6;
1471
+ const colon = host.lastIndexOf(":");
1472
+ if (colon !== -1) host = host.slice(0, colon);
1473
+ host = host.toLowerCase();
1474
+ if (isIP(host) !== 0) return true;
1475
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
1476
+ for (const entry of allowedHosts ?? []) {
1477
+ const allowed = entry.toLowerCase();
1478
+ if (allowed.startsWith(".")) {
1479
+ if (host === allowed.slice(1) || host.endsWith(allowed)) return true;
1480
+ } else if (host === allowed) return true;
1481
+ }
1482
+ return false;
1483
+ }
1484
+ /**
1485
+ * Non-http(s) origins admitted without a host check: Vite's own
1486
+ * `file:` / `*-extension:` set (its host-validation regex), plus
1487
+ * `vscode-webview:` — VS Code webviews fetch with that Origin, and the
1488
+ * extension is this endpoint's primary consumer. Vite itself never
1489
+ * origin-checks plain HTTP GETs (only WebSocket upgrades), so this
1490
+ * layer is strictly additional; the additions keep it from rejecting
1491
+ * consumers Vite would serve.
1492
+ *
1493
+ * NOT admitted: `Origin: null`. Browsers serialize file:// pages'
1494
+ * opaque origin as the literal string `null` — but so do sandboxed
1495
+ * iframes and data: documents, exactly the untrusted embedders the
1496
+ * check exists to stop, and the header offers nothing to tell them
1497
+ * apart. file:// pages therefore cannot use this endpoint from a
1498
+ * browser; the `file:` scheme entry covers non-browser clients that
1499
+ * send a real scheme-form Origin (Vite admits the same set).
1500
+ */
1501
+ var TRUSTED_ORIGIN_SCHEMES = /^(?:file|vscode-webview|.+-extension):/i;
1502
+ /** Origin check: absent is fine (CLI probe); present must pass the host allowlist. */
1503
+ function isAllowedGraphOrigin(originHeader, allowedHosts) {
1504
+ if (originHeader === void 0) return true;
1505
+ if (TRUSTED_ORIGIN_SCHEMES.test(originHeader)) return true;
1506
+ let originUrl;
1507
+ try {
1508
+ originUrl = new URL(originHeader);
1509
+ } catch {
1510
+ return false;
1511
+ }
1512
+ if (originUrl.host === "") return false;
1513
+ const bareHost = originUrl.hostname.replace(/^\[|\]$/g, "");
1514
+ if (isIP(bareHost) !== 0) {
1515
+ if (bareHost === "127.0.0.1" || bareHost === "::1") return true;
1516
+ if (allowedHosts === true) return true;
1517
+ return (allowedHosts ?? []).some((entry) => entry.toLowerCase() === bareHost);
1518
+ }
1519
+ return isAllowedGraphHost(originUrl.host, allowedHosts);
1520
+ }
1521
+ function sendJson(res, status, body) {
1522
+ res.statusCode = status;
1523
+ res.setHeader("content-type", "application/json; charset=utf-8");
1524
+ res.end(JSON.stringify(body));
1525
+ }
1526
+ /**
1527
+ * Echo an ADMITTED origin back as CORS headers. A webview fetch from
1528
+ * `vscode-webview://…` passes the origin check but the browser rejects
1529
+ * the response body without `Access-Control-Allow-Origin` — the exact
1530
+ * transport design/47 specifies for the extension. Only the validated
1531
+ * origin is echoed (never `*`), with `Vary: Origin` for any cache in
1532
+ * between; unadmitted origins were already 403'd and get nothing.
1533
+ */
1534
+ function reflectCors(res, origin) {
1535
+ if (!origin) return;
1536
+ res.setHeader("Access-Control-Allow-Origin", origin);
1537
+ res.setHeader("Vary", "Origin");
1538
+ }
1539
+ /**
1540
+ * Create the Connect middleware handling GET /__timber/graph.
1541
+ *
1542
+ * Query surface: `?file=<path>` (absolute or project-root-relative) or
1543
+ * `?all=1`. Anything else is a 400 — fail loudly, like the CLI.
1544
+ */
1545
+ function createGraphEndpointMiddleware(server, ctx) {
1546
+ const sourceExtensions = buildSourceExtensions(ctx.config.pageExtensions);
1547
+ return async (req, res, next) => {
1548
+ const rawUrl = req.url;
1549
+ if (!rawUrl || !rawUrl.startsWith("/__timber/")) {
1550
+ next();
1551
+ return;
1552
+ }
1553
+ const url = new URL(rawUrl, "http://placeholder.invalid");
1554
+ if (url.pathname !== "/__timber/graph") {
1555
+ sendJson(res, 404, { error: `unknown /__timber/ endpoint: ${url.pathname}` });
1556
+ return;
1557
+ }
1558
+ if (req.method !== "GET") {
1559
+ res.setHeader("Allow", "GET");
1560
+ sendJson(res, 405, { error: "GET only" });
1561
+ return;
1562
+ }
1563
+ const origin = req.headers.origin;
1564
+ const allowedHosts = server.config.server.allowedHosts;
1565
+ if (!server.config.server.https && !isAllowedGraphHost(req.headers.host, allowedHosts)) {
1566
+ sendJson(res, 403, { error: "host not allowed" });
1567
+ return;
1568
+ }
1569
+ if (!isAllowedGraphOrigin(origin, allowedHosts)) {
1570
+ sendJson(res, 403, { error: "origin not allowed" });
1571
+ return;
1572
+ }
1573
+ reflectCors(res, origin);
1574
+ try {
1575
+ const { classifyLiveFile, classifyLiveProject } = await import("./live-graph-VjHFF5EV.js");
1576
+ const preserveSymlinks = server.config.resolve.preserveSymlinks;
1577
+ const root = normalizePath(preserveSymlinks ? ctx.root : await realpath(ctx.root));
1578
+ const appDir = normalizePath(preserveSymlinks ? ctx.appDir : await realpath(ctx.appDir).catch(() => join(root, relative(ctx.root, ctx.appDir))));
1579
+ const liveCtx = {
1580
+ rsc: server.environments.rsc.moduleGraph,
1581
+ ssr: server.environments.ssr.moduleGraph,
1582
+ client: server.environments.client.moduleGraph,
1583
+ root,
1584
+ appDir,
1585
+ pageExtensions: ctx.config.pageExtensions
1586
+ };
1587
+ const allParam = url.searchParams.get("all");
1588
+ if (allParam !== null && allParam !== "1") {
1589
+ sendJson(res, 400, { error: `unsupported all value: ${allParam} (use ?all=1)` });
1590
+ return;
1591
+ }
1592
+ if (allParam === "1") {
1593
+ sendJson(res, 200, {
1594
+ version: 1,
1595
+ source: "dev-server",
1596
+ cached: false,
1597
+ ...await classifyLiveProject(liveCtx)
1598
+ });
1599
+ return;
1600
+ }
1601
+ const fileParam = url.searchParams.get("file");
1602
+ if (!fileParam) {
1603
+ sendJson(res, 400, { error: "missing ?file=<path> (or ?all=1)" });
1604
+ return;
1605
+ }
1606
+ const absolute = normalizePath(isAbsolute(fileParam) ? resolve(fileParam) : resolve(root, fileParam));
1607
+ let target;
1608
+ try {
1609
+ const physical = normalizePath(await realpath(absolute));
1610
+ const rootPhysical = preserveSymlinks ? normalizePath(await realpath(root)) : root;
1611
+ if (physical !== rootPhysical && !physical.startsWith(rootPhysical + "/") && !physical.startsWith(rootPhysical + "\\")) {
1612
+ sendJson(res, 404, { error: `not a project file under ${root}` });
1613
+ return;
1614
+ }
1615
+ if (preserveSymlinks) {
1616
+ if (physical !== rootPhysical + absolute.slice(root.length)) {
1617
+ sendJson(res, 404, { error: `not a project source file: ${absolute}` });
1618
+ return;
1619
+ }
1620
+ }
1621
+ target = normalizePath(preserveSymlinks ? absolute : physical);
1622
+ } catch {
1623
+ sendJson(res, 404, { error: `file not found: ${absolute}` });
1624
+ return;
1625
+ }
1626
+ if (target !== root && !target.startsWith(root + "/") && !target.startsWith(root + "\\")) {
1627
+ sendJson(res, 404, { error: `not a project file under ${root}` });
1628
+ return;
1629
+ }
1630
+ if (!(await stat(target).catch(() => null))?.isFile() || !isScannableSourceFile(normalizePath(target), normalizePath(root), sourceExtensions)) {
1631
+ sendJson(res, 404, { error: `not a project source file: ${target}` });
1632
+ return;
1633
+ }
1634
+ const errors = [];
1635
+ const module = await classifyLiveFile(target, liveCtx, void 0, errors);
1636
+ sendJson(res, 200, {
1637
+ version: 1,
1638
+ source: "dev-server",
1639
+ cached: false,
1640
+ target: {
1641
+ input: fileParam,
1642
+ resolved: target
1643
+ },
1644
+ kind: module.kind,
1645
+ pending: module.pending,
1646
+ subtreeUnknown: module.subtreeUnknown,
1647
+ envs: module.envs,
1648
+ boundary: module.boundary,
1649
+ chains: module.chains,
1650
+ poisonings: module.poisonings,
1651
+ errors
1652
+ });
1653
+ } catch (error) {
1654
+ sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
1655
+ }
1656
+ };
1657
+ }
1658
+ /**
1659
+ * Build the push channel: `timber:graph-update` custom HMR events per
1660
+ * project-source change. Consumers (the VS Code extension) re-query
1661
+ * and debounce on their side; the channel just emits. Non-source noise
1662
+ * (build output, dotdirs) is filtered by the same extension set the
1663
+ * scanners use.
1664
+ *
1665
+ * Two phases per edit, because one is not enough (codex review, PR
1666
+ * #1057): the `invalidated` event fires from the plugin's `hotUpdate`
1667
+ * hook — after Vite clears the module's transform result, but its
1668
+ * importer EDGES are only rebuilt when the module is next transformed.
1669
+ * The `transformed` event fires for exactly the files with a pending
1670
+ * invalidation, and only once the node's `transformResult` is set
1671
+ * again — the plugin transform hook itself runs BEFORE Vite's trailing
1672
+ * `vite:import-analysis` (which rebuilds the edges), so the hook only
1673
+ * marks the file "settling" and a short bounded timer confirms
1674
+ * completion via `isTransformed` before emitting. (A module nothing
1675
+ * re-requests never re-transforms — its `transformed` event waits for
1676
+ * the next render, which is also when the graph has new information.)
1677
+ */
1678
+ function makeGraphUpdateChannel(ctx, send, isTransformed, canonicalize = (file) => file) {
1679
+ /** Files invalidated but not yet re-transformed, per environment. */
1680
+ const pending = /* @__PURE__ */ new Set();
1681
+ const key = (file, environment) => `${environment}\0${file}`;
1682
+ const root = normalizePath(canonicalize(ctx.root));
1683
+ const extensions = buildSourceExtensions(ctx.config.pageExtensions);
1684
+ const emitTransformed = (file, environment, structural) => {
1685
+ send({
1686
+ type: "custom",
1687
+ event: GRAPH_UPDATE_EVENT,
1688
+ data: structural ? {
1689
+ file,
1690
+ event: "update",
1691
+ environment,
1692
+ phase: "transformed",
1693
+ structural: true
1694
+ } : {
1695
+ file,
1696
+ event: "update",
1697
+ environment,
1698
+ phase: "transformed"
1699
+ }
1700
+ });
1701
+ };
1702
+ /**
1703
+ * Confirm the transform request finished (transformResult set again)
1704
+ * before emitting phase `transformed`. If confirmation never arrives
1705
+ * within the bounded schedule below, the pending key is RE-ARMED
1706
+ * rather than emitted — a `transformed` event over a still-stale
1707
+ * graph would be a false all-clear with no corrective event.
1708
+ */
1709
+ /**
1710
+ * Delays between confirmation re-checks: dense while a normal
1711
+ * transform request finishes, then exponential backoff for slow
1712
+ * import-analysis passes (large graphs, cold dep optimizer) — a
1713
+ * completed slow transform is CACHED, so no later transform hook may
1714
+ * ever fire for it and the tail checks are the only chance to emit.
1715
+ * Total window ≈ 16 s, then give up (bounded — the async-safety
1716
+ * rules ban open-ended polling) and re-arm the pending key.
1717
+ */
1718
+ const CONFIRM_DELAYS_MS = [
1719
+ ...Array.from({ length: 25 }, () => 20),
1720
+ 500,
1721
+ 1e3,
1722
+ 2e3,
1723
+ 4e3,
1724
+ 8e3
1725
+ ];
1726
+ const confirmAndEmit = (file, environment, attempt = 0, structural = false) => {
1727
+ if (!isTransformed || isTransformed(file, environment)) {
1728
+ emitTransformed(file, environment, structural);
1729
+ return;
1730
+ }
1731
+ if (attempt >= CONFIRM_DELAYS_MS.length) {
1732
+ if (!structural) pending.add(key(file, environment));
1733
+ return;
1734
+ }
1735
+ setTimeout(() => confirmAndEmit(file, environment, attempt + 1, structural), CONFIRM_DELAYS_MS[attempt]);
1736
+ };
1737
+ return {
1738
+ onHotUpdate(update) {
1739
+ const file = normalizePath(canonicalize(update.file));
1740
+ if (!isScannableSourceFile(file, root, extensions)) return;
1741
+ if (update.type === "delete") pending.delete(key(file, update.environment));
1742
+ else pending.add(key(file, update.environment));
1743
+ send({
1744
+ type: "custom",
1745
+ event: GRAPH_UPDATE_EVENT,
1746
+ data: {
1747
+ file,
1748
+ event: update.type,
1749
+ environment: update.environment,
1750
+ phase: "invalidated"
1751
+ }
1752
+ });
1753
+ },
1754
+ onTransform(id, environment) {
1755
+ const file = normalizePath(id.split("?")[0]);
1756
+ if (file.includes("virtual:timber-route-manifest")) {
1757
+ confirmAndEmit(file, environment, 25, true);
1758
+ return;
1759
+ }
1760
+ const pendingKey = key(file, environment);
1761
+ if (!pending.has(pendingKey)) return;
1762
+ pending.delete(pendingKey);
1763
+ confirmAndEmit(file, environment);
1764
+ }
1765
+ };
1766
+ }
1767
+ //#endregion
1768
+ //#region src/server/compress.ts
1769
+ /**
1770
+ * MIME types that benefit from compression.
1771
+ * text/* is handled via prefix matching; these are the specific
1772
+ * application/* and image/* types that are compressible.
1773
+ */
1774
+ var COMPRESSIBLE_TYPES = /* @__PURE__ */ new Set([
1775
+ "text/html",
1776
+ "text/css",
1777
+ "text/plain",
1778
+ "text/xml",
1779
+ "text/javascript",
1780
+ RSC_CONTENT_TYPE,
1781
+ "application/json",
1782
+ "application/javascript",
1783
+ "application/xml",
1784
+ "application/xhtml+xml",
1785
+ "application/rss+xml",
1786
+ "application/atom+xml",
1787
+ "image/svg+xml"
1788
+ ]);
1789
+ /**
1790
+ * Status codes that should never be compressed (no body or special semantics).
1791
+ */
1792
+ var NO_COMPRESS_STATUSES = /* @__PURE__ */ new Set([204, 304]);
1793
+ /**
1794
+ * Parse Accept-Encoding and return the best supported encoding.
1795
+ * Returns 'gzip' if the client accepts it, null otherwise.
1796
+ *
1797
+ * Brotli (br) is intentionally not handled at the application level.
1798
+ * At the streaming-friendly quality levels (0–4), brotli's compression
1799
+ * ratio advantage over gzip is marginal, and node:zlib's brotli transform
1800
+ * buffers output internally — turning smooth streaming responses into
1801
+ * large infrequent bursts. Brotli's real wins come from offline/static
1802
+ * compression at higher quality levels (5–11), which CDNs and reverse
1803
+ * proxies (Cloudflare, nginx, Caddy) apply on cached responses.
1804
+ *
1805
+ * See design/25-production-deployments.md.
1806
+ */
1807
+ function negotiateEncoding(acceptEncoding) {
1808
+ if (!acceptEncoding) return null;
1809
+ const parts = acceptEncoding.split(",");
1810
+ for (const part of parts) {
1811
+ const [token, ...params] = part.split(";");
1812
+ if (token.trim().toLowerCase() !== "gzip") continue;
1813
+ let qValue = 1;
1814
+ for (const param of params) {
1815
+ const trimmed = param.trim().toLowerCase();
1816
+ if (trimmed.startsWith("q=")) {
1817
+ qValue = parseFloat(trimmed.slice(2));
1818
+ if (Number.isNaN(qValue)) qValue = 1;
1819
+ break;
1820
+ }
1821
+ }
1822
+ if (qValue > 0) return "gzip";
1823
+ }
1824
+ return null;
1825
+ }
1826
+ /**
1827
+ * Determine if a response should be compressed.
1828
+ *
1829
+ * Returns false for:
1830
+ * - Responses without a body (204, 304, null body)
1831
+ * - Already-encoded responses (Content-Encoding set)
1832
+ * - Non-compressible content types (images, binary)
1833
+ * - SSE streams (text/event-stream — must not be buffered)
1834
+ */
1835
+ function shouldCompress(response) {
1836
+ if (!response.body) return false;
1837
+ if (NO_COMPRESS_STATUSES.has(response.status)) return false;
1838
+ if (response.headers.has("Content-Encoding")) return false;
1839
+ const contentType = response.headers.get("Content-Type");
1840
+ if (!contentType) return false;
1841
+ const mimeType = contentType.split(";")[0].trim().toLowerCase();
1842
+ if (mimeType === "text/event-stream") return false;
1843
+ return COMPRESSIBLE_TYPES.has(mimeType);
1844
+ }
1845
+ /**
1846
+ * Compress a Web Response if the client supports it and the content is compressible.
1847
+ *
1848
+ * Returns the original response unchanged if compression is not applicable.
1849
+ * Returns a new Response with the compressed body, Content-Encoding, and Vary headers.
1850
+ *
1851
+ * The body is piped through a compression stream — no buffering of the full response.
1852
+ * This preserves streaming behavior for HTML shell + deferred Suspense chunks.
1853
+ */
1854
+ function compressResponse(request, response) {
1855
+ if (!shouldCompress(response)) return response;
1856
+ const encoding = negotiateEncoding(request.headers.get("Accept-Encoding") ?? "");
1857
+ if (!encoding) return response;
1858
+ const compressedBody = compressWithGzip(response.body);
1859
+ const headers = new Headers(response.headers);
1860
+ headers.set("Content-Encoding", encoding);
1861
+ headers.delete("Content-Length");
1862
+ const existingVary = headers.get("Vary");
1863
+ if (existingVary) {
1864
+ if (!existingVary.toLowerCase().includes("accept-encoding")) headers.set("Vary", `${existingVary}, Accept-Encoding`);
1865
+ } else headers.set("Vary", "Accept-Encoding");
1866
+ return new Response(compressedBody, {
1867
+ status: response.status,
1868
+ statusText: response.statusText,
1869
+ headers
1870
+ });
1871
+ }
1872
+ /**
1873
+ * Compress a ReadableStream with gzip, flushing each chunk immediately.
1874
+ *
1875
+ * Uses node:zlib's createGzip with Z_SYNC_FLUSH to ensure each HTML chunk
1876
+ * (shell, Suspense resolution, RSC payload) is delivered to the browser
1877
+ * as soon as it's available — preserving streaming semantics.
1878
+ */
1879
+ function compressWithGzip(body) {
1880
+ const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
1881
+ const nodeInput = Readable.fromWeb(body);
1882
+ pipeline(nodeInput, gzip, () => {});
1883
+ return Readable.toWeb(gzip);
1884
+ }
1885
+ //#endregion
1886
+ //#region src/plugins/dev-server.ts
1887
+ var dev_server_exports = /* @__PURE__ */ __exportAll({
1888
+ RESTART_EXIT_CODE: () => 75,
1889
+ timberDevServer: () => timberDevServer,
1890
+ toWebRequest: () => toWebRequest
1891
+ });
1892
+ var RSC_ENTRY_ID = "virtual:timber-rsc-entry";
1893
+ /**
1894
+ * Config file names that trigger a full dev server restart when changed.
1895
+ * See 21-dev-server.md §HMR Wiring — config is loaded once at startup.
1896
+ */
1897
+ var CONFIG_FILE_NAMES = [
1898
+ "timber.config.ts",
1899
+ "timber.config.js",
1900
+ "timber.config.mjs"
1901
+ ];
1902
+ /**
1903
+ * URL prefixes that are Vite-internal and should never be intercepted.
1904
+ * These are passed through to Vite's built-in middleware.
1905
+ */
1906
+ var VITE_INTERNAL_PREFIXES = [
1907
+ "/@",
1908
+ "/__vite",
1909
+ "/node_modules/"
1910
+ ];
1911
+ /**
1912
+ * File extensions that indicate static asset requests.
1913
+ * These are passed through to Vite's static file serving.
1914
+ */
1915
+ var ASSET_EXTENSIONS = /\.(?:js|ts|tsx|jsx|css|map|json|svg|png|jpg|jpeg|gif|webp|avif|ico|woff|woff2|ttf|eot|mp4|webm|ogg|mp3|wav)(?:\?.*)?$/;
1916
+ /**
1917
+ * Create the timber-dev-server Vite plugin.
1918
+ *
1919
+ * Hook: configureServer (returns post-hook to register after Vite's middleware)
1920
+ */
1921
+ function timberDevServer(ctx) {
1922
+ let graphChannel = null;
1923
+ return {
1924
+ name: "timber-dev-server",
1925
+ apply: "serve",
1926
+ /**
1927
+ * Push channel for the graph endpoint, phase 1: `invalidated`, one
1928
+ * per (file, environment). Emitted from hotUpdate — which runs
1929
+ * AFTER Vite invalidates the environment module graphs — never
1930
+ * from a raw watcher listener, which would race consumers into
1931
+ * querying pre-invalidation memberships (design/47 §"Dev-server
1932
+ * endpoint").
1933
+ */
1934
+ hotUpdate({ file, type }) {
1935
+ graphChannel?.onHotUpdate({
1936
+ file,
1937
+ type,
1938
+ environment: this.environment.name
1939
+ });
1940
+ },
1941
+ /**
1942
+ * Push channel phase 2: `transformed`, when a pending-invalidated
1943
+ * module's import edges are actually rebuilt. A Set lookup for
1944
+ * everything without a pending invalidation — free on the hot
1945
+ * path. Returning nothing leaves the code untouched.
1946
+ */
1947
+ transform(_code, id) {
1948
+ graphChannel?.onTransform(id, this.environment.name);
1949
+ },
1950
+ /**
1951
+ * Register the dev server middleware and config file watcher.
1952
+ *
1953
+ * Registers as a pre-hook (no return value) so our middleware runs
1954
+ * before Vite's built-in SPA fallback / historyApiFallback. This
1955
+ * ensures we see the original URL (e.g. /blog) rather than a
1956
+ * rewritten /index.html. Vite-internal and asset requests are
1957
+ * filtered out explicitly and passed through to Vite.
1958
+ */
1959
+ async configureServer(server) {
1960
+ const configPaths = CONFIG_FILE_NAMES.map((name) => join(ctx.root, name));
1961
+ server.watcher.add(configPaths);
1962
+ const configPathSet = new Set(configPaths);
1963
+ const hasSupervisor = Boolean(process.env.__TIMBER_DEV_WORKER);
1964
+ const onConfigChange = (filePath) => {
1965
+ if (!configPathSet.has(filePath)) return;
1966
+ if (hasSupervisor) {
1967
+ console.log(`\n[timber] ${basename(filePath)} changed — restarting dev server...\n`);
1968
+ process.exit(75);
1969
+ } else {
1970
+ console.log(`\n[timber] ${basename(filePath)} changed — restarting dev server...\n[timber] Run \`timber dev\` instead of \`vite dev\` for guaranteed fresh config on restart.\n`);
1971
+ server.restart();
1972
+ }
1973
+ };
1974
+ server.watcher.on("change", onConfigChange);
1975
+ server.watcher.on("add", onConfigChange);
1976
+ server.watcher.on("unlink", onConfigChange);
1977
+ listenForClientErrors(server, ctx.root);
1978
+ server.middlewares.use(createGraphEndpointMiddleware(server, ctx));
1979
+ graphChannel = makeGraphUpdateChannel(ctx, (payload) => server.hot.send(payload), (file, environment) => {
1980
+ const graph = server.environments[environment]?.moduleGraph;
1981
+ if (!graph) return false;
1982
+ const nodes = graph.getModulesByFile(file);
1983
+ if (nodes) {
1984
+ for (const node of nodes) if (node.transformResult != null) return true;
1985
+ return false;
1986
+ }
1987
+ return graph.getModuleById(file)?.transformResult != null;
1988
+ }, (file) => {
1989
+ if (server.config?.resolve?.preserveSymlinks || file.includes("virtual:")) return file;
1990
+ try {
1991
+ return realpathSync(file);
1992
+ } catch {
1993
+ try {
1994
+ return join(realpathSync(dirname(file)), basename(file));
1995
+ } catch {
1996
+ return file;
1997
+ }
1998
+ }
1999
+ });
2000
+ server.middlewares.use(createTimberMiddleware(server, ctx));
2001
+ registerDevDiscovery(server.httpServer, ctx.root, server.config);
2002
+ if (ctx.holdingServer) {
2003
+ await ctx.holdingServer.close().catch(() => {});
2004
+ ctx.holdingServer = null;
2005
+ }
2006
+ ctx.timer.end("dev-server-setup");
2007
+ const summary = ctx.timer.formatSummary();
2008
+ if (summary) console.log(summary);
2009
+ }
2010
+ };
2011
+ }
2012
+ /**
2013
+ * Create the Connect middleware that routes requests through the timber pipeline.
2014
+ *
2015
+ * For route requests (HTML pages, API endpoints), the middleware:
2016
+ * 1. Loads the RSC entry via ssrLoadModule
2017
+ * 2. Converts the Node request to a Web Request
2018
+ * 3. Passes it through the RSC handler (which runs the full pipeline)
2019
+ * 4. Converts the Web Response back to a Node response
2020
+ *
2021
+ * For non-route requests (assets, Vite internals, HMR), the middleware
2022
+ * calls next() to let Vite handle them.
2023
+ */
2024
+ function createTimberMiddleware(server, ctx) {
2025
+ const projectRoot = ctx.root;
2026
+ const hmrOptions = extractHmrOptions(server.config);
2027
+ const debugRegistry = new DebugChannelRegistry();
2028
+ server.hot.on(DEBUG_SUBSCRIBE_EVENT, (data, client) => {
2029
+ const msg = data;
2030
+ if (msg?.id) debugRegistry.subscribe(msg.id, (event, payload) => {
2031
+ client.send(event, payload);
2032
+ });
2033
+ });
2034
+ return async (req, res, next) => {
2035
+ const url = req.url;
2036
+ if (!url) {
2037
+ next();
2038
+ return;
2039
+ }
2040
+ if (isViteInternal(url)) {
2041
+ next();
2042
+ return;
2043
+ }
2044
+ if (isPublicFile(server, url)) {
2045
+ next();
2046
+ return;
2047
+ }
2048
+ if (isAssetRequest(url) && !isMetadataRouteServePath(url) && !isRouteOwnedAsset(ctx.routeTree, server, url)) {
2049
+ next();
2050
+ return;
2051
+ }
2052
+ let handler;
2053
+ try {
2054
+ const rscEnv = server.environments.rsc;
2055
+ if (!rscEnv?.runner?.import) throw new Error("[timber] RSC environment is not runnable");
2056
+ const rscModule = await rscEnv.runner.import(RSC_ENTRY_ID);
2057
+ handler = rscModule.default;
2058
+ const config = rscModule.pipelineConfig;
2059
+ if (config) {
2060
+ config.devPipelineErrorHandler = (error, _phase, debugComponents) => {
2061
+ sendErrorToOverlay(server, error, classifyErrorPhase(error, projectRoot), projectRoot, debugComponents);
2062
+ };
2063
+ config.devHmrOptions = hmrOptions;
2064
+ config.devDebugRegistry = debugRegistry;
2065
+ }
2066
+ const setSourceMap = rscModule.setDevSourceMapHandler;
2067
+ if (typeof setSourceMap === "function") setSourceMap((error) => {
2068
+ fixErrorStacktrace(server, error);
2069
+ });
2070
+ } catch (error) {
2071
+ if (error instanceof Error) {
2072
+ addTimberContext(error);
2073
+ sendErrorToOverlay(server, error, "module-transform", projectRoot);
2074
+ }
2075
+ respond500(res, error, "module-transform", projectRoot, hmrOptions);
2076
+ return;
2077
+ }
2078
+ if (typeof handler !== "function") {
2079
+ console.error("[timber] RSC entry module does not export a default function");
2080
+ next();
2081
+ return;
2082
+ }
2083
+ try {
2084
+ const webRequest = toWebRequest(req);
2085
+ const wrapper = server[Symbol.for("timber:dev-request-wrapper")];
2086
+ const finalResponse = compressResponse(webRequest, wrapper ? await wrapper(() => handler(webRequest)) : await handler(webRequest));
2087
+ res.writeHead(finalResponse.status, [...finalResponse.headers].flat());
2088
+ res.flushHeaders();
2089
+ await sendNodeResponse(res, finalResponse);
2090
+ } catch (error) {
2091
+ if (error instanceof Error) {
2092
+ const displayError = isSsrStreamError(error) && error.cause instanceof Error ? error.cause : error;
2093
+ const phase = classifyErrorPhase(displayError, projectRoot);
2094
+ sendErrorToOverlay(server, displayError, phase, projectRoot);
2095
+ respond500(res, displayError, phase, projectRoot, hmrOptions);
2096
+ } else {
2097
+ process.stderr.write(`\x1b[31m[timber] Dev server error:\x1b[0m ${String(error)}\n`);
2098
+ respond500(res, error, "render", projectRoot, hmrOptions);
2099
+ }
2100
+ }
2101
+ };
2102
+ }
2103
+ /**
2104
+ * Send a 500 response without crashing the dev server.
2105
+ *
2106
+ * In dev mode, renders a styled HTML error page with source context,
2107
+ * classified stack trace, and auto-reload on HMR. Falls back to
2108
+ * text/plain if the HTML generator fails (must never crash).
2109
+ */
2110
+ function respond500(res, error, phase, projectRoot, hmrOptions) {
2111
+ if (res.headersSent) return;
2112
+ if (error instanceof Error) try {
2113
+ const html = generateDevErrorPage(error, phase, projectRoot, hmrOptions);
2114
+ res.statusCode = 500;
2115
+ res.setHeader("content-type", "text/html; charset=utf-8");
2116
+ res.end(html);
2117
+ return;
2118
+ } catch (e) {
2119
+ console.debug("[timber] dev error page generation failed, falling back to text/plain:", e instanceof Error ? e.message : e);
2120
+ }
2121
+ res.statusCode = 500;
2122
+ res.setHeader("content-type", "text/plain");
2123
+ res.end(`[timber] Internal server error\n\n${error instanceof Error ? error.stack ?? error.message : String(error)}`);
2124
+ }
2125
+ /**
2126
+ * Add timber-specific context to an error's message if it references
2127
+ * internal virtual modules (virtual:timber-*). Mutates the error in place.
2128
+ */
2129
+ function addTimberContext(error) {
2130
+ const enriched = addVirtualModuleContext(error.message);
2131
+ if (enriched !== error.message) error.message = enriched;
2132
+ }
2133
+ /**
2134
+ * Convert a Node IncomingMessage to a Web Request.
2135
+ *
2136
+ * Constructs the full URL from the Host header and request URL,
2137
+ * and forwards the method, headers, and body.
2138
+ *
2139
+ * The scheme comes from the socket's TLS state — with `server.https` the
2140
+ * browser sends `Origin: https://host`, and a hardcoded `http://` would
2141
+ * fail CSRF origin comparison on every POST. X-Forwarded-Proto is NOT
2142
+ * consulted: dev connections are direct, so the header would be
2143
+ * attacker-controlled. (TIM-1067, B32)
2144
+ *
2145
+ * Exported for tests.
2146
+ */
2147
+ function toWebRequest(nodeReq) {
2148
+ const protocol = nodeReq.socket?.encrypted ? "https" : "http";
2149
+ const host = nodeReq.headers.host ?? nodeReq.headers[":authority"] ?? "localhost";
2150
+ const url = `${protocol}://${host}${nodeReq.url}`;
2151
+ const headers = new Headers();
2152
+ for (const [key, value] of Object.entries(nodeReq.headers)) {
2153
+ if (value === void 0) continue;
2154
+ if (key.startsWith(":")) continue;
2155
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
2156
+ else headers.set(key, value);
2157
+ }
2158
+ if (!headers.has("host")) headers.set("host", host);
2159
+ const method = nodeReq.method ?? "GET";
2160
+ const hasBody = method !== "GET" && method !== "HEAD";
2161
+ return new Request(url, {
2162
+ method,
2163
+ headers,
2164
+ body: hasBody ? nodeReadableToWebStream(nodeReq) : void 0,
2165
+ duplex: hasBody ? "half" : void 0
2166
+ });
2167
+ }
2168
+ /**
2169
+ * Convert a Node Readable stream to a Web ReadableStream.
2170
+ */
2171
+ function nodeReadableToWebStream(nodeStream) {
2172
+ return new ReadableStream({ start(controller) {
2173
+ nodeStream.on("data", (chunk) => {
2174
+ controller.enqueue(new Uint8Array(chunk));
2175
+ });
2176
+ nodeStream.on("end", () => {
2177
+ controller.close();
2178
+ });
2179
+ nodeStream.on("error", (err) => {
2180
+ controller.error(err);
2181
+ });
2182
+ } });
2183
+ }
2184
+ /**
2185
+ * Check if a URL is a Vite-internal request that should be passed through.
2186
+ */
2187
+ function isViteInternal(url) {
2188
+ return VITE_INTERNAL_PREFIXES.some((prefix) => url.startsWith(prefix));
2189
+ }
2190
+ /**
2191
+ * Check if a URL looks like a static asset request.
2192
+ */
2193
+ function isAssetRequest(url) {
2194
+ return ASSET_EXTENSIONS.test(url);
2195
+ }
2196
+ /**
2197
+ * Check if a URL maps to a real file in the project's public/ directory.
2198
+ * Vite serves public/ files as static assets — if a file exists there it
2199
+ * must be passed through to Vite regardless of extension.
2200
+ */
2201
+ function isPublicFile(server, url) {
2202
+ const { publicDir } = server.config;
2203
+ if (!publicDir) return false;
2204
+ const pathname = url.split("?")[0];
2205
+ const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
2206
+ if (!relativePath) return false;
2207
+ const fullPath = join(publicDir, relativePath);
2208
+ try {
2209
+ return statSync(fullPath).isFile();
2210
+ } catch {
2211
+ return false;
2212
+ }
2213
+ }
2214
+ /**
2215
+ * Check whether an asset-extension URL belongs to a scanned route rather
2216
+ * than a static file.
2217
+ *
2218
+ * Route handlers may live at paths ending in asset-like extensions
2219
+ * (app/feed.json/route.ts → /feed.json), which the asset bypass would
2220
+ * otherwise hand to Vite — 404ing in dev while working in production.
2221
+ * Uses the same canonicalization and tree walker as the runtime matcher
2222
+ * so dev classification agrees with what the pipeline would match.
2223
+ *
2224
+ * A file that actually exists on disk wins over the route match: the
2225
+ * production server serves public/ statics before the RSC handler (see
2226
+ * adapters/nitro.ts), and Vite serves root-relative source files (e.g.
2227
+ * /app/global.css) in dev. Without this, a broad catch-all route like
2228
+ * app/[...slug] would shadow every static asset. Joining the canonical
2229
+ * pathname is traversal-safe — canonicalize() resolves `..` segments and
2230
+ * rejects root escapes and encoded separators.
2231
+ *
2232
+ * Reads ctx.routeTree per request — the routing plugin rescans it on
2233
+ * file add/unlink/change, so newly created routes are picked up live.
2234
+ */
2235
+ function isRouteOwnedAsset(routeTree, server, url) {
2236
+ if (!routeTree) return false;
2237
+ const rawPathname = url.split("?")[0];
2238
+ const result = canonicalize(rawPathname);
2239
+ if (!result.ok) return false;
2240
+ const parts = result.pathname === "/" ? [] : result.pathname.slice(1).split("/");
2241
+ if (matchUrlParts(routeTree.root, parts) === null) return false;
2242
+ const relativePath = result.pathname.slice(1);
2243
+ const { publicDir, root } = server.config;
2244
+ if (publicDir && existsSync(join(publicDir, relativePath))) return false;
2245
+ if (existsSync(join(root, relativePath))) return false;
2246
+ return true;
2247
+ }
2248
+ /**
2249
+ * Listen for client-side errors forwarded from the browser via HMR.
2250
+ *
2251
+ * The browser entry catches uncaught errors and unhandled rejections,
2252
+ * then sends them as 'timber:client-error' custom events. We parse
2253
+ * the first app frame for the overlay's loc field and forward the
2254
+ * error to Vite's overlay protocol.
2255
+ */
2256
+ function listenForClientErrors(server, projectRoot) {
2257
+ server.hot.on("timber:client-error", (data) => {
2258
+ const loc = parseFirstAppFrame(data.stack, projectRoot);
2259
+ let message = data.message;
2260
+ if (data.componentStack) message = `${data.message}\n\nComponent Stack:\n${data.componentStack.trim()}`;
2261
+ const RED = "\x1B[31m";
2262
+ const BOLD = "\x1B[1m";
2263
+ const RESET = "\x1B[0m";
2264
+ process.stderr.write(`${RED}${BOLD}[timber] Client Error${RESET}\n${RED}${data.message}${RESET}\n\n`);
2265
+ try {
2266
+ server.hot.send({
2267
+ type: "error",
2268
+ err: {
2269
+ message,
2270
+ stack: data.stack,
2271
+ id: loc?.file,
2272
+ plugin: "timber (Client)",
2273
+ loc: loc ? {
2274
+ file: loc.file,
2275
+ line: loc.line,
2276
+ column: loc.column
2277
+ } : void 0
2278
+ }
2279
+ });
2280
+ } catch (e) {
2281
+ console.debug("[timber] overlay send failed:", e instanceof Error ? e.message : e);
2282
+ }
2283
+ });
2284
+ }
2285
+ //#endregion
2286
+ export { formatMissingPeers as a, formatConfigErrors as i, timberDevServer as n, validateConfig as o, checkPeerDependencies as r, dev_server_exports as t };
2287
+
2288
+ //# sourceMappingURL=dev-server-DioP7tkQ.js.map