@timber-js/app 0.2.0-alpha.158 → 0.2.0-alpha.159

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 (286) hide show
  1. package/dist/_chunks/{actions-BA5tIpVu.js → actions-CBbP1hHO.js} +3 -3
  2. package/dist/_chunks/{actions-BA5tIpVu.js.map → actions-CBbP1hHO.js.map} +1 -1
  3. package/dist/_chunks/{build-output-helper-C8q4Zpf5.js → build-output-helper-DF5uScqS.js} +4 -1
  4. package/dist/_chunks/{build-output-helper-C8q4Zpf5.js.map → build-output-helper-DF5uScqS.js.map} +1 -1
  5. package/dist/_chunks/cache-api-DsWEYwVZ.js +1120 -0
  6. package/dist/_chunks/cache-api-DsWEYwVZ.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js → cli-schema-sync-Bivx4Rn7.js} +4 -3
  8. package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js.map → cli-schema-sync-Bivx4Rn7.js.map} +1 -1
  9. package/dist/_chunks/dist-BA3u1z3W.js +423 -0
  10. package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
  11. package/dist/_chunks/{logger-C5v1IMuh.js → logger-CrRmZMlo.js} +81 -56
  12. package/dist/_chunks/logger-CrRmZMlo.js.map +1 -0
  13. package/dist/_chunks/{plugin-context-DUl3dnCF.js → plugin-context-DeAxFRMq.js} +3 -36
  14. package/dist/_chunks/plugin-context-DeAxFRMq.js.map +1 -0
  15. package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
  16. package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
  17. package/dist/_chunks/{walkers-D5jCGPum.js → walkers-DXbJkeCY.js} +217 -19
  18. package/dist/_chunks/walkers-DXbJkeCY.js.map +1 -0
  19. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  20. package/dist/adapters/cloudflare.d.ts +28 -0
  21. package/dist/adapters/cloudflare.d.ts.map +1 -1
  22. package/dist/adapters/cloudflare.js +143 -9
  23. package/dist/adapters/cloudflare.js.map +1 -1
  24. package/dist/adapters/nitro.js +1 -1
  25. package/dist/cache/cache-api.d.ts +2 -0
  26. package/dist/cache/cache-api.d.ts.map +1 -1
  27. package/dist/cache/handler-store.d.ts +2 -0
  28. package/dist/cache/handler-store.d.ts.map +1 -1
  29. package/dist/cache/index.d.ts +2 -0
  30. package/dist/cache/index.d.ts.map +1 -1
  31. package/dist/cache/index.js +3 -186
  32. package/dist/cache/redis-handler.d.ts +17 -5
  33. package/dist/cache/redis-handler.d.ts.map +1 -1
  34. package/dist/cache/singleflight.d.ts +1 -1
  35. package/dist/cache/singleflight.d.ts.map +1 -1
  36. package/dist/cache/store.d.ts +53 -0
  37. package/dist/cache/store.d.ts.map +1 -0
  38. package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
  39. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
  40. package/dist/cache/stores/cloudflare-kv.js +29 -0
  41. package/dist/cache/stores/cloudflare-kv.js.map +1 -0
  42. package/dist/cache/stores/memory.d.ts +42 -0
  43. package/dist/cache/stores/memory.d.ts.map +1 -0
  44. package/dist/cache/stores/memory.js +90 -0
  45. package/dist/cache/stores/memory.js.map +1 -0
  46. package/dist/cache/stores/redis.d.ts +69 -0
  47. package/dist/cache/stores/redis.d.ts.map +1 -0
  48. package/dist/cache/stores/redis.js +35 -0
  49. package/dist/cache/stores/redis.js.map +1 -0
  50. package/dist/cache/stores/vercel.d.ts +36 -0
  51. package/dist/cache/stores/vercel.d.ts.map +1 -0
  52. package/dist/cache/stores/vercel.js +33 -0
  53. package/dist/cache/stores/vercel.js.map +1 -0
  54. package/dist/cache/tag-aware-handler.d.ts +51 -0
  55. package/dist/cache/tag-aware-handler.d.ts.map +1 -0
  56. package/dist/cache/timber-cache.d.ts.map +1 -1
  57. package/dist/cdn/cloudflare-purge.d.ts +23 -0
  58. package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
  59. package/dist/cdn/fastly-purge.d.ts +25 -0
  60. package/dist/cdn/fastly-purge.d.ts.map +1 -0
  61. package/dist/cdn/headers.d.ts +82 -0
  62. package/dist/cdn/headers.d.ts.map +1 -0
  63. package/dist/cdn/index.d.ts +5 -0
  64. package/dist/cdn/index.d.ts.map +1 -0
  65. package/dist/cdn/purge-store.d.ts +23 -0
  66. package/dist/cdn/purge-store.d.ts.map +1 -0
  67. package/dist/cli.js +2 -2
  68. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  69. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  70. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  71. package/dist/client/history.d.ts +0 -3
  72. package/dist/client/history.d.ts.map +1 -1
  73. package/dist/client/index.js +0 -1
  74. package/dist/client/index.js.map +1 -1
  75. package/dist/client/internal.js +11 -86
  76. package/dist/client/internal.js.map +1 -1
  77. package/dist/client/navigation-api.d.ts +1 -3
  78. package/dist/client/navigation-api.d.ts.map +1 -1
  79. package/dist/client/router.d.ts +1 -4
  80. package/dist/client/router.d.ts.map +1 -1
  81. package/dist/client/rsc-fetch.d.ts +1 -11
  82. package/dist/client/rsc-fetch.d.ts.map +1 -1
  83. package/dist/client/segment-cache.d.ts +1 -3
  84. package/dist/client/segment-cache.d.ts.map +1 -1
  85. package/dist/client/use-cookie.d.ts.map +1 -1
  86. package/dist/config-types.d.ts +3 -1
  87. package/dist/config-types.d.ts.map +1 -1
  88. package/dist/config-validation.d.ts.map +1 -1
  89. package/dist/cookies/index.js +15 -11
  90. package/dist/cookies/index.js.map +1 -1
  91. package/dist/dev-tools/browser-logs.d.ts +28 -72
  92. package/dist/dev-tools/browser-logs.d.ts.map +1 -1
  93. package/dist/dev-tools/index.d.ts +1 -1
  94. package/dist/dev-tools/index.d.ts.map +1 -1
  95. package/dist/dev-tools/logger.d.ts.map +1 -1
  96. package/dist/dev-tools/terminal.d.ts.map +1 -1
  97. package/dist/fonts/transform.d.ts +4 -0
  98. package/dist/fonts/transform.d.ts.map +1 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1269 -359
  101. package/dist/index.js.map +1 -1
  102. package/dist/plugin-context.d.ts +36 -0
  103. package/dist/plugin-context.d.ts.map +1 -1
  104. package/dist/plugins/build-report.d.ts +21 -0
  105. package/dist/plugins/build-report.d.ts.map +1 -1
  106. package/dist/plugins/cache.d.ts +5 -14
  107. package/dist/plugins/cache.d.ts.map +1 -1
  108. package/dist/plugins/callsite-ast.d.ts +95 -0
  109. package/dist/plugins/callsite-ast.d.ts.map +1 -0
  110. package/dist/plugins/callsite-jsx.d.ts +35 -0
  111. package/dist/plugins/callsite-jsx.d.ts.map +1 -0
  112. package/dist/plugins/entries.d.ts.map +1 -1
  113. package/dist/plugins/fonts.d.ts.map +1 -1
  114. package/dist/plugins/prebuilt-capture.d.ts +60 -0
  115. package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
  116. package/dist/plugins/prebuilt.d.ts +82 -0
  117. package/dist/plugins/prebuilt.d.ts.map +1 -0
  118. package/dist/plugins/prerender-sugar.d.ts +42 -0
  119. package/dist/plugins/prerender-sugar.d.ts.map +1 -0
  120. package/dist/plugins/server-bundle.d.ts.map +1 -1
  121. package/dist/plugins/shims.d.ts.map +1 -1
  122. package/dist/plugins/static-build.d.ts.map +1 -1
  123. package/dist/routing/convention-lint.d.ts.map +1 -1
  124. package/dist/routing/export-detect.d.ts +56 -0
  125. package/dist/routing/export-detect.d.ts.map +1 -0
  126. package/dist/routing/index.js +2 -2
  127. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  128. package/dist/rsc-runtime/rsc.d.ts +1 -1
  129. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/actions.d.ts +0 -3
  132. package/dist/server/actions.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +7 -0
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/cookie-parsing.d.ts +15 -1
  136. package/dist/server/cookie-parsing.d.ts.map +1 -1
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/html-injectors.d.ts +1 -0
  139. package/dist/server/html-injectors.d.ts.map +1 -1
  140. package/dist/server/index.js +2 -2
  141. package/dist/server/internal.d.ts +6 -1
  142. package/dist/server/internal.d.ts.map +1 -1
  143. package/dist/server/internal.js +563 -74
  144. package/dist/server/internal.js.map +1 -1
  145. package/dist/server/prebuilt/cache-key.d.ts +58 -0
  146. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  147. package/dist/server/prebuilt/capture-state.d.ts +57 -0
  148. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  149. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  150. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  151. package/dist/server/prebuilt/overlay.d.ts +100 -0
  152. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  153. package/dist/server/prebuilt/payload-source.d.ts +75 -0
  154. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  155. package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
  156. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  157. package/dist/server/prebuilt-builder.d.ts +110 -0
  158. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  159. package/dist/server/prebuilt-runtime.d.ts +101 -0
  160. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  161. package/dist/server/route-element-builder.d.ts +10 -8
  162. package/dist/server/route-element-builder.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/helpers.d.ts +0 -5
  164. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/index.d.ts +10 -0
  166. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  167. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  168. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  169. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
  170. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  171. package/dist/server/sitemap-generator.d.ts +6 -6
  172. package/dist/server/sitemap-generator.d.ts.map +1 -1
  173. package/dist/server/tracing.d.ts +37 -3
  174. package/dist/server/tracing.d.ts.map +1 -1
  175. package/docs/api/32-api-cache.mdx +10 -4
  176. package/docs/learn/00-introduction.mdx +8 -13
  177. package/docs/learn/02-pages-and-layouts.mdx +9 -0
  178. package/docs/learn/06-forms-and-actions.mdx +0 -4
  179. package/docs/learn/07-typed-routes.mdx +127 -0
  180. package/docs/learn/09-caching.mdx +12 -1
  181. package/docs/learn/10-middleware.mdx +1 -0
  182. package/docs/learn/11-error-handling.mdx +9 -0
  183. package/docs/learn/13-configuration.mdx +28 -0
  184. package/docs/learn/14-deploying.mdx +52 -0
  185. package/docs/more/06-instrumentation.mdx +27 -1
  186. package/docs/more/07-security.mdx +8 -2
  187. package/docs/more/08-developer-experience.mdx +134 -0
  188. package/package.json +31 -2
  189. package/src/adapters/build-output-helper.ts +10 -0
  190. package/src/adapters/cloudflare.ts +178 -4
  191. package/src/cache/cache-api.ts +60 -0
  192. package/src/cache/handler-store.ts +8 -0
  193. package/src/cache/index.ts +4 -0
  194. package/src/cache/redis-handler.ts +25 -13
  195. package/src/cache/singleflight.ts +11 -11
  196. package/src/cache/store.ts +71 -0
  197. package/src/cache/stores/cloudflare-kv.ts +52 -0
  198. package/src/cache/stores/memory.ts +125 -0
  199. package/src/cache/stores/redis.ts +97 -0
  200. package/src/cache/stores/vercel.ts +59 -0
  201. package/src/cache/tag-aware-handler.ts +233 -0
  202. package/src/cache/timber-cache.ts +17 -7
  203. package/src/cdn/cloudflare-purge.ts +50 -0
  204. package/src/cdn/fastly-purge.ts +63 -0
  205. package/src/cdn/headers.ts +127 -0
  206. package/src/cdn/index.ts +16 -0
  207. package/src/cdn/purge-store.ts +34 -0
  208. package/src/client/browser-entry/action-dispatch.ts +1 -10
  209. package/src/client/browser-entry/post-hydration.ts +0 -1
  210. package/src/client/browser-entry/router-init.ts +0 -5
  211. package/src/client/history.ts +0 -4
  212. package/src/client/navigation-api.ts +5 -11
  213. package/src/client/router.ts +9 -43
  214. package/src/client/rsc-fetch.ts +5 -27
  215. package/src/client/segment-cache.ts +1 -4
  216. package/src/client/use-cookie.ts +18 -12
  217. package/src/config-types.ts +3 -1
  218. package/src/config-validation.ts +1 -0
  219. package/src/dev-tools/browser-logs.ts +37 -273
  220. package/src/dev-tools/index.ts +2 -2
  221. package/src/dev-tools/instrumentation.ts +1 -1
  222. package/src/dev-tools/logger.ts +33 -51
  223. package/src/dev-tools/terminal.ts +21 -22
  224. package/src/fonts/transform.ts +4 -43
  225. package/src/index.ts +16 -3
  226. package/src/plugin-context.ts +36 -0
  227. package/src/plugins/build-report.ts +133 -1
  228. package/src/plugins/cache.ts +32 -131
  229. package/src/plugins/callsite-ast.ts +167 -0
  230. package/src/plugins/callsite-jsx.ts +243 -0
  231. package/src/plugins/entries.ts +23 -4
  232. package/src/plugins/fonts.ts +16 -2
  233. package/src/plugins/prebuilt-capture.ts +208 -0
  234. package/src/plugins/prebuilt.ts +673 -0
  235. package/src/plugins/prerender-sugar.ts +329 -0
  236. package/src/plugins/server-bundle.ts +12 -0
  237. package/src/plugins/shims.ts +9 -0
  238. package/src/plugins/static-build.ts +0 -1
  239. package/src/routing/codegen.ts +1 -21
  240. package/src/routing/convention-lint.ts +85 -49
  241. package/src/routing/export-detect.ts +304 -0
  242. package/src/routing/manifest-codegen.ts +2 -11
  243. package/src/rsc-runtime/rsc.ts +7 -0
  244. package/src/server/action-handler.ts +11 -27
  245. package/src/server/actions.ts +3 -4
  246. package/src/server/als-registry.ts +7 -0
  247. package/src/server/cookie-parsing.ts +75 -59
  248. package/src/server/deny-renderer.ts +7 -15
  249. package/src/server/html-injectors.ts +11 -3
  250. package/src/server/internal.ts +17 -1
  251. package/src/server/prebuilt/cache-key.ts +224 -0
  252. package/src/server/prebuilt/capture-state.ts +89 -0
  253. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  254. package/src/server/prebuilt/overlay.ts +256 -0
  255. package/src/server/prebuilt/payload-source.ts +192 -0
  256. package/src/server/prebuilt/synthetic-store.ts +108 -0
  257. package/src/server/prebuilt-builder.ts +640 -0
  258. package/src/server/prebuilt-runtime.ts +706 -0
  259. package/src/server/route-element-builder.ts +40 -12
  260. package/src/server/rsc-entry/helpers.ts +0 -17
  261. package/src/server/rsc-entry/index.ts +83 -0
  262. package/src/server/rsc-entry/render-route.ts +6 -33
  263. package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
  264. package/src/server/rsc-entry/rsc-payload.ts +5 -11
  265. package/src/server/sitemap-generator.ts +16 -10
  266. package/src/server/tracing.ts +88 -3
  267. package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
  268. package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
  269. package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
  270. package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
  271. package/dist/_chunks/validation-CnKhGJsp.js +0 -113
  272. package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
  273. package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
  274. package/dist/cache/index.js.map +0 -1
  275. package/dist/client/head.d.ts +0 -16
  276. package/dist/client/head.d.ts.map +0 -1
  277. package/dist/client/navigation-api-types.d.ts +0 -90
  278. package/dist/client/navigation-api-types.d.ts.map +0 -1
  279. package/dist/server/client-module-map.d.ts +0 -47
  280. package/dist/server/client-module-map.d.ts.map +0 -1
  281. package/dist/shared/head-header.d.ts +0 -54
  282. package/dist/shared/head-header.d.ts.map +0 -1
  283. package/src/client/head.ts +0 -61
  284. package/src/client/navigation-api-types.ts +0 -112
  285. package/src/server/client-module-map.ts +0 -58
  286. package/src/shared/head-header.ts +0 -69
