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

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 (143) 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-dev.d.ts +5 -1
  53. package/dist/client/browser-dev.d.ts.map +1 -1
  54. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  55. package/dist/client/browser-entry/action-queue.d.ts +20 -1
  56. package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
  57. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  58. package/dist/client/error-boundary.d.ts +3 -15
  59. package/dist/client/error-boundary.d.ts.map +1 -1
  60. package/dist/client/error-boundary.js +1 -1
  61. package/dist/client/error-reconstituter.d.ts +4 -4
  62. package/dist/client/error-reconstituter.d.ts.map +1 -1
  63. package/dist/client/internal.js +1 -1
  64. package/dist/dev-tools/debug-channel.d.ts +55 -0
  65. package/dist/dev-tools/debug-channel.d.ts.map +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +302 -2245
  68. package/dist/index.js.map +1 -1
  69. package/dist/plugins/dev-server.d.ts +7 -0
  70. package/dist/plugins/dev-server.d.ts.map +1 -1
  71. package/dist/plugins/mdx.d.ts.map +1 -1
  72. package/dist/routing/convention-lint.d.ts.map +1 -1
  73. package/dist/routing/index.js +2 -2
  74. package/dist/routing/scanner.d.ts +4 -4
  75. package/dist/routing/scanner.d.ts.map +1 -1
  76. package/dist/server/access-gate.d.ts +2 -2
  77. package/dist/server/deny-boundary.d.ts +3 -5
  78. package/dist/server/deny-boundary.d.ts.map +1 -1
  79. package/dist/server/deny-renderer.d.ts +0 -1
  80. package/dist/server/deny-renderer.d.ts.map +1 -1
  81. package/dist/server/error-boundary-wrapper.d.ts +5 -12
  82. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  83. package/dist/server/index.js +2 -2
  84. package/dist/server/internal.js +20 -195
  85. package/dist/server/internal.js.map +1 -1
  86. package/dist/server/pipeline.d.ts +8 -0
  87. package/dist/server/pipeline.d.ts.map +1 -1
  88. package/dist/server/primitives.d.ts +15 -13
  89. package/dist/server/primitives.d.ts.map +1 -1
  90. package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
  91. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  92. package/dist/server/rsc-entry/helpers.d.ts +6 -27
  93. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  94. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  95. package/dist/server/rsc-entry/render-route.d.ts +1 -0
  96. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
  98. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  99. package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
  100. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  101. package/dist/server/slot-resolver.d.ts.map +1 -1
  102. package/dist/server/slot-subtree-contain.d.ts +1 -1
  103. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  104. package/docs/api/30-api-server.mdx +5 -5
  105. package/docs/api/34-api-config.mdx +2 -2
  106. package/docs/api/36-cli.mdx +1 -1
  107. package/docs/learn/12-error-handling.mdx +7 -8
  108. package/package.json +2 -3
  109. package/src/cli.ts +62 -8
  110. package/src/client/browser-dev.ts +35 -3
  111. package/src/client/browser-entry/action-dispatch.ts +12 -0
  112. package/src/client/browser-entry/action-queue.ts +70 -8
  113. package/src/client/browser-entry/rsc-stream.ts +78 -22
  114. package/src/client/error-boundary.tsx +13 -39
  115. package/src/client/error-reconstituter.tsx +5 -5
  116. package/src/dev-tools/debug-channel.ts +151 -0
  117. package/src/index.ts +8 -12
  118. package/src/plugins/dev-server.ts +58 -7
  119. package/src/plugins/mdx.ts +2 -1
  120. package/src/routing/convention-lint.ts +4 -111
  121. package/src/routing/scanner.ts +33 -22
  122. package/src/server/access-gate.tsx +3 -3
  123. package/src/server/deny-boundary.ts +5 -23
  124. package/src/server/deny-renderer.ts +4 -7
  125. package/src/server/error-boundary-wrapper.ts +11 -35
  126. package/src/server/pipeline.ts +9 -0
  127. package/src/server/primitives.ts +20 -26
  128. package/src/server/rsc-entry/error-renderer.ts +15 -43
  129. package/src/server/rsc-entry/helpers.ts +10 -67
  130. package/src/server/rsc-entry/index.ts +1 -0
  131. package/src/server/rsc-entry/render-route.ts +12 -1
  132. package/src/server/rsc-entry/rsc-stream.ts +27 -24
  133. package/src/server/rsc-entry/ssr-renderer.ts +7 -1
  134. package/src/server/slot-resolver.ts +29 -7
  135. package/src/server/slot-subtree-contain.ts +13 -4
  136. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
  137. package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
  138. package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
  139. package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
  140. package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
  141. package/dist/server/utils/mdx-file.d.ts +0 -17
  142. package/dist/server/utils/mdx-file.d.ts.map +0 -1
  143. package/src/server/utils/mdx-file.ts +0 -22
package/dist/index.js CHANGED
@@ -1,2211 +1,113 @@
1
- import { l as registerDevDiscovery } from "./_chunks/graph-cache-CP4GEmf9.js";
2
- import { n as isScannableSourceFile, t as buildSourceExtensions } from "./_chunks/scan-BoP17W3w.js";
3
- import { t as RSC_CONTENT_TYPE } from "./_chunks/rsc-media-type-DDc7duTD.js";
4
- import { M as isSsrStreamError, h as swallow } from "./_chunks/logger-uLBuGKDI.js";
5
- import { c as isMetadataRouteServePath, s as isDynamicMetadataExtension } from "./_chunks/segment-keys-lqtdookO.js";
6
- import { r as canonicalize } from "./_chunks/canonicalize-CgHoscYO.js";
7
- import { i as matchUrlParts, n as scanRoutes, r as collectInterceptionRewrites } from "./_chunks/scanner-AiazgH_f.js";
8
- import { c as writeRouteCodegen, i as getPrerenderExport, l as buildDirToRouteMap, n as formatConventionWarnings, r as lintConventions, s as flushCodegenWrites, t as checkAppDirExists, u as MDX_EXTENSIONS } from "./_chunks/convention-lint-fRkwVwEH.js";
1
+ import { a as formatMissingPeers, i as formatConfigErrors, n as timberDevServer, o as validateConfig, r as checkPeerDependencies } from "./_chunks/dev-server-DioP7tkQ.js";
2
+ import { h as swallow } from "./_chunks/logger-DqJ2VoAY.js";
3
+ import "./_chunks/csp-nonce-hOGniaG4.js";
4
+ import { a as isDynamicMetadataExtension } from "./_chunks/metadata-routes-DSDjM_hJ.js";
5
+ import { n as scanRoutes, r as collectInterceptionRewrites } from "./_chunks/scanner-B_tnqFcF.js";
6
+ import { c as writeRouteCodegen, i as getPrerenderExport, l as buildDirToRouteMap, n as formatConventionWarnings, r as lintConventions, s as flushCodegenWrites, t as checkAppDirExists } from "./_chunks/convention-lint-DHOFvX5s.js";
9
7
  import { c as resolveClientJavascript, i as mergeFileConfig, l as createNoopTimer, o as resolveAppDir, r as loadTimberConfigFile, s as resolveBuildDir, t as createPluginContext } from "./_chunks/plugin-context-8idfR3UZ.js";
