@timber-js/app 0.2.0-alpha.145 → 0.2.0-alpha.147

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 (183) hide show
  1. package/dist/_chunks/{actions-DzFMnUAR.js → actions-OIZ8siQu.js} +28 -43
  2. package/dist/_chunks/actions-OIZ8siQu.js.map +1 -0
  3. package/dist/_chunks/{cli-schema-sync-DH-M8Poy.js → cli-schema-sync-CBiMEtqa.js} +1 -1
  4. package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-CQaxupHv.js} +47 -54
  5. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +1 -0
  6. package/dist/_chunks/{handler-store-DjISKYEB.js → handler-store-BVDgyTh7.js} +26 -2
  7. package/dist/_chunks/handler-store-BVDgyTh7.js.map +1 -0
  8. package/dist/_chunks/{href-validation-EZktELp8.js → href-validation-DyZIzfMJ.js} +18 -2
  9. package/dist/_chunks/{href-validation-EZktELp8.js.map → href-validation-DyZIzfMJ.js.map} +1 -1
  10. package/dist/_chunks/{navigation-root-DLc4s4ZC.js → navigation-root-bUDZLw74.js} +23 -2
  11. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -0
  12. package/dist/_chunks/tracing-M_zKubhk.js.map +1 -1
  13. package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
  14. package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
  15. package/dist/adapters/cloudflare.d.ts +1 -15
  16. package/dist/adapters/cloudflare.d.ts.map +1 -1
  17. package/dist/adapters/cloudflare.js +10 -28
  18. package/dist/adapters/cloudflare.js.map +1 -1
  19. package/dist/adapters/compress-module.d.ts.map +1 -1
  20. package/dist/adapters/nitro.d.ts.map +1 -1
  21. package/dist/adapters/nitro.js +18 -5
  22. package/dist/adapters/nitro.js.map +1 -1
  23. package/dist/cache/index.js +39 -12
  24. package/dist/cache/index.js.map +1 -1
  25. package/dist/cache/redis-handler.d.ts +72 -8
  26. package/dist/cache/redis-handler.d.ts.map +1 -1
  27. package/dist/cache/stable-stringify.d.ts +9 -0
  28. package/dist/cache/stable-stringify.d.ts.map +1 -1
  29. package/dist/cache/timber-cache.d.ts +3 -1
  30. package/dist/cache/timber-cache.d.ts.map +1 -1
  31. package/dist/cli.js +1 -1
  32. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  33. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  34. package/dist/client/browser-entry/scroll.d.ts +9 -0
  35. package/dist/client/browser-entry/scroll.d.ts.map +1 -1
  36. package/dist/client/error-boundary.d.ts.map +1 -1
  37. package/dist/client/error-boundary.js +24 -10
  38. package/dist/client/error-boundary.js.map +1 -1
  39. package/dist/client/error-reconstituter.d.ts +8 -1
  40. package/dist/client/error-reconstituter.d.ts.map +1 -1
  41. package/dist/client/index.js +3 -3
  42. package/dist/client/index.js.map +1 -1
  43. package/dist/client/internal.js +99 -24
  44. package/dist/client/internal.js.map +1 -1
  45. package/dist/client/link.d.ts.map +1 -1
  46. package/dist/client/navigation-api.d.ts.map +1 -1
  47. package/dist/client/navigation-root.d.ts +13 -0
  48. package/dist/client/navigation-root.d.ts.map +1 -1
  49. package/dist/client/router.d.ts +7 -0
  50. package/dist/client/router.d.ts.map +1 -1
  51. package/dist/client/rsc-fetch.d.ts.map +1 -1
  52. package/dist/dev-tools/404-page.d.ts +6 -2
  53. package/dist/dev-tools/404-page.d.ts.map +1 -1
  54. package/dist/dev-tools/dev-page-shell.d.ts +77 -0
  55. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
  56. package/dist/dev-tools/error-page.d.ts +4 -30
  57. package/dist/dev-tools/error-page.d.ts.map +1 -1
  58. package/dist/dev-tools/index.d.ts +2 -1
  59. package/dist/dev-tools/index.d.ts.map +1 -1
  60. package/dist/dev-tools/instrumentation.d.ts +11 -1
  61. package/dist/dev-tools/instrumentation.d.ts.map +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +432 -255
  64. package/dist/index.js.map +1 -1
  65. package/dist/plugins/cache.d.ts +43 -9
  66. package/dist/plugins/cache.d.ts.map +1 -1
  67. package/dist/plugins/cloudflare-wasm.d.ts +4 -1
  68. package/dist/plugins/cloudflare-wasm.d.ts.map +1 -1
  69. package/dist/plugins/dev-server.d.ts +16 -0
  70. package/dist/plugins/dev-server.d.ts.map +1 -1
  71. package/dist/plugins/server-bundle.d.ts.map +1 -1
  72. package/dist/plugins/shims.d.ts +23 -0
  73. package/dist/plugins/shims.d.ts.map +1 -1
  74. package/dist/routing/codegen-types.d.ts +4 -0
  75. package/dist/routing/codegen-types.d.ts.map +1 -1
  76. package/dist/routing/index.js +2 -2
  77. package/dist/server/action-handler.d.ts.map +1 -1
  78. package/dist/server/actions.d.ts +3 -2
  79. package/dist/server/actions.d.ts.map +1 -1
  80. package/dist/server/cookie-context.d.ts.map +1 -1
  81. package/dist/server/cookie-parsing.d.ts.map +1 -1
  82. package/dist/server/deny-boundary.d.ts +27 -2
  83. package/dist/server/deny-boundary.d.ts.map +1 -1
  84. package/dist/server/deny-renderer.d.ts.map +1 -1
  85. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  86. package/dist/server/fallback-error.d.ts +2 -2
  87. package/dist/server/fallback-error.d.ts.map +1 -1
  88. package/dist/server/index.js +1 -1
  89. package/dist/server/internal.js +227 -179
  90. package/dist/server/internal.js.map +1 -1
  91. package/dist/server/node-stream-transforms.d.ts +24 -6
  92. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  93. package/dist/server/pipeline.d.ts +11 -0
  94. package/dist/server/pipeline.d.ts.map +1 -1
  95. package/dist/server/primitives.d.ts +1 -11
  96. package/dist/server/primitives.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  98. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  99. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  100. package/dist/server/slot-resolver.d.ts +6 -4
  101. package/dist/server/slot-resolver.d.ts.map +1 -1
  102. package/dist/server/ssr-entry.d.ts.map +1 -1
  103. package/dist/server/status-code-resolver.d.ts +6 -0
  104. package/dist/server/status-code-resolver.d.ts.map +1 -1
  105. package/dist/server/tracing.d.ts +2 -1
  106. package/dist/server/tracing.d.ts.map +1 -1
  107. package/dist/server/utils/mdx-file.d.ts +15 -0
  108. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  109. package/dist/shared/head-header.d.ts +54 -0
  110. package/dist/shared/head-header.d.ts.map +1 -0
  111. package/dist/shared/href-validation.d.ts +11 -0
  112. package/dist/shared/href-validation.d.ts.map +1 -1
  113. package/dist/shared/redirect-type.d.ts +17 -0
  114. package/dist/shared/redirect-type.d.ts.map +1 -0
  115. package/dist/shims/headers.d.ts +52 -1
  116. package/dist/shims/headers.d.ts.map +1 -1
  117. package/dist/shims/navigation-client.d.ts +33 -11
  118. package/dist/shims/navigation-client.d.ts.map +1 -1
  119. package/package.json +11 -10
  120. package/src/adapters/cloudflare.ts +13 -47
  121. package/src/adapters/compress-module.ts +9 -3
  122. package/src/adapters/nitro.ts +9 -2
  123. package/src/cache/redis-handler.ts +77 -13
  124. package/src/cache/stable-stringify.ts +65 -2
  125. package/src/cache/timber-cache.ts +25 -13
  126. package/src/cli.ts +0 -0
  127. package/src/client/browser-entry/action-dispatch.ts +7 -2
  128. package/src/client/browser-entry/router-init.ts +2 -1
  129. package/src/client/browser-entry/scroll.ts +23 -0
  130. package/src/client/error-boundary.tsx +33 -5
  131. package/src/client/error-reconstituter.tsx +15 -1
  132. package/src/client/link.tsx +4 -1
  133. package/src/client/navigation-api.ts +5 -1
  134. package/src/client/navigation-root.tsx +16 -0
  135. package/src/client/router.ts +107 -29
  136. package/src/client/rsc-fetch.ts +6 -6
  137. package/src/dev-tools/404-page.ts +22 -123
  138. package/src/dev-tools/dev-page-shell.ts +256 -0
  139. package/src/dev-tools/error-page.ts +27 -203
  140. package/src/dev-tools/index.ts +7 -1
  141. package/src/dev-tools/instrumentation.ts +39 -2
  142. package/src/index.ts +1 -0
  143. package/src/plugins/cache.ts +79 -21
  144. package/src/plugins/cloudflare-wasm.ts +48 -10
  145. package/src/plugins/dev-server.ts +32 -8
  146. package/src/plugins/server-bundle.ts +25 -9
  147. package/src/plugins/shims.ts +94 -52
  148. package/src/routing/codegen-types.ts +4 -0
  149. package/src/routing/codegen.ts +63 -6
  150. package/src/routing/scanner.ts +80 -63
  151. package/src/server/action-handler.ts +10 -2
  152. package/src/server/actions.ts +3 -2
  153. package/src/server/compress.ts +11 -2
  154. package/src/server/cookie-context.ts +11 -0
  155. package/src/server/cookie-parsing.ts +16 -4
  156. package/src/server/deny-boundary.ts +106 -13
  157. package/src/server/deny-renderer.ts +40 -10
  158. package/src/server/error-boundary-wrapper.ts +1 -14
  159. package/src/server/fallback-error.ts +9 -4
  160. package/src/server/node-stream-transforms.ts +61 -24
  161. package/src/server/pipeline.ts +12 -0
  162. package/src/server/primitives.ts +10 -31
  163. package/src/server/route-matcher.ts +22 -6
  164. package/src/server/rsc-entry/error-renderer.ts +1 -12
  165. package/src/server/rsc-entry/index.ts +7 -2
  166. package/src/server/rsc-entry/rsc-payload.ts +4 -3
  167. package/src/server/slot-resolver.ts +27 -14
  168. package/src/server/ssr-entry.ts +17 -28
  169. package/src/server/status-code-resolver.ts +3 -2
  170. package/src/server/tracing.ts +17 -1
  171. package/src/server/tree-match.ts +15 -5
  172. package/src/server/utils/mdx-file.ts +22 -0
  173. package/src/shared/head-header.ts +69 -0
  174. package/src/shared/href-validation.ts +24 -0
  175. package/src/shared/redirect-type.ts +17 -0
  176. package/src/shims/headers.ts +119 -2
  177. package/src/shims/navigation-client.ts +48 -27
  178. package/LICENSE +0 -8
  179. package/dist/_chunks/actions-DzFMnUAR.js.map +0 -1
  180. package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
  181. package/dist/_chunks/handler-store-DjISKYEB.js.map +0 -1
  182. package/dist/_chunks/navigation-root-DLc4s4ZC.js.map +0 -1
  183. package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import { n as setViteServer } from "./_chunks/warnings-CNPXaypM.js";