@@ -1,23 +1,19 @@
1
1
  /**
2
- * timber-dev-browser-logs — Forwards browser console output to the server terminal.
2
+ * Browser log forwarding — maps timber's devBrowserLogs config to Vite 8's
3
+ * server.forwardConsole option.
3
4
  *
4
- * Injects a small inline script in dev mode that intercepts browser
5
- * `console.error`, `console.warn`, and `console.info`, then forwards
6
- * messages to the server via Vite's HMR WebSocket.
5
+ * Vite 8 ships built-in browser→terminal console forwarding via
6
+ * server.forwardConsole, which patches console methods in /@vite/client
7
+ * and prints forwarded output server-side. This replaces timber's custom
8
+ * client-side interception script and HMR WebSocket transport.
7
9
  *
8
- * The server-side listener formats and prints the messages with color-coded
9
- * prefixes: [browser:error], [browser:warn], [browser:info].
10
+ * Client error forwarding (overlay echo, digest filtering, componentStack)
11
+ * remains in browser-dev.ts — Vite's forwardConsole only logs to terminal
12
+ * and has no client-side filter hook. See TIM-1152.
10
13
  *
11
- * Dev-only: this plugin only runs during `vite dev`.
12
- * No runtime overhead in production.
13
- *
14
- * See TIM-513 for design context.
15
- * See design/18-build-system.md §"Dev Server" for sub-plugin architecture.
14
+ * See design/21-dev-server.md §"Browser Log Forwarding"
16
15
  */
