@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.5

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 (33) hide show
  1. package/dist/_chunks/debug-gwlJkDuf.js +108 -0
  2. package/dist/_chunks/debug-gwlJkDuf.js.map +1 -0
  3. package/dist/_chunks/{format-CwdaB0_2.js → format-DviM89f0.js} +2 -2
  4. package/dist/_chunks/{format-CwdaB0_2.js.map → format-DviM89f0.js.map} +1 -1
  5. package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-DIkVh_jG.js} +2 -2
  6. package/dist/_chunks/{request-context-CZJi4CuK.js.map → request-context-DIkVh_jG.js.map} +1 -1
  7. package/dist/cookies/index.js +1 -1
  8. package/dist/fonts/local.d.ts +4 -2
  9. package/dist/fonts/local.d.ts.map +1 -1
  10. package/dist/index.js +190 -9
  11. package/dist/index.js.map +1 -1
  12. package/dist/plugins/entries.d.ts +7 -0
  13. package/dist/plugins/entries.d.ts.map +1 -1
  14. package/dist/plugins/fonts.d.ts +2 -1
  15. package/dist/plugins/fonts.d.ts.map +1 -1
  16. package/dist/plugins/mdx.d.ts +6 -0
  17. package/dist/plugins/mdx.d.ts.map +1 -1
  18. package/dist/server/action-client.d.ts.map +1 -1
  19. package/dist/server/debug.d.ts +46 -15
  20. package/dist/server/debug.d.ts.map +1 -1
  21. package/dist/server/index.js +4 -4
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/fonts/local.ts +7 -3
  26. package/src/plugins/entries.ts +7 -4
  27. package/src/plugins/fonts.ts +106 -5
  28. package/src/plugins/mdx.ts +9 -5
  29. package/src/server/action-client.ts +7 -4
  30. package/src/server/debug.ts +55 -17
  31. package/src/server/rsc-entry/index.ts +17 -6
  32. package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
  33. package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAgFA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AA8YD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BA7P3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA+PhD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAgFA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAE/F;AAyZD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;8BA7P3C,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA+PhD,wBAAiE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.4",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -100,18 +100,22 @@ export function generateFamilyName(sources: LocalFontSrc[]): string {
100
100
  * Generate @font-face descriptors for local font sources.
101
101
  *
102
102
  * Each source entry produces one @font-face rule. The `src` descriptor
103
- * uses a `url()` pointing to the resolved file path with the inferred format.
103
+ * uses a `url()` pointing to the served path under `/_timber/fonts/`.
104
+ * The `urlPrefix` defaults to `/_timber/fonts` — the path used by both
105
+ * the dev server middleware and the production build output.
104
106
  */