2
2
  import { a as isDynamicMetadataExtension, o as isMetadataRouteServePath } from "./_chunks/metadata-routes-BjepjefT.js";
3
3
  import { h as swallow } from "./_chunks/logger-Dt-jQaBO.js";
4
- import { c as loadTimberConfigFile, d as resolveBuildDir, f as resolveClientJavascript, l as mergeFileConfig, m as scanRoutes, o as DEFAULT_BUILD_DIR, p as createNoopTimer, s as createPluginContext, u as resolveAppDir } from "./_chunks/cli-schema-sync-D2IpXicg.js";
5
- import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-7FBPM-hX.js";
4
+ import { c as loadTimberConfigFile, d as resolveBuildDir, f as resolveClientJavascript, l as mergeFileConfig, m as scanRoutes, o as DEFAULT_BUILD_DIR, p as createNoopTimer, s as createPluginContext, u as resolveAppDir } from "./_chunks/cli-schema-sync-CQaxupHv.js";
5
+ import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-Ck4Y45XN.js";
6
6
  import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
7
7
  import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
8
8
  import { basename, dirname, extname, join, normalize, relative, resolve } from "node:path";
9
- import { createRequire } from "node:module";
9
+ import { builtinModules, createRequire } from "node:module";
10
10
  import react, { reactCompilerPreset } from "@vitejs/plugin-react";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { fileURLToPath, pathToFileURL } from "node:url";
13
13
  import { constants, createGzip, gzipSync } from "node:zlib";
14
- import { Readable } from "node:stream";
14
+ import { Readable, pipeline } from "node:stream";
15
15
  import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
16
16
  import { createHash, randomUUID } from "node:crypto";
17
17
  import { parseAst } from "vite";
@@ -42,10 +42,6 @@ var __copyProps = (to, from, except, desc) => {
42
42
  return to;
43
43
  };
44
44
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
45
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
46
- if (typeof require !== "undefined") return require.apply(this, arguments);
47
- throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
48
- });
49
45
  //#endregion
50
46
  //#region src/plugins/content.ts