17
16
 
18
- import type { Plugin, ViteDevServer } from 'vite';
19
- import type { PluginContext } from '../plugin-context.js';
20
-
21
17
  // ─── Types ───────────────────────────────────────────────────────────────
22
18
 
23
19
  /** Log levels forwarded from the browser. */
@@ -26,266 +22,34 @@ export type BrowserLogLevel = 'error' | 'warn' | 'info';
26
22
  /** Configuration value for devBrowserLogs in timber.config.ts. */
27
23
  export type DevBrowserLogsConfig = BrowserLogLevel | 'none' | undefined;
28
24
 
29
- /**
30
- * Payload sent from browser to server via Vite's HMR WebSocket.
31
- * Kept small — truncated before sending.
32
- */
33
- export interface BrowserLogPayload {
34
- level: BrowserLogLevel;
35
- /** Serialized message string. */
36
- message: string;
37
- /** Error stack trace, if the first argument was an Error. */
38
- stack: string | null;
39
- /** Source URL and line number, if available. */
40
- source: string | null;
41
- /** Timestamp in ms (Date.now()) from the browser. */
42
- timestamp: number;
43
- }
44
-
45
- // ─── Constants ───────────────────────────────────────────────────────────
46
-
47
- /** Max message size in bytes before truncation. */
48
- const MAX_MESSAGE_BYTES = 2048;
49
-
50
- /** HMR event name for browser→server log forwarding. */
51
- const HMR_EVENT = 'timber:browser-log';
52
-
53
- /** ANSI color codes for terminal output. */
54
- const COLORS = {
55
- red: '\x1b[31m',
56
- yellow: '\x1b[33m',
57
- blue: '\x1b[34m',
58
- dim: '\x1b[2m',
59
- reset: '\x1b[0m',
60
- } as const;
61
-
62
- /** Level severity ordering for threshold comparison. */
63
- const LEVEL_SEVERITY: Record<BrowserLogLevel | 'none', number> = {
64
- none: 0,
65
- info: 1,
66
- warn: 2,
67
- error: 3,
68
- };
69
-
70
- // ─── Formatting ──────────────────────────────────────────────────────────
71
-
72
- /**
73
- * Format a browser log payload for server terminal output.
74
- *
75
- * Produces color-coded output like:
76
- * [browser:error] Uncaught TypeError: x is not a function
77
- * at App (app.tsx:10:5)
78
- * [browser:warn] Deprecation warning (app/page.tsx:42:12)
79
- */
80
- export function formatBrowserLog(payload: BrowserLogPayload): string {
81
- const { level, message, stack, source } = payload;
82
-
83
- // Color-coded prefix
84
- const color = level === 'error' ? COLORS.red : level === 'warn' ? COLORS.yellow : COLORS.blue;
85
- const prefix = `${color}[browser:${level}]${COLORS.reset}`;
86
-
87
- // Source suffix
88
- const sourceSuffix = source ? ` ${COLORS.dim}(${source})${COLORS.reset}` : '';
89
-
90
- let output = `${prefix} ${message}${sourceSuffix}`;
91
-
92
- // Append stack trace indented
93
- if (stack) {
94
- const indented = stack
95
- .split('\n')
96
- .map((line) => ` ${COLORS.dim}${line}${COLORS.reset}`)
97
- .join('\n');
98
- output += `\n${indented}`;
99
- }
100
-
101
- return output;
102
- }
103
-
104
- // ─── Level Filtering ─────────────────────────────────────────────────────
105
-
106
- /**
107
- * Check if a log at `level` should be forwarded given the configured threshold.
108
- *
109
- * The threshold acts as a minimum severity:
110
- * - 'error' → only errors
111
- * - 'warn' → errors + warnings
112
- * - 'info' → errors + warnings + info
113
- * - 'none' → nothing
114
- */
115
- export function shouldForwardLevel(
116
- level: BrowserLogLevel,
117
- threshold: BrowserLogLevel | 'none'
118
- ): boolean {
119
- if (threshold === 'none') return false;
120
- return LEVEL_SEVERITY[level] >= LEVEL_SEVERITY[threshold];
121
- }
122
-
123
- // ─── Truncation ──────────────────────────────────────────────────────────
124
-
125
- /**
126
- * Truncate a message to `maxBytes` to avoid flooding the terminal.
127
- * Appends a suffix indicating how many bytes were dropped.
128
- */
129
- export function truncateMessage(message: string, maxBytes: number): string {
130
- if (message.length <= maxBytes) return message;
131
-
132
- const truncated = message.slice(0, maxBytes);
133
- const droppedBytes = message.length - maxBytes;
134
- return `${truncated}… [truncated ${droppedBytes} bytes]`;
135
- }
136
-
137
- // ─── Client Injection Script ─────────────────────────────────────────────
138
-
139
- /**
140
- * Generate the inline script injected into the browser in dev mode.
141
- *
142
- * This script:
143
- * 1. Saves references to the original console methods
144
- * 2. Wraps `console.error`, `console.warn`, `console.info`
145
- * 3. Calls the original first (browser devtools still work)
146
- * 4. Serializes and forwards via `import.meta.hot.send()`
147
- * 5. Truncates messages to MAX_MESSAGE_BYTES
148
- *
149
- * The script is minimal and self-contained — no imports, no dependencies.
150
- */
151
- export function generateClientScript(threshold: BrowserLogLevel | 'none'): string {
152
- if (threshold === 'none') return '';
153
-
154
- // Only intercept levels that meet the threshold
155
- const levels: BrowserLogLevel[] = ['error', 'warn', 'info'].filter((l) =>
156
- shouldForwardLevel(l as BrowserLogLevel, threshold)
157
- ) as BrowserLogLevel[];
158
-
159
- if (levels.length === 0) return '';
160
-
161
- return `
162
- (function() {
163
- if (!import.meta.hot) return;
164
- var MAX_BYTES = ${MAX_MESSAGE_BYTES};
165
- var levels = ${JSON.stringify(levels)};
166
- var originals = {};
167
-
168
- function serialize(arg) {
169
- if (arg === null) return 'null';
170
- if (arg === undefined) return 'undefined';
171
- if (arg instanceof Error) return arg.stack || arg.message || String(arg);
172
- if (typeof arg === 'object') {
173
- try { return JSON.stringify(arg); } catch(e) { return String(arg); }
174
- }
175
- return String(arg);
176
- }
177
-
178
- function truncate(s) {
179
- if (s.length <= MAX_BYTES) return s;
180
- return s.slice(0, MAX_BYTES) + '... [truncated ' + (s.length - MAX_BYTES) + ' bytes]';
181
- }
182
-
183
- levels.forEach(function(level) {
184
- originals[level] = console[level];
185
- console[level] = function() {
186
- originals[level].apply(console, arguments);
187
- try {
188
- var args = Array.prototype.slice.call(arguments);
189
- var firstArg = args[0];
190
- var stack = null;
191
- var source = null;
192
-
193
- if (firstArg instanceof Error) {
194
- stack = firstArg.stack || null;
195
- source = null;
196
- }
197
-
198
- var message = args.map(serialize).join(' ');
199
- message = truncate(message);
200
-
201
- import.meta.hot.send('${HMR_EVENT}', {
202
- level: level,
203
- message: message,
204
- stack: stack,
205
- source: source,
206
- timestamp: Date.now()
207
- });
208
- } catch(e) {
209
- // Never let log forwarding break the page
210
- }
211
- };
212
- });
213
- })();
214
- `.trim();
215
- }
216
-
217
- // ─── Plugin ──────────────────────────────────────────────────────────────
25
+ // ─── Vite Config Mapping ─────────────────────────────────────────────────
218
26
 