10
- import { r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-B6XUtmqK.js";
8
+ import { r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-Cm3PC5JT.js";
11
9
  import { i as poisonPillSource, r as markerFor, t as EXPLICIT_MARKERS } from "./_chunks/poison-rules-DoEhbqaY.js";
12
10
  import { i as withBasePath, n as normalizeBasePath, t as isRelativeBase } from "./_chunks/base-path-DaQrzbez.js";
13
- import { t as EMPTY_BUILD_MANIFEST } from "./_chunks/build-manifest-DTmSGLRz.js";
14
11
  import { t as rscPayloadPath } from "./_chunks/rsc-payload-path-B_LBodc2.js";
15
12
  import { a as REDIRECTS_FILE, n as detectFsIdentity, o as RESERVED_OUTPUT_PATHS, r as fsKey, t as EXACT_FS_IDENTITY } from "./_chunks/fs-identity-D7vb6GdI.js";
13
+ import { t as EMPTY_BUILD_MANIFEST } from "./_chunks/build-manifest-DWppEdLB.js";
16
14
  import { t as fnv1aHash } from "./_chunks/fast-hash-C-tVYrLb.js";
17
15
  import { t as formatSize } from "./_chunks/format-BKclgVbk.js";
18
- import { createLogger, normalizePath, parseAst } from "vite";
16
+ import { createLogger, parseAst } from "vite";
19
17
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, win32 } from "node:path";
20
18
  import { builtinModules, createRequire } from "node:module";
21
19
  import react from "@vitejs/plugin-react";
22
- import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
20
+ import { existsSync, readFileSync } from "node:fs";
23
21
  import nodeHTTP from "node:http";
24
- import { Readable, pipeline } from "node:stream";
25
- import { pipeline as pipeline$1 } from "node:stream/promises";
26
- import "node:https";
27
- import "node:http2";
28
22
  import { fileURLToPath, pathToFileURL } from "node:url";
29
- import { styleText } from "node:util";
30
23
  import { createHash, randomUUID } from "node:crypto";
31
- import { copyFile, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
32
- import { isIP } from "node:net";
33
- import { constants, createGzip, gzipSync } from "node:zlib";
24
+ import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
25
+ import { gzipSync } from "node:zlib";
34
26
  //#region src/plugins/content.ts
35
- var CONFIG_FILE_NAMES$1 = [
36
- "content-collections.ts",
37
- "content-collections.js",
38
- "content-collections.mts",
39
- "content-collections.mjs"
40
- ];
41
- /**
42
- * Find the content-collections config file at the project root.
43
- * Returns the filename (not full path) if found, otherwise undefined.
44
- */
45
- function findConfigFile(root) {
46
- for (const name of CONFIG_FILE_NAMES$1) if (existsSync(join(root, name))) return name;
47
- }
48
- /**
49
- * Create the timber-content Vite plugin.
50
- *
51
- * Delegates all content scanning, validation, code generation, and file watching
52
- * to @content-collections/vite. This plugin only handles detection and activation.
53
- */
54
- function timberContent(ctx) {
55
- let innerPlugin = null;
56
- async function activate(root) {
57
- if (innerPlugin !== null) return;
58
- const configFile = findConfigFile(root);
59
- if (!configFile) return;
60
- let createPlugin;
61
- try {
62
- const mod = await import("@content-collections/vite");
63
- createPlugin = mod.default ?? mod;
64
- } catch {
65
- throw new Error([
66
- "[timber] Content collections are enabled but @content-collections/vite is not installed.",
67
- "",
68
- "Install content-collections:",
69
- " pnpm add -D @content-collections/core @content-collections/vite",
70
- "",
71
- "For MDX content, also install:",
72
- " pnpm add -D @content-collections/mdx",
73
- "",
74
- "Content collections are activated because a content-collections.ts file exists."
75
- ].join("\n"));
76
- }
77
- if (createPlugin) innerPlugin = createPlugin({ configPath: configFile });
78
- }
79
- return {
80
- name: "timber-content",
81
- async config(config, env) {
82
- const root = config.root ?? ctx.root;
83
- ctx.timer.start("content-activate");
84
- await activate(root);
85
- ctx.timer.end("content-activate");
86
- if (!innerPlugin) return;
87
- if (typeof innerPlugin.config === "function") return innerPlugin.config.call(this, config, env);
88
- },
89
- async configResolved(config) {
90
- if (!innerPlugin) return;
91
- if (typeof innerPlugin.configResolved === "function") await innerPlugin.configResolved.call(this, config);
92
- },
93
- async buildStart(options) {
94
- if (!innerPlugin) return;
95
- if (typeof innerPlugin.buildStart === "function") await innerPlugin.buildStart.call(this, options);
96
- },
97
- async resolveId(source, importer, options) {
98
- if (!innerPlugin) return null;
99
- if (typeof innerPlugin.resolveId === "function") return innerPlugin.resolveId.call(this, source, importer, options);
100
- return null;
101
- },
102
- async load(id) {
103
- if (!innerPlugin) return null;
104
- if (typeof innerPlugin.load === "function") return innerPlugin.load.call(this, id);
105
- return null;
106
- },
107
- async transform(code, id) {
108
- if (!innerPlugin) return null;
109
- if (typeof innerPlugin.transform === "function") return innerPlugin.transform.call(this, code, id);
110
- return null;
111
- },
112
- async configureServer(server) {
113
- if (process.env.TIMBER_ANALYZE === "1") return;
114
- if (!innerPlugin) return;
115
- if (typeof innerPlugin.configureServer === "function") await innerPlugin.configureServer.call(this, server);
116
- }
117
- };
118
- }
119
- //#endregion
120
- //#region ../../node_modules/.pnpm/srvx@1.0.5/node_modules/srvx/dist/adapters/node.mjs
121
- function sendNodeResponse(nodeRes, webRes) {
122
- try {
123
- return _sendNodeResponse(nodeRes, webRes, false) || Promise.resolve();
124
- } catch (error) {
125
- return Promise.reject(error);
126
- }
127
- }
128
- function _sendNodeResponse(nodeRes, webRes, detached) {
129
- if (!webRes) {
130
- nodeRes.statusCode = 500;
131
- return endNodeResponse(nodeRes, detached);
132
- }
133
- if (webRes._toNodeResponse) {
134
- const res = webRes._toNodeResponse();
135
- if (res.body) {
136
- if (res.body instanceof ReadableStream) {
137
- writeHead(nodeRes, res.status, res.statusText, res.headers);
138
- return streamBody(res.body, nodeRes);
139
- } else if (typeof res.body?.pipe === "function") return pipeBody(res.body, nodeRes, res.status, res.statusText, res.headers);
140
- writeHead(nodeRes, res.status, res.statusText, res.headers);
141
- nodeRes.write(res.body);
142
- } else writeHead(nodeRes, res.status, res.statusText, res.headers);
143
- return endNodeResponse(nodeRes, detached);
144
- }
145
- const rawHeaders = [];
146
- for (const [key, value] of webRes.headers) rawHeaders.push(key, value);
147
- writeHead(nodeRes, webRes.status, webRes.statusText, rawHeaders);
148
- return webRes.body ? streamBody(webRes.body, nodeRes) : endNodeResponse(nodeRes, detached);
149
- }
150
- function writeHead(nodeRes, status, statusText, rawHeaders) {
151
- if (!nodeRes.headersSent) {
152
- if (nodeRes.req?.httpVersion === "2.0") nodeRes.writeHead(status, rawHeaders);
153
- else nodeRes.writeHead(status, safeStatusText(statusText), rawHeaders);
154
- }
155
- }
156
- var INVALID_REASON_PHRASE_RE = /[^\t\u0020-\u007E\u0080-\u00FF]/g;
157
- function safeStatusText(statusText) {
158
- return typeof statusText === "string" && statusText ? statusText.replace(INVALID_REASON_PHRASE_RE, "") : statusText;
159
- }
160
- function endNodeResponse(nodeRes, detached) {
161
- if (detached) {
162
- nodeRes.end();
163
- return;
164
- }
165
- return new Promise((resolve) => nodeRes.end(resolve));
166
- }
167
- function pipeBody(stream, nodeRes, status, statusText, headers) {
168
- if (nodeRes.destroyed) {
169
- stream.destroy?.();
170
- return;
171
- }
172
- if (nodeRes.req?.method === "HEAD") {
173
- if (typeof stream.destroy === "function") stream.destroy();
174
- else stream.abort?.();
175
- writeHead(nodeRes, status, statusText, headers);
176
- return endNodeResponse(nodeRes);
177
- }
178
- if (typeof stream.on !== "function" || typeof stream.destroy !== "function") {
179
- writeHead(nodeRes, status, statusText, headers);
180
- stream.pipe(nodeRes);
181
- return new Promise((resolve) => nodeRes.on("close", resolve));
182
- }
183
- if (stream.destroyed) {
184
- writeHead(nodeRes, 500, "Internal Server Error", []);
185
- return endNodeResponse(nodeRes);
186
- }
187
- return new Promise((resolve) => {
188
- function cleanup() {
189
- stream.off("error", onEarlyError);
190
- stream.off("readable", onReadable);
191
- nodeRes.off("close", onResClose);
192
- }
193
- function onEarlyError() {
194
- cleanup();
195
- stream.destroy();
196
- writeHead(nodeRes, 500, "Internal Server Error", []);
197
- endNodeResponse(nodeRes).then(resolve);
198
- }
199
- function onReadable() {
200
- cleanup();
201
- if (nodeRes.destroyed) {
202
- stream.destroy();
203
- return resolve();
204
- }
205
- writeHead(nodeRes, status, statusText, headers);
206
- pipeline$1(stream, nodeRes).catch(() => {}).then(() => resolve());
207
- }
208
- function onResClose() {
209
- cleanup();
210
- stream.destroy();
211
- resolve();
212
- }
213
- stream.once("error", onEarlyError);
214
- stream.once("readable", onReadable);
215
- nodeRes.once("close", onResClose);
216
- });
217
- }
218
- function streamBody(stream, nodeRes) {
219
- if (nodeRes.destroyed) {
220
- stream.cancel().catch(() => {});
221
- return;
222
- }
223
- if (nodeRes.req?.method === "HEAD") {
224
- stream.cancel().catch(() => {});
225
- return endNodeResponse(nodeRes);
226
- }
227
- const reader = stream.getReader();
228
- function streamCancel(error) {
229
- reader.cancel(error).catch(() => {});
230
- if (error) nodeRes.destroy(error);
231
- }
232
- function streamHandle({ done, value }) {
233
- try {
234
- if (done) nodeRes.end();
235
- else if (nodeRes.write(value)) reader.read().then(streamHandle, streamCancel);
236
- else nodeRes.once("drain", () => reader.read().then(streamHandle, streamCancel));
237
- } catch (error) {
238
- streamCancel(error instanceof Error ? error : void 0);
239
- }
240
- }
241
- nodeRes.on("close", streamCancel);
242
- nodeRes.on("error", streamCancel);
243
- reader.read().then(streamHandle, streamCancel);
244
- return reader.closed.catch(streamCancel).finally(() => {
245
- nodeRes.off("close", streamCancel);
246
- nodeRes.off("error", streamCancel);
247
- });
248
- }
249
- //#endregion
250
- //#region src/dev-tools/stack-classifier.ts
251
- /** Parse file/line/col from a stack frame line. */
252
- function parseFrame(frameLine) {
253
- const parenMatch = /\(([^)]+):(\d+):(\d+)\)/.exec(frameLine);
254
- if (parenMatch) return {
255
- file: parenMatch[1],
256
- line: Number(parenMatch[2]),
257
- col: Number(parenMatch[3])
258
- };
259
- const bareMatch = /at (\/[^:]+):(\d+):(\d+)/.exec(frameLine);
260
- if (bareMatch) return {
261
- file: bareMatch[1],
262
- line: Number(bareMatch[2]),
263
- col: Number(bareMatch[3])
264
- };
265
- return {};
266
- }
267
- /**
268
- * Classify a single stack frame line by origin.
269
- *
270
- * - 'app': user application code (in project root, not node_modules)
271
- * - 'framework': timber-app internal code
272
- * - 'internal': node_modules, Node.js internals
273
- */
274
- function classifyFrame(frameLine, projectRoot) {
275
- const trimmed = frameLine.trim();
276
- if (trimmed.includes("packages/timber-app/")) return "framework";
277
- if (trimmed.includes("node_modules/")) return "internal";
278
- if (trimmed.startsWith("at node:") || trimmed.includes("(node:")) return "internal";
279
- if (trimmed.includes(projectRoot)) return "app";
280
- return "internal";
281
- }
282
- /**
283
- * Classify all frames in a full stack trace string.
284
- *
285
- * Parses the stack, skips the first line (error message), filters to
286
- * lines starting with "at ", and returns classified frames with optional
287
- * file/line/col metadata.
288
- */
289
- function classifyStack(stack, projectRoot) {
290
- return stack.split("\n").slice(1).filter((line) => line.trim().startsWith("at ")).map((raw) => {
291
- const type = classifyFrame(raw, projectRoot);
292
- const { file, line, col } = parseFrame(raw);
293
- return {
294
- raw,
295
- type,
296
- file,
297
- line,
298
- col
299
- };
300
- });
301
- }
302
- //#endregion
303
- //#region src/dev-tools/terminal.ts
304
- /**
305
- * Terminal error formatting — boxed, color-coded error output for dev mode.
306
- *
307
- * Produces a visually scannable error block with:
308
- * - Unicode box-drawing border around the error
309
- * - Phase badge and error message
310
- * - First app frame highlighted as the primary action item
311
- * - OSC 8 clickable file:line links (VSCode terminal, iTerm2, etc.)
312
- * - Internal/framework frames collapsed with a count
313
- * - Component stack (for React render errors)
314
- *
315
- * Dev-only: this module is only imported by dev-error-overlay.ts.
316
- *
317
- * Design doc: 21-dev-server.md §"Error Overlay"
318
- */
319
- var noValidate = { validateStream: false };
320
- var style = (fmt, text) => styleText(fmt, text, noValidate);
321
- /**
322
- * Wrap text in an OSC 8 hyperlink escape sequence.
323
- *
324
- * Terminals that support OSC 8 (VSCode, iTerm2, Windows Terminal, etc.)
325
- * render this as a clickable link. Others ignore the escape sequences
326
- * and show the text normally.
327
- *
328
- * Format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
329
- */
330
- function hyperlink(text, url) {
331
- return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
332
- }
333
- /**
334
- * Format a file:line:col reference as a clickable terminal link.
335
- *
336
- * Uses file:// URLs so terminals open the file in the configured editor.
337
- * The link text is styled with cyan + underline for visibility.
338
- */
339
- function fileLink(filePath, line, col) {
340
- const display = line ? `${filePath}:${line}${col ? `:${col}` : ""}` : filePath;
341
- const base = pathToFileURL(filePath).href;
342
- return style(["cyan", "underline"], hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base));
343
- }
344
- var BOX = {
345
- topLeft: "╭",
346
- topRight: "╮",
347
- bottomLeft: "╰",
348
- bottomRight: "╯",
349
- horizontal: "─",
350
- vertical: "│"
351
- };
352
- /**
353
- * Wrap lines of text in a Unicode box with a colored left border.
354
- *
355
- * @param lines - Content lines (no ANSI length calculation — keeps it simple)
356
- * @param width - Box width (characters). Lines longer than this are not truncated.
357
- */
358
- function box(lines, borderFormat, width = 80) {
359
- const bar = BOX.horizontal.repeat(width - 2);
360
- const output = [];
361
- output.push(style(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
362
- for (const line of lines) output.push(`${style(borderFormat, BOX.vertical)} ${line}`);
363
- output.push(style(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
364
- return output.join("\n");
365
- }
366
- /**
367
- * Format an error for terminal output with a boxed layout.
368
- *
369
- * The output is designed to be scannable at a glance:
370
- * 1. Red box with phase badge and error message
371
- * 2. First app frame as a clickable link (the primary action item)
372
- * 3. App frames listed normally
373
- * 4. Internal/framework frames collapsed with count
374
- * 5. Component stack (if present)
375
- */
376
- function formatTerminalError$1(error, phase, projectRoot) {
377
- const sections = [];
378
- const componentStack = extractComponentStack(error);
379
- const loc = parseFirstAppFrame(error.stack ?? "", projectRoot);
380
- const frames = error.stack ? classifyStack(error.stack, projectRoot) : [];
381
- const appFrames = frames.filter((f) => f.type === "app");
382
- const internalCount = frames.filter((f) => f.type !== "app").length;
383
- const boxLines = [];
384
- boxLines.push(style(["red", "bold"], `${PHASE_LABELS$1[phase]} Error`));
385
- boxLines.push("");
386
- for (const msgLine of error.message.split("\n")) boxLines.push(style("red", msgLine));
387
- if (loc) {
388
- boxLines.push("");
389
- const relPath = loc.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc.file;
390
- boxLines.push(`${style("bold", "→")} ${fileLink(loc.file, loc.line, loc.column)} ${style("dim", `(${relPath})`)}`);
391
- }
392
- sections.push(box(boxLines, "red"));
393
- if (componentStack) {
394
- sections.push("");
395
- sections.push(` ${style("bold", "Component Stack:")}`);
396
- for (const csLine of componentStack.trim().split("\n")) sections.push(` ${style("dim", csLine.trim())}`);
397
- }
398
- if (appFrames.length > 0) {
399
- sections.push("");
400
- sections.push(` ${style("bold", "Application Frames:")}`);
401
- for (let i = 0; i < appFrames.length; i++) {
402
- const f = appFrames[i];
403
- if (f.file && f.line) {
404
- const prefix = i === 0 ? style("bold", "▸") : " ";
405
- sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style("dim", extractFnName(f.raw))}`);
406
- } else sections.push(` ${f.raw}`);
407
- }
408
- }
409
- if (internalCount > 0) sections.push(` ${style("dim", `… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden`)}`);
410
- sections.push("");
411
- return sections.join("\n");
412
- }
413
- /** Extract the function name from a stack frame line like " at fnName (/path:1:2)". */
414
- function extractFnName(frameLine) {
415
- const match = /at\s+(\S+)\s+\(/.exec(frameLine.trim());
416
- return match ? match[1] : "";
417
- }
418
- //#endregion
419
- //#region src/dev-tools/overlay.ts
420
- var _traceMapping = null;
421
- /**
422
- * Lazy-load @jridgewell/trace-mapping from Vite's dependency tree.
423
- * Vite bundles it internally; we resolve from Vite's package to avoid
424
- * adding a direct dependency.
425
- */
426
- function getTraceMapping() {
427
- if (_traceMapping) return _traceMapping;
428
- const vitePath = createRequire(import.meta.url).resolve("vite");
429
- _traceMapping = createRequire(vitePath)("@jridgewell/trace-mapping");
430
- return _traceMapping;
431
- }
432
- /** Labels for terminal output. */
433
- var PHASE_LABELS$1 = {
434
- "module-transform": "Module Transform",
435
- "proxy": "Proxy",
436
- "middleware": "Middleware",
437
- "access": "Access Check",
438
- "render": "RSC Render",
439
- "handler": "Route Handler"
440
- };
441
- /**
442
- * Extract the React component stack from an error, if present.
443
- * React attaches this as `componentStack` during renderToReadableStream errors.
444
- */
445
- function extractComponentStack(error) {
446
- if (error && typeof error === "object" && "componentStack" in error && typeof error.componentStack === "string") return error.componentStack;
447
- return null;
448
- }
449
- /**
450
- * Parse the first application frame from a stack trace.
451
- * Returns file/line/column for the overlay's `loc` field.
452
- */
453
- function parseFirstAppFrame(stack, projectRoot) {
454
- const lines = stack.split("\n");
455
- const parenRegex = /\(([^)]+):(\d+):(\d+)\)/;
456
- const bareRegex = /at (\/[^:]+):(\d+):(\d+)/;
457
- for (const line of lines) {
458
- if (classifyFrame(line, projectRoot) !== "app") continue;
459
- const match = parenRegex.exec(line) ?? bareRegex.exec(line);
460
- if (!match) continue;
461
- const [, file, lineNum, col] = match;
462
- if (file && lineNum && col) return {
463
- file,
464
- line: parseInt(lineNum, 10),
465
- column: parseInt(col, 10)
466
- };
467
- }
468
- return null;
469
- }
470
- /**
471
- * Classify the error phase by inspecting the error's stack trace.
472
- * Falls back to 'render' if no specific phase can be determined.
473
- */
474
- function classifyErrorPhase(error, projectRoot) {
475
- const stack = error.stack ?? "";
476
- if (extractComponentStack(error)) return "render";
477
- const appRoot = projectRoot.replace(/\/$/, "");
478
- if (stack.includes(`${appRoot}/app/`) || stack.includes("/app/")) {
479
- if (stack.includes("/middleware.ts") || stack.includes("/middleware.js")) return "middleware";
480
- if (stack.includes("/access.ts") || stack.includes("/access.js")) return "access";
481
- if (stack.includes("/route.ts") || stack.includes("/route.js")) return "handler";
482
- }
483
- return "render";
484
- }
485
- var formatTerminalError = formatTerminalError$1;
486
- /**
487
- * Format RSC debug component info into a readable string for the overlay.
488
- *
489
- * Renders the server component tree that was active when an error occurred,
490
- * including component names and source locations from stack frames. This
491
- * gives developers visibility into which server components were rendering
492
- * without exposing source code.
493
- *
494
- * Returns an empty string if no components are provided.
495
- */
496
- function formatRscDebugContext(components) {
497
- if (!components || components.length === 0) return "";
498
- const seen = /* @__PURE__ */ new Set();
499
- const unique = [];
500
- for (const c of components) if (!seen.has(c.name)) {
501
- seen.add(c.name);
502
- unique.push(c);
503
- }
504
- const lines = ["Server Component Tree:"];
505
- for (let i = 0; i < unique.length; i++) {
506
- const c = unique[i];
507
- const indent = " ".repeat(i + 1);
508
- const envLabel = c.env ? ` [${c.env}]` : "";
509
- let locStr = "";
510
- if (c.stack && c.stack.length > 0) {
511
- const frame = c.stack[0];
512
- if (Array.isArray(frame) && frame.length >= 3) locStr = ` (${frame[1]}:${frame[2]})`;
513
- }
514
- lines.push(`${indent}${c.name}${envLabel}${locStr}`);
515
- }
516
- return lines.join("\n");
517
- }
518
- /**
519
- * Dynamically compute the line offset that Vite's module runner adds
520
- * when wrapping modules in an async function.
521
- *
522
- * Vite's `calculateOffsetOnce()` uses the same technique: create a new
523
- * AsyncFunction, throw from line 1, and check where the engine reports
524
- * the error. The difference between the reported line and 1 is the offset.
525
- *
526
- * This is engine-dependent (currently 2 on Node 18-22) and could change
527
- * in future Node.js or V8 versions. Computing it at runtime ensures we
528
- * always match the actual behavior.
529
- */
530
- var _cachedOffset = null;
531
- function calculateModuleRunnerOffset() {
532
- if (_cachedOffset !== null) return _cachedOffset;
533
- try {
534
- const AsyncFunction = async function() {}.constructor;
535
- const src = new AsyncFunction("BODY").toString();
536
- const bodyIndex = src.indexOf("BODY");
537
- if (bodyIndex === -1) {
538
- _cachedOffset = 2;
539
- return _cachedOffset;
540
- }
541
- _cachedOffset = (src.slice(0, bodyIndex).match(/\n/g) || []).length;
542
- } catch (e) {
543
- console.debug("[timber] overlay offset calculation failed:", e instanceof Error ? e.message : e);
544
- _cachedOffset = 2;
545
- }
546
- return _cachedOffset;
547
- }
548
- /**
549
- * Phases where the error originated in the RSC environment.
550
- * These use `server.environments.rsc.moduleGraph` for source-mapping.
551
- */
552
- var RSC_PHASES = /* @__PURE__ */ new Set([
553
- "render",
554
- "access",
555
- "middleware",
556
- "handler"
557
- ]);
558
- /**
559
- * Rewrite an error's stack trace using the correct Vite environment module graph.
560
- *
561
- * `server.ssrFixStacktrace()` hardcodes `server.environments.ssr.moduleGraph`,
562
- * but RSC errors have stack frames pointing to modules loaded in the RSC
563
- * environment — a separate Vite module graph with separate transform results
564
- * and source maps. Using the SSR module graph silently fails to find the
565
- * modules, leaving transpiled/bundled line numbers in the stack trace.
566
- *
567
- * This function picks the RSC module graph for render-phase errors and falls
568
- * back to SSR for module-transform/proxy errors. If the first pass doesn't
569
- * rewrite any frames (e.g., mixed RSC+SSR stack), it tries the other graph.
570
- */
571
- function fixStacktraceForEnvironment(server, error, phase) {
572
- if (!error.stack) return;
573
- const primaryEnvName = RSC_PHASES.has(phase) ? "rsc" : "ssr";
574
- const fallbackEnvName = primaryEnvName === "rsc" ? "ssr" : "rsc";
575
- const primaryEnv = server.environments[primaryEnvName];
576
- const fallbackEnv = server.environments[fallbackEnvName];
577
- if (primaryEnv?.moduleGraph) {
578
- const rewritten = rewriteStacktrace(error.stack, primaryEnv.moduleGraph);
579
- if (rewritten.changed) {
580
- error.stack = rewritten.stack;
581
- return;
582
- }
583
- }
584
- if (fallbackEnv?.moduleGraph) {
585
- const rewritten = rewriteStacktrace(error.stack, fallbackEnv.moduleGraph);
586
- if (rewritten.changed) {
587
- error.stack = rewritten.stack;
588
- return;
589
- }
590
- }
591
- }
592
- /**
593
- * Rewrite stack trace frames using source maps from an environment's module graph.
594
- *
595
- * Mirrors Vite's internal `ssrRewriteStacktrace` logic but works with any
596
- * `EnvironmentModuleGraph`, not just the SSR one.
597
- *
598
- * Returns the rewritten stack and whether any frames were actually changed.
599
- */
600
- function rewriteStacktrace(stack, moduleGraph) {
601
- let changed = false;
602
- return {
603
- stack: stack.split("\n").map((line) => {
604
- return line.replace(/^ {4}at (?:(\S.*?)\s\()?(.+?):(\d+)(?::(\d+))?\)?/, (input, varName, id, lineStr, colStr) => {
605
- if (!id) return input;
606
- const rawSourceMap = moduleGraph.getModuleById(id)?.transformResult?.map;
607
- if (!rawSourceMap) return input;
608
- const OFFSET = calculateModuleRunnerOffset();
609
- const origLine = Number(lineStr) - OFFSET;
610
- const origCol = Number(colStr) - 1;
611
- if (origLine <= 0 || origCol < 0) return input;
612
- let pos;
613
- try {
614
- const { TraceMap: TM, originalPositionFor: opf } = getTraceMapping();
615
- pos = opf(new TM(rawSourceMap), {
616
- line: origLine,
617
- column: origCol
618
- });
619
- } catch (e) {
620
- console.debug("[timber] source map tracing failed:", e instanceof Error ? e.message : e);
621
- return input;
622
- }
623
- if (!pos.source || pos.line == null) return input;
624
- changed = true;
625
- const source = `${resolve(dirname(id), pos.source)}:${pos.line}:${(pos.column ?? 0) + 1}`;
626
- const trimmedVarName = varName?.trim();
627
- if (!trimmedVarName || trimmedVarName === "eval") return ` at ${source}`;
628
- return ` at ${trimmedVarName} (${source})`;
629
- });
630
- }).join("\n"),
631
- changed
632
- };
633
- }
634
- /**
635
- * Source-map an error's stack trace using the Vite dev server's module graph.
636
- *
637
- * Exported so that the fallback error renderer (fallback-error.ts) can
638
- * source-map errors before rendering the dev error page. Without this,
639
- * the dev error page shows transpiled positions (e.g. page.tsx:1:1 instead
640
- * of page.tsx:4:9).
641
- *
642
- * See TIM-811.
643
- */
644
- function fixErrorStacktrace(server, error, phase) {
645
- fixStacktraceForEnvironment(server, error, phase ?? classifyErrorPhase(error, server.config.root));
646
- }
647
- /**
648
- * Send an error to Vite's browser overlay and log it to stderr.
649
- *
650
- * Uses `server.ssrFixStacktrace()` to map stack traces back to source,
651
- * then sends the error via `server.hot.send()` for the browser overlay.
652
- *
653
- * When `rscDebugComponents` is provided (dev mode only), the server
654
- * component tree context is appended to the error message. This helps
655
- * developers identify which server component caused the error without
656
- * exposing source code.
657
- *
658
- * The dev server remains running — errors are handled, not fatal.
659
- */
660
- function sendErrorToOverlay(server, error, phase, projectRoot, rscDebugComponents) {
661
- fixStacktraceForEnvironment(server, error, phase);
662
- const formatted = formatTerminalError(error, phase, projectRoot);
663
- process.stderr.write(`${formatted}\n`);
664
- const loc = parseFirstAppFrame(error.stack ?? "", projectRoot);
665
- const componentStack = extractComponentStack(error);
666
- let message = error.message;
667
- if (componentStack) message = `${error.message}\n\nComponent Stack:\n${componentStack.trim()}`;
668
- const debugContext = formatRscDebugContext(rscDebugComponents ?? []);
669
- if (debugContext) message = `${message}\n\n${debugContext}`;
670
- try {
671
- server.hot.send({
672
- type: "error",
673
- err: {
674
- message,
675
- stack: error.stack ?? "",
676
- id: loc?.file,
677
- plugin: `timber (${PHASE_LABELS$1[phase]})`,
678
- loc: loc ? {
679
- file: loc.file,
680
- line: loc.line,
681
- column: loc.column
682
- } : void 0
683
- }
684
- });
685
- } catch (e) {
686
- console.debug("[timber] overlay error send failed:", e instanceof Error ? e.message : e);
687
- }
688
- }
689
- //#endregion
690
- //#region src/server/utils/escape-html.ts
691
- /**
692
- * Shared HTML escaping utility.
693
- *
694
- * Used by both dev-only paths (error pages, 404 page) and production
695
- * paths (deny-renderer, RSC helpers). Lives in server/utils/ because
696
- * it's not dev-only.
697
- */
698
- /**
699
- * Escape a string for safe embedding in HTML content or attributes.
700
- *
701
- * Replaces `&`, `<`, `>`, and `"` with their HTML entity equivalents.
702
- */
703
- function escapeHtml(str) {
704
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
705
- }
706
- //#endregion
707
- //#region src/dev-tools/dev-page-shell.ts
708
- /**
709
- * Shared scaffold for dev-mode HTML pages (dev error page, dev 404 page).
710
- *
711
- * Both pages are self-contained HTML documents served when the normal
712
- * render pipeline can't produce a page. They share:
713
- * - The document scaffold (head, container, footer)
714
- * - Base CSS (layout, light/dark variables, common components)
715
- * - The HMR auto-reload script that watches Vite's WebSocket and reloads
716
- * when the developer fixes the error / adds the missing route
717
- *
718
- * Dev-only: this module is only imported by dev-mode code paths. It is
719
- * never included in production builds.
720
- *
721
- * Design doc: 21-dev-server.md §"Error Overlay", §"Dev 404 Page"
722
- */
723
- /**
724
- * Extract HMR connection options from a Vite resolved config.
725
- * Used by the dev server to pass HMR config to the dev page generators
726
- * so the auto-reload WebSocket connects to the correct endpoint.
727
- */
728
- function extractHmrOptions(config) {
729
- const hmr = config.server?.hmr;
730
- if (hmr === false) return void 0;
731
- const hmrOpts = typeof hmr === "object" ? hmr : {};
732
- const opts = {
733
- protocol: hmrOpts.protocol,
734
- host: hmrOpts.host,
735
- port: hmrOpts.clientPort || hmrOpts.port,
736
- path: hmrOpts.path,
737
- token: config.webSocketToken
738
- };
739
- if (!opts.protocol && !opts.host && !opts.port && !opts.path && !opts.token) return;
740
- return opts;
741
- }
742
- /**
743
- * Build the inline auto-reload script for dev pages.
744
- *
745
- * The page is static HTML, not a Vite-managed module, so the normal HMR
746
- * update flow can't reach it. Instead we connect to Vite's HMR WebSocket
747
- * directly and reload on any update event.
748
- *
749
- * URL construction mirrors Vite's own client.mjs: explicitly configured
750
- * values win, and the browser's location fills any gaps at runtime. This
751
- * matters because Vite 8 requires a `?token=` on browser WS upgrades in
752
- * every config — a config with only a token still needs the host and port
753
- * from `location` (TIM-1067).
754
- */
755
- function buildReloadScript(hmrOptions) {
756
- return ` (function() {
757
- try {
758
- var o = ${JSON.stringify(hmrOptions ?? {}).replace(/</g, "\\u003c")};
759
- var protocol = o.protocol || (location.protocol === 'https:' ? 'wss' : 'ws');
760
- var host = o.host || location.hostname;
761
- var port = o.port || location.port;
762
- var url = protocol + '://' + host + (port ? ':' + port : '') + (o.path || '')
763
- + (o.token ? '?token=' + encodeURIComponent(o.token) : '');
764
- var ws = new WebSocket(url, 'vite-hmr');
765
- ws.addEventListener('message', function(e) {
766
- try {
767
- var data = JSON.parse(e.data);
768
- if (data.type === 'full-reload' || data.type === 'update') {
769
- location.reload();
770
- }
771
- } catch (ex) {}
772
- });
773
- } catch (ex) {}
774
- })();`;
775
- }
776
- /**
777
- * Render a self-contained dev page: scaffold + shared CSS + footer +
778
- * HMR auto-reload script.
779
- */
780
- function renderDevPageShell(opts) {
781
- return `<!DOCTYPE html>
782
- <html lang="en">
783
- <head>
784
- <meta charset="utf-8">
785
- <meta name="viewport" content="width=device-width, initial-scale=1">
786
- <title>${escapeHtml(opts.title)}</title>
787
- <style>${SHARED_CSS}${opts.extraCss ?? ""}</style>
788
- </head>
789
- <body>
790
- <div class="container">
791
- ${opts.bodyHtml}
792
-
793
- <footer class="footer">
794
- <span class="timber-logo">🪵 timber.js</span>
795
- <span class="footer-hint">${opts.footerHint}</span>
796
- </footer>
797
- </div>
798
-
799
- <script>
800
- ${opts.extraScript ?? ""}
801
- // Auto-reload when Vite HMR signals an update (file fixed, route added).
802
- ${buildReloadScript(opts.hmrOptions)}
803
- <\/script>
804
- </body>
805
- </html>`;
806
- }
807
- /**
808
- * Base CSS shared by all dev pages. Pages override the --badge-* variables
809
- * (and add their own component styles) via `extraCss`.
810
- */
811
- var SHARED_CSS = `
812
- :root {
813
- --bg: #fff;
814
- --fg: #1a1a1a;
815
- --fg-dim: #6b7280;
816
- --border: #e5e7eb;
817
- --source-bg: #fafafa;
818
- --link: #2563eb;
819
- --code-bg: #f3f4f6;
820
- --btn-bg: #f3f4f6;
821
- --btn-fg: #374151;
822
- --btn-border: #d1d5db;
823
- --code-font: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
824
- }
825
-
826
- @media (prefers-color-scheme: dark) {
827
- :root {
828
- --bg: #0a0a0a;
829
- --fg: #f5f5f5;
830
- --fg-dim: #9ca3af;
831
- --border: #27272a;
832
- --source-bg: #18181b;
833
- --link: #60a5fa;
834
- --code-bg: #27272a;
835
- --btn-bg: #27272a;
836
- --btn-fg: #e5e7eb;
837
- --btn-border: #3f3f46;
838
- }
839
- }
840
-
841
- * { margin: 0; padding: 0; box-sizing: border-box; }
842
-
843
- body {
844
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
845
- background: var(--bg);
846
- color: var(--fg);
847
- line-height: 1.6;
848
- padding: 2rem;
849
- }
850
-
851
- .container { max-width: 56rem; margin: 0 auto; }
852
- .header { margin-bottom: 1.5rem; }
853
-
854
- .badge {
855
- display: inline-block;
856
- font-size: 0.75rem;
857
- font-weight: 600;
858
- text-transform: uppercase;
859
- letter-spacing: 0.05em;
860
- padding: 0.25rem 0.625rem;
861
- border-radius: 0.375rem;
862
- background: var(--badge-bg);
863
- color: var(--badge-fg);
864
- border: 1px solid var(--badge-border);
865
- margin-bottom: 0.75rem;
866
- }
867
-
868
- .message {
869
- font-size: 1.375rem;
870
- font-weight: 600;
871
- line-height: 1.3;
872
- word-break: break-word;
873
- }
874
-
875
- .hint {
876
- margin-top: 0.5rem;
877
- color: var(--fg-dim);
878
- font-size: 0.875rem;
879
- }
880
-
881
- .section {
882
- margin-bottom: 1rem;
883
- border: 1px solid var(--border);
884
- border-radius: 0.5rem;
885
- overflow: hidden;
886
- }
887
-
888
- .section-header {
889
- font-size: 0.75rem;
890
- font-weight: 600;
891
- text-transform: uppercase;
892
- letter-spacing: 0.05em;
893
- padding: 0.5rem 0.75rem;
894
- background: var(--source-bg);
895
- border-bottom: 1px solid var(--border);
896
- color: var(--fg-dim);
897
- }
898
-
899
- .footer {
900
- display: flex;
901
- align-items: center;
902
- gap: 0.75rem;
903
- padding-top: 1rem;
904
- border-top: 1px solid var(--border);
905
- font-size: 0.75rem;
906
- color: var(--fg-dim);
907
- }
908
-
909
- .timber-logo { font-weight: 600; white-space: nowrap; }
910
- `;
911
- //#endregion
912
- //#region src/dev-tools/error-page.ts
913
- /**
914
- * Dev error page — self-contained HTML error page for dev server 500s.
915
- *
916
- * Generates a styled, self-contained HTML page when the RSC pipeline fails
917
- * and the Vite error overlay can't fire (e.g., first page load before HMR
918
- * WebSocket connects, RSC entry module crash, early pipeline errors).
919
- *
920
- * This is NOT a replacement for Vite's error overlay — it's the fallback
921
- * for when the overlay's transport (WebSocket) isn't available yet.
922
- *
923
- * Dev-only: this module is only imported by dev-server.ts (apply: 'serve')
924
- * and via dynamic import from fallback-error.ts in dev mode. It is never
925
- * included in production builds.
926
- *
927
- * Design doc: 21-dev-server.md §"Error Overlay"
928
- */
929
- var PHASE_LABELS = {
930
- "module-transform": "Module Transform",
931
- "proxy": "Proxy",
932
- "middleware": "Middleware",
933
- "access": "Access Check",
934
- "render": "RSC Render",
935
- "handler": "Route Handler"
936
- };
937
- var PHASE_HINTS = {
938
- "module-transform": "This error occurred while Vite was transforming a module. Check for syntax errors or missing imports.",
939
- "proxy": "This error occurred in proxy.ts. Check your proxy configuration.",
940
- "middleware": "This error occurred in a middleware.ts file. Check the middleware function for unhandled exceptions.",
941
- "access": "This error occurred in an access.ts file. Check your access control logic.",
942
- "render": "This error occurred while rendering a server component. Check the component for runtime errors.",
943
- "handler": "This error occurred in a route handler (route.ts). Check the handler function."
944
- };
945
- /**
946
- * Try to read a few lines of source code around the error location.
947
- * Returns null if the file can't be read (e.g., virtual modules).
948
- *
949
- * Uses a top-level `node:fs` import — this module is dev-server-only and
950
- * Node-only. A bare `require()` here would throw in the ESM build and the
951
- * code frame would silently never render (TIM-1067, B48).
952
- */
953
- function readSourceContext(filePath, line, contextLines = 3) {
954
- try {
955
- const allLines = readFileSync(filePath, "utf-8").split("\n");
956
- const start = Math.max(0, line - 1 - contextLines);
957
- const end = Math.min(allLines.length, line + contextLines);
958
- return {
959
- startLine: start + 1,
960
- lines: allLines.slice(start, end).map((text, i) => ({
961
- num: start + 1 + i,
962
- text,
963
- highlight: start + 1 + i === line
964
- }))
965
- };
966
- } catch (e) {
967
- console.debug("[timber] source context extraction failed:", e instanceof Error ? e.message : e);
968
- return null;
969
- }
970
- }
971
- var esc = escapeHtml;
972
- /**
973
- * Escape a JSON string for safe embedding in an HTML `<script>` block.
974
- *
975
- * `JSON.stringify` does not escape `<\/script>`, so error text containing
976
- * that sequence breaks out of the script block — an XSS vector.
977
- * We replace all `<` with the equivalent unicode escape, which is valid in
978
- * JSON strings and prevents the HTML parser from seeing a closing
979
- * `<\/script>` tag.
980
- *
981
- * Security: TIM-788
982
- */
983
- function escJsonForScript(json) {
984
- return json.replace(/</g, "\\u003c");
985
- }
986
- /**
987
- * Generate a self-contained HTML error page for dev server 500 responses.
988
- *
989
- * The page includes:
990
- * - Error message and phase label
991
- * - Source code context around the first app frame (if readable)
992
- * - Component stack (for React render errors)
993
- * - Classified stack trace (app frames highlighted, internals collapsed)
994
- * - Copy button for the full error
995
- * - Dark/light mode via prefers-color-scheme
996
- * - Auto-reconnect script that watches for Vite HMR and reloads
997
- */
998
- function generateDevErrorPage(error, phase, projectRoot, hmrOptions) {
999
- const message = error.message || "Unknown error";
1000
- const phaseLabel = PHASE_LABELS[phase];
1001
- const phaseHint = PHASE_HINTS[phase];
1002
- const componentStack = extractComponentStack(error);
1003
- const loc = parseFirstAppFrame(error.stack ?? "", projectRoot);
1004
- const frames = error.stack ? classifyStack(error.stack, projectRoot) : [];
1005
- const appFrames = frames.filter((f) => f.type === "app");
1006
- const internalFrameCount = frames.filter((f) => f.type !== "app").length;
1007
- let sourceContext = null;
1008
- if (loc) sourceContext = readSourceContext(loc.file, loc.line);
1009
- const relPath = loc?.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc?.file;
1010
- const bodyHtml = `<header class="header">
1011
- <div class="badge">${esc(phaseLabel)} Error</div>
1012
- <h1 class="message">${esc(message)}</h1>
1013
- ${phaseHint ? `<p class="hint">${esc(phaseHint)}</p>` : ""}
1014
- </header>
1015
-
1016
- ${relPath ? `<div class="location">${esc(relPath)}${loc ? `:${loc.line}:${loc.column}` : ""}</div>` : ""}
1017
-
1018
- ${sourceContext ? `<div class="source-context">
1019
- <div class="section-header">Source</div>
1020
- <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>
1021
- </div>` : ""}
1022
-
1023
- ${componentStack ? `<div class="section">
1024
- <div class="section-header">Component Stack</div>
1025
- <pre class="component-stack">${esc(componentStack.trim())}</pre>
1026
- </div>` : ""}
1027
-
1028
- ${appFrames.length > 0 ? `<div class="section">
1029
- <div class="section-header">Application Frames</div>
1030
- <pre class="stack">${appFrames.map((f) => esc(f.raw)).join("\n")}</pre>
1031
- </div>` : ""}
1032
-
1033
- ${internalFrameCount > 0 ? `<details class="section internal-frames">
1034
- <summary class="section-header clickable">${internalFrameCount} internal frame${internalFrameCount !== 1 ? "s" : ""}</summary>
1035
- <pre class="stack dimmed">${frames.filter((f) => f.type !== "app").map((f) => esc(f.raw)).join("\n")}</pre>
1036
- </details>` : ""}
1037
-
1038
- <div class="actions">
1039
- <button class="btn" onclick="copyError()">Copy Error</button>
1040
- </div>`;
1041
- const copyScript = ` function copyError() {
1042
- 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 ?? ""}`))};
1043
- navigator.clipboard.writeText(text).then(function() {
1044
- var btn = document.querySelector('.btn');
1045
- if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy Error'; }, 1500); }
1046
- });
1047
- }
1048
- `;
1049
- return renderDevPageShell({
1050
- title: `Error — ${phaseLabel} | timber.js`,
1051
- extraCss: ERROR_CSS,
1052
- bodyHtml,
1053
- footerHint: "Fix the error and save — the page will reload automatically via HMR.",
1054
- extraScript: copyScript,
1055
- hmrOptions
1056
- });
1057
- }
1058
- var ERROR_CSS = `
1059
- :root {
1060
- --badge-bg: #fef2f2;
1061
- --badge-fg: #991b1b;
1062
- --badge-border: #fecaca;
1063
- --highlight-bg: #fef2f2;
1064
- --highlight-border: #ef4444;
1065
- --line-num: #9ca3af;
1066
- }
1067
-
1068
- @media (prefers-color-scheme: dark) {
1069
- :root {
1070
- --badge-bg: #450a0a;
1071
- --badge-fg: #fca5a5;
1072
- --badge-border: #7f1d1d;
1073
- --highlight-bg: #450a0a;
1074
- --highlight-border: #dc2626;
1075
- --line-num: #6b7280;
1076
- }
1077
- }
1078
-
1079
- .location {
1080
- font-family: var(--code-font);
1081
- font-size: 0.8125rem;
1082
- color: var(--link);
1083
- margin-bottom: 1rem;
1084
- padding: 0.5rem 0.75rem;
1085
- background: var(--source-bg);
1086
- border-radius: 0.375rem;
1087
- border: 1px solid var(--border);
1088
- }
1089
-
1090
- .source-context {
1091
- margin-bottom: 1rem;
1092
- border: 1px solid var(--border);
1093
- border-radius: 0.5rem;
1094
- overflow: hidden;
1095
- }
1096
-
1097
- .source-code {
1098
- overflow-x: auto;
1099
- font-family: var(--code-font);
1100
- font-size: 0.8125rem;
1101
- line-height: 1.7;
1102
- padding: 0;
1103
- margin: 0;
1104
- }
1105
-
1106
- .source-code code {
1107
- display: block;
1108
- }
1109
-
1110
- .source-line {
1111
- display: block;
1112
- padding: 0 0.75rem;
1113
- border-left: 3px solid transparent;
1114
- }
1115
-
1116
- .source-line-highlight {
1117
- background: var(--highlight-bg);
1118
- border-left-color: var(--highlight-border);
1119
- }
1120
-
1121
- .line-num {
1122
- display: inline-block;
1123
- width: 3rem;
1124
- text-align: right;
1125
- margin-right: 1rem;
1126
- color: var(--line-num);
1127
- user-select: none;
1128
- }
1129
-
1130
- .clickable { cursor: pointer; }
1131
- .clickable:hover { background: var(--btn-bg); }
1132
-
1133
- .component-stack, .stack {
1134
- font-family: var(--code-font);
1135
- font-size: 0.8125rem;
1136
- line-height: 1.7;
1137
- padding: 0.75rem;
1138
- overflow-x: auto;
1139
- white-space: pre;
1140
- margin: 0;
1141
- }
1142
-
1143
- .dimmed { color: var(--fg-dim); }
1144
-
1145
- .actions {
1146
- margin: 1.5rem 0;
1147
- }
1148
-
1149
- .btn {
1150
- font-family: inherit;
1151
- font-size: 0.8125rem;
1152
- font-weight: 500;
1153
- padding: 0.5rem 1rem;
1154
- border-radius: 0.375rem;
1155
- border: 1px solid var(--btn-border);
1156
- background: var(--btn-bg);
1157
- color: var(--btn-fg);
1158
- cursor: pointer;
1159
- transition: background 0.15s;
1160
- }
1161
-
1162
- .btn:hover { opacity: 0.85; }
1163
- `;
1164
- //#endregion
1165
- //#region src/config-validation.ts
1166
- /**
1167
- * Config validation — validates timber.config.ts at startup.
1168
- *
1169
- * Runs in the plugin's configResolved hook (once, at startup/build).
1170
- * Each check produces a clear error message with the invalid value,
1171
- * what's expected, and how to fix it.
1172
- *
1173
- * Design doc: 18-build-system.md
1174
- */
1175
- var knownKeys = new Set(Object.keys({
1176
- output: true,
1177
- debug: true,
1178
- clientJavascript: true,
1179
- adapter: true,
1180
- allowedOrigins: true,
1181
- csrf: true,
1182
- limits: true,
1183
- forms: true,
1184
- pageExtensions: true,
1185
- slowRequestMs: true,
1186
- renderTimeoutMs: true,
1187
- devBrowserLogs: true,
1188
- dev: true,
1189
- serverTiming: true,
1190
- appDir: true,
1191
- mdx: true,
1192
- actionEncryption: true,
1193
- reactCompiler: true,
1194
- sitemap: true,
1195
- buildDir: true,
1196
- cache: true,
1197
- clientSegmentCache: true,
1198
- topLoader: true,
1199
- budget: true
1200
- }));
1201
- /**
1202
- * Validate a TimberUserConfig object.
1203
- *
1204
- * Returns an array of errors. Empty array means the config is valid.
1205
- * Does not throw — the caller decides how to surface errors.
1206
- */
1207
- function validateConfig(config) {
1208
- const errors = [];
1209
- if (config.output !== void 0 && config.output !== "server" && config.output !== "static") errors.push({
1210
- field: "output",
1211
- message: `Invalid output mode: "${String(config.output)}". Must be "server" or "static".`,
1212
- value: config.output,
1213
- suggestion: "Use output: \"server\" (default) or output: \"static\" for static site generation."
1214
- });
1215
- if (config.pageExtensions !== void 0) {
1216
- if (!Array.isArray(config.pageExtensions)) errors.push({
1217
- field: "pageExtensions",
1218
- message: "pageExtensions must be an array of strings.",
1219
- value: config.pageExtensions,
1220
- suggestion: "Example: pageExtensions: [\"tsx\", \"ts\", \"jsx\", \"js\", \"mdx\"]"
1221
- });
1222
- else for (const ext of config.pageExtensions) {
1223
- if (typeof ext !== "string") {
1224
- errors.push({
1225
- field: "pageExtensions",
1226
- message: `pageExtensions contains a non-string value: ${JSON.stringify(ext)}`,
1227
- value: ext
1228
- });
1229
- break;
1230
- }
1231
- if (ext.startsWith(".")) {
1232
- errors.push({
1233
- field: "pageExtensions",
1234
- message: `pageExtensions should not include the leading dot: "${ext}"`,
1235
- value: ext,
1236
- suggestion: `Use "${ext.slice(1)}" instead of "${ext}".`
1237
- });
1238
- break;
1239
- }
1240
- }
1241
- }
1242
- if (config.slowRequestMs !== void 0) {
1243
- if (typeof config.slowRequestMs !== "number" || config.slowRequestMs < 0) errors.push({
1244
- field: "slowRequestMs",
1245
- message: `slowRequestMs must be a non-negative number (got ${JSON.stringify(config.slowRequestMs)}).`,
1246
- value: config.slowRequestMs,
1247
- suggestion: "Use slowRequestMs: 3000 (default) or slowRequestMs: 0 to disable."
1248
- });
1249
- }
1250
- if (config.renderTimeoutMs !== void 0) {
1251
- if (typeof config.renderTimeoutMs !== "number" || config.renderTimeoutMs < 0) errors.push({
1252
- field: "renderTimeoutMs",
1253
- message: `renderTimeoutMs must be a non-negative number (got ${JSON.stringify(config.renderTimeoutMs)}).`,
1254
- value: config.renderTimeoutMs,
1255
- suggestion: "Use renderTimeoutMs: 30000 (default). Setting 0 uses a 120s safety ceiling."
1256
- });
1257
- }
1258
- if (config.serverTiming !== void 0) {
1259
- if (config.serverTiming !== "detailed" && config.serverTiming !== "total" && config.serverTiming !== false) errors.push({
1260
- field: "serverTiming",
1261
- message: `Invalid serverTiming value: ${JSON.stringify(config.serverTiming)}.`,
1262
- value: config.serverTiming,
1263
- suggestion: "Use \"detailed\", \"total\", or false."
1264
- });
1265
- }
1266
- if (config.devBrowserLogs !== void 0) {
1267
- const valid = [
1268
- "error",
1269
- "warn",
1270
- "info",
1271
- "none"
1272
- ];
1273
- if (!valid.includes(config.devBrowserLogs)) errors.push({
1274
- field: "devBrowserLogs",
1275
- message: `Invalid devBrowserLogs value: ${JSON.stringify(config.devBrowserLogs)}.`,
1276
- value: config.devBrowserLogs,
1277
- suggestion: `Use one of: ${valid.map((v) => `"${v}"`).join(", ")}.`
1278
- });
1279
- }
1280
- if (config.sitemap != null && typeof config.sitemap === "object") {
1281
- if (config.sitemap.enabled && !config.sitemap.baseUrl) errors.push({
1282
- field: "sitemap.baseUrl",
1283
- message: "sitemap.baseUrl is required when sitemap is enabled.",
1284
- suggestion: "Add sitemap: { enabled: true, baseUrl: \"https://example.com\" }."
1285
- });
1286
- if (config.sitemap.defaultPriority !== void 0 && (config.sitemap.defaultPriority < 0 || config.sitemap.defaultPriority > 1)) errors.push({
1287
- field: "sitemap.defaultPriority",
1288
- message: `sitemap.defaultPriority must be between 0.0 and 1.0 (got ${config.sitemap.defaultPriority}).`,
1289
- value: config.sitemap.defaultPriority
1290
- });
1291
- }
1292
- if (config.budget != null && typeof config.budget === "object") {
1293
- if (config.budget.firstLoadJs !== void 0 && (typeof config.budget.firstLoadJs !== "number" || config.budget.firstLoadJs <= 0)) errors.push({
1294
- field: "budget.firstLoadJs",
1295
- message: `budget.firstLoadJs must be a positive number in bytes (got ${JSON.stringify(config.budget.firstLoadJs)}).`,
1296
- value: config.budget.firstLoadJs,
1297
- suggestion: "Example: budget: { firstLoadJs: 150_000 } for a ~150 kB limit."
1298
- });
1299
- }
1300
- if (config.cache != null && typeof config.cache === "object") {
1301
- if (config.cache.strictKeyDiscipline !== void 0 && typeof config.cache.strictKeyDiscipline !== "boolean") errors.push({
1302
- field: "cache.strictKeyDiscipline",
1303
- message: `cache.strictKeyDiscipline must be a boolean (got ${JSON.stringify(config.cache.strictKeyDiscipline)}).`,
1304
- value: config.cache.strictKeyDiscipline,
1305
- suggestion: "Use cache: { strictKeyDiscipline: true } to promote key-discipline warnings to errors."
1306
- });
1307
- }
1308
- const MOVED_TO_CACHE_FILE = /* @__PURE__ */ new Set(["cacheHandler", "cdnPurge"]);
1309
- for (const key of Object.keys(config)) if (!knownKeys.has(key)) errors.push({
1310
- field: key,
1311
- message: `Unknown config option: "${key}".`,
1312
- 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(", ")}.`
1313
- });
1314
- return errors;
1315
- }
1316
- var RED = "\x1B[31m";
1317
- var BOLD = "\x1B[1m";
1318
- var DIM = "\x1B[2m";
1319
- var RESET = "\x1B[0m";
1320
- /**
1321
- * Format config errors for terminal output.
1322
- */
1323
- function formatConfigErrors(errors) {
1324
- if (errors.length === 0) return "";
1325
- const lines = [];
1326
- lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}${errors.length} config error${errors.length !== 1 ? "s" : ""} in timber.config.ts:${RESET}`);
1327
- for (const err of errors) {
1328
- lines.push("");
1329
- lines.push(` ${RED}✗${RESET} ${BOLD}${err.field}${RESET}: ${err.message}`);
1330
- if (err.suggestion) lines.push(` ${DIM}${err.suggestion}${RESET}`);
1331
- }
1332
- return lines.join("\n");
1333
- }
1334
- var VIRTUAL_MODULE_NAMES = {
1335
- "virtual:timber-rsc-entry": "RSC entry (server component handler)",
1336
- "virtual:timber-ssr-entry": "SSR entry (HTML renderer)",
1337
- "virtual:timber-browser-entry": "Browser entry (client hydration)",
1338
- "virtual:timber-config": "Runtime config (timber.config.ts)",
1339
- "virtual:timber-route-manifest": "Route manifest (app/ file tree)",
1340
- "virtual:timber-instrumentation": "Instrumentation (instrumentation.ts)",
1341
- "virtual:timber-cache-handler": "Cache handler (timber.cache.ts)",
1342
- "virtual:timber-build-manifest": "Build manifest (asset mapping)"
1343
- };
1344
- /**
1345
- * Add timber-specific context to an error message that references virtual modules.
1346
- *
1347
- * If the error message contains a `virtual:timber-*` ID, appends a
1348
- * human-readable explanation. Does not replace the original message.
1349
- */
1350
- function addVirtualModuleContext(errorMessage) {
1351
- 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.`;
1352
- return errorMessage;
1353
- }
1354
- /**
1355
- * Required (non-optional) peer dependencies that must be installed.
1356
- */
1357
- var REQUIRED_PEERS = [
1358
- "react",
1359
- "react-dom",
1360
- "@vitejs/plugin-react",
1361
- "@vitejs/plugin-rsc"
1362
- ];
1363
- /**
1364
- * Check that required peer dependencies are installed.
1365
- *
1366
- * Uses require.resolve with a try/catch — no fs scanning.
1367
- * Returns only the missing packages.
1368
- */
1369
- function checkPeerDependencies(projectRoot) {
1370
- const results = [];
1371
- const userRequire = createRequire(`${projectRoot}/package.json`);
1372
- for (const name of REQUIRED_PEERS) try {
1373
- userRequire.resolve(name);
1374
- results.push({
1375
- name,
1376
- status: "ok"
1377
- });
1378
- } catch {
1379
- results.push({
1380
- name,
1381
- status: "missing"
1382
- });
1383
- }
1384
- return results;
1385
- }
1386
- /**
1387
- * Format missing peer dependencies as an actionable warning.
1388
- */
1389
- function formatMissingPeers(results) {
1390
- const missing = results.filter((r) => r.status === "missing");
1391
- if (missing.length === 0) return "";
1392
- const names = missing.map((r) => r.name);
1393
- const lines = [];
1394
- lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}Missing required dependencies:${RESET}`);
1395
- lines.push("");
1396
- for (const name of names) lines.push(` ${RED}✗${RESET} ${name}`);
1397
- lines.push("");
1398
- lines.push(` ${DIM}Install with:${RESET}`);
1399
- lines.push(` ${BOLD}pnpm add ${names.join(" ")}${RESET}`);
1400
- return lines.join("\n");
1401
- }
1402
- /** HMR custom event name for graph-affecting file changes. */
1403
- var GRAPH_UPDATE_EVENT = "timber:graph-update";
1404
- /**
1405
- * Host-header allowlist check, ported branch-for-branch from Vite's
1406
- * host-validation middleware (`isHostAllowedWithoutCache`) so this
1407
- * endpoint's reachability matches the rest of the dev server:
1408
- *
1409
- * - `allowedHosts: true` allows everything
1410
- * - a MISSING Host header is allowed (Vite parity — HTTP/1.0 probes)
1411
- * - any IP-literal host is allowed (`vite --host` LAN access sends
1412
- * `Host: 192.168.x.x:port`; rejecting it would 403 this endpoint
1413
- * while every other dev route works)
1414
- * - `localhost` and `*.localhost` are always allowed
1415
- * - allowedHosts entries: exact match, or `.suffix` subdomain entries
1416
- */
1417
- function isAllowedGraphHost(hostHeader, allowedHosts) {
1418
- if (allowedHosts === true) return true;
1419
- if (hostHeader === void 0) return true;
1420
- let host = hostHeader;
1421
- const bracket = host.match(/^\[([^\]]+)\](?::\d+)?$/);
1422
- if (bracket) return isIP(bracket[1]) === 6;
1423
- const colon = host.lastIndexOf(":");
1424
- if (colon !== -1) host = host.slice(0, colon);
1425
- host = host.toLowerCase();
1426
- if (isIP(host) !== 0) return true;
1427
- if (host === "localhost" || host.endsWith(".localhost")) return true;
1428
- for (const entry of allowedHosts ?? []) {
1429
- const allowed = entry.toLowerCase();
1430
- if (allowed.startsWith(".")) {
1431
- if (host === allowed.slice(1) || host.endsWith(allowed)) return true;
1432
- } else if (host === allowed) return true;
1433
- }
1434
- return false;
1435
- }
1436
- /**
1437
- * Non-http(s) origins admitted without a host check: Vite's own
1438
- * `file:` / `*-extension:` set (its host-validation regex), plus
1439
- * `vscode-webview:` — VS Code webviews fetch with that Origin, and the
1440
- * extension is this endpoint's primary consumer. Vite itself never
1441
- * origin-checks plain HTTP GETs (only WebSocket upgrades), so this
1442
- * layer is strictly additional; the additions keep it from rejecting
1443
- * consumers Vite would serve.
1444
- *
1445
- * NOT admitted: `Origin: null`. Browsers serialize file:// pages'
1446
- * opaque origin as the literal string `null` — but so do sandboxed
1447
- * iframes and data: documents, exactly the untrusted embedders the
1448
- * check exists to stop, and the header offers nothing to tell them
1449
- * apart. file:// pages therefore cannot use this endpoint from a
1450
- * browser; the `file:` scheme entry covers non-browser clients that
1451
- * send a real scheme-form Origin (Vite admits the same set).
1452
- */
1453
- var TRUSTED_ORIGIN_SCHEMES = /^(?:file|vscode-webview|.+-extension):/i;
1454
- /** Origin check: absent is fine (CLI probe); present must pass the host allowlist. */
1455
- function isAllowedGraphOrigin(originHeader, allowedHosts) {
1456
- if (originHeader === void 0) return true;
1457
- if (TRUSTED_ORIGIN_SCHEMES.test(originHeader)) return true;
1458
- let originUrl;
1459
- try {
1460
- originUrl = new URL(originHeader);
1461
- } catch {
1462
- return false;
1463
- }
1464
- if (originUrl.host === "") return false;
1465
- const bareHost = originUrl.hostname.replace(/^\[|\]$/g, "");
1466
- if (isIP(bareHost) !== 0) {
1467
- if (bareHost === "127.0.0.1" || bareHost === "::1") return true;
1468
- if (allowedHosts === true) return true;
1469
- return (allowedHosts ?? []).some((entry) => entry.toLowerCase() === bareHost);
1470
- }
1471
- return isAllowedGraphHost(originUrl.host, allowedHosts);
1472
- }
1473
- function sendJson(res, status, body) {
1474
- res.statusCode = status;
1475
- res.setHeader("content-type", "application/json; charset=utf-8");
1476
- res.end(JSON.stringify(body));
1477
- }
1478
- /**
1479
- * Echo an ADMITTED origin back as CORS headers. A webview fetch from
1480
- * `vscode-webview://…` passes the origin check but the browser rejects
1481
- * the response body without `Access-Control-Allow-Origin` — the exact
1482
- * transport design/47 specifies for the extension. Only the validated
1483
- * origin is echoed (never `*`), with `Vary: Origin` for any cache in
1484
- * between; unadmitted origins were already 403'd and get nothing.
1485
- */
1486
- function reflectCors(res, origin) {
1487
- if (!origin) return;
1488
- res.setHeader("Access-Control-Allow-Origin", origin);
1489
- res.setHeader("Vary", "Origin");
1490
- }
1491
- /**
1492
- * Create the Connect middleware handling GET /__timber/graph.
1493
- *
1494
- * Query surface: `?file=<path>` (absolute or project-root-relative) or
1495
- * `?all=1`. Anything else is a 400 — fail loudly, like the CLI.
1496
- */
1497
- function createGraphEndpointMiddleware(server, ctx) {
1498
- const sourceExtensions = buildSourceExtensions(ctx.config.pageExtensions);
1499
- return async (req, res, next) => {
1500
- const rawUrl = req.url;
1501
- if (!rawUrl || !rawUrl.startsWith("/__timber/")) {
1502
- next();
1503
- return;
1504
- }
1505
- const url = new URL(rawUrl, "http://placeholder.invalid");
1506
- if (url.pathname !== "/__timber/graph") {
1507
- sendJson(res, 404, { error: `unknown /__timber/ endpoint: ${url.pathname}` });
1508
- return;
1509
- }
1510
- if (req.method !== "GET") {
1511
- res.setHeader("Allow", "GET");
1512
- sendJson(res, 405, { error: "GET only" });
1513
- return;
1514
- }
1515
- const origin = req.headers.origin;
1516
- const allowedHosts = server.config.server.allowedHosts;
1517
- if (!server.config.server.https && !isAllowedGraphHost(req.headers.host, allowedHosts)) {
1518
- sendJson(res, 403, { error: "host not allowed" });
1519
- return;
1520
- }
1521
- if (!isAllowedGraphOrigin(origin, allowedHosts)) {
1522
- sendJson(res, 403, { error: "origin not allowed" });
1523
- return;
1524
- }
1525
- reflectCors(res, origin);
1526
- try {
1527
- const { classifyLiveFile, classifyLiveProject } = await import("./_chunks/live-graph-D_2D32Ad.js");
1528
- const preserveSymlinks = server.config.resolve.preserveSymlinks;
1529
- const root = normalizePath(preserveSymlinks ? ctx.root : await realpath(ctx.root));
1530
- const appDir = normalizePath(preserveSymlinks ? ctx.appDir : await realpath(ctx.appDir).catch(() => join(root, relative(ctx.root, ctx.appDir))));
1531
- const liveCtx = {
1532
- rsc: server.environments.rsc.moduleGraph,
1533
- ssr: server.environments.ssr.moduleGraph,
1534
- client: server.environments.client.moduleGraph,
1535
- root,
1536
- appDir,
1537
- pageExtensions: ctx.config.pageExtensions
1538
- };
1539
- const allParam = url.searchParams.get("all");
1540
- if (allParam !== null && allParam !== "1") {
1541
- sendJson(res, 400, { error: `unsupported all value: ${allParam} (use ?all=1)` });
1542
- return;
1543
- }
1544
- if (allParam === "1") {
1545
- sendJson(res, 200, {
1546
- version: 1,
1547
- source: "dev-server",
1548
- cached: false,
1549
- ...await classifyLiveProject(liveCtx)
1550
- });
1551
- return;
1552
- }
1553
- const fileParam = url.searchParams.get("file");
1554
- if (!fileParam) {
1555
- sendJson(res, 400, { error: "missing ?file=<path> (or ?all=1)" });
1556
- return;
1557
- }
1558
- const absolute = normalizePath(isAbsolute(fileParam) ? resolve(fileParam) : resolve(root, fileParam));
1559
- let target;
1560
- try {
1561
- const physical = normalizePath(await realpath(absolute));
1562
- const rootPhysical = preserveSymlinks ? normalizePath(await realpath(root)) : root;
1563
- if (physical !== rootPhysical && !physical.startsWith(rootPhysical + "/") && !physical.startsWith(rootPhysical + "\\")) {
1564
- sendJson(res, 404, { error: `not a project file under ${root}` });
1565
- return;
1566
- }
1567
- if (preserveSymlinks) {
1568
- if (physical !== rootPhysical + absolute.slice(root.length)) {
1569
- sendJson(res, 404, { error: `not a project source file: ${absolute}` });
1570
- return;
1571
- }
1572
- }
1573
- target = normalizePath(preserveSymlinks ? absolute : physical);
1574
- } catch {
1575
- sendJson(res, 404, { error: `file not found: ${absolute}` });
1576
- return;
1577
- }
1578
- if (target !== root && !target.startsWith(root + "/") && !target.startsWith(root + "\\")) {
1579
- sendJson(res, 404, { error: `not a project file under ${root}` });
1580
- return;
1581
- }
1582
- if (!(await stat(target).catch(() => null))?.isFile() || !isScannableSourceFile(normalizePath(target), normalizePath(root), sourceExtensions)) {
1583
- sendJson(res, 404, { error: `not a project source file: ${target}` });
1584
- return;
1585
- }
1586
- const errors = [];
1587
- const module = await classifyLiveFile(target, liveCtx, void 0, errors);
1588
- sendJson(res, 200, {
1589
- version: 1,
1590
- source: "dev-server",
1591
- cached: false,
1592
- target: {
1593
- input: fileParam,
1594
- resolved: target
1595
- },
1596
- kind: module.kind,
1597
- pending: module.pending,
1598
- subtreeUnknown: module.subtreeUnknown,
1599
- envs: module.envs,
1600
- boundary: module.boundary,
1601
- chains: module.chains,
1602
- poisonings: module.poisonings,
1603
- errors
1604
- });
1605
- } catch (error) {
1606
- sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
1607
- }
1608
- };
1609
- }
1610
- /**
1611
- * Build the push channel: `timber:graph-update` custom HMR events per
1612
- * project-source change. Consumers (the VS Code extension) re-query
1613
- * and debounce on their side; the channel just emits. Non-source noise
1614
- * (build output, dotdirs) is filtered by the same extension set the
1615
- * scanners use.
1616
- *
1617
- * Two phases per edit, because one is not enough (codex review, PR
1618
- * #1057): the `invalidated` event fires from the plugin's `hotUpdate`
1619
- * hook — after Vite clears the module's transform result, but its
1620
- * importer EDGES are only rebuilt when the module is next transformed.
1621
- * The `transformed` event fires for exactly the files with a pending
1622
- * invalidation, and only once the node's `transformResult` is set
1623
- * again — the plugin transform hook itself runs BEFORE Vite's trailing
1624
- * `vite:import-analysis` (which rebuilds the edges), so the hook only
1625
- * marks the file "settling" and a short bounded timer confirms
1626
- * completion via `isTransformed` before emitting. (A module nothing
1627
- * re-requests never re-transforms — its `transformed` event waits for
1628
- * the next render, which is also when the graph has new information.)
1629
- */
1630
- function makeGraphUpdateChannel(ctx, send, isTransformed, canonicalize = (file) => file) {
1631
- /** Files invalidated but not yet re-transformed, per environment. */
1632
- const pending = /* @__PURE__ */ new Set();
1633
- const key = (file, environment) => `${environment}\0${file}`;
1634
- const root = normalizePath(canonicalize(ctx.root));
1635
- const extensions = buildSourceExtensions(ctx.config.pageExtensions);
1636
- const emitTransformed = (file, environment, structural) => {
1637
- send({
1638
- type: "custom",
1639
- event: GRAPH_UPDATE_EVENT,
1640
- data: structural ? {
1641
- file,
1642
- event: "update",
1643
- environment,
1644
- phase: "transformed",
1645
- structural: true
1646
- } : {
1647
- file,
1648
- event: "update",
1649
- environment,
1650
- phase: "transformed"
1651
- }
1652
- });
1653
- };
1654
- /**
1655
- * Confirm the transform request finished (transformResult set again)
1656
- * before emitting phase `transformed`. If confirmation never arrives
1657
- * within the bounded schedule below, the pending key is RE-ARMED
1658
- * rather than emitted — a `transformed` event over a still-stale
1659
- * graph would be a false all-clear with no corrective event.
1660
- */
1661
- /**
1662
- * Delays between confirmation re-checks: dense while a normal
1663
- * transform request finishes, then exponential backoff for slow
1664
- * import-analysis passes (large graphs, cold dep optimizer) — a
1665
- * completed slow transform is CACHED, so no later transform hook may
1666
- * ever fire for it and the tail checks are the only chance to emit.
1667
- * Total window ≈ 16 s, then give up (bounded — the async-safety
1668
- * rules ban open-ended polling) and re-arm the pending key.
1669
- */
1670
- const CONFIRM_DELAYS_MS = [
1671
- ...Array.from({ length: 25 }, () => 20),
1672
- 500,
1673
- 1e3,
1674
- 2e3,
1675
- 4e3,
1676
- 8e3
1677
- ];
1678
- const confirmAndEmit = (file, environment, attempt = 0, structural = false) => {
1679
- if (!isTransformed || isTransformed(file, environment)) {
1680
- emitTransformed(file, environment, structural);
1681
- return;
1682
- }
1683
- if (attempt >= CONFIRM_DELAYS_MS.length) {
1684
- if (!structural) pending.add(key(file, environment));
1685
- return;
1686
- }
1687
- setTimeout(() => confirmAndEmit(file, environment, attempt + 1, structural), CONFIRM_DELAYS_MS[attempt]);
1688
- };
1689
- return {
1690
- onHotUpdate(update) {
1691
- const file = normalizePath(canonicalize(update.file));
1692
- if (!isScannableSourceFile(file, root, extensions)) return;
1693
- if (update.type === "delete") pending.delete(key(file, update.environment));
1694
- else pending.add(key(file, update.environment));
1695
- send({
1696
- type: "custom",
1697
- event: GRAPH_UPDATE_EVENT,
1698
- data: {
1699
- file,
1700
- event: update.type,
1701
- environment: update.environment,
1702
- phase: "invalidated"
1703
- }
1704
- });
1705
- },
1706
- onTransform(id, environment) {
1707
- const file = normalizePath(id.split("?")[0]);
1708
- if (file.includes("virtual:timber-route-manifest")) {
1709
- confirmAndEmit(file, environment, 25, true);
1710
- return;
1711
- }
1712
- const pendingKey = key(file, environment);
1713
- if (!pending.has(pendingKey)) return;
1714
- pending.delete(pendingKey);
1715
- confirmAndEmit(file, environment);
1716
- }
1717
- };
1718
- }
1719
- //#endregion
1720
- //#region src/server/compress.ts
1721
- /**
1722
- * MIME types that benefit from compression.
1723
- * text/* is handled via prefix matching; these are the specific
1724
- * application/* and image/* types that are compressible.
1725
- */
1726
- var COMPRESSIBLE_TYPES = /* @__PURE__ */ new Set([
1727
- "text/html",
1728
- "text/css",
1729
- "text/plain",
1730
- "text/xml",
1731
- "text/javascript",
1732
- RSC_CONTENT_TYPE,
1733
- "application/json",
1734
- "application/javascript",
1735
- "application/xml",
1736
- "application/xhtml+xml",
1737
- "application/rss+xml",
1738
- "application/atom+xml",
1739
- "image/svg+xml"
1740
- ]);
1741
- /**
1742
- * Status codes that should never be compressed (no body or special semantics).
1743
- */
1744
- var NO_COMPRESS_STATUSES = /* @__PURE__ */ new Set([204, 304]);
1745
- /**
1746
- * Parse Accept-Encoding and return the best supported encoding.
1747
- * Returns 'gzip' if the client accepts it, null otherwise.
1748
- *
1749
- * Brotli (br) is intentionally not handled at the application level.
1750
- * At the streaming-friendly quality levels (0–4), brotli's compression
1751
- * ratio advantage over gzip is marginal, and node:zlib's brotli transform
1752
- * buffers output internally — turning smooth streaming responses into
1753
- * large infrequent bursts. Brotli's real wins come from offline/static
1754
- * compression at higher quality levels (5–11), which CDNs and reverse
1755
- * proxies (Cloudflare, nginx, Caddy) apply on cached responses.
1756
- *
1757
- * See design/25-production-deployments.md.
1758
- */
1759
- function negotiateEncoding(acceptEncoding) {
1760
- if (!acceptEncoding) return null;
1761
- const parts = acceptEncoding.split(",");
1762
- for (const part of parts) {
1763
- const [token, ...params] = part.split(";");
1764
- if (token.trim().toLowerCase() !== "gzip") continue;
1765
- let qValue = 1;
1766
- for (const param of params) {
1767
- const trimmed = param.trim().toLowerCase();
1768
- if (trimmed.startsWith("q=")) {
1769
- qValue = parseFloat(trimmed.slice(2));
1770
- if (Number.isNaN(qValue)) qValue = 1;
1771
- break;
1772
- }
1773
- }
1774
- if (qValue > 0) return "gzip";
1775
- }
1776
- return null;
1777
- }
1778
- /**
1779
- * Determine if a response should be compressed.
1780
- *
1781
- * Returns false for:
1782
- * - Responses without a body (204, 304, null body)
1783
- * - Already-encoded responses (Content-Encoding set)
1784
- * - Non-compressible content types (images, binary)
1785
- * - SSE streams (text/event-stream — must not be buffered)
1786
- */
1787
- function shouldCompress(response) {
1788
- if (!response.body) return false;
1789
- if (NO_COMPRESS_STATUSES.has(response.status)) return false;
1790
- if (response.headers.has("Content-Encoding")) return false;
1791
- const contentType = response.headers.get("Content-Type");
1792
- if (!contentType) return false;
1793
- const mimeType = contentType.split(";")[0].trim().toLowerCase();
1794
- if (mimeType === "text/event-stream") return false;
1795
- return COMPRESSIBLE_TYPES.has(mimeType);
1796
- }
1797
- /**
1798
- * Compress a Web Response if the client supports it and the content is compressible.
1799
- *
1800
- * Returns the original response unchanged if compression is not applicable.
1801
- * Returns a new Response with the compressed body, Content-Encoding, and Vary headers.
1802
- *
1803
- * The body is piped through a compression stream — no buffering of the full response.
1804
- * This preserves streaming behavior for HTML shell + deferred Suspense chunks.
1805
- */
1806
- function compressResponse(request, response) {
1807
- if (!shouldCompress(response)) return response;
1808
- const encoding = negotiateEncoding(request.headers.get("Accept-Encoding") ?? "");
1809
- if (!encoding) return response;
1810
- const compressedBody = compressWithGzip(response.body);
1811
- const headers = new Headers(response.headers);
1812
- headers.set("Content-Encoding", encoding);
1813
- headers.delete("Content-Length");
1814
- const existingVary = headers.get("Vary");
1815
- if (existingVary) {
1816
- if (!existingVary.toLowerCase().includes("accept-encoding")) headers.set("Vary", `${existingVary}, Accept-Encoding`);
1817
- } else headers.set("Vary", "Accept-Encoding");
1818
- return new Response(compressedBody, {
1819
- status: response.status,
1820
- statusText: response.statusText,
1821
- headers
1822
- });
1823
- }
1824
- /**
1825
- * Compress a ReadableStream with gzip, flushing each chunk immediately.
1826
- *
1827
- * Uses node:zlib's createGzip with Z_SYNC_FLUSH to ensure each HTML chunk
1828
- * (shell, Suspense resolution, RSC payload) is delivered to the browser
1829
- * as soon as it's available — preserving streaming semantics.
1830
- */
1831
- function compressWithGzip(body) {
1832
- const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
1833
- const nodeInput = Readable.fromWeb(body);
1834
- pipeline(nodeInput, gzip, () => {});
1835
- return Readable.toWeb(gzip);
1836
- }
1837
- //#endregion
1838
- //#region src/plugins/dev-server.ts
1839
- var RSC_ENTRY_ID = "virtual:timber-rsc-entry";
1840
- /**
1841
- * Config file names that trigger a full dev server restart when changed.
1842
- * See 21-dev-server.md §HMR Wiring — config is loaded once at startup.
1843
- */
1844
27
  var CONFIG_FILE_NAMES = [
1845
- "timber.config.ts",
1846
- "timber.config.js",
1847
- "timber.config.mjs"
1848
- ];
1849
- /**
1850
- * URL prefixes that are Vite-internal and should never be intercepted.
1851
- * These are passed through to Vite's built-in middleware.
1852
- */
1853
- var VITE_INTERNAL_PREFIXES = [
1854
- "/@",
1855
- "/__vite",
1856
- "/node_modules/"
28
+ "content-collections.ts",
29
+ "content-collections.js",
30
+ "content-collections.mts",
31
+ "content-collections.mjs"
1857
32
  ];