51
47
  var CONFIG_FILE_NAMES$1 = [
@@ -592,6 +588,211 @@ function escapeHtml(str) {
592
588
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
593
589
  }
594
590
  //#endregion
591
+ //#region src/dev-tools/dev-page-shell.ts
592
+ /**
593
+ * Shared scaffold for dev-mode HTML pages (dev error page, dev 404 page).
594
+ *
595
+ * Both pages are self-contained HTML documents served when the normal
596
+ * render pipeline can't produce a page. They share:
597
+ * - The document scaffold (head, container, footer)
598
+ * - Base CSS (layout, light/dark variables, common components)
599
+ * - The HMR auto-reload script that watches Vite's WebSocket and reloads
600
+ * when the developer fixes the error / adds the missing route
601
+ *
602
+ * Dev-only: this module is only imported by dev-mode code paths. It is
603
+ * never included in production builds.
604
+ *
605
+ * Design doc: 21-dev-server.md §"Error Overlay", §"Dev 404 Page"
606
+ */
607
+ /**
608
+ * Extract HMR connection options from a Vite resolved config.
609
+ * Used by the dev server to pass HMR config to the dev page generators
610
+ * so the auto-reload WebSocket connects to the correct endpoint.
611
+ */
612
+ function extractHmrOptions(config) {
613
+ const hmr = config.server?.hmr;
614
+ if (hmr === false) return void 0;
615
+ const hmrOpts = typeof hmr === "object" ? hmr : {};
616
+ const opts = {
617
+ protocol: hmrOpts.protocol,
618
+ host: hmrOpts.host,
619
+ port: hmrOpts.port,
620
+ path: hmrOpts.path,
621
+ token: config.webSocketToken
622
+ };
623
+ if (!opts.protocol && !opts.host && !opts.port && !opts.path && !opts.token) return;
624
+ return opts;
625
+ }
626
+ /**
627
+ * Build the inline auto-reload script for dev pages.
628
+ *
629
+ * The page is static HTML, not a Vite-managed module, so the normal HMR
630
+ * update flow can't reach it. Instead we connect to Vite's HMR WebSocket
631
+ * directly and reload on any update event.
632
+ *
633
+ * URL construction mirrors Vite's own client.mjs: explicitly configured
634
+ * values win, and the browser's location fills any gaps at runtime. This
635
+ * matters because Vite 8 requires a `?token=` on browser WS upgrades in
636
+ * every config — a config with only a token still needs the host and port
637
+ * from `location` (TIM-1067).
638
+ */
639
+ function buildReloadScript(hmrOptions) {
640
+ return ` (function() {
641
+ try {
642
+ var o = ${JSON.stringify(hmrOptions ?? {}).replace(/</g, "\\u003c")};
643
+ var protocol = o.protocol || (location.protocol === 'https:' ? 'wss' : 'ws');
644
+ var host = o.host || location.hostname;
645
+ var port = o.port || location.port;
646
+ var url = protocol + '://' + host + (port ? ':' + port : '') + (o.path || '')
647
+ + (o.token ? '?token=' + encodeURIComponent(o.token) : '');
648
+ var ws = new WebSocket(url, 'vite-hmr');
649
+ ws.addEventListener('message', function(e) {
650
+ try {
651
+ var data = JSON.parse(e.data);
652
+ if (data.type === 'full-reload' || data.type === 'update') {
653
+ location.reload();
654
+ }
655
+ } catch (ex) {}
656
+ });
657
+ } catch (ex) {}
658
+ })();`;
659
+ }
660
+ /**
661
+ * Render a self-contained dev page: scaffold + shared CSS + footer +
662
+ * HMR auto-reload script.
663
+ */
664
+ function renderDevPageShell(opts) {
665
+ return `<!DOCTYPE html>
666
+ <html lang="en">
667
+ <head>
668
+ <meta charset="utf-8">
669
+ <meta name="viewport" content="width=device-width, initial-scale=1">
670
+ <title>${escapeHtml(opts.title)}</title>
671
+ <style>${SHARED_CSS}${opts.extraCss ?? ""}</style>
672
+ </head>
673
+ <body>
674
+ <div class="container">
675
+ ${opts.bodyHtml}
676
+
677
+ <footer class="footer">
678
+ <span class="timber-logo">🪵 timber.js</span>
679
+ <span class="footer-hint">${opts.footerHint}</span>
680
+ </footer>
681
+ </div>
682
+
683
+ <script>
684
+ ${opts.extraScript ?? ""}
685
+ // Auto-reload when Vite HMR signals an update (file fixed, route added).
686
+ ${buildReloadScript(opts.hmrOptions)}
687
+ <\/script>
688
+ </body>
689
+ </html>`;
690
+ }
691
+ /**
692
+ * Base CSS shared by all dev pages. Pages override the --badge-* variables
693
+ * (and add their own component styles) via `extraCss`.
694
+ */
695
+ var SHARED_CSS = `
696
+ :root {
697
+ --bg: #fff;
698
+ --fg: #1a1a1a;
699
+ --fg-dim: #6b7280;
700
+ --border: #e5e7eb;
701
+ --source-bg: #fafafa;
702
+ --link: #2563eb;
703
+ --code-bg: #f3f4f6;
704
+ --btn-bg: #f3f4f6;
705
+ --btn-fg: #374151;
706
+ --btn-border: #d1d5db;
707
+ --code-font: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
708
+ }
709
+
710
+ @media (prefers-color-scheme: dark) {
711
+ :root {
712
+ --bg: #0a0a0a;
713
+ --fg: #f5f5f5;
714
+ --fg-dim: #9ca3af;
715
+ --border: #27272a;
716
+ --source-bg: #18181b;
717
+ --link: #60a5fa;
718
+ --code-bg: #27272a;
719
+ --btn-bg: #27272a;
720
+ --btn-fg: #e5e7eb;
721
+ --btn-border: #3f3f46;
722
+ }
723
+ }
724
+
725
+ * { margin: 0; padding: 0; box-sizing: border-box; }
726
+
727
+ body {
728
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
729
+ background: var(--bg);
730
+ color: var(--fg);
731
+ line-height: 1.6;
732
+ padding: 2rem;
733
+ }
734
+
735
+ .container { max-width: 56rem; margin: 0 auto; }
736
+ .header { margin-bottom: 1.5rem; }
737
+
738
+ .badge {
739
+ display: inline-block;
740
+ font-size: 0.75rem;
741
+ font-weight: 600;
742
+ text-transform: uppercase;
743
+ letter-spacing: 0.05em;
744
+ padding: 0.25rem 0.625rem;
745
+ border-radius: 0.375rem;
746
+ background: var(--badge-bg);
747
+ color: var(--badge-fg);
748
+ border: 1px solid var(--badge-border);
749
+ margin-bottom: 0.75rem;
750
+ }
751
+
752
+ .message {
753
+ font-size: 1.375rem;
754
+ font-weight: 600;
755
+ line-height: 1.3;
756
+ word-break: break-word;
757
+ }
758
+
759
+ .hint {
760
+ margin-top: 0.5rem;
761
+ color: var(--fg-dim);
762
+ font-size: 0.875rem;
763
+ }
764
+
765
+ .section {
766
+ margin-bottom: 1rem;
767
+ border: 1px solid var(--border);
768
+ border-radius: 0.5rem;
769
+ overflow: hidden;
770
+ }
771
+
772
+ .section-header {
773
+ font-size: 0.75rem;
774
+ font-weight: 600;
775
+ text-transform: uppercase;
776
+ letter-spacing: 0.05em;
777
+ padding: 0.5rem 0.75rem;
778
+ background: var(--source-bg);
779
+ border-bottom: 1px solid var(--border);
780
+ color: var(--fg-dim);
781
+ }
782
+
783
+ .footer {
784
+ display: flex;
785
+ align-items: center;
786
+ gap: 0.75rem;
787
+ padding-top: 1rem;
788
+ border-top: 1px solid var(--border);
789
+ font-size: 0.75rem;
790
+ color: var(--fg-dim);
791
+ }
792
+
793
+ .timber-logo { font-weight: 600; white-space: nowrap; }
794
+ `;
795
+ //#endregion
595
796
  //#region src/dev-tools/error-page.ts
596
797
  /**
597
798
  * Dev error page — self-contained HTML error page for dev server 500s.
@@ -603,8 +804,9 @@ function escapeHtml(str) {
603
804
  * This is NOT a replacement for Vite's error overlay — it's the fallback
604
805
  * for when the overlay's transport (WebSocket) isn't available yet.
605
806
  *
606
- * Dev-only: this module is only imported by dev-server.ts (apply: 'serve').
607
- * It is never included in production builds.
807
+ * Dev-only: this module is only imported by dev-server.ts (apply: 'serve')
808
+ * and via dynamic import from fallback-error.ts in dev mode. It is never
809
+ * included in production builds.
608
810
  *
609
811
  * Design doc: 21-dev-server.md §"Error Overlay"
610
812
  */
@@ -627,10 +829,13 @@ var PHASE_HINTS = {
627
829
  /**
628
830
  * Try to read a few lines of source code around the error location.
629
831
  * Returns null if the file can't be read (e.g., virtual modules).
832
+ *
833
+ * Uses a top-level `node:fs` import — this module is dev-server-only and
834
+ * Node-only. A bare `require()` here would throw in the ESM build and the
835
+ * code frame would silently never render (TIM-1067, B48).
630
836
  */
631
837
  function readSourceContext(filePath, line, contextLines = 3) {
632
838
  try {
633
- const { readFileSync } = __require("node:fs");
634
839
  const allLines = readFileSync(filePath, "utf-8").split("\n");
635
840
  const start = Math.max(0, line - 1 - contextLines);
636
841
  const end = Math.min(allLines.length, line + contextLines);
@@ -653,8 +858,9 @@ var esc = escapeHtml;
653
858
  *
654
859
  * `JSON.stringify` does not escape `<\/script>`, so error text containing
655
860
  * that sequence breaks out of the script block — an XSS vector.
656
- * We replace all `<` with `\u003c` which is valid in JSON strings and
657
- * prevents the HTML parser from seeing a closing `<\/script>` tag.
861
+ * We replace all `<` with the equivalent unicode escape, which is valid in
862
+ * JSON strings and prevents the HTML parser from seeing a closing
863
+ * `<\/script>` tag.
658
864
  *
659
865
  * Security: TIM-788
660
866
  */
@@ -662,32 +868,6 @@ function escJsonForScript(json) {
662
868
  return json.replace(/</g, "\\u003c");
663
869
  }
664
870
  /**
665
- * Build a WebSocket URL from Vite HMR options.
666
- * Mirrors the logic in Vite's client.mjs for constructing the WS connection URL.
667
- */
668
- function buildWsUrl(opts) {
669
- return `${opts.protocol ?? "ws"}://${opts.host ?? "localhost"}${opts.port ? `:${opts.port}` : ""}${opts.path ?? ""}${opts.token ? `?token=${opts.token}` : ""}`;
670
- }
671
- /**
672
- * Extract HMR connection options from a Vite resolved config.
673
- * Used by the dev server to pass HMR config to the error page generator
674
- * so the auto-reload WebSocket connects to the correct endpoint.
675
- */
676
- function extractHmrOptions(config) {
677
- const hmr = config.server?.hmr;
678
- if (hmr === false) return void 0;
679
- const hmrOpts = typeof hmr === "object" ? hmr : {};
680
- const opts = {
681
- protocol: hmrOpts.protocol,
682
- host: hmrOpts.host,
683
- port: hmrOpts.port,
684
- path: hmrOpts.path,
685
- token: config.webSocketToken
686
- };
687
- if (!opts.protocol && !opts.host && !opts.port && !opts.path && !opts.token) return;
688
- return opts;
689
- }
690
- /**
691
871
  * Generate a self-contained HTML error page for dev server 500 responses.
692
872
  *
693
873
  * The page includes:
@@ -711,17 +891,7 @@ function generateDevErrorPage(error, phase, projectRoot, hmrOptions) {
711
891
  let sourceContext = null;
712
892
  if (loc) sourceContext = readSourceContext(loc.file, loc.line);
713
893
  const relPath = loc?.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc?.file;
714
- return `<!DOCTYPE html>
715
- <html lang="en">
716
- <head>
717
- <meta charset="utf-8">
718
- <meta name="viewport" content="width=device-width, initial-scale=1">
719
- <title>Error — ${esc(phaseLabel)} | timber.js</title>
720
- <style>${CSS}</style>
721
- </head>
722
- <body>
723
- <div class="container">
724
- <header class="header">
894
+ const bodyHtml = `<header class="header">
725
895
  <div class="badge">${esc(phaseLabel)} Error</div>
726
896
  <h1 class="message">${esc(message)}</h1>
727
897
  ${phaseHint ? `<p class="hint">${esc(phaseHint)}</p>` : ""}
@@ -730,7 +900,7 @@ function generateDevErrorPage(error, phase, projectRoot, hmrOptions) {
730
900
  ${relPath ? `<div class="location">${esc(relPath)}${loc ? `:${loc.line}:${loc.column}` : ""}</div>` : ""}
731
901
 
732
902
  ${sourceContext ? `<div class="source-context">
733
- <div class="source-header">Source</div>
903
+ <div class="section-header">Source</div>
734
904
  <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>
735
905
  </div>` : ""}
736
906
 
@@ -751,130 +921,45 @@ function generateDevErrorPage(error, phase, projectRoot, hmrOptions) {
751
921
 
752
922
  <div class="actions">
753
923
  <button class="btn" onclick="copyError()">Copy Error</button>
754
- </div>
755
-
756
- <footer class="footer">
757
- <span class="timber-logo">🪵 timber.js</span>
758
- <span class="footer-hint">Fix the error and save — the page will reload automatically via HMR.</span>
759
- </footer>
760
- </div>
761
-
762
- <script>
763
- function copyError() {
924
+ </div>`;
925
+ const copyScript = ` function copyError() {
764
926
  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 ?? ""}`))};
765
927
  navigator.clipboard.writeText(text).then(function() {
766
928
  var btn = document.querySelector('.btn');
767
929
  if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy Error'; }, 1500); }
768
930
  });
769
931
  }
770
-
771
- // Auto-reload when Vite HMR reconnects.
772
- // When the developer fixes the error and saves, Vite's module graph
773
- // invalidates. We can't rely on the normal HMR update flow (the page
774
- // is a static error page, not a Vite-managed module), so we connect
775
- // to Vite's WebSocket and reload on any update event.
776
- (function() {
777
- try {
778
- var wsUrl = ${hmrOptions ? escJsonForScript(JSON.stringify(buildWsUrl(hmrOptions))) : "'ws://' + location.host"};
779
- var ws = new WebSocket(wsUrl, 'vite-hmr');
780
- ws.addEventListener('message', function(e) {
781
- try {
782
- var data = JSON.parse(e.data);
783
- if (data.type === 'full-reload' || data.type === 'update' || data.type === 'connected') {
784
- if (data.type !== 'connected') location.reload();
785
- }
786
- } catch(ex) {}
787
- });
788
- } catch(ex) {}
789
- })();
790
- <\/script>
791
- </body>
792
- </html>`;
932
+ `;
933
+ return renderDevPageShell({
934
+ title: `Error — ${phaseLabel} | timber.js`,
935
+ extraCss: ERROR_CSS,
936
+ bodyHtml,
937
+ footerHint: "Fix the error and save — the page will reload automatically via HMR.",
938
+ extraScript: copyScript,
939
+ hmrOptions
940
+ });
793
941
  }
794
- var CSS = `
942
+ var ERROR_CSS = `
795
943
  :root {
796
- --bg: #fff;
797
- --fg: #1a1a1a;
798
- --fg-dim: #6b7280;
799
- --border: #e5e7eb;
800
944
  --badge-bg: #fef2f2;
801
945
  --badge-fg: #991b1b;
802
946
  --badge-border: #fecaca;
803
- --source-bg: #fafafa;
804
947
  --highlight-bg: #fef2f2;
805
948
  --highlight-border: #ef4444;
806
949
  --line-num: #9ca3af;
807
- --btn-bg: #f3f4f6;
808
- --btn-fg: #374151;
809
- --btn-border: #d1d5db;
810
- --link: #2563eb;
811
- --code-font: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
812
950
  }
813
951
 
814
952
  @media (prefers-color-scheme: dark) {
815
953
  :root {
816
- --bg: #0a0a0a;
817
- --fg: #f5f5f5;
818
- --fg-dim: #9ca3af;
819
- --border: #27272a;
820
954
  --badge-bg: #450a0a;
821
955
  --badge-fg: #fca5a5;
822
956
  --badge-border: #7f1d1d;
823
- --source-bg: #18181b;
824
957
  --highlight-bg: #450a0a;
825
958
  --highlight-border: #dc2626;
826
959
  --line-num: #6b7280;
827
- --btn-bg: #27272a;
828
- --btn-fg: #e5e7eb;
829
- --btn-border: #3f3f46;
830
- --link: #60a5fa;
831
960
  }
832
961
  }
833
962
 
834
- * { margin: 0; padding: 0; box-sizing: border-box; }
835
-
836
- body {
837
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
838
- background: var(--bg);
839
- color: var(--fg);
840
- line-height: 1.6;
841
- padding: 2rem;
842
- }
843
-
844
- .container {
845
- max-width: 56rem;
846
- margin: 0 auto;
847
- }
848
-
849
- .header { margin-bottom: 1.5rem; }
850
-
851
- .badge {
852
- display: inline-block;
853
- font-size: 0.75rem;
854
- font-weight: 600;
855
- text-transform: uppercase;
856
- letter-spacing: 0.05em;
857
- padding: 0.25rem 0.625rem;
858
- border-radius: 0.375rem;
859
- background: var(--badge-bg);
860
- color: var(--badge-fg);
861
- border: 1px solid var(--badge-border);
862
- margin-bottom: 0.75rem;
863
- }
864
-
865
- .message {
866
- font-size: 1.375rem;
867
- font-weight: 600;
868
- line-height: 1.3;
869
- word-break: break-word;
870
- }
871
-
872
- .hint {
873
- margin-top: 0.5rem;
874
- color: var(--fg-dim);
875
- font-size: 0.875rem;
876
- }
877
-
878
963
  .location {
879
964
  font-family: var(--code-font);
880
965
  font-size: 0.8125rem;
@@ -893,17 +978,6 @@ var CSS = `
893
978
  overflow: hidden;
894
979
  }
895
980
 
896
- .source-header, .section-header {
897
- font-size: 0.75rem;
898
- font-weight: 600;
899
- text-transform: uppercase;
900
- letter-spacing: 0.05em;
901
- padding: 0.5rem 0.75rem;
902
- background: var(--source-bg);
903
- border-bottom: 1px solid var(--border);
904
- color: var(--fg-dim);
905
- }
906
-
907
981
  .source-code {
908
982
  overflow-x: auto;
909
983
  font-family: var(--code-font);
@@ -937,13 +1011,6 @@ var CSS = `
937
1011
  user-select: none;
938
1012
  }
939
1013
 
940
- .section {
941
- margin-bottom: 1rem;
942
- border: 1px solid var(--border);
943
- border-radius: 0.5rem;
944
- overflow: hidden;
945
- }
946
-
947
1014
  .clickable { cursor: pointer; }
948
1015
  .clickable:hover { background: var(--btn-bg); }
949
1016
 
@@ -977,21 +1044,6 @@ var CSS = `
977
1044
  }
978
1045
 
979
1046
  .btn:hover { opacity: 0.85; }
980
-
981
- .footer {
982
- display: flex;
983
- align-items: center;
984
- gap: 0.75rem;
985
- padding-top: 1rem;
986
- border-top: 1px solid var(--border);
987
- font-size: 0.75rem;
988
- color: var(--fg-dim);
989
- }
990
-
991
- .timber-logo {
992
- font-weight: 600;
993
- white-space: nowrap;
994
- }
995
1047
  `;
996
1048
  //#endregion
997
1049
  //#region src/config-validation.ts
@@ -1331,7 +1383,7 @@ function compressResponse(request, response) {
1331
1383
  */
1332
1384
  function compressWithGzip(body) {
1333
1385
  const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
1334
- Readable.fromWeb(body).pipe(gzip);
1386
+ pipeline(Readable.fromWeb(body), gzip, () => {});
1335
1387
  return Readable.toWeb(gzip);
1336
1388
  }
1337
1389
  //#endregion
@@ -1434,9 +1486,12 @@ function createTimberMiddleware(server, projectRoot) {
1434
1486
  const rscModule = await rscEnv.runner.import(RSC_ENTRY_ID);
1435
1487
  handler = rscModule.default;
1436
1488
  const config = rscModule.pipelineConfig;
1437
- if (config) config.devPipelineErrorHandler = (error, _phase, debugComponents) => {
1438
- sendErrorToOverlay(server, error, classifyErrorPhase(error, projectRoot), projectRoot, debugComponents);
1439
- };
1489
+ if (config) {
1490
+ config.devPipelineErrorHandler = (error, _phase, debugComponents) => {
1491
+ sendErrorToOverlay(server, error, classifyErrorPhase(error, projectRoot), projectRoot, debugComponents);
1492
+ };
1493
+ config.devHmrOptions = hmrOptions;
1494
+ }
1440
1495
  const setSourceMap = rscModule.setDevSourceMapHandler;
1441
1496
  if (typeof setSourceMap === "function") setSourceMap((error) => {
1442
1497
  fixErrorStacktrace(server, error);
@@ -1506,15 +1561,27 @@ function addTimberContext(error) {
1506
1561
  *
1507
1562
  * Constructs the full URL from the Host header and request URL,
1508
1563
  * and forwards the method, headers, and body.
1564
+ *
1565
+ * The scheme comes from the socket's TLS state — with `server.https` the
1566
+ * browser sends `Origin: https://host`, and a hardcoded `http://` would
1567
+ * fail CSRF origin comparison on every POST. X-Forwarded-Proto is NOT
1568
+ * consulted: dev connections are direct, so the header would be
1569
+ * attacker-controlled. (TIM-1067, B32)
1570
+ *
1571
+ * Exported for tests.
1509
1572
  */
1510
1573
  function toWebRequest(nodeReq) {
1511
- const url = `http://${nodeReq.headers.host ?? "localhost"}${nodeReq.url}`;
1574
+ const protocol = nodeReq.socket?.encrypted ? "https" : "http";
1575
+ const host = nodeReq.headers.host ?? nodeReq.headers[":authority"] ?? "localhost";
1576
+ const url = `${protocol}://${host}${nodeReq.url}`;
1512
1577
  const headers = new Headers();
1513
1578
  for (const [key, value] of Object.entries(nodeReq.headers)) {
1514
1579
  if (value === void 0) continue;
1580
+ if (key.startsWith(":")) continue;
1515
1581
  if (Array.isArray(value)) for (const v of value) headers.append(key, v);
1516
1582
  else headers.set(key, value);
1517
1583
  }
1584
+ if (!headers.has("host")) headers.set("host", host);
1518
1585
  const method = nodeReq.method ?? "GET";
1519
1586
  const hasBody = method !== "GET" && method !== "HEAD";
1520
1587
  return new Request(url, {
@@ -2811,18 +2878,57 @@ var CLIENT_ONLY_VIRTUAL = "\0timber:client-only";
2811
2878
  *
2812
2879
  * Used by the catch-all resolveId rule to remap subpath imports to src/
2813
2880
  * in server environments. When adding a new subpath export to package.json,
2814
- * add it here too. See TIM-568.
2881
+ * add it here too — tests/shims-drift.test.ts enumerates package.json
2882
+ * exports and fails if an entry is missing. See TIM-568.
2883
+ *
2884
+ * Exported for the drift test.
2815
2885
  */
2816
2886
  var SUBPATH_SRC_MAP = {
2817
2887
  "cache": "cache/index.ts",
2888
+ "codec": "codec.ts",
2818
2889
  "cookies": "cookies/index.ts",
2890
+ "params": "params/index.ts",
2819
2891
  "search-params": "search-params/index.ts",
2820
2892
  "segment-params": "segment-params/index.ts",
2821
2893
  "adapters/cloudflare": "adapters/cloudflare.ts",
2894
+ "adapters/cloudflare/cache": "adapters/cloudflare-kv-cache.ts",
2822
2895
  "adapters/cloudflare/dev": "adapters/cloudflare-dev.ts",
2823
2896
  "adapters/nitro": "adapters/nitro.ts"
2824
2897
  };
2825
2898
  /**
2899
+ * Runtime value exports of @timber-js/app/server, stubbed in the client
2900
+ * (browser) environment so a stray client-side import fails with a clear
2901
+ * call-time error instead of a confusing bundler error.
2902
+ *
2903
+ * Must stay in sync with the actual runtime exports of server/index.ts —
2904
+ * tests/shims-drift.test.ts compares this list against
2905
+ * Object.keys(import('../server/index.ts')) and fails on drift.
2906
+ * Type-only exports (DenyOptions, AccessContext, …) are erased at runtime
2907
+ * and must NOT appear here.
2908
+ */
2909
+ var SERVER_STUB_EXPORTS = [
2910
+ "getHeaders",
2911
+ "getSegmentParams",
2912
+ "getCookieJar",
2913
+ "deny",
2914
+ "redirect",
2915
+ "redirectExternal",
2916
+ "RedirectType",
2917
+ "waitUntil",
2918
+ "createActionClient",
2919
+ "ActionError",
2920
+ "validated",
2921
+ "coerce",
2922
+ "parseFormData",
2923
+ "getFormFlash",
2924
+ "revalidatePath",
2925
+ "revalidateTag",
2926
+ "getTraceId",
2927
+ "getSpanId",
2928
+ "withSpan",
2929
+ "addSpanEvent"
2930
+ ];
2931
+ /**
2826
2932
  * Map from next/* import specifiers to shim file paths.
2827
2933
  *
2828
2934
  * The shim map is a separate data structure (not embedded in the plugin)
@@ -2848,6 +2954,15 @@ function stripJsExtension(id) {
2848
2954
  return id.endsWith(".js") ? id.slice(0, -3) : id;
2849
2955
  }
2850
2956
  /**
2957
+ * Read the Vite environment name from a plugin hook's `this` context.
2958
+ *
2959
+ * Duck-typed because the hook context type doesn't expose `environment`
2960
+ * in a way that survives our Plugin typing — see Vite's Environment API.
2961
+ */
2962
+ function envName(hookCtx) {
2963
+ return hookCtx.environment?.name;
2964
+ }
2965
+ /**
2851
2966
  * Create the timber-shims Vite plugin.
2852
2967
  *
2853
2968
  * Hooks: resolveId, load
@@ -2865,45 +2980,47 @@ function timberShims(_ctx) {
2865
2980
  * 3. Check next/* shim map
2866
2981
  * 4. Return null (pass through) for everything else
2867
2982
  *
2868
- * All @timber-js/app/* subpath imports resolve to dist/ via package.json
2869
- * exports in all environments. This ensures a single module instance
2870
- * for each module — the dist/ chunks share code via rollup's code
2871
- * splitting. No src/ remapping is needed (see TIM-568).
2983
+ * In server environments (RSC + SSR), @timber-js/app/* subpath imports
2984
+ * are remapped to src/ via SUBPATH_SRC_MAP and the explicit branches
2985
+ * below — dist/ chunks miss Vite's RSC transforms and would duplicate
2986
+ * mutable module state (see TIM-568 and the rationale comments below).
2872
2987
  *
2873
- * The only exception is @timber-js/app/server in the client (browser)
2874
- * environment, which returns a virtual stub — server code must never
2875
- * be bundled into the browser.
2988
+ * In the client (browser) environment, imports resolve to dist/ via
2989
+ * package.json exports — except @timber-js/app/client (remapped to src/
2990
+ * to share module instances with browser-entry) and @timber-js/app/server,
2991
+ * which returns a virtual stub: server code must never be bundled into
2992
+ * the browser.
2876
2993
  */
2877
2994
  resolveId(id) {
2878
2995
  if (id === "server-only") return SERVER_ONLY_VIRTUAL;
2879
2996
  if (id === "client-only") return CLIENT_ONLY_VIRTUAL;
2880
2997
  const cleanId = stripJsExtension(id);
2881
2998
  if (cleanId in SHIM_MAP) {
2882
- if (this.environment?.name === "client" && cleanId === "next/navigation") return resolve(SHIMS_DIR, "navigation-client.ts");
2999
+ if (envName(this) === "client" && cleanId === "next/navigation") return resolve(SHIMS_DIR, "navigation-client.ts");
2883
3000
  return SHIM_MAP[cleanId];
2884
3001
  }
2885
3002
  if (cleanId === "#server-internal") {
2886
- if (this.environment?.name === "client") return "\0timber:server-empty";
3003
+ if (envName(this) === "client") return "\0timber:server-empty";
2887
3004
  return resolve(PKG_ROOT, "src", "server", "internal.ts");
2888
3005
  }
2889
3006
  if (cleanId === "#client-internal") return resolve(PKG_ROOT, "src", "client", "internal.ts");
2890
3007
  if (cleanId === "#routing") return resolve(PKG_ROOT, "src", "routing", "index.ts");
2891
3008
  if (cleanId === "@timber-js/app/server/internal") {
2892
- if (this.environment?.name === "client") return "\0timber:server-empty";
3009
+ if (envName(this) === "client") return "\0timber:server-empty";
2893
3010
  return resolve(PKG_ROOT, "src", "server", "internal.ts");
2894
3011
  }
2895
3012
  if (cleanId === "@timber-js/app/client/internal") return resolve(PKG_ROOT, "src", "client", "internal.ts");
2896
3013
  if (cleanId === "@timber-js/app/server") {
2897
- if (this.environment?.name === "client") return "\0timber:server-empty";
3014
+ if (envName(this) === "client") return "\0timber:server-empty";
2898
3015
  return resolve(PKG_ROOT, "src", "server", "index.ts");
2899
3016
  }
2900
3017
  if (cleanId === "@timber-js/app/client") {
2901
- const envName = this.environment?.name;
2902
- if (envName === "ssr" || envName === "client") return resolve(PKG_ROOT, "src", "client", "index.ts");
3018
+ const env = envName(this);
3019
+ if (env === "ssr" || env === "client") return resolve(PKG_ROOT, "src", "client", "index.ts");
2903
3020
  return null;
2904
3021
  }
2905
3022
  if (cleanId.startsWith("@timber-js/app/") && cleanId !== "@timber-js/app/package.json") {
2906
- if (this.environment?.name !== "client") {
3023
+ if (envName(this) !== "client") {
2907
3024
  const srcPath = SUBPATH_SRC_MAP[cleanId.slice(15)];
2908
3025
  if (srcPath) return resolve(PKG_ROOT, "src", srcPath);
2909
3026
  }
@@ -2918,7 +3035,7 @@ function timberShims(_ctx) {
2918
3035
  * clearly identifies the boundary violation.
2919
3036
  */
2920
3037
  load(id) {
2921
- const isClient = this.environment?.name === "client";
3038
+ const isClient = envName(this) === "client";
2922
3039
  if (id === SERVER_ONLY_VIRTUAL) {
2923
3040
  if (isClient) return `throw new Error(
2924
3041
  "This module cannot be imported from a Client Component module. " +
@@ -2938,34 +3055,7 @@ const msg = "[timber] @timber-js/app/server was imported from client code. " +
2938
3055
  "Server modules (headers, cookies, redirect, deny, etc.) cannot be used in client components. " +
2939
3056
  "If you need these APIs, move the import to a server component or middleware.";
2940
3057
  function stub() { throw new Error(msg); }
2941
- export const headers = stub;
2942
- export const cookies = stub;
2943
- export const getHeaders = stub;
2944
- export const getHeader = stub;
2945
- export const getCookieJar = stub;
2946
- export const getCookie = stub;
2947
- export const getSearchParams = stub;
2948
- export const getSegmentParams = stub;
2949
- export const searchParams = stub;
2950
- export const deny = stub;
2951
- export const notFound = stub;
2952
- export const redirect = stub;
2953
- export const permanentRedirect = stub;
2954
- export const redirectExternal = stub;
2955
- export const waitUntil = stub;
2956
- export const DenyOptions = {};
2957
- export const RedirectType = {};
2958
- export const DenySignal = stub;
2959
- export const RedirectSignal = stub;
2960
- export const createPipeline = stub;
2961
- export const revalidatePath = stub;
2962
- export const revalidateTag = stub;
2963
- export const createActionClient = stub;
2964
- export const ActionError = stub;
2965
- export const validated = stub;
2966
- export const getFormFlash = stub;
2967
- export const parseFormData = stub;
2968
- export const coerce = stub;
3058
+ ${SERVER_STUB_EXPORTS.map((name) => name === "RedirectType" ? `export const RedirectType = { push: 'push', replace: 'replace' };` : `export const ${name} = stub;`).join("\n")}
2969
3059
  `;
2970
3060
  }
2971
3061
  };
@@ -6554,7 +6644,20 @@ function clientChunkGroup(meta, appDir, root) {
6554
6644
  }
6555
6645
  //#endregion
6556
6646
  //#region src/plugins/server-bundle.ts
6647
+ /**
6648
+ * timber-server-bundle — Bundle all dependencies for server environments.
6649
+ *
6650
+ * In production builds, sets `resolve.noExternal: true` for the rsc and ssr
6651
+ * environments. This ensures all npm dependencies are bundled into the output,
6652
+ * which is required for platforms like Cloudflare Workers that don't have
6653
+ * access to node_modules at runtime.
6654
+ *
6655
+ * In dev mode, Vite's default externalization is preserved for fast HMR.
6656
+ *
6657
+ * Design docs: design/11-platform.md, design/25-production-deployments.md
6658
+ */
6557
6659
  var BUILD_ONLY_EXTERNALS = [
6660
+ ...builtinModules.flatMap((m) => [m, `node:${m}`]),
6558
6661
  "nitro",
6559
6662
  "rollup",
6560
6663
  "@mdx-js/rollup",
@@ -6606,8 +6709,13 @@ function timberServerBundle() {
6606
6709
  if (!code.includes(lazy)) return null;
6607
6710
  const eager = [
6608
6711
  "var __esmMin = (fn, res) => {",
6609
- " var l = () => { if (fn) { var f = fn; try { res = f(); fn = 0; } catch(e) {} } return res; };",
6712
+ " var eager = 1;",
6713
+ " var l = () => {",
6714
+ " if (fn) { if (eager) { try { res = fn(); fn = 0; } catch(e) {} } else { res = fn(fn = 0); } }",
6715
+ " return res;",
6716
+ " };",
6610
6717
  " l();",
6718
+ " eager = 0;",
6611
6719
  " return l;",
6612
6720
  "};"
6613
6721
  ].join(" ");
@@ -6647,9 +6755,36 @@ function timberServerBundle() {
6647
6755
  * The plugin transforms:
6648
6756
  * cache(fn, opts) → cache(fn, opts, "stable-id")
6649
6757
  *
6650
- * where stable-id = fnv1a(relativeModulePath + ":cache:" + callIndex).
6651
- *
6652
- * See TIM-997, design/06-caching.md §"Cache Key Security".
6758
+ * where stable-id =
6759
+ * fnv1a(relPath + ":cache:" + fnv1a(stmtText) + ":" + fnv1a(callSpanText) + ":" + occurrence)
6760
+ *
6761
+ * stmtText is the source text of the enclosing *top-level statement* and
6762
+ * callSpanText is the source text of the cache(...) call expression itself.
6763
+ *
6764
+ * The ID is derived from the *content* of the call, not its ordinal position
6765
+ * in the file. A position-based index (the original TIM-997 scheme) was stable
6766
+ * within one build but shifted across builds: inserting, deleting, or
6767
+ * reordering a cache() call changed the index of every call below it, so with
6768
+ * a persistent shared CacheHandler (Redis/KV) a new function could inherit
6769
+ * another function's old ID and serve its persisted data — cross-deploy cache
6770
+ * poisoning (TIM-1053). Content hashing makes IDs immune to reordering and
6771
+ * unrelated edits, while a changed call gets a fresh ID — which is exactly
6772
+ * when invalidation is wanted.
6773
+ *
6774
+ * The enclosing-statement text is included so that byte-identical cache()
6775
+ * calls in different scopes — e.g. two factory helpers that each `return
6776
+ * cache(fn, { ttl: 60 })` — get distinct, insertion-stable IDs: their
6777
+ * enclosing declarations differ by name/body even when the call spans are
6778
+ * identical. Without it, a source-order occurrence counter alone would shift
6779
+ * when an identical span is inserted above, reassigning IDs across deploys.
6780
+ * Calls that are byte-identical INCLUDING their enclosing top-level statement
6781
+ * are disambiguated by a source-order occurrence index. That residue is safe:
6782
+ * identical top-level declarations are a redeclaration error, so fully
6783
+ * identical (statement, call) pairs can only be expression statements
6784
+ * referencing the same bindings — the same function, where an ID swap is
6785
+ * harmless.
6786
+ *
6787
+ * See TIM-997, TIM-1053, design/06-caching.md §"Cache Key Security".
6653
6788
  */
6654
6789
  var CACHE_MODULE_SOURCES = new Set(["@timber-js/app/cache", "timber/cache"]);
6655
6790
  /**
@@ -6690,16 +6825,23 @@ function findCacheCallSites(node, cacheBindings, results) {
6690
6825
  }
6691
6826
  }
6692
6827
  /**
6693
- * Generate a stable callsite ID from a module path and call index.
6828
+ * Generate a stable callsite ID from a module path, the source text of the
6829
+ * enclosing top-level statement, the source text of the cache(...) call
6830
+ * expression, and an occurrence index among fully byte-identical
6831
+ * (statement, call) pairs in the same file.
6694
6832
  *
6695
- * The ID is an FNV-1a hash of the relative module path + call index,
6696
- * providing a deterministic identifier that is:
6697
- * - Stable across cold starts of the same build
6698
- * - Unique across different modules
6699
- * - Unique across multiple cache() calls within the same module
6833
+ * The ID is deterministic and:
6834
+ * - Stable across cold starts AND across builds/deploys for unchanged calls —
6835
+ * inserting, deleting, or reordering other cache() calls does not affect it
6836
+ * - Unique across different modules (relPath component)
6837
+ * - Unique across distinct cache() calls within a module (statement + span
6838
+ * hashes), with fully identical duplicates disambiguated by source-order
6839
+ * occurrence
6840
+ * - Fresh when the wrapped fn, opts, or enclosing statement change, which
6841
+ * intentionally invalidates the old persisted entries
6700
6842
  */
6701
- function generateStableId(relativeModulePath, callIndex) {
6702
- return fnv1aHash(`${relativeModulePath}:cache:${callIndex}`);
6843
+ function generateStableId(relativeModulePath, statementText, callSpanText, occurrence) {
6844
+ return fnv1aHash(`${relativeModulePath}:cache:${fnv1aHash(statementText)}:${fnv1aHash(callSpanText)}:${occurrence}`);
6703
6845
  }
6704
6846
  function timberCacheTransform(ctx) {
6705
6847
  return {
@@ -6716,16 +6858,28 @@ function timberCacheTransform(ctx) {
6716
6858
  const cacheBindings = findCacheBindings(program);
6717
6859
  if (cacheBindings.size === 0) return;
6718
6860
  const callSites = [];
6719
- findCacheCallSites(program, cacheBindings, callSites);
6861
+ for (const stmt of program.body) {
6862
+ const calls = [];
6863
+ findCacheCallSites(stmt, cacheBindings, calls);
6864
+ for (const call of calls) callSites.push({
6865
+ call,
6866
+ stmt
6867
+ });
6868
+ }
6720
6869
  if (callSites.length === 0) return;
6721
- callSites.sort((a, b) => a.start - b.start);
6870
+ callSites.sort((a, b) => a.call.start - b.call.start);
6722
6871
  const relPath = relative(ctx.root || process.cwd(), id);
6723
6872
  const s = new MagicString(code);
6724
- for (let i = 0; i < callSites.length; i++) {
6725
- const call = callSites[i];
6726
- const stableId = generateStableId(relPath, i);
6727
- const insertPos = call.end - 1;
6728
- s.appendLeft(insertPos, `, "${stableId}"`);
6873
+ const occurrences = /* @__PURE__ */ new Map();
6874
+ for (const { call, stmt } of callSites) {
6875
+ const stmtText = code.slice(stmt.start, stmt.end);
6876
+ const spanText = code.slice(call.start, call.end);
6877
+ const dupKey = stmtText + String.fromCharCode(0) + spanText;
6878
+ const occurrence = occurrences.get(dupKey) ?? 0;
6879
+ occurrences.set(dupKey, occurrence + 1);
6880
+ const stableId = generateStableId(relPath, stmtText, spanText, occurrence);
6881
+ const lastArg = call.arguments[call.arguments.length - 1];
6882
+ s.appendLeft(lastArg.end, `, "${stableId}"`);
6729
6883
  }
6730
6884
  return {
6731
6885
  code: s.toString(),
@@ -6748,18 +6902,24 @@ function timberCacheTransform(ctx) {
6748
6902
  * 2. Externalizes `.wasm` imports so Rolldown skips them
6749
6903
  * 3. Copies the WASM files to the build output so wrangler can find them
6750
6904
  *
6751
- * Only active for Cloudflare adapter production builds.
6905
+ * Only active for Cloudflare adapter production builds — every hook bails
6906
+ * unless `isCloudflareBuild` was set in configResolved. In dev and non-CF
6907
+ * builds, `.wasm` imports are left to Vite's own handling (?init / ?url or
6908
+ * a clear build error), never silently externalized.
6752
6909
  */
6753
6910
  function isCloudflareAdapter(ctx) {
6754
6911
  return ctx.config.adapter?.name === "cloudflare";
6755
6912
  }
6756
6913
  function timberCloudflareWasm(ctx) {
6757
6914
  const wasmFiles = /* @__PURE__ */ new Map();
6915
+ const usedBasenames = /* @__PURE__ */ new Set();
6916
+ let isCloudflareBuild = false;
6758
6917
  return [{
6759
6918
  name: "timber-cloudflare-wasm",
6760
6919
  configResolved(resolved) {
6761
6920
  if (resolved.command !== "build") return;
6762
6921
  if (!isCloudflareAdapter(ctx)) return;
6922
+ isCloudflareBuild = true;
6763
6923
  for (const envName of ["rsc", "ssr"]) {
6764
6924
  const env = resolved.environments[envName];
6765
6925
  if (!env) continue;
@@ -6767,35 +6927,51 @@ function timberCloudflareWasm(ctx) {
6767
6927
  if (!conditions.includes("workerd")) conditions.unshift("workerd");
6768
6928
  }
6769
6929
  },
6930
+ transform(code, id) {
6931
+ if (!isCloudflareBuild) return;
6932
+ if (!id.includes("takumi")) return;
6933
+ const stripped = code.replaceAll(/\/\*\s*@vite-ignore\s*\*\/\s*\n\s*"@takumi-rs\/wasm"/g, "\"@takumi-rs/wasm\"");
6934
+ if (stripped !== code) return stripped;
6935
+ },
6770
6936
  resolveId: {
6771
6937
  order: "pre",
6772
6938
  async handler(source, importer) {
6939
+ if (!isCloudflareBuild) return;
6773
6940
  if (!source.endsWith(".wasm")) return;
6774
6941
  let absolutePath;
6775
6942
  if (source.startsWith(".") && importer) absolutePath = resolve(dirname(importer), source);
6776
6943
  else return;
6777
- const filename = basename(absolutePath);
6778
- wasmFiles.set(filename, absolutePath);
6944
+ let outputName = wasmFiles.get(absolutePath);
6945
+ if (!outputName) {
6946
+ outputName = basename(absolutePath);
6947
+ if (usedBasenames.has(outputName)) {
6948
+ const hash = createHash("sha256").update(await readFile(absolutePath)).digest("hex").slice(0, 8);
6949
+ outputName = outputName.replace(/\.wasm$/, `.${hash}.wasm`);
6950
+ }
6951
+ usedBasenames.add(outputName);
6952
+ wasmFiles.set(absolutePath, outputName);
6953
+ }
6779
6954
  return {
6780
- id: "./" + filename,
6955
+ id: "./" + outputName,
6781
6956
  external: true
6782
6957
  };
6783
6958
  }
6784
6959
  },
6785
6960
  async writeBundle(options, bundle) {
6961
+ if (!isCloudflareBuild) return;
6786
6962
  if (wasmFiles.size === 0) return;
6787
6963
  const outDir = options.dir;
6788
6964
  if (!outDir) return;
6789
6965
  const targetDirs = /* @__PURE__ */ new Set();
6790
6966
  for (const chunk of Object.values(bundle)) {
6791
6967
  if (chunk.type !== "chunk") continue;
6792
- for (const filename of wasmFiles.keys()) if (chunk.code.includes(filename)) targetDirs.add(dirname(resolve(outDir, chunk.fileName)));
6968
+ for (const outputName of wasmFiles.values()) if (chunk.code.includes(outputName)) targetDirs.add(dirname(resolve(outDir, chunk.fileName)));
6793
6969
  }
6794
6970
  if (targetDirs.size === 0) return;
6795
6971
  const copies = [];
6796
6972
  for (const dir of targetDirs) {
6797
6973
  await mkdir(dir, { recursive: true });
6798
- for (const [filename, absolutePath] of wasmFiles) copies.push(copyFile(absolutePath, resolve(dir, filename)));
6974
+ for (const [absolutePath, outputName] of wasmFiles) copies.push(copyFile(absolutePath, resolve(dir, outputName)));
6799
6975
  }
6800
6976
  await Promise.all(copies);
6801
6977
  }
@@ -7623,6 +7799,7 @@ function timber(config) {
7623
7799
  "@timber-js/app/cache",
7624
7800
  "@timber-js/app/search-params",
7625
7801
  "@timber-js/app/segment-params",
7802
+ "@timber-js/app/params",
7626
7803
  "@timber-js/app/codec"
7627
7804
  ];
7628
7805
  if (command === "build") return {