105
107
  export function generateLocalFontFaces(
106
108
  family: string,
107
109
  sources: LocalFontSrc[],
108
- display: string
110
+ display: string,
111
+ urlPrefix = '/_timber/fonts'
109
112
  ): FontFaceDescriptor[] {
110
113
  return sources.map((entry) => {
111
114
  const format = inferFontFormat(entry.path);
115
+ const basename = entry.path.split('/').pop() ?? entry.path;
112
116
  return {
113
117
  family,
114
- src: `url('${entry.path}') format('${format}')`,
118
+ src: `url('${urlPrefix}/${basename}') format('${format}')`,
115
119
  weight: entry.weight,
116
120
  style: entry.style,
117
121
  display,
@@ -130,11 +130,14 @@ function generateConfigModule(ctx: PluginContext): string {
130
130
  * Checks for instrumentation.ts, .js, and .mjs — matching the same
131
131
  * extensions as timber.config.ts detection.
132
132
  */
133
- function detectInstrumentationFile(root: string): string | null {
133
+ export function detectInstrumentationFile(root: string): string | null {
134
134
  const extensions = ['.ts', '.js', '.mjs'];
135
- for (const ext of extensions) {
136
- const candidate = resolve(root, `instrumentation${ext}`);
137
- if (existsSync(candidate)) return candidate;
135
+ const dirs = [root, resolve(root, 'src')];
136
+ for (const dir of dirs) {
137
+ for (const ext of extensions) {
138
+ const candidate = resolve(dir, `instrumentation${ext}`);
139
+ if (existsSync(candidate)) return candidate;
140
+ }
138
141
  }
139
142
  return null;
140
143
  }
@@ -14,13 +14,15 @@
14
14
  * Design doc: 24-fonts.md
15
15
  */
16
16
 
17
- import type { Plugin } from 'vite';
17
+ import type { Plugin, ViteDevServer } from 'vite';
18
+ import { readFileSync, existsSync } from 'node:fs';
19
+ import { resolve, normalize } from 'node:path';
18
20
  import type { PluginContext } from '#/index.js';
19
21
  import type { ExtractedFont, GoogleFontConfig } from '#/fonts/types.js';
20
22
  import type { ManifestFontEntry } from '#/server/build-manifest.js';
21
- import { generateVariableClass, generateFontFamilyClass } from '#/fonts/css.js';
23
+ import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from '#/fonts/css.js';
22
24
  import { generateFallbackCss, buildFontStack } from '#/fonts/fallbacks.js';
23
- import { processLocalFont } from '#/fonts/local.js';
25
+ import { processLocalFont, generateLocalFontFaces } from '#/fonts/local.js';
24
26
  import { inferFontFormat } from '#/fonts/local.js';
25
27
  import { downloadAndCacheFonts, type CachedFont } from '#/fonts/google.js';
26
28
  import {
@@ -31,8 +33,10 @@ import {
31
33
 
32
34
  const VIRTUAL_GOOGLE = '@timber/fonts/google';
33
35
  const VIRTUAL_LOCAL = '@timber/fonts/local';
36
+ const VIRTUAL_FONT_CSS = 'virtual:timber-fonts-css';
34
37
  const RESOLVED_GOOGLE = '\0@timber/fonts/google';
35
38
  const RESOLVED_LOCAL = '\0@timber/fonts/local';
39
+ const RESOLVED_FONT_CSS = '\0virtual:timber-fonts-css';
36
40
 
37
41
  /**
38
42
  * Registry of fonts extracted during transform.
@@ -245,12 +249,20 @@ function generateLocalVirtualModule(): string {
245
249
  /**
246
250
  * Generate the CSS output for all extracted fonts.
247
251
  *
248
- * Includes @font-face rules, fallback @font-face rules, and scoped classes.
252
+ * Includes @font-face rules for local fonts, fallback @font-face rules,
253
+ * and scoped classes.
249
254
  */
250
255
  export function generateAllFontCss(registry: FontRegistry): string {
251
256
  const cssParts: string[] = [];
252
257
 
253
258
  for (const font of registry.values()) {
259
+ // Generate @font-face rules for local fonts
260
+ if (font.provider === 'local' && font.localSources) {
261
+ const faces = generateLocalFontFaces(font.family, font.localSources, font.display);
262
+ const faceCss = generateFontFaces(faces);
263
+ if (faceCss) cssParts.push(faceCss);
264
+ }
265
+
254
266
  // Generate fallback @font-face if metrics are available
255
267
  const fallbackCss = generateFallbackCss(font.family);
256
268
  if (fallbackCss) cssParts.push(fallbackCss);
@@ -359,23 +371,83 @@ export function timberFonts(ctx: PluginContext): Plugin {
359
371
  name: 'timber-fonts',
360
372
 
361
373
  /**
362
- * Resolve `@timber/fonts/google` and `@timber/fonts/local` to virtual modules.
374
+ * Resolve `@timber/fonts/google`, `@timber/fonts/local`, and the
375
+ * virtual font CSS module to internal IDs.
363
376
  */
364
377
  resolveId(id: string) {
365
378
  if (id === VIRTUAL_GOOGLE) return RESOLVED_GOOGLE;
366
379
  if (id === VIRTUAL_LOCAL) return RESOLVED_LOCAL;
380
+ if (id === VIRTUAL_FONT_CSS) return RESOLVED_FONT_CSS;
367
381
  return null;
368
382
  },
369
383
 
370
384
  /**
371
385
  * Return generated source for font virtual modules.
386
+ *
387
+ * The font CSS virtual module returns the combined @font-face rules,
388
+ * fallback CSS, and scoped classes for all registered fonts.
372
389
  */
373
390
  load(id: string) {
374
391
  if (id === RESOLVED_GOOGLE) return generateGoogleVirtualModule(registry);
375
392
  if (id === RESOLVED_LOCAL) return generateLocalVirtualModule();
393
+ if (id === RESOLVED_FONT_CSS) return generateAllFontCss(registry);
376
394
  return null;
377
395
  },
378
396
 
397
+ /**
398
+ * Serve local font files in dev mode under `/_timber/fonts/`.
399
+ *
400
+ * Only files that are registered in the font registry (via localSources)
401
+ * are served. Paths are validated to prevent directory traversal.
402
+ */
403
+ configureServer(server: ViteDevServer) {
404
+ server.middlewares.use((req, res, next) => {
405
+ const url = req.url;
406
+ if (!url || !url.startsWith('/_timber/fonts/')) return next();
407
+
408
+ const requestedFilename = url.slice('/_timber/fonts/'.length);
409
+ // Reject path traversal attempts
410
+ if (requestedFilename.includes('..') || requestedFilename.includes('/')) {
411
+ res.statusCode = 400;
412
+ res.end('Bad request');
413
+ return;
414
+ }
415
+
416
+ // Find the matching font file in the registry
417
+ for (const font of registry.values()) {
418
+ if (font.provider !== 'local' || !font.localSources) continue;
419
+ for (const src of font.localSources) {
420
+ const basename = src.path.split('/').pop() ?? '';
421
+ if (basename === requestedFilename) {
422
+ const absolutePath = normalize(resolve(src.path));
423
+ // Verify the resolved path hasn't escaped (extra safety)
424
+ if (!existsSync(absolutePath)) {
425
+ res.statusCode = 404;
426
+ res.end('Not found');
427
+ return;
428
+ }
429
+ const data = readFileSync(absolutePath);
430
+ const ext = absolutePath.split('.').pop()?.toLowerCase();
431
+ const mimeMap: Record<string, string> = {
432
+ woff2: 'font/woff2',
433
+ woff: 'font/woff',
434
+ ttf: 'font/ttf',
435
+ otf: 'font/otf',
436
+ eot: 'application/vnd.ms-fontopen',
437
+ };
438
+ res.setHeader('Content-Type', mimeMap[ext ?? ''] ?? 'application/octet-stream');
439
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
440
+ res.setHeader('Access-Control-Allow-Origin', '*');
441
+ res.end(data);
442
+ return;
443
+ }
444
+ }
445
+ }
446
+
447
+ next();
448
+ });
449
+ },
450
+
379
451
  /**
380
452
  * Download and cache Google Fonts during production builds.
381
453
  *
@@ -525,6 +597,35 @@ export function timberFonts(ctx: PluginContext): Plugin {
525
597
  });
526
598
  }
527
599
 
600
+ // Emit local font files as assets
601
+ for (const font of registry.values()) {
602
+ if (font.provider !== 'local' || !font.localSources) continue;
603
+ for (const src of font.localSources) {
604
+ const absolutePath = normalize(resolve(src.path));
605
+ if (!existsSync(absolutePath)) {
606
+ this.warn(`Local font file not found: ${absolutePath}`);
607
+ continue;
608
+ }
609
+ const basename = src.path.split('/').pop() ?? src.path;
610
+ const data = readFileSync(absolutePath);
611
+ this.emitFile({
612
+ type: 'asset',
613
+ fileName: `_timber/fonts/${basename}`,
614
+ source: data,
615
+ });
616
+ }
617
+ }
618
+
619
+ // Emit the combined font CSS as an asset
620
+ const fontCss = generateAllFontCss(registry);
621
+ if (fontCss) {
622
+ this.emitFile({
623
+ type: 'asset',
624
+ fileName: '_timber/fonts/fonts.css',
625
+ source: fontCss,
626
+ });
627
+ }
628
+
528
629
  if (!ctx.buildManifest) return;
529
630
 
530
631
  // Build a lookup from font family → cached files for manifest entries
@@ -16,19 +16,23 @@ import type { PluginContext } from '#/index.js';
16
16
  const MDX_EXTENSIONS = ['mdx', 'md'];
17
17
 
18
18
  /**
19
- * Check if mdx-components.tsx (or .ts, .jsx, .js) exists at the project root.
19
+ * Check if mdx-components.tsx (or .ts, .jsx, .js) exists at the project root
20
+ * or in src/. Root takes precedence, matching Next.js behavior.
20
21
  * Returns the absolute path if found, otherwise undefined.
21
22
  */
22
- function findMdxComponents(root: string): string | undefined {
23
+ export function findMdxComponents(root: string): string | undefined {
23
24
  const candidates = [
24
25
  'mdx-components.tsx',
25
26
  'mdx-components.ts',
26
27
  'mdx-components.jsx',
27
28
  'mdx-components.js',
28
29
  ];
29
- for (const name of candidates) {
30
- const p = join(root, name);
31
- if (existsSync(p)) return p;
30
+ const dirs = [root, join(root, 'src')];
31
+ for (const dir of dirs) {
32
+ for (const name of candidates) {
33
+ const p = join(dir, name);
34
+ if (existsSync(p)) return p;
35
+ }
32
36
  }
33
37
  return undefined;
34
38
  }
@@ -184,7 +184,7 @@ async function runActionMiddleware<TCtx>(
184
184
  // Re-export parseFormData for use throughout the framework
185
185
  import { parseFormData } from './form-data.js';
186
186
  import { formatSize } from '#/utils/format.js';
187
- import { isDebug } from './debug.js';
187
+ import { isDebug, isDevMode } from './debug.js';
188
188
 
189
189
  /**
190
190
  * Extract validation errors from a schema error.
@@ -247,12 +247,15 @@ export function handleActionError(error: unknown): ActionResult<never> {
247
247
  };
248
248
  }
249
249
 
250
- // In dev, include the message for debugging
251
- const isDev = isDebug();
250
+ // In dev, include the message for debugging.
251
+ // Uses isDevMode() — NOT isDebug() — because this data is sent to the
252
+ // browser. TIMBER_DEBUG must never cause error messages to leak to clients.
253
+ // See design/13-security.md principle 4: "Errors don't leak."
254
+ const devMode = isDevMode();
252
255
  return {
253
256
  serverError: {
254
257
  code: 'INTERNAL_ERROR',
255
- ...(isDev && error instanceof Error ? { data: { message: error.message } } : {}),
258
+ ...(devMode && error instanceof Error ? { data: { message: error.message } } : {}),
256
259
  },
257
260
  };
258
261
  }
@@ -1,18 +1,30 @@
1
1
  /**
2
2
  * Runtime debug flag for timber.js.
3
3
  *
4
- * Provides `isDebug()` a runtime check that returns true when timber's
5
- * debug/warning logging should be active. This is true in two cases:
4
+ * Two distinct functions for two distinct security levels:
6
5
  *
7
- * 1. Development mode: `process.env.NODE_ENV !== 'production'`
8
- * (statically replaced and tree-shaken in production builds — zero cost)
6
+ * ## `isDebug()` server-side logging only
9
7
  *
10
- * 2. TIMBER_DEBUG flag: A runtime environment variable that survives
11
- * production builds. When set to any truthy value ("1", "true", etc.),
12
- * timber's own diagnostics are re-enabled without affecting React's mode.
8
+ * Returns true when timber's debug/warning messages should be written to
9
+ * stderr / the server console. This NEVER affects what is sent to the
10
+ * client (no error details, no timing headers, no stack traces).
13
11
  *
14
- * The TIMBER_DEBUG check uses a dynamic property access pattern that
15
- * prevents the bundler from statically replacing or eliminating it.
12
+ * Active when any of:
13
+ * - `NODE_ENV !== 'production'` (standard dev mode)
14
+ * - `TIMBER_DEBUG` env var is set to a truthy value at runtime
15
+ * - `timber.config.ts` has `debug: true`
16
+ *
17
+ * ## `isDevMode()` — client-visible dev behavior
18
+ *
19
+ * Returns true ONLY when `NODE_ENV !== 'production'`. This gates anything
20
+ * that changes what clients can observe:
21
+ * - Dev error pages with stack traces (fallback-error.ts)
22
+ * - Detailed Server-Timing headers (pipeline.ts)
23
+ * - Error messages in action INTERNAL_ERROR payloads (action-client.ts)
24
+ * - Pipeline error handler wiring (Vite overlay)
25
+ *
26
+ * `isDevMode()` is statically replaced in production builds → the guarded
27
+ * code is tree-shaken to zero bytes. TIMBER_DEBUG cannot enable it.
16
28
  *
17
29
  * Usage:
18
30
  * In Cloudflare Workers wrangler.toml:
@@ -25,10 +37,33 @@
25
37
  * In timber.config.ts:
26
38
  * export default { debug: true }
27
39
  *
40
+ * See design/13-security.md for the security taxonomy.
28
41
  * See design/18-build-system.md for build pipeline details.
29
42
  */
30
43
 
31
- // ─── Debug Flag ─────────────────────────────────────────────────────────────
44
+ // ─── Dev Mode (client-visible) ──────────────────────────────────────────────
45
+
46
+ /**
47
+ * Check if the application is running in development mode.
48
+ *
49
+ * This is the ONLY function that should gate client-visible dev behavior:
50
+ * - Dev error pages with stack traces
51
+ * - Detailed Server-Timing response headers
52
+ * - Error messages in action `INTERNAL_ERROR` payloads
53
+ * - Pipeline error handler wiring (Vite overlay)
54
+ *
55
+ * Returns `process.env.NODE_ENV !== 'production'`, which is statically
56
+ * replaced by the bundler in production builds. Code guarded by this
57
+ * function is tree-shaken to zero bytes in production.
58
+ *
59
+ * TIMBER_DEBUG does NOT enable this — that would leak server internals
60
+ * to clients. Use `isDebug()` for server-side-only logging.
61
+ */
62
+ export function isDevMode(): boolean {
63
+ return process.env.NODE_ENV !== 'production';
64
+ }
65
+
66
+ // ─── Debug Flag (server-side logging only) ──────────────────────────────────
32
67
 
33
68
  /**
34
69
  * Config-level debug override. Set via `setDebugFromConfig()` during
@@ -45,19 +80,20 @@ export function setDebugFromConfig(debug: boolean): void {
45
80
  }
46
81
 
47
82
  /**
48
- * Check if timber debug logging is active.
83
+ * Check if timber debug logging is active (server-side only).
49
84
  *
50
85
  * Returns true if ANY of these conditions hold:
51
86
  * - NODE_ENV is not 'production' (standard dev mode)
52
87
  * - TIMBER_DEBUG environment variable is set to a truthy value at runtime
53
88
  * - timber.config.ts has `debug: true`
54
89
  *
55
- * The TIMBER_DEBUG check is deliberately written as a dynamic property
56
- * access so bundlers cannot statically replace it. The `_envKey` variable
57
- * prevents the bundler from seeing `process.env.TIMBER_DEBUG` as a
58
- * compile-time constant.
90
+ * This function controls ONLY server-side logging messages written to
91
+ * stderr or the server console. It NEVER affects client-visible behavior
92
+ * (error pages, response headers, action payloads). For client-visible
93
+ * behavior, use `isDevMode()`.
59
94
  *
60
- * This function is intentionally NOT inlineable it reads runtime state.
95
+ * The TIMBER_DEBUG check is deliberately written as a dynamic property
96
+ * access so bundlers cannot statically replace it.
61
97
  */
62
98
  export function isDebug(): boolean {
63
99
  // Fast path: dev mode (statically replaced to `true` in dev, `false` in prod)
@@ -89,7 +125,9 @@ function _readTimberDebugEnv(): boolean {
89
125
  try {
90
126
  const key = 'TIMBER_DEBUG';
91
127
  const val =
92
- typeof process !== 'undefined' && process.env ? (process.env as Record<string, string | undefined>)[key] : undefined;
128
+ typeof process !== 'undefined' && process.env
129
+ ? (process.env as Record<string, string | undefined>)[key]
130
+ : undefined;
93
131
  if (val && val !== '0' && val !== 'false') return true;
94
132
  } catch {
95
133
  // process may not exist or env may throw — safe to ignore
@@ -72,7 +72,7 @@ import { buildRscPayloadResponse } from './rsc-payload.js';
72
72
  import { renderRscStream } from './rsc-stream.js';
73
73
  import { renderSsrResponse } from './ssr-renderer.js';
74
74
  import { callSsr } from './ssr-bridge.js';
75
- import { isDebug, setDebugFromConfig } from '#/server/debug.js';
75
+ import { isDebug, isDevMode, setDebugFromConfig } from '#/server/debug.js';
76
76
 
77
77
  // Dev-only pipeline error handler, set by the dev server after import.
78
78
  // In production this is always undefined — no overhead.
@@ -127,9 +127,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
127
127
  buildManifest: buildManifest as BuildManifest,
128
128
  });
129
129
 
130
- // Dev logging — initialize OTEL-based dev tracing once at handler creation.
131
- // In production, isDev is false — no tracing, no overhead.
132
- // The DevSpanProcessor handles all formatting and stderr output.
133
130
  // Initialize debug flag from config before anything else.
134
131
  // This allows timber.config.ts `debug: true` to enable debug logging
135
132
  // in production without the TIMBER_DEBUG env var.
@@ -137,10 +134,24 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
137
134
  setDebugFromConfig(true);
138
135
  }
139
136
 
140
- const isDev = isDebug();
137
+ // Two separate flags for two different security levels:
138
+ //
139
+ // isDev (isDevMode) — gates client-visible behavior: dev error pages with
140
+ // stack traces, detailed Server-Timing headers, error messages in action
141
+ // payloads. Statically replaced in production → tree-shaken to zero.
142
+ // TIMBER_DEBUG cannot enable this.
143
+ //
144
+ // debugEnabled (isDebug) — gates server-side logging only: stderr warnings,
145
+ // OTEL dev tracing, console.error fallbacks. TIMBER_DEBUG enables this.
146
+ // Never affects what clients see.
147
+ const isDev = isDevMode();
148
+ const debugEnabled = isDebug();
141
149
  const slowPhaseMs = (runtimeConfig as Record<string, unknown>).slowPhaseMs as number | undefined;
142
150
 
143
- if (isDev) {
151
+ // Dev logging — initialize OTEL-based dev tracing once at handler creation.
152
+ // In production with TIMBER_DEBUG, this enables server-side tracing output
153
+ // without exposing anything to clients.
154
+ if (debugEnabled) {
144
155
  const devLogMode = resolveLogMode();
145
156
  if (devLogMode !== 'quiet') {
146
157
  await initDevTracing({ mode: devLogMode, slowPhaseMs });
@@ -1,75 +0,0 @@
1
- //#region src/server/debug.ts
2
- /**
3
- * Runtime debug flag for timber.js.
4
- *
5
- * Provides `isDebug()` — a runtime check that returns true when timber's
6
- * debug/warning logging should be active. This is true in two cases:
7
- *
8
- * 1. Development mode: `process.env.NODE_ENV !== 'production'`
9
- * (statically replaced and tree-shaken in production builds — zero cost)
10
- *
11
- * 2. TIMBER_DEBUG flag: A runtime environment variable that survives
12
- * production builds. When set to any truthy value ("1", "true", etc.),
13
- * timber's own diagnostics are re-enabled without affecting React's mode.
14
- *
15
- * The TIMBER_DEBUG check uses a dynamic property access pattern that
16
- * prevents the bundler from statically replacing or eliminating it.
17
- *
18
- * Usage:
19
- * In Cloudflare Workers wrangler.toml:
20
- * [vars]
21
- * TIMBER_DEBUG = "1"
22
- *
23
- * In Node.js:
24
- * TIMBER_DEBUG=1 node server.js
25
- *
26
- * In timber.config.ts:
27
- * export default { debug: true }
28
- *
29
- * See design/18-build-system.md for build pipeline details.
30
- */
31
- /**
32
- * Config-level debug override. Set via `setDebugFromConfig()` during
33
- * initialization when timber.config.ts has `debug: true`.
34
- */
35
- var _configDebug = false;
36
- /**
37
- * Check if timber debug logging is active.
38
- *
39
- * Returns true if ANY of these conditions hold:
40
- * - NODE_ENV is not 'production' (standard dev mode)
41
- * - TIMBER_DEBUG environment variable is set to a truthy value at runtime
42
- * - timber.config.ts has `debug: true`
43
- *
44
- * The TIMBER_DEBUG check is deliberately written as a dynamic property
45
- * access so bundlers cannot statically replace it. The `_envKey` variable
46
- * prevents the bundler from seeing `process.env.TIMBER_DEBUG` as a
47
- * compile-time constant.
48
- *
49
- * This function is intentionally NOT inlineable — it reads runtime state.
50
- */
51
- function isDebug() {
52
- if (process.env.NODE_ENV !== "production") return true;
53
- if (_configDebug) return true;
54
- return _readTimberDebugEnv();
55
- }
56
- /**
57
- * Read TIMBER_DEBUG from the environment at runtime.
58
- *
59
- * Extracted to a separate function to:
60
- * 1. Prevent bundler inlining (cross-module function calls are not inlined)
61
- * 2. Handle platforms where `process` may not exist (Cloudflare Workers)
62
- * 3. Support globalThis.__TIMBER_DEBUG for programmatic control
63
- */
64
- function _readTimberDebugEnv() {
65
- if (globalThis.__TIMBER_DEBUG) return true;
66
- try {
67
- const val = typeof process !== "undefined" && process.env ? process.env["TIMBER_DEBUG"] : void 0;
68
- if (val && val !== "0" && val !== "false") return true;
69
- } catch {}
70
- return false;
71
- }
72
- //#endregion
73
- export { isDebug as t };
74
-
75
- //# sourceMappingURL=debug-B4WUeqJ-.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"debug-B4WUeqJ-.js","names":[],"sources":["../../src/server/debug.ts"],"sourcesContent":["/**\n * Runtime debug flag for timber.js.\n *\n * Provides `isDebug()` — a runtime check that returns true when timber's\n * debug/warning logging should be active. This is true in two cases:\n *\n * 1. Development mode: `process.env.NODE_ENV !== 'production'`\n * (statically replaced and tree-shaken in production builds — zero cost)\n *\n * 2. TIMBER_DEBUG flag: A runtime environment variable that survives\n * production builds. When set to any truthy value (\"1\", \"true\", etc.),\n * timber's own diagnostics are re-enabled without affecting React's mode.\n *\n * The TIMBER_DEBUG check uses a dynamic property access pattern that\n * prevents the bundler from statically replacing or eliminating it.\n *\n * Usage:\n * In Cloudflare Workers wrangler.toml:\n * [vars]\n * TIMBER_DEBUG = \"1\"\n *\n * In Node.js:\n * TIMBER_DEBUG=1 node server.js\n *\n * In timber.config.ts:\n * export default { debug: true }\n *\n * See design/18-build-system.md for build pipeline details.\n */\n\n// ─── Debug Flag ─────────────────────────────────────────────────────────────\n\n/**\n * Config-level debug override. Set via `setDebugFromConfig()` during\n * initialization when timber.config.ts has `debug: true`.\n */\nlet _configDebug = false;\n\n/**\n * Set the debug flag from timber.config.ts.\n * Called during handler initialization.\n */\nexport function setDebugFromConfig(debug: boolean): void {\n _configDebug = debug;\n}\n\n/**\n * Check if timber debug logging is active.\n *\n * Returns true if ANY of these conditions hold:\n * - NODE_ENV is not 'production' (standard dev mode)\n * - TIMBER_DEBUG environment variable is set to a truthy value at runtime\n * - timber.config.ts has `debug: true`\n *\n * The TIMBER_DEBUG check is deliberately written as a dynamic property\n * access so bundlers cannot statically replace it. The `_envKey` variable\n * prevents the bundler from seeing `process.env.TIMBER_DEBUG` as a\n * compile-time constant.\n *\n * This function is intentionally NOT inlineable — it reads runtime state.\n */\nexport function isDebug(): boolean {\n // Fast path: dev mode (statically replaced to `true` in dev, `false` in prod)\n if (process.env.NODE_ENV !== 'production') return true;\n\n // Config override\n if (_configDebug) return true;\n\n // Runtime env var check — uses dynamic access to prevent static replacement.\n // In production builds, process.env.NODE_ENV is statically replaced, but\n // TIMBER_DEBUG must survive as a runtime check. The dynamic key access\n // pattern ensures the bundler treats this as opaque.\n return _readTimberDebugEnv();\n}\n\n/**\n * Read TIMBER_DEBUG from the environment at runtime.\n *\n * Extracted to a separate function to:\n * 1. Prevent bundler inlining (cross-module function calls are not inlined)\n * 2. Handle platforms where `process` may not exist (Cloudflare Workers)\n * 3. Support globalThis.__TIMBER_DEBUG for programmatic control\n */\nfunction _readTimberDebugEnv(): boolean {\n // globalThis override — useful for programmatic control and testing\n if ((globalThis as Record<string, unknown>).__TIMBER_DEBUG) return true;\n\n // process.env — works in Node.js and platforms that polyfill process\n try {\n const key = 'TIMBER_DEBUG';\n const val =\n typeof process !== 'undefined' && process.env ? (process.env as Record<string, string | undefined>)[key] : undefined;\n if (val && val !== '0' && val !== 'false') return true;\n } catch {\n // process may not exist or env may throw — safe to ignore\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAI,eAAe;;;;;;;;;;;;;;;;AAyBnB,SAAgB,UAAmB;AAEjC,KAAA,QAAA,IAAA,aAA6B,aAAc,QAAO;AAGlD,KAAI,aAAc,QAAO;AAMzB,QAAO,qBAAqB;;;;;;;;;;AAW9B,SAAS,sBAA+B;AAEtC,KAAK,WAAuC,eAAgB,QAAO;AAGnE,KAAI;EAEF,MAAM,MACJ,OAAO,YAAY,eAAe,QAAQ,MAAO,QAAQ,IAF/C,kBAEiG,KAAA;AAC7G,MAAI,OAAO,QAAQ,OAAO,QAAQ,QAAS,QAAO;SAC5C;AAIR,QAAO"}