1858
33
  /**
1859
- * File extensions that indicate static asset requests.
1860
- * These are passed through to Vite's static file serving.
34
+ * Find the content-collections config file at the project root.
35
+ * Returns the filename (not full path) if found, otherwise undefined.
1861
36
  */
1862
- 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)(?:\?.*)?$/;
37
+ function findConfigFile(root) {
38
+ for (const name of CONFIG_FILE_NAMES) if (existsSync(join(root, name))) return name;
39
+ }
1863
40
  /**
1864
- * Create the timber-dev-server Vite plugin.
41
+ * Create the timber-content Vite plugin.
1865
42
  *
1866
- * Hook: configureServer (returns post-hook to register after Vite's middleware)
43
+ * Delegates all content scanning, validation, code generation, and file watching
44
+ * to @content-collections/vite. This plugin only handles detection and activation.
1867
45
  */
1868
- function timberDevServer(ctx) {
1869
- let graphChannel = null;
46
+ function timberContent(ctx) {
47
+ let innerPlugin = null;
48
+ async function activate(root) {
49
+ if (innerPlugin !== null) return;
50
+ const configFile = findConfigFile(root);
51
+ if (!configFile) return;
52
+ let createPlugin;
53
+ try {
54
+ const mod = await import("@content-collections/vite");
55
+ createPlugin = mod.default ?? mod;
56
+ } catch {
57
+ throw new Error([
58
+ "[timber] Content collections are enabled but @content-collections/vite is not installed.",
59
+ "",
60
+ "Install content-collections:",
61
+ " pnpm add -D @content-collections/core @content-collections/vite",
62
+ "",
63
+ "For MDX content, also install:",
64
+ " pnpm add -D @content-collections/mdx",
65
+ "",
66
+ "Content collections are activated because a content-collections.ts file exists."
67
+ ].join("\n"));
68
+ }
69
+ if (createPlugin) innerPlugin = createPlugin({ configPath: configFile });
70
+ }
1870
71
  return {
1871
- name: "timber-dev-server",
1872
- apply: "serve",
1873
- /**
1874
- * Push channel for the graph endpoint, phase 1: `invalidated`, one
1875
- * per (file, environment). Emitted from hotUpdate — which runs
1876
- * AFTER Vite invalidates the environment module graphs — never
1877
- * from a raw watcher listener, which would race consumers into
1878
- * querying pre-invalidation memberships (design/47 §"Dev-server
1879
- * endpoint").
1880
- */
1881
- hotUpdate({ file, type }) {
1882
- graphChannel?.onHotUpdate({
1883
- file,
1884
- type,
1885
- environment: this.environment.name
1886
- });
72
+ name: "timber-content",
73
+ async config(config, env) {
74
+ const root = config.root ?? ctx.root;
75
+ ctx.timer.start("content-activate");
76
+ await activate(root);
77
+ ctx.timer.end("content-activate");
78
+ if (!innerPlugin) return;
79
+ if (typeof innerPlugin.config === "function") return innerPlugin.config.call(this, config, env);
1887
80
  },
1888
- /**
1889
- * Push channel phase 2: `transformed`, when a pending-invalidated
1890
- * module's import edges are actually rebuilt. A Set lookup for
1891
- * everything without a pending invalidation — free on the hot
1892
- * path. Returning nothing leaves the code untouched.
1893
- */
1894
- transform(_code, id) {
1895
- graphChannel?.onTransform(id, this.environment.name);
81
+ async configResolved(config) {
82
+ if (!innerPlugin) return;
83
+ if (typeof innerPlugin.configResolved === "function") await innerPlugin.configResolved.call(this, config);
84
+ },
85
+ async buildStart(options) {
86
+ if (!innerPlugin) return;
87
+ if (typeof innerPlugin.buildStart === "function") await innerPlugin.buildStart.call(this, options);
88
+ },
89
+ async resolveId(source, importer, options) {
90
+ if (!innerPlugin) return null;
91
+ if (typeof innerPlugin.resolveId === "function") return innerPlugin.resolveId.call(this, source, importer, options);
92
+ return null;
93
+ },
94
+ async load(id) {
95
+ if (!innerPlugin) return null;
96
+ if (typeof innerPlugin.load === "function") return innerPlugin.load.call(this, id);
97
+ return null;
98
+ },
99
+ async transform(code, id) {
100
+ if (!innerPlugin) return null;
101
+ if (typeof innerPlugin.transform === "function") return innerPlugin.transform.call(this, code, id);
102
+ return null;
1896
103
  },
1897
- /**
1898
- * Register the dev server middleware and config file watcher.
1899
- *
1900
- * Registers as a pre-hook (no return value) so our middleware runs
1901
- * before Vite's built-in SPA fallback / historyApiFallback. This
1902
- * ensures we see the original URL (e.g. /blog) rather than a
1903
- * rewritten /index.html. Vite-internal and asset requests are
1904
- * filtered out explicitly and passed through to Vite.
1905
- */
1906
104
  async configureServer(server) {
1907
- const configPaths = CONFIG_FILE_NAMES.map((name) => join(ctx.root, name));
1908
- server.watcher.add(configPaths);
1909
- listenForClientErrors(server, ctx.root);
1910
- server.middlewares.use(createGraphEndpointMiddleware(server, ctx));
1911
- graphChannel = makeGraphUpdateChannel(ctx, (payload) => server.hot.send(payload), (file, environment) => {
1912
- const graph = server.environments[environment]?.moduleGraph;
1913
- if (!graph) return false;
1914
- const nodes = graph.getModulesByFile(file);
1915
- if (nodes) {
1916
- for (const node of nodes) if (node.transformResult != null) return true;
1917
- return false;
1918
- }
1919
- return graph.getModuleById(file)?.transformResult != null;
1920
- }, (file) => {
1921
- if (server.config?.resolve?.preserveSymlinks || file.includes("virtual:")) return file;
1922
- try {
1923
- return realpathSync(file);
1924
- } catch {
1925
- try {
1926
- return join(realpathSync(dirname(file)), basename(file));
1927
- } catch {
1928
- return file;
1929
- }
1930
- }
1931
- });
1932
- server.middlewares.use(createTimberMiddleware(server, ctx));
1933
- registerDevDiscovery(server.httpServer, ctx.root, server.config);
1934
- if (ctx.holdingServer) {
1935
- await ctx.holdingServer.close().catch(() => {});
1936
- ctx.holdingServer = null;
1937
- }
1938
- ctx.timer.end("dev-server-setup");
1939
- const summary = ctx.timer.formatSummary();
1940
- if (summary) console.log(summary);
1941
- }
1942
- };
1943
- }
1944
- /**
1945
- * Create the Connect middleware that routes requests through the timber pipeline.
1946
- *
1947
- * For route requests (HTML pages, API endpoints), the middleware:
1948
- * 1. Loads the RSC entry via ssrLoadModule
1949
- * 2. Converts the Node request to a Web Request
1950
- * 3. Passes it through the RSC handler (which runs the full pipeline)
1951
- * 4. Converts the Web Response back to a Node response
1952
- *
1953
- * For non-route requests (assets, Vite internals, HMR), the middleware
1954
- * calls next() to let Vite handle them.
1955
- */
1956
- function createTimberMiddleware(server, ctx) {
1957
- const projectRoot = ctx.root;
1958
- const hmrOptions = extractHmrOptions(server.config);
1959
- return async (req, res, next) => {
1960
- const url = req.url;
1961
- if (!url) {
1962
- next();
1963
- return;
1964
- }
1965
- if (isViteInternal(url)) {
1966
- next();
1967
- return;
1968
- }
1969
- if (isPublicFile(server, url)) {
1970
- next();
1971
- return;
1972
- }
1973
- if (isAssetRequest(url) && !isMetadataRouteServePath(url) && !isRouteOwnedAsset(ctx.routeTree, server, url)) {
1974
- next();
1975
- return;
1976
- }
1977
- let handler;
1978
- try {
1979
- const rscEnv = server.environments.rsc;
1980
- if (!rscEnv?.runner?.import) throw new Error("[timber] RSC environment is not runnable");
1981
- const rscModule = await rscEnv.runner.import(RSC_ENTRY_ID);
1982
- handler = rscModule.default;
1983
- const config = rscModule.pipelineConfig;
1984
- if (config) {
1985
- config.devPipelineErrorHandler = (error, _phase, debugComponents) => {
1986
- sendErrorToOverlay(server, error, classifyErrorPhase(error, projectRoot), projectRoot, debugComponents);
1987
- };
1988
- config.devHmrOptions = hmrOptions;
1989
- }
1990
- const setSourceMap = rscModule.setDevSourceMapHandler;
1991
- if (typeof setSourceMap === "function") setSourceMap((error) => {
1992
- fixErrorStacktrace(server, error);
1993
- });
1994
- } catch (error) {
1995
- if (error instanceof Error) {
1996
- addTimberContext(error);
1997
- sendErrorToOverlay(server, error, "module-transform", projectRoot);
1998
- }
1999
- respond500(res, error, "module-transform", projectRoot, hmrOptions);
2000
- return;
2001
- }
2002
- if (typeof handler !== "function") {
2003
- console.error("[timber] RSC entry module does not export a default function");
2004
- next();
2005
- return;
2006
- }
2007
- try {
2008
- const webRequest = toWebRequest(req);
2009
- const wrapper = server[Symbol.for("timber:dev-request-wrapper")];
2010
- const finalResponse = compressResponse(webRequest, wrapper ? await wrapper(() => handler(webRequest)) : await handler(webRequest));
2011
- res.writeHead(finalResponse.status, [...finalResponse.headers].flat());
2012
- res.flushHeaders();
2013
- await sendNodeResponse(res, finalResponse);
2014
- } catch (error) {
2015
- if (error instanceof Error) {
2016
- const displayError = isSsrStreamError(error) && error.cause instanceof Error ? error.cause : error;
2017
- const phase = classifyErrorPhase(displayError, projectRoot);
2018
- sendErrorToOverlay(server, displayError, phase, projectRoot);
2019
- respond500(res, displayError, phase, projectRoot, hmrOptions);
2020
- } else {
2021
- process.stderr.write(`\x1b[31m[timber] Dev server error:\x1b[0m ${String(error)}\n`);
2022
- respond500(res, error, "render", projectRoot, hmrOptions);
2023
- }
105
+ if (process.env.TIMBER_ANALYZE === "1") return;
106
+ if (!innerPlugin) return;
107
+ if (typeof innerPlugin.configureServer === "function") await innerPlugin.configureServer.call(this, server);
2024
108
  }
2025
109
  };
2026
110
  }
2027
- /**
2028
- * Send a 500 response without crashing the dev server.
2029
- *
2030
- * In dev mode, renders a styled HTML error page with source context,
2031
- * classified stack trace, and auto-reload on HMR. Falls back to
2032
- * text/plain if the HTML generator fails (must never crash).
2033
- */
2034
- function respond500(res, error, phase, projectRoot, hmrOptions) {
2035
- if (res.headersSent) return;
2036
- if (error instanceof Error) try {
2037
- const html = generateDevErrorPage(error, phase, projectRoot, hmrOptions);
2038
- res.statusCode = 500;
2039
- res.setHeader("content-type", "text/html; charset=utf-8");
2040
- res.end(html);
2041
- return;
2042
- } catch (e) {
2043
- console.debug("[timber] dev error page generation failed, falling back to text/plain:", e instanceof Error ? e.message : e);
2044
- }
2045
- res.statusCode = 500;
2046
- res.setHeader("content-type", "text/plain");
2047
- res.end(`[timber] Internal server error\n\n${error instanceof Error ? error.stack ?? error.message : String(error)}`);
2048
- }
2049
- /**
2050
- * Add timber-specific context to an error's message if it references
2051
- * internal virtual modules (virtual:timber-*). Mutates the error in place.
2052
- */
2053
- function addTimberContext(error) {
2054
- const enriched = addVirtualModuleContext(error.message);
2055
- if (enriched !== error.message) error.message = enriched;
2056
- }
2057
- /**
2058
- * Convert a Node IncomingMessage to a Web Request.
2059
- *
2060
- * Constructs the full URL from the Host header and request URL,
2061
- * and forwards the method, headers, and body.
2062
- *
2063
- * The scheme comes from the socket's TLS state — with `server.https` the
2064
- * browser sends `Origin: https://host`, and a hardcoded `http://` would
2065
- * fail CSRF origin comparison on every POST. X-Forwarded-Proto is NOT
2066
- * consulted: dev connections are direct, so the header would be
2067
- * attacker-controlled. (TIM-1067, B32)
2068
- *
2069
- * Exported for tests.
2070
- */
2071
- function toWebRequest(nodeReq) {
2072
- const protocol = nodeReq.socket?.encrypted ? "https" : "http";
2073
- const host = nodeReq.headers.host ?? nodeReq.headers[":authority"] ?? "localhost";
2074
- const url = `${protocol}://${host}${nodeReq.url}`;
2075
- const headers = new Headers();
2076
- for (const [key, value] of Object.entries(nodeReq.headers)) {
2077
- if (value === void 0) continue;
2078
- if (key.startsWith(":")) continue;
2079
- if (Array.isArray(value)) for (const v of value) headers.append(key, v);
2080
- else headers.set(key, value);
2081
- }
2082
- if (!headers.has("host")) headers.set("host", host);
2083
- const method = nodeReq.method ?? "GET";
2084
- const hasBody = method !== "GET" && method !== "HEAD";
2085
- return new Request(url, {
2086
- method,
2087
- headers,
2088
- body: hasBody ? nodeReadableToWebStream(nodeReq) : void 0,
2089
- duplex: hasBody ? "half" : void 0
2090
- });
2091
- }
2092
- /**
2093
- * Convert a Node Readable stream to a Web ReadableStream.
2094
- */
2095
- function nodeReadableToWebStream(nodeStream) {
2096
- return new ReadableStream({ start(controller) {
2097
- nodeStream.on("data", (chunk) => {
2098
- controller.enqueue(new Uint8Array(chunk));
2099
- });
2100
- nodeStream.on("end", () => {
2101
- controller.close();
2102
- });
2103
- nodeStream.on("error", (err) => {
2104
- controller.error(err);
2105
- });
2106
- } });
2107
- }
2108
- /**
2109
- * Check if a URL is a Vite-internal request that should be passed through.
2110
- */
2111
- function isViteInternal(url) {
2112
- return VITE_INTERNAL_PREFIXES.some((prefix) => url.startsWith(prefix));
2113
- }
2114
- /**
2115
- * Check if a URL looks like a static asset request.
2116
- */
2117
- function isAssetRequest(url) {
2118
- return ASSET_EXTENSIONS.test(url);
2119
- }
2120
- /**
2121
- * Check if a URL maps to a real file in the project's public/ directory.
2122
- * Vite serves public/ files as static assets — if a file exists there it
2123
- * must be passed through to Vite regardless of extension.
2124
- */
2125
- function isPublicFile(server, url) {
2126
- const { publicDir } = server.config;
2127
- if (!publicDir) return false;
2128
- const pathname = url.split("?")[0];
2129
- const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
2130
- if (!relativePath) return false;
2131
- const fullPath = join(publicDir, relativePath);
2132
- try {
2133
- return statSync(fullPath).isFile();
2134
- } catch {
2135
- return false;
2136
- }
2137
- }
2138
- /**
2139
- * Check whether an asset-extension URL belongs to a scanned route rather
2140
- * than a static file.
2141
- *
2142
- * Route handlers may live at paths ending in asset-like extensions
2143
- * (app/feed.json/route.ts → /feed.json), which the asset bypass would
2144
- * otherwise hand to Vite — 404ing in dev while working in production.
2145
- * Uses the same canonicalization and tree walker as the runtime matcher
2146
- * so dev classification agrees with what the pipeline would match.
2147
- *
2148
- * A file that actually exists on disk wins over the route match: the
2149
- * production server serves public/ statics before the RSC handler (see
2150
- * adapters/nitro.ts), and Vite serves root-relative source files (e.g.
2151
- * /app/global.css) in dev. Without this, a broad catch-all route like
2152
- * app/[...slug] would shadow every static asset. Joining the canonical
2153
- * pathname is traversal-safe — canonicalize() resolves `..` segments and
2154
- * rejects root escapes and encoded separators.
2155
- *
2156
- * Reads ctx.routeTree per request — the routing plugin rescans it on
2157
- * file add/unlink/change, so newly created routes are picked up live.
2158
- */
2159
- function isRouteOwnedAsset(routeTree, server, url) {
2160
- if (!routeTree) return false;
2161
- const rawPathname = url.split("?")[0];
2162
- const result = canonicalize(rawPathname);
2163
- if (!result.ok) return false;
2164
- const parts = result.pathname === "/" ? [] : result.pathname.slice(1).split("/");
2165
- if (matchUrlParts(routeTree.root, parts) === null) return false;
2166
- const relativePath = result.pathname.slice(1);
2167
- const { publicDir, root } = server.config;
2168
- if (publicDir && existsSync(join(publicDir, relativePath))) return false;
2169
- if (existsSync(join(root, relativePath))) return false;
2170
- return true;
2171
- }
2172
- /**
2173
- * Listen for client-side errors forwarded from the browser via HMR.
2174
- *
2175
- * The browser entry catches uncaught errors and unhandled rejections,
2176
- * then sends them as 'timber:client-error' custom events. We parse
2177
- * the first app frame for the overlay's loc field and forward the
2178
- * error to Vite's overlay protocol.
2179
- */
2180
- function listenForClientErrors(server, projectRoot) {
2181
- server.hot.on("timber:client-error", (data) => {
2182
- const loc = parseFirstAppFrame(data.stack, projectRoot);
2183
- let message = data.message;
2184
- if (data.componentStack) message = `${data.message}\n\nComponent Stack:\n${data.componentStack.trim()}`;
2185
- const RED = "\x1B[31m";
2186
- const BOLD = "\x1B[1m";
2187
- const RESET = "\x1B[0m";
2188
- process.stderr.write(`${RED}${BOLD}[timber] Client Error${RESET}\n${RED}${data.message}${RESET}\n\n`);
2189
- try {
2190
- server.hot.send({
2191
- type: "error",
2192
- err: {
2193
- message,
2194
- stack: data.stack,
2195
- id: loc?.file,
2196
- plugin: "timber (Client)",
2197
- loc: loc ? {
2198
- file: loc.file,
2199
- line: loc.line,
2200
- column: loc.column
2201
- } : void 0
2202
- }
2203
- });
2204
- } catch (e) {
2205
- console.debug("[timber] overlay send failed:", e instanceof Error ? e.message : e);
2206
- }
2207
- });
2208
- }
2209
111
  //#endregion
2210
112
  //#region src/plugins/entries.ts
2211
113
  var __dirname$1 = dirname(fileURLToPath(import.meta.url));
@@ -2465,7 +367,7 @@ function timberEntries(ctx) {
2465
367
  }
2466
368
  //#endregion
2467
369
  //#region src/plugins/mdx.ts
2468
- var MDX_EXTENSIONS_UNDOTTED = [...MDX_EXTENSIONS].map((ext) => ext.slice(1));
370
+ var MDX_EXTENSIONS_UNDOTTED = [.../* @__PURE__ */ new Set([".mdx", ".md"])].map((ext) => ext.slice(1));
2469
371
  /**
2470
372
  * Check if mdx-components.tsx (or .ts, .jsx, .js) exists at the project root
2471
373
  * or in src/. Root takes precedence, matching Next.js behavior.
@@ -4813,11 +2715,11 @@ function renderEntryCss(entry, urlPrefix) {
4813
2715
  var comma = ",".charCodeAt(0);
4814
2716
  var semicolon = ";".charCodeAt(0);
4815
2717
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4816
- var intToChar = /* @__PURE__ */ new Uint8Array(64);
2718
+ var intToChar$1 = /* @__PURE__ */ new Uint8Array(64);
4817
2719
  var charToInt = /* @__PURE__ */ new Uint8Array(128);
4818
2720
  for (let i = 0; i < chars.length; i++) {
4819
2721
  const c = chars.charCodeAt(i);
4820
- intToChar[i] = c;
2722
+ intToChar$1[i] = c;
4821
2723
  charToInt[c] = i;
4822
2724
  }
4823
2725
  function encodeInteger(builder, num) {
@@ -4825,7 +2727,7 @@ function encodeInteger(builder, num) {
4825
2727
  let clamped = num & 31;
4826
2728
  num >>>= 5;
4827
2729
  if (num > 0) clamped |= 32;
4828
- builder.write(intToChar[clamped]);
2730
+ builder.write(intToChar$1[clamped]);
4829
2731
  } while (num > 0);
4830
2732
  }
4831
2733
  function encodeSign(num) {
@@ -4904,7 +2806,7 @@ function encode(decoded) {
4904
2806
  return writer.flush();
4905
2807
  }
4906
2808
  //#endregion
4907
- //#region ../../node_modules/.pnpm/magic-string@1.3.1/node_modules/magic-string/dist/index.mjs
2809
+ //#region ../../node_modules/.pnpm/magic-string@1.4.1/node_modules/magic-string/dist/index.mjs
4908
2810
  var BitSet = class BitSet {
4909
2811
  constructor(arg) {
4910
2812
  this.bits = arg instanceof BitSet ? arg.bits.slice() : [];
@@ -5076,7 +2978,7 @@ var SourceMap = class {
5076
2978
  this.sources = properties.sources;
5077
2979
  this.sourcesContent = properties.sourcesContent;
5078
2980
  this.names = properties.names;
5079
- this.mappings = encode(properties.mappings);
2981
+ this.mappings = typeof properties.mappings === "string" ? properties.mappings : encode(properties.mappings);
5080
2982
  if (typeof properties.x_google_ignoreList !== "undefined") this.x_google_ignoreList = properties.x_google_ignoreList;
5081
2983
  if (typeof properties.debugId !== "undefined") this.debugId = properties.debugId;
5082
2984
  if (typeof properties.rangeMappings !== "undefined") {
@@ -5150,12 +3052,100 @@ var toString = Object.prototype.toString;
5150
3052
  function isObject(thing) {
5151
3053
  return toString.call(thing) === "[object Object]";
5152
3054
  }
3055
+ var BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3056
+ var intToChar = /* #__PURE__ */ (() => {
3057
+ const chars = /* @__PURE__ */ new Uint8Array(64);
3058
+ for (let i = 0; i < 64; i++) chars[i] = BASE64_CHARS.charCodeAt(i);
3059
+ return chars;
3060
+ })();
3061
+ var COMMA = 44;
3062
+ var SEMICOLON = 59;
3063
+ var BUFFER_SIZE = 16384;
3064
+ var FLUSH_THRESHOLD = 16348;
3065
+ var scratch = /* #__PURE__ */ new Uint8Array(BUFFER_SIZE);
3066
+ function writeVlq(pos, num) {
3067
+ num = num < 0 ? -num << 1 | 1 : num << 1;
3068
+ do {
3069
+ let clamped = num & 31;
3070
+ num >>>= 5;
3071
+ if (num > 0) clamped |= 32;
3072
+ scratch[pos++] = intToChar[clamped];
3073
+ } while (num > 0);
3074
+ return pos;
3075
+ }
3076
+ var decoder = /* #__PURE__ */ new TextDecoder();
3077
+ var MappingsEncoder = class {
3078
+ constructor() {
3079
+ this.out = "";
3080
+ this.pos = 0;
3081
+ this.lines = [];
3082
+ this.buffered = 0;
3083
+ this.needsComma = false;
3084
+ this.prevGenColumn = 0;
3085
+ this.prevSourceIndex = 0;
3086
+ this.prevSourceLine = 0;
3087
+ this.prevSourceColumn = 0;
3088
+ this.prevNameIndex = 0;
3089
+ }
3090
+ endLine(segments) {
3091
+ this.lines.push(segments);
3092
+ this.buffered += segments.length + 1;
3093
+ if (this.buffered >= 4096) this.drain(null);
3094
+ }
3095
+ segments(segments) {
3096
+ this.drain(segments);
3097
+ }
3098
+ finish(segments) {
3099
+ this.drain(segments);
3100
+ this.flush();
3101
+ return this.out;
3102
+ }
3103
+ drain(trailing) {
3104
+ const lines = this.lines;
3105
+ const lineCount = lines.length;
3106
+ for (let l = 0; l <= lineCount; l++) {
3107
+ const line = l < lineCount ? lines[l] : trailing;
3108
+ if (line === null) break;
3109
+ for (let i = 0; i < line.length; i++) {
3110
+ if (this.pos > FLUSH_THRESHOLD) this.flush();
3111
+ const segment = line[i];
3112
+ if (this.needsComma) scratch[this.pos++] = COMMA;
3113
+ this.needsComma = true;
3114
+ this.pos = writeVlq(this.pos, segment[0] - this.prevGenColumn);
3115
+ this.prevGenColumn = segment[0];
3116
+ this.pos = writeVlq(this.pos, segment[1] - this.prevSourceIndex);
3117
+ this.prevSourceIndex = segment[1];
3118
+ this.pos = writeVlq(this.pos, segment[2] - this.prevSourceLine);
3119
+ this.prevSourceLine = segment[2];
3120
+ this.pos = writeVlq(this.pos, segment[3] - this.prevSourceColumn);
3121
+ this.prevSourceColumn = segment[3];
3122
+ if (segment.length === 5) {
3123
+ this.pos = writeVlq(this.pos, segment[4] - this.prevNameIndex);
3124
+ this.prevNameIndex = segment[4];
3125
+ }
3126
+ }
3127
+ if (l < lineCount) {
3128
+ if (this.pos > FLUSH_THRESHOLD) this.flush();
3129
+ scratch[this.pos++] = SEMICOLON;
3130
+ this.needsComma = false;
3131
+ this.prevGenColumn = 0;
3132
+ }
3133
+ }
3134
+ lines.length = 0;
3135
+ this.buffered = 0;
3136
+ this.flush();
3137
+ }
3138
+ flush() {
3139
+ this.out += decoder.decode(scratch.subarray(0, this.pos));
3140
+ this.pos = 0;
3141
+ }
3142
+ };
5153
3143
  var NEWLINE_CHAR$1 = 10;
5154
3144
  function isWordCode(code) {
5155
3145
  return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95;
5156
3146
  }
5157
3147
  var Mappings = class {
5158
- constructor(hires) {
3148
+ constructor(hires, encoder = null) {
5159
3149
  this.hires = hires;
5160
3150
  this.generatedCodeLine = 0;
5161
3151
  this.generatedCodeColumn = 0;
@@ -5163,10 +3153,21 @@ var Mappings = class {
5163
3153
  this.rawSegments = this.raw[this.generatedCodeLine] = [];
5164
3154
  this.rawRangeMappings = [];
5165
3155
  this.rawRangeMappingsIndices = this.rawRangeMappings[this.generatedCodeLine] = [];
5166
- this.pending = null;
3156
+ this.encoder = encoder;
3157
+ }
3158
+ nextLine() {
3159
+ if (this.encoder === null) {
3160
+ this.generatedCodeLine += 1;
3161
+ this.raw[this.generatedCodeLine] = this.rawSegments = [];
3162
+ } else {
3163
+ this.encoder.endLine(this.rawSegments);
3164
+ this.rawSegments = [];
3165
+ this.generatedCodeLine += 1;
3166
+ }
3167
+ this.generatedCodeColumn = 0;
3168
+ this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
5167
3169
  }
5168
3170
  addEdit(sourceIndex, content, loc, nameIndex) {
5169
- /* v8 ignore else -- `pending` is never assigned a truthy value */
5170
3171
  if (content.length) {
5171
3172
  const contentLengthMinusOne = content.length - 1;
5172
3173
  let contentLineEnd = content.indexOf("\n", 0);
@@ -5180,10 +3181,7 @@ var Mappings = class {
5180
3181
  ];
5181
3182
  if (nameIndex >= 0) segment.push(nameIndex);
5182
3183
  this.rawSegments.push(segment);
5183
- this.generatedCodeLine += 1;
5184
- this.raw[this.generatedCodeLine] = this.rawSegments = [];
5185
- this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
5186
- this.generatedCodeColumn = 0;
3184
+ this.nextLine();
5187
3185
  previousContentLineEnd = contentLineEnd;
5188
3186
  contentLineEnd = content.indexOf("\n", contentLineEnd + 1);
5189
3187
  }
@@ -5196,11 +3194,7 @@ var Mappings = class {
5196
3194
  if (nameIndex >= 0) segment.push(nameIndex);
5197
3195
  this.rawSegments.push(segment);
5198
3196
  this.advance(content.slice(previousContentLineEnd + 1));
5199
- } else if (this.pending) {
5200
- this.rawSegments.push(this.pending);
5201
- this.advance(content);
5202
3197
  }
5203
- this.pending = null;
5204
3198
  }
5205
3199
  addUneditedChunk(sourceIndex, chunk, original, loc, sourcemapLocations) {
5206
3200
  const end = chunk.end;
@@ -5208,8 +3202,13 @@ var Mappings = class {
5208
3202
  if (this.hires) {
5209
3203
  const boundary = this.hires === "boundary";
5210
3204
  const experimentalRange = this.hires === "experimental-range";
3205
+ const encoder = experimentalRange ? null : this.encoder;
5211
3206
  let charInHiresBoundary = false;
5212
3207
  while (i < end) {
3208
+ if (encoder !== null && this.rawSegments.length >= 4096) {
3209
+ encoder.segments(this.rawSegments);
3210
+ this.rawSegments.length = 0;
3211
+ }
5213
3212
  if (experimentalRange && i + 1 >= end) this.rawSegments.push([
5214
3213
  this.generatedCodeColumn,
5215
3214
  sourceIndex,
@@ -5220,10 +3219,7 @@ var Mappings = class {
5220
3219
  if (code === NEWLINE_CHAR$1) {
5221
3220
  loc.line += 1;
5222
3221
  loc.column = 0;
5223
- this.generatedCodeLine += 1;
5224
- this.raw[this.generatedCodeLine] = this.rawSegments = [];
5225
- this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
5226
- this.generatedCodeColumn = 0;
3222
+ this.nextLine();
5227
3223
  charInHiresBoundary = false;
5228
3224
  } else {
5229
3225
  if (boundary) {
@@ -5304,26 +3300,15 @@ var Mappings = class {
5304
3300
  if (newline === end) break;
5305
3301
  loc.line += 1;
5306
3302
  loc.column = 0;
5307
- this.generatedCodeLine += 1;
5308
- this.raw[this.generatedCodeLine] = this.rawSegments = [];
5309
- this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
5310
- this.generatedCodeColumn = 0;
3303
+ this.nextLine();
5311
3304
  i = newline + 1;
5312
3305
  }
5313
3306
  }
5314
- this.pending = null;
5315
3307
  }
5316
3308
  advance(str) {
5317
3309
  if (!str) return;
5318
3310
  const lastNewline = str.lastIndexOf("\n");
5319
- if (lastNewline !== -1) {
5320
- for (let i = str.indexOf("\n"); i !== -1; i = str.indexOf("\n", i + 1)) {
5321
- this.generatedCodeLine++;
5322
- this.raw[this.generatedCodeLine] = this.rawSegments = [];
5323
- this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
5324
- }
5325
- this.generatedCodeColumn = 0;
5326
- }
3311
+ for (let i = str.indexOf("\n"); i !== -1; i = str.indexOf("\n", i + 1)) this.nextLine();
5327
3312
  this.generatedCodeColumn += str.length - lastNewline - 1;
5328
3313
  }
5329
3314
  };
@@ -5542,9 +3527,32 @@ var MagicString = class MagicString {
5542
3527
  */
5543
3528
  generateDecodedMap(options) {
5544
3529
  options = options || {};
3530
+ const mappings = new Mappings(options.hires);
3531
+ const names = this._generateMappings(mappings);
3532
+ return {
3533
+ ...this._mapProperties(options, names),
3534
+ mappings: mappings.raw,
3535
+ rangeMappings: mappings.rawRangeMappings
3536
+ };
3537
+ }
3538
+ /**
3539
+ * Generates a version 3 sourcemap.
3540
+ */
3541
+ generateMap(options) {
3542
+ options = options || {};
3543
+ const encoder = new MappingsEncoder();
3544
+ const mappings = new Mappings(options.hires, encoder);
3545
+ const names = this._generateMappings(mappings);
3546
+ return new SourceMap({
3547
+ ...this._mapProperties(options, names),
3548
+ mappings: encoder.finish(mappings.rawSegments),
3549
+ rangeMappings: mappings.rawRangeMappings
3550
+ });
3551
+ }
3552
+ /** @internal */
3553
+ _generateMappings(mappings) {
5545
3554
  const sourceIndex = 0;
5546
3555
  const names = Object.keys(this.storedNames);
5547
- const mappings = new Mappings(options.hires);
5548
3556
  const locate = getLocator(this.original);
5549
3557
  if (this.intro) mappings.advance(this.intro);
5550
3558
  this.firstChunk.eachNext((chunk) => {
@@ -5555,22 +3563,18 @@ var MagicString = class MagicString {
5555
3563
  if (chunk.outro.length) mappings.advance(chunk.outro);
5556
3564
  });
5557
3565
  if (this.outro) mappings.advance(this.outro);
3566
+ return names;
3567
+ }
3568
+ /** @internal */
3569
+ _mapProperties(options, names) {
5558
3570
  return {
5559
3571
  file: options.file ? options.file.split(/[/\\]/).pop() : void 0,
5560
3572
  sources: [options.source ? getRelativePath(options.file || "", options.source) : options.file || ""],
5561
3573
  sourcesContent: options.includeContent ? [this.original] : void 0,
5562
3574
  names,
5563
- mappings: mappings.raw,
5564
- x_google_ignoreList: this.ignoreList ? [sourceIndex] : void 0,
5565
- rangeMappings: mappings.rawRangeMappings
3575
+ x_google_ignoreList: this.ignoreList ? [0] : void 0
5566
3576
  };
5567
3577
  }
5568
- /**
5569
- * Generates a version 3 sourcemap.
5570
- */
5571
- generateMap(options) {
5572
- return new SourceMap(this.generateDecodedMap(options));
5573
- }
5574
3578
  /** @internal */
5575
3579
  _ensureindentStr() {
5576
3580
  if (this.indentStr === void 0) this.indentStr = guessIndent(this.original);
@@ -5683,8 +3687,14 @@ var MagicString = class MagicString {
5683
3687
  }
5684
3688
  /**
5685
3689
  * Moves the characters from `start` and `end` to `index`.
3690
+ *
3691
+ * `affinity` controls where the range is anchored at `index`. With the
3692
+ * default `'right'`, it is inserted before the content that starts at `index`;
3693
+ * with `'left'`, it is inserted after the content that ends at `index`. The
3694
+ * two differ only when other content has already been moved to that boundary,
3695
+ * mirroring the `appendLeft`/`appendRight` distinction.
5686
3696
  */
5687
- move(start, end, index) {
3697
+ move(start, end, index, affinity = "right") {
5688
3698
  start = start + this.offset;
5689
3699
  end = end + this.offset;
5690
3700
  index = index + this.offset;
@@ -5704,9 +3714,27 @@ var MagicString = class MagicString {
5704
3714
  }
5705
3715
  const oldLeft = first.previous;
5706
3716
  const oldRight = last.next;
5707
- const newRight = this.byStart.get(index);
5708
- if (!newRight && last === this.lastChunk) return this;
5709
- const newLeft = newRight ? newRight.previous : this.lastChunk;
3717
+ let newLeft;
3718
+ let newRight;
3719
+ if (affinity === "left") {
3720
+ newLeft = this.byEnd.get(index) ?? null;
3721
+ if (!newLeft) {
3722
+ if (first === this.firstChunk) return this;
3723
+ newRight = this.firstChunk;
3724
+ } else {
3725
+ if (newLeft.next === first) return this;
3726
+ newRight = newLeft.next;
3727
+ }
3728
+ } else {
3729
+ newRight = this.byStart.get(index) ?? null;
3730
+ if (!newRight) {
3731
+ if (last === this.lastChunk) return this;
3732
+ newLeft = this.lastChunk;
3733
+ } else {
3734
+ if (newRight.previous === last) return this;
3735
+ newLeft = newRight.previous;
3736
+ }
3737
+ }
5710
3738
  if (oldLeft) oldLeft.next = oldRight;
5711
3739
  if (oldRight) oldRight.previous = oldLeft;
5712
3740
  if (newLeft) newLeft.next = first;
@@ -5831,6 +3859,9 @@ var MagicString = class MagicString {
5831
3859
  }
5832
3860
  /**
5833
3861
  * Removes the characters from `start` to `end` (of the original string, **not** the generated string).
3862
+ * Content appended or prepended at positions strictly inside the range is removed with it, while
3863
+ * content attached at `start` or `end` is preserved — use `s.overwrite(start, end, '')` to remove
3864
+ * the range including its edge inserts.
5834
3865
  * Removing the same content twice, or making removals that partially overlap, will cause an error.
5835
3866
  */
5836
3867
  remove(start, end) {
@@ -5847,9 +3878,9 @@ var MagicString = class MagicString {
5847
3878
  this._split(end);
5848
3879
  let chunk = this.byStart.get(start);
5849
3880
  while (chunk) {
5850
- chunk.intro = "";
5851
- chunk.outro = "";
5852
- chunk.edit("");
3881
+ if (chunk.start > start) chunk.intro = "";
3882
+ if (chunk.end < end) chunk.outro = "";
3883
+ chunk.edit("", false, true);
5853
3884
  chunk = end > chunk.end ? this.byStart.get(chunk.end) : null;
5854
3885
  }
5855
3886
  return this;
@@ -5929,12 +3960,13 @@ var MagicString = class MagicString {
5929
3960
  let result = "";
5930
3961
  let chunk = this.firstChunk;
5931
3962
  while (chunk && (chunk.start > start || chunk.end <= start)) {
5932
- if (chunk.start < end && chunk.end >= end) return result;
3963
+ if (chunk.start < end && chunk.end >= end || end === 0 && chunk.start === 0) return result;
5933
3964
  chunk = chunk.next;
5934
3965
  }
5935
3966
  if (chunk && chunk.edited && chunk.start !== start) throw new MagicStringError(`cannot use edited character ${start} as slice start anchor`);
5936
3967
  const startChunk = chunk;
5937
3968
  while (chunk) {
3969
+ if (end === 0 && chunk.start === 0) break;
5938
3970
  if (chunk.intro && (startChunk !== chunk || chunk.start === start)) result += chunk.intro;
5939
3971
  const containsEnd = chunk.start < end && chunk.end >= end;
5940
3972
  if (containsEnd && chunk.edited && chunk.end !== end) throw new MagicStringError(`cannot use edited character ${end} as slice end anchor`);
@@ -6047,7 +4079,8 @@ var MagicString = class MagicString {
6047
4079
  if (aborted) return true;
6048
4080
  chunk = chunk.previous;
6049
4081
  } while (chunk);
6050
- return false;
4082
+ this.intro = this.intro.replace(rx, "");
4083
+ return this.intro.length > 0;
6051
4084
  }
6052
4085
  /**
6053
4086
  * Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the end.
@@ -6074,7 +4107,8 @@ var MagicString = class MagicString {
6074
4107
  if (aborted) return true;
6075
4108
  chunk = chunk.next;
6076
4109
  } while (chunk);
6077
- return false;
4110
+ this.outro = this.outro.replace(rx, "");
4111
+ return this.outro.length > 0;
6078
4112
  }
6079
4113
  /**
6080
4114
  * Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the start.
@@ -6101,6 +4135,22 @@ var MagicString = class MagicString {
6101
4135
  }
6102
4136
  return outputIndex !== this.original.length;
6103
4137
  }
4138
+ /**
4139
+ * Whether the original range [start, end) has had any of its characters
4140
+ * removed. `replace`/`replaceAll` search `original`, so a match can land on
4141
+ * text that is no longer in the output - overwriting it would resurrect the
4142
+ * removed characters, so such matches are skipped instead.
4143
+ *
4144
+ * @internal
4145
+ */
4146
+ _hasRemovedContent(start, end) {
4147
+ let chunk = this.firstChunk;
4148
+ while (chunk) {
4149
+ if (chunk.content === "" && chunk.start < end && chunk.end > start) return true;
4150
+ chunk = chunk.next;
4151
+ }
4152
+ return false;
4153
+ }
6104
4154
  /** @internal */
6105
4155
  _replaceRegexp(searchValue, replacement) {
6106
4156
  function getReplacement(match, str) {
@@ -6109,32 +4159,42 @@ var MagicString = class MagicString {
6109
4159
  }
6110
4160
  const replaceMatch = (match) => {
6111
4161
  /* v8 ignore next 2 -- `match.index` is always defined for matches from `matchAll` */
6112
- if (match.index == null) return;
4162
+ if (match.index == null) return false;
4163
+ if (this._hasRemovedContent(match.index, match.index + match[0].length)) return false;
6113
4164
  const replacement = getReplacement(match, this.original);
6114
- if (replacement === match[0]) return;
4165
+ if (replacement === match[0]) return true;
6115
4166
  if (match[0].length === 0) this.appendRight(match.index, replacement);
6116
4167
  else this.overwrite(match.index, match.index + match[0].length, replacement);
4168
+ return true;
6117
4169
  };
6118
4170
  if (searchValue.global) {
6119
4171
  searchValue.lastIndex = 0;
6120
4172
  for (const match of this.original.matchAll(searchValue)) replaceMatch(match);
6121
4173
  } else {
6122
4174
  const match = this.original.match(searchValue);
6123
- if (match) replaceMatch(match);
4175
+ if (match && !replaceMatch(match)) {
4176
+ const global = new RegExp(searchValue.source, `${searchValue.flags}g`);
4177
+ for (const next of this.original.matchAll(global)) if (replaceMatch(next)) break;
4178
+ }
6124
4179
  }
6125
4180
  return this;
6126
4181
  }
6127
4182
  /** @internal */
6128
4183
  _replaceString(string, replacement) {
6129
4184
  const { original } = this;
6130
- const index = original.indexOf(string);
6131
- if (index !== -1) {
4185
+ let index = original.indexOf(string);
4186
+ while (index !== -1) {
4187
+ if (this._hasRemovedContent(index, index + string.length)) {
4188
+ index = original.indexOf(string, index + string.length);
4189
+ continue;
4190
+ }
6132
4191
  if (typeof replacement === "function") replacement = replacement(string, index, original);
6133
4192
  else replacement = expandReplacement(replacement, string, index, original, [], void 0);
6134
4193
  if (string !== replacement) {
6135
4194
  if (string.length === 0) this.appendRight(index, replacement);
6136
4195
  else this.overwrite(index, index + string.length, replacement);
6137
4196
  }
4197
+ break;
6138
4198
  }
6139
4199
  return this;
6140
4200
  }
@@ -6157,6 +4217,7 @@ var MagicString = class MagicString {
6157
4217
  return this;
6158
4218
  }
6159
4219
  for (let index = original.indexOf(string); index !== -1; index = original.indexOf(string, index + stringLength)) {
4220
+ if (this._hasRemovedContent(index, index + stringLength)) continue;
6160
4221
  const previous = original.slice(index, index + stringLength);
6161
4222
  const _replacement = typeof replacement === "function" ? replacement(previous, index, original) : expandReplacement(replacement, previous, index, original, [], void 0);
6162
4223
  if (previous !== _replacement) this.overwrite(index, index + stringLength, _replacement);
@@ -11313,17 +9374,13 @@ function timber(config) {
11313
9374
  ctx.appDir = resolveAppDir(resolved.root, ctx.config.appDir);
11314
9375
  ctx.dev = resolved.command === "serve";
11315
9376
  ctx.base = resolved.base;
11316
- for (const name of [
11317
- "timber.config.ts",
11318
- "timber.config.js",
11319
- "timber.config.mjs"
11320
- ]) resolved.configFileDependencies.push(resolve(resolved.root, name));
11321
9377
  ctx.buildDir = resolve(resolved.root, resolved.build.outDir);
11322
9378
  if (!ctx.dev) ctx.timer = createNoopTimer();
11323
9379
  else ctx.timer.start("dev-server-setup");
11324
9380
  const configErrors = validateConfig(ctx.config);
11325
9381
  if (configErrors.length > 0) process.stderr.write(`${formatConfigErrors(configErrors)}\n\n`);
11326
- const peerWarning = formatMissingPeers(checkPeerDependencies(ctx.root));
9382
+ const peerResults = checkPeerDependencies(ctx.root);
9383
+ const peerWarning = formatMissingPeers(peerResults);
11327
9384
  if (peerWarning) process.stderr.write(`${peerWarning}\n\n`);
11328
9385
  }
11329
9386
  };