219
27
  /**
220
- * Create the timber-dev-browser-logs Vite plugin.
221
- *
222
- * - `configureServer`: Listens for HMR messages and prints them to the terminal
223
- * - `transformIndexHtml`: Injects the client-side interception script
224
- *
225
- * Only active during `vite dev` (apply: 'serve').
28
+ * Map timber's devBrowserLogs threshold to Vite 8's server.forwardConsole.
29
+ *
30
+ * Vite defaults forwardConsole OFF for non-agent dev; timber enables it
31
+ * based on the configured threshold (default: 'warn'). The threshold acts
32
+ * as a minimum severity — 'warn' forwards errors + warnings, 'info'
33
+ * forwards everything, 'error' forwards only errors, 'none' disables.
34
+ *
35
+ * Vite's unhandledErrors is explicitly disabled — timber's own
36
+ * setupClientErrorForwarding (browser-dev.ts) already catches window
37
+ * error/unhandledrejection, filters framework control-flow signals
38
+ * (redirect/deny digests) and page-unload aborts (TIM-838), and
39
+ * echoes errors to the Vite overlay. Enabling Vite's unhandledErrors
40
+ * would duplicate terminal output for normal errors and reintroduce
41
+ * unfiltered noise for framework signals.
226
42
  */
227
- export function timberDevBrowserLogs(ctx: PluginContext): Plugin {
228
- return {
229
- name: 'timber-dev-browser-logs',
230
- apply: 'serve',
231
-
232
- configureServer(server: ViteDevServer) {
233
- const threshold = ctx.config.devBrowserLogs ?? 'warn';
234
- if (threshold === 'none') {
235
- // Clear any stale script from a prior server instance (e.g. HMR reconfigure)
236
- delete (globalThis as Record<string, unknown>).__timber_dev_browser_log_script;
237
- return;
238
- }
239
-
240
- // Register the client injection script on globalThis so the RSC entry
241
- // can include it in headHtml for all Timber route responses.
242
- // transformIndexHtml only runs for Vite's index.html fallback, not
243
- // for responses served by createTimberMiddleware (TIM-575).
244
- const script = generateClientScript(threshold);
245
- if (script) {
246
- (globalThis as Record<string, unknown>).__timber_dev_browser_log_script =
247
- `<script type="module">${script}</script>`;
248
- }
249
-
250
- // Listen for browser log messages via HMR WebSocket
251
- server.hot.on(HMR_EVENT, (payload: BrowserLogPayload) => {
252
- try {
253
- // Validate level
254
- if (!shouldForwardLevel(payload.level, threshold)) return;
255
-
256
- // Truncate server-side too (defense in depth)
257
- payload.message = truncateMessage(payload.message, MAX_MESSAGE_BYTES);
258
-
259
- const formatted = formatBrowserLog(payload);
260
-
261
- // Use the correct console method for the log level
262
- if (payload.level === 'error') {
263
- process.stderr.write(formatted + '\n');
264
- } else {
265
- process.stdout.write(formatted + '\n');
266
- }
267
- } catch (e) {
268
- // Use process.stderr directly to avoid re-entering patched console
269
- process.stderr.write(
270
- `[timber] log forwarding failed: ${e instanceof Error ? e.message : e}\n`
271
- );
272
- }
273
- });
274
- },
275
-
276
- transformIndexHtml() {
277
- const threshold = ctx.config.devBrowserLogs ?? 'warn';
278
- const script = generateClientScript(threshold);
279
- if (!script) return [];
280
-
281
- return [
282
- {
283
- tag: 'script',
284
- attrs: { type: 'module' },
285
- children: script,
286
- injectTo: 'head' as const,
287
- },
288
- ];
289
- },
290
- };
43
+ export function resolveForwardConsole(
44
+ threshold: DevBrowserLogsConfig
45
+ ): boolean | { unhandledErrors: boolean; logLevels: string[] } {
46
+ const resolved = threshold ?? 'warn';
47
+ if (resolved === 'none') return false;
48
+
49
+ const logLevels: string[] = [];
50
+ if (resolved === 'info') logLevels.push('info', 'warn', 'error');
51
+ else if (resolved === 'warn') logLevels.push('warn', 'error');
52
+ else logLevels.push('error');
53
+
54
+ return { unhandledErrors: false, logLevels };
291
55
  }
@@ -63,9 +63,9 @@ export {
63
63
  type DevLoggerConfig,
64
64
  } from './logger.js';
65
65
 
66
- // Browser log forwarding
66
+ // Browser log forwarding (Vite 8 config mapping)
67
67
  export {
68
- timberDevBrowserLogs,
68
+ resolveForwardConsole,
69
69
  type BrowserLogLevel,
70
70
  type DevBrowserLogsConfig,
71
71
  } from './browser-logs.js';
@@ -35,7 +35,7 @@ export type DevFetchCleanup = () => void;
35
35
  * instrumentDevFetch() again. Without this guard each call would wrap the
36
36
  * previous wrapper — N nested wrappers emitting N duplicate timber.fetch
37
37
  * spans per request. Symbol.for() survives module re-evaluation (same
38
- * pattern as __timber_dev_browser_log_script). See TIM-1067, B49.
38
+ * pattern as globalThis Symbol guards). See TIM-1067, B49.
39
39
  */
