@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.
- package/dist/_chunks/{actions-DzFMnUAR.js → actions-OIZ8siQu.js} +28 -43
- package/dist/_chunks/actions-OIZ8siQu.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-DH-M8Poy.js → cli-schema-sync-CBiMEtqa.js} +1 -1
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-CQaxupHv.js} +47 -54
- package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +1 -0
- package/dist/_chunks/{handler-store-DjISKYEB.js → handler-store-BVDgyTh7.js} +26 -2
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +1 -0
- package/dist/_chunks/{href-validation-EZktELp8.js → href-validation-DyZIzfMJ.js} +18 -2
- package/dist/_chunks/{href-validation-EZktELp8.js.map → href-validation-DyZIzfMJ.js.map} +1 -1
- package/dist/_chunks/{navigation-root-DLc4s4ZC.js → navigation-root-bUDZLw74.js} +23 -2
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -0
- package/dist/_chunks/tracing-M_zKubhk.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-Ck4Y45XN.js} +49 -8
- package/dist/_chunks/walkers-Ck4Y45XN.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +1 -15
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +10 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +18 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +39 -12
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/redis-handler.d.ts +72 -8
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/stable-stringify.d.ts +9 -0
- package/dist/cache/stable-stringify.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +3 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/scroll.d.ts +9 -0
- package/dist/client/browser-entry/scroll.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +24 -10
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/index.js +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +99 -24
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +13 -0
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts +7 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/dev-tools/404-page.d.ts +6 -2
- package/dist/dev-tools/404-page.d.ts.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +77 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
- package/dist/dev-tools/error-page.d.ts +4 -30
- package/dist/dev-tools/error-page.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +2 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts +11 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +432 -255
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache.d.ts +43 -9
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/cloudflare-wasm.d.ts +4 -1
- package/dist/plugins/cloudflare-wasm.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +16 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +23 -0
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +3 -2
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/cookie-parsing.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +2 -2
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +227 -179
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +24 -6
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +1 -11
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +6 -4
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +2 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/head-header.d.ts +54 -0
- package/dist/shared/head-header.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts +11 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +17 -0
- package/dist/shared/redirect-type.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +52 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +33 -11
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/adapters/cloudflare.ts +13 -47
- package/src/adapters/compress-module.ts +9 -3
- package/src/adapters/nitro.ts +9 -2
- package/src/cache/redis-handler.ts +77 -13
- package/src/cache/stable-stringify.ts +65 -2
- package/src/cache/timber-cache.ts +25 -13
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/action-dispatch.ts +7 -2
- package/src/client/browser-entry/router-init.ts +2 -1
- package/src/client/browser-entry/scroll.ts +23 -0
- package/src/client/error-boundary.tsx +33 -5
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/link.tsx +4 -1
- package/src/client/navigation-api.ts +5 -1
- package/src/client/navigation-root.tsx +16 -0
- package/src/client/router.ts +107 -29
- package/src/client/rsc-fetch.ts +6 -6
- package/src/dev-tools/404-page.ts +22 -123
- package/src/dev-tools/dev-page-shell.ts +256 -0
- package/src/dev-tools/error-page.ts +27 -203
- package/src/dev-tools/index.ts +7 -1
- package/src/dev-tools/instrumentation.ts +39 -2
- package/src/index.ts +1 -0
- package/src/plugins/cache.ts +79 -21
- package/src/plugins/cloudflare-wasm.ts +48 -10
- package/src/plugins/dev-server.ts +32 -8
- package/src/plugins/server-bundle.ts +25 -9
- package/src/plugins/shims.ts +94 -52
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +63 -6
- package/src/routing/scanner.ts +80 -63
- package/src/server/action-handler.ts +10 -2
- package/src/server/actions.ts +3 -2
- package/src/server/compress.ts +11 -2
- package/src/server/cookie-context.ts +11 -0
- package/src/server/cookie-parsing.ts +16 -4
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +40 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/fallback-error.ts +9 -4
- package/src/server/node-stream-transforms.ts +61 -24
- package/src/server/pipeline.ts +12 -0
- package/src/server/primitives.ts +10 -31
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +1 -12
- package/src/server/rsc-entry/index.ts +7 -2
- package/src/server/rsc-entry/rsc-payload.ts +4 -3
- package/src/server/slot-resolver.ts +27 -14
- package/src/server/ssr-entry.ts +17 -28
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tracing.ts +17 -1
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/head-header.ts +69 -0
- package/src/shared/href-validation.ts +24 -0
- package/src/shared/redirect-type.ts +17 -0
- package/src/shims/headers.ts +119 -2
- package/src/shims/navigation-client.ts +48 -27
- package/LICENSE +0 -8
- package/dist/_chunks/actions-DzFMnUAR.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/handler-store-DjISKYEB.js.map +0 -1
- package/dist/_chunks/navigation-root-DLc4s4ZC.js.map +0 -1
- 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-
|
|
5
|
-
import { a as generateRouteMap, i as collectInterceptionRewrites, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
*
|
|
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
|
|
657
|
-
* prevents the HTML parser from seeing a closing
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
|
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)
|
|
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)
|
|
1438
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
2869
|
-
*
|
|
2870
|
-
*
|
|
2871
|
-
*
|
|
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
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2902
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
6651
|
-
*
|
|
6652
|
-
*
|
|
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
|
|
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
|
|
6696
|
-
*
|
|
6697
|
-
*
|
|
6698
|
-
* - Unique across different modules
|
|
6699
|
-
* - Unique across
|
|
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,
|
|
6702
|
-
return fnv1aHash(`${relativeModulePath}:cache:${
|
|
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
|
-
|
|
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
|
-
|
|
6725
|
-
|
|
6726
|
-
const
|
|
6727
|
-
const
|
|
6728
|
-
|
|
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
|
-
|
|
6778
|
-
|
|
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: "./" +
|
|
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
|
|
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 [
|
|
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 {
|