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