40
40
  const FETCH_PATCH_KEY = Symbol.for('timber.dev.fetch-instrumentation-cleanup');
41
41
 
@@ -14,6 +14,9 @@
14
14
  * Design doc: 21-dev-server.md §"Dev Logging", 17-logging.md §"Dev Logging"
15
15
  */
16
16
 
17
+ import { STATUS_CODES } from 'node:http';
18
+ import type { InspectColor } from 'node:util';
19
+ import { styleText } from 'node:util';
17
20
  import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
18
21
 
19
22
  // ─── Configuration ──────────────────────────────────────────────────────────
@@ -27,16 +30,11 @@ export interface DevLoggerConfig {
27
30
  slowPhaseMs?: number;
28
31
  }
29
32
 
30
- // ─── ANSI Codes ─────────────────────────────────────────────────────────────
33
+ // ─── Styling ────────────────────────────────────────────────────────────────
31
34
 
32
- const DIM = '\x1b[2m';
33
- const BOLD = '\x1b[1m';
34
- const RESET = '\x1b[0m';
35
- const GREEN = '\x1b[32m';
36
- const YELLOW = '\x1b[33m';
37
- const RED = '\x1b[31m';
38
- const CYAN = '\x1b[36m';
39
- const MAGENTA = '\x1b[35m';
35
+ type Format = InspectColor | readonly InspectColor[];
36
+ const noValidate = { validateStream: false } as const;
37
+ const style = (fmt: Format, text: string) => styleText(fmt, text, noValidate);
40
38
 
41
39
  /**
42
40
  * Color an HTTP method for dev log output.
@@ -45,21 +43,21 @@ const MAGENTA = '\x1b[35m';
45
43
  function colorMethod(method: string): string {
46
44
  switch (method) {
47
45
  case 'GET':
48
- return `${DIM}${method}${RESET}`;
46
+ return style('dim', method);
49
47
  case 'POST':
50
- return `${GREEN}${BOLD}${method}${RESET}`;
48
+ return style(['green', 'bold'], method);
51
49
  case 'PUT':
52
- return `${YELLOW}${BOLD}${method}${RESET}`;
50
+ return style(['yellow', 'bold'], method);
53
51
  case 'DELETE':
54
- return `${RED}${BOLD}${method}${RESET}`;
52
+ return style(['red', 'bold'], method);
55
53
  case 'PATCH':
56
- return `${CYAN}${BOLD}${method}${RESET}`;
54
+ return style(['cyan', 'bold'], method);
57
55
  case 'HEAD':
58
- return `${DIM}${method}${RESET}`;
56
+ return style('dim', method);
59
57
  case 'OPTIONS':
60
- return `${MAGENTA}${BOLD}${method}${RESET}`;
58
+ return style(['magenta', 'bold'], method);
61
59
  default:
62
- return `${BOLD}${method}${RESET}`;
60
+ return style('bold', method);
63
61
  }
64
62
  }
65
63
 
@@ -251,14 +249,14 @@ export function formatSpanTree(spans: ReadableSpan[], config?: DevLoggerConfig):
251
249
  const traceId = root.spanContext().traceId;
252
250
  const actionName = root.attributes['timber.action_name'] as string | undefined;
253
251
 
254
- const dimTrace = `${DIM}trace_id: ${traceId}${RESET}`;
252
+ const dimTrace = style('dim', `trace_id: ${traceId}`);
255
253
  if (actionName) {
256
254
  const actionFile = root.attributes['timber.action_file'] as string | undefined;
257
255
  lines.push(
258
- `${BOLD}ACTION ${actionName}${actionFile ? ` (${actionFile})` : ''}${RESET} ${dimTrace}`
256
+ `${style('bold', `ACTION ${actionName}${actionFile ? ` (${actionFile})` : ''}`)} ${dimTrace}`
259
257
  );
260
258
  } else {
261
- lines.push(`${colorMethod(method)} ${BOLD}${path}${RESET} ${dimTrace}`);
259
+ lines.push(`${colorMethod(method)} ${style('bold', path)} ${dimTrace}`);
262
260
  }
263
261
 
264
262
  // Render child span nodes
@@ -271,17 +269,17 @@ export function formatSpanTree(spans: ReadableSpan[], config?: DevLoggerConfig):
271
269
  const statusCode = root.attributes['http.response.status_code'] as number | undefined;
272
270
  const status = statusCode ?? (root.status.code === 2 ? 500 : 200);
273
271
  const totalMs = Math.round(hrTimeToMs(root.duration));
274
- const statusColor = status < 400 ? GREEN : status < 500 ? YELLOW : RED;
272
+ const statusFmt = status < 400 ? 'green' : status < 500 ? 'yellow' : 'red';
275
273
  const statusText = `${status} ${httpStatusText(status)}`;
276
274
 
277
275
  // Surface deny() signal info for 500s caused by deny-inside-suspense
278
276
  const denyInfo = root.attributes['timber.deny_status'] as number | undefined;
279
277
  const denyNote = denyInfo
280
- ? `${DIM} (caused by deny(${denyInfo}) inside Suspense — status already committed)${RESET}`
278
+ ? style('dim', ` (caused by deny(${denyInfo}) inside Suspense — status already committed)`)
281
279
  : '';
282
280
 
283
281
  lines.push(
284
- `${statusColor}└─ ✓ ${statusText}${RESET}${DIM} total ${totalMs}ms${RESET}${denyNote}`
282
+ `${style(statusFmt, `└─ ✓ ${statusText}`)}${style('dim', ` total ${totalMs}ms`)}${denyNote}`
285
283
  );
286
284
 
287
285
  return lines.join('\n') + '\n';
@@ -307,18 +305,18 @@ function formatFetchLine(
307
305
  const fetchError = span.attributes['timber.fetch_error'] as string | undefined;
308
306
  const isError = span.status.code === 2; // SpanStatusCode.ERROR
309
307
 
310
- let line = `${prefix}${connector} ${DIM}fetch ${method}${RESET} ${url}`;
311
- line += ` ${DIM}${startMs}ms → ${endMs}ms (${durationMs}ms)${RESET}`;
308
+ let line = `${prefix}${connector} ${style('dim', `fetch ${method}`)} ${url}`;
309
+ line += ` ${style('dim', `${startMs}ms → ${endMs}ms (${durationMs}ms)`)}`;
312
310
 
313
311
  if (cacheStatus) {
314
- line += ` ${DIM}[cdn: ${cacheStatus}]${RESET}`;
312
+ line += ` ${style('dim', `[cdn: ${cacheStatus}]`)}`;
315
313
  }
316
314
 
317
315
  if (isError) {
318
316
  const errMsg = fetchError ? `: ${fetchError}` : '';
319
- line += ` ${RED}ERROR${errMsg}${RESET}`;
317
+ line += ` ${style('red', `ERROR${errMsg}`)}`;
320
318
  } else if (statusCode && statusCode >= 400) {
321
- line += ` ${YELLOW}${statusCode}${RESET}`;
319
+ line += ` ${style('yellow', String(statusCode))}`;
322
320
  }
323
321
 
324
322
  return line;
@@ -363,10 +361,9 @@ function formatSpanNode(
363
361
  }
364
362
  }
365
363
 
366
- const slowHighlight = isSlow ? YELLOW : '';
367
- const slowReset = isSlow ? RESET : '';
368
- const envTag = `${CYAN}[${env}]${RESET}`;
369
- const line = `${prefix}${connector} ${envTag} ${slowHighlight}${label}${slowReset} ${DIM}${timing}${RESET}`;
364
+ const styledLabel = isSlow ? style('yellow', label) : label;
365
+ const envTag = style('cyan', `[${env}]`);
366
+ const line = `${prefix}${connector} ${envTag} ${styledLabel} ${style('dim', timing)}`;
370
367
  lines.push(line);
371
368
 
372
369
  // Span events (cache hits/misses) as child annotations
@@ -378,7 +375,7 @@ function formatSpanNode(
378
375
  const durStr = durMs !== undefined ? ` ${durMs < 1 ? '<1' : Math.round(durMs)}ms` : '';
379
376
  const stale = event.attributes?.['stale'] ? ' (stale)' : '';
380
377
  lines.push(
381
- `${childPrefix}${DIM}└── ${key} timber.cache ${hitMiss}${durStr}${stale}${RESET}`
378
+ `${childPrefix}${style('dim', `└── ${key} timber.cache ${hitMiss}${durStr}${stale}`)}`
382
379
  );
383
380
  }
384
381
  }
@@ -405,8 +402,8 @@ export function formatSpanSummary(spans: ReadableSpan[], _config?: DevLoggerConf
405
402
  const traceId = root.spanContext().traceId;
406
403
  const traceIdShort = traceId.slice(0, 8);
407
404
 
408
- const statusColor = status < 400 ? GREEN : status < 500 ? YELLOW : RED;
409
- return `${colorMethod(method)} ${path} → ${statusColor}${status} ${httpStatusText(status)}${RESET} ${totalMs}ms ${DIM}trace_id: ${traceIdShort}...${RESET}\n`;
405
+ const statusFmt = status < 400 ? 'green' : status < 500 ? 'yellow' : 'red';
406
+ return `${colorMethod(method)} ${path} → ${style(statusFmt, `${status} ${httpStatusText(status)}`)} ${totalMs}ms ${style('dim', `trace_id: ${traceIdShort}...`)}\n`;
410
407
  }
411
408
 
412
409
  /**
@@ -449,20 +446,5 @@ export function formatJson(spans: ReadableSpan[]): string {
449
446
  // ─── Helpers ────────────────────────────────────────────────────────────────
450
447
 
451
448
  function httpStatusText(status: number): string {
452
- const texts: Record<number, string> = {
453
- 200: 'OK',
454
- 201: 'Created',
455
- 204: 'No Content',
456
- 301: 'Moved Permanently',
457
- 302: 'Found',
458
- 304: 'Not Modified',
459
- 400: 'Bad Request',
460
- 401: 'Unauthorized',
461
- 403: 'Forbidden',
462
- 404: 'Not Found',
463
- 405: 'Method Not Allowed',
464
- 413: 'Payload Too Large',
465
- 500: 'Internal Server Error',
466
- };
467
- return texts[status] ?? String(status);
449
+ return STATUS_CODES[status] ?? String(status);
468
450
  }
@@ -15,6 +15,8 @@
15
15
  */
16
16
 
17
17
  import { pathToFileURL } from 'node:url';
18
+ import type { InspectColor } from 'node:util';
19
+ import { styleText } from 'node:util';
18
20
  import {
19
21
  extractComponentStack,
20
22
  parseFirstAppFrame,
@@ -23,14 +25,11 @@ import {
23
25
  } from './overlay.js';
24
26
  import { classifyStack } from './stack-classifier.js';
25
27
 
26
- // ─── ANSI Codes ─────────────────────────────────────────────────────────────
28
+ // ─── Styling ────────────────────────────────────────────────────────────────
27
29
 
28
- const RED = '\x1b[31m';
29
- const CYAN = '\x1b[36m';
30
- const DIM = '\x1b[2m';
31
- const RESET = '\x1b[0m';
32
- const BOLD = '\x1b[1m';
33
- const UNDERLINE = '\x1b[4m';
30
+ type Format = InspectColor | readonly InspectColor[];
31
+ const noValidate = { validateStream: false } as const;
32
+ const style = (fmt: Format, text: string) => styleText(fmt, text, noValidate);
34
33
 
35
34
  // ─── OSC 8 Hyperlinks ──────────────────────────────────────────────────────
36
35
 
@@ -59,7 +58,7 @@ function fileLink(filePath: string, line?: number, col?: number): string {
59
58
  // drive letters (TIM-792). Append :line:col after the URL.
60
59
  const base = pathToFileURL(filePath).href;
61
60
  const url = line ? `${base}:${line}${col ? `:${col}` : ''}` : base;
62
- return `${CYAN}${UNDERLINE}${hyperlink(display, url)}${RESET}`;
61
+ return style(['cyan', 'underline'], hyperlink(display, url));
63
62
  }
64
63
 
65
64
  // ─── Box Drawing ────────────────────────────────────────────────────────────
@@ -79,15 +78,15 @@ const BOX = {
79
78
  * @param lines - Content lines (no ANSI length calculation — keeps it simple)
80
79
  * @param width - Box width (characters). Lines longer than this are not truncated.
81
80
  */
82
- function box(lines: string[], borderColor: string, width = 80): string {
81
+ function box(lines: string[], borderFormat: Format, width = 80): string {
83
82
  const bar = BOX.horizontal.repeat(width - 2);
84
83
  const output: string[] = [];
85
84
 
86
- output.push(`${borderColor}${BOX.topLeft}${bar}${BOX.topRight}${RESET}`);
85
+ output.push(style(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
87
86
  for (const line of lines) {
88
- output.push(`${borderColor}${BOX.vertical}${RESET} ${line}`);
87
+ output.push(`${style(borderFormat, BOX.vertical)} ${line}`);
89
88
  }
90
- output.push(`${borderColor}${BOX.bottomLeft}${bar}${BOX.bottomRight}${RESET}`);
89
+ output.push(style(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
91
90
 
92
91
  return output.join('\n');
93
92
  }
@@ -116,12 +115,12 @@ export function formatTerminalError(error: Error, phase: ErrorPhase, projectRoot
116
115
 
117
116
  // ── Box: Phase + Message ──────────────────────────────────────────
118
117
  const boxLines: string[] = [];
119
- boxLines.push(`${RED}${BOLD}${PHASE_LABELS[phase]} Error${RESET}`);
118
+ boxLines.push(style(['red', 'bold'], `${PHASE_LABELS[phase]} Error`));
120
119
  boxLines.push('');
121
120
 
122
121
  // Error message — may be multi-line
123
122
  for (const msgLine of error.message.split('\n')) {
124
- boxLines.push(`${RED}${msgLine}${RESET}`);
123
+ boxLines.push(style('red', msgLine));
125
124
  }
126
125
 
127
126
  // Primary file location (clickable)
@@ -131,31 +130,31 @@ export function formatTerminalError(error: Error, phase: ErrorPhase, projectRoot
131
130
  ? loc.file.slice(projectRoot.length + 1)
132
131
  : loc.file;
133
132
  boxLines.push(
134
- `${BOLD}→${RESET} ${fileLink(loc.file, loc.line, loc.column)} ${DIM}(${relPath})${RESET}`
133
+ `${style('bold', '→')} ${fileLink(loc.file, loc.line, loc.column)} ${style('dim', `(${relPath})`)}`
135
134
  );
136
135
  }
137
136
 
138
- sections.push(box(boxLines, RED));
137
+ sections.push(box(boxLines, 'red'));
139
138
 
140
139
  // ── Component Stack ───────────────────────────────────────────────
141
140
  if (componentStack) {
142
141
  sections.push('');
143
- sections.push(` ${BOLD}Component Stack:${RESET}`);
142
+ sections.push(` ${style('bold', 'Component Stack:')}`);
144
143
  for (const csLine of componentStack.trim().split('\n')) {
145
- sections.push(` ${DIM}${csLine.trim()}${RESET}`);
144
+ sections.push(` ${style('dim', csLine.trim())}`);
146
145
  }
147
146
  }
148
147
 
149
148
  // ── App Frames ────────────────────────────────────────────────────
150
149
  if (appFrames.length > 0) {
151
150
  sections.push('');
152
- sections.push(` ${BOLD}Application Frames:${RESET}`);
151
+ sections.push(` ${style('bold', 'Application Frames:')}`);
153
152
  for (let i = 0; i < appFrames.length; i++) {
154
153
  const f = appFrames[i]!;
155
154
  if (f.file && f.line) {
156
- const prefix = i === 0 ? `${BOLD}▸${RESET}` : ' ';
155
+ const prefix = i === 0 ? style('bold', '▸') : ' ';
157
156
  sections.push(
158
- ` ${prefix} ${fileLink(f.file, f.line, f.col)} ${DIM}${extractFnName(f.raw)}${RESET}`
157
+ ` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style('dim', extractFnName(f.raw))}`
159
158
  );
160
159
  } else {
161
160
  sections.push(` ${f.raw}`);
@@ -166,7 +165,7 @@ export function formatTerminalError(error: Error, phase: ErrorPhase, projectRoot
166
165
  // ── Internal Frames (collapsed) ───────────────────────────────────
167
166
  if (internalCount > 0) {
168
167
  sections.push(
169
- ` ${DIM}… ${internalCount} internal frame${internalCount !== 1 ? 's' : ''} hidden${RESET}`
168
+ ` ${style('dim', `… ${internalCount} internal frame${internalCount !== 1 ? 's' : ''} hidden`)}`
170
169
  );
171
170
  }
172
171