@vertz/ui-server 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bun-dev-server.d.ts +29 -1
- package/dist/bun-dev-server.js +307 -34
- package/dist/bun-plugin/fast-refresh-dom-state.js +1 -1
- package/dist/bun-plugin/fast-refresh-runtime.js +6 -2
- package/dist/bun-plugin/index.js +138 -47
- package/dist/dom-shim/index.d.ts +2 -0
- package/dist/dom-shim/index.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +12 -5
- package/dist/shared/{chunk-2qsqp9xj.js → chunk-eenfpa59.js} +20 -10
- package/dist/shared/{chunk-qzdhe6bn.js → chunk-g0zqrb60.js} +7 -1
- package/dist/shared/{chunk-1363r1qw.js → chunk-zs75v8qj.js} +29 -3
- package/dist/ssr/index.d.ts +5 -3
- package/dist/ssr/index.js +5 -3
- package/package.json +5 -5
package/dist/bun-dev-server.d.ts
CHANGED
|
@@ -72,6 +72,32 @@ interface BunDevServerOptions {
|
|
|
72
72
|
* optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
|
|
73
73
|
*/
|
|
74
74
|
sessionResolver?: SessionResolver;
|
|
75
|
+
/**
|
|
76
|
+
* Watch workspace-linked package dist directories for changes.
|
|
77
|
+
* When a dist directory changes, automatically restart the server.
|
|
78
|
+
*
|
|
79
|
+
* Accepts an array of package names (e.g., ['@vertz/theme-shadcn', '@vertz/ui'])
|
|
80
|
+
* or `true` to auto-detect all `@vertz/*` packages linked via workspace symlinks.
|
|
81
|
+
*
|
|
82
|
+
* @default false
|
|
83
|
+
*/
|
|
84
|
+
watchDeps?: boolean | string[];
|
|
85
|
+
/**
|
|
86
|
+
* Resolve theme from request for SSR. Returns the value for `data-theme`
|
|
87
|
+
* on the `<html>` tag and patches any `data-theme` attributes in the SSR body.
|
|
88
|
+
* Use this to read a theme cookie and eliminate dark→light flash on reload.
|
|
89
|
+
*/
|
|
90
|
+
themeFromRequest?: (request: Request) => string | null | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Called when the SSR module fails to recover from a broken state and a
|
|
93
|
+
* process restart is needed. Bun's ESM module cache retains failed imports
|
|
94
|
+
* process-wide — the only way to clear it is to restart the process.
|
|
95
|
+
*
|
|
96
|
+
* The dev server calls stop() before invoking this callback.
|
|
97
|
+
* Typically, the callback calls `process.exit(75)` and a supervisor
|
|
98
|
+
* script restarts the process.
|
|
99
|
+
*/
|
|
100
|
+
onRestartNeeded?: () => void;
|
|
75
101
|
}
|
|
76
102
|
interface ErrorDetail {
|
|
77
103
|
message: string;
|
|
@@ -147,11 +173,13 @@ interface SSRPageHtmlOptions {
|
|
|
147
173
|
headTags?: string;
|
|
148
174
|
/** Pre-built session + access set script tags for SSR injection. */
|
|
149
175
|
sessionScript?: string;
|
|
176
|
+
/** Theme value for `data-theme` attribute on `<html>`. */
|
|
177
|
+
htmlDataTheme?: string;
|
|
150
178
|
}
|
|
151
179
|
/**
|
|
152
180
|
* Generate a full SSR HTML page with the given content, CSS, SSR data, and script tag.
|
|
153
181
|
*/
|
|
154
|
-
declare function generateSSRPageHtml({ title, css, bodyHtml, ssrData, scriptTag, editor, headTags, sessionScript }: SSRPageHtmlOptions): string;
|
|
182
|
+
declare function generateSSRPageHtml({ title, css, bodyHtml, ssrData, scriptTag, editor, headTags, sessionScript, htmlDataTheme }: SSRPageHtmlOptions): string;
|
|
155
183
|
interface FetchInterceptorOptions {
|
|
156
184
|
apiHandler: (req: Request) => Promise<Response>;
|
|
157
185
|
origin: string;
|
package/dist/bun-dev-server.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/bun-dev-server.ts
|
|
11
11
|
import { execSync } from "child_process";
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, watch, writeFileSync as writeFileSync2 } from "fs";
|
|
12
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, watch as watch2, writeFileSync as writeFileSync2 } from "fs";
|
|
13
13
|
import { dirname, normalize, resolve } from "path";
|
|
14
14
|
|
|
15
15
|
// src/debug-logger.ts
|
|
@@ -286,6 +286,7 @@ function runWithScopedFetch(interceptor, fn) {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// src/font-metrics.ts
|
|
289
|
+
import { access as fsAccess } from "fs/promises";
|
|
289
290
|
import { readFile } from "fs/promises";
|
|
290
291
|
import { join as join2 } from "path";
|
|
291
292
|
import { fromBuffer } from "@capsizecss/unpack";
|
|
@@ -354,9 +355,15 @@ function getPrimarySrcPath(descriptor) {
|
|
|
354
355
|
return first.path;
|
|
355
356
|
return null;
|
|
356
357
|
}
|
|
357
|
-
function resolveFilePath(urlPath, rootDir) {
|
|
358
|
+
async function resolveFilePath(urlPath, rootDir) {
|
|
358
359
|
const cleaned = urlPath.startsWith("/") ? urlPath.slice(1) : urlPath;
|
|
359
|
-
|
|
360
|
+
const direct = join2(rootDir, cleaned);
|
|
361
|
+
try {
|
|
362
|
+
await fsAccess(direct);
|
|
363
|
+
return direct;
|
|
364
|
+
} catch {
|
|
365
|
+
return join2(rootDir, "public", cleaned);
|
|
366
|
+
}
|
|
360
367
|
}
|
|
361
368
|
async function extractFontMetrics(fonts, rootDir) {
|
|
362
369
|
const result = {};
|
|
@@ -370,7 +377,7 @@ async function extractFontMetrics(fonts, rootDir) {
|
|
|
370
377
|
if (!srcPath.toLowerCase().endsWith(".woff2"))
|
|
371
378
|
continue;
|
|
372
379
|
try {
|
|
373
|
-
const filePath = resolveFilePath(srcPath, rootDir);
|
|
380
|
+
const filePath = await resolveFilePath(srcPath, rootDir);
|
|
374
381
|
const buffer = await readFile(filePath);
|
|
375
382
|
const metrics = await fromBuffer(buffer);
|
|
376
383
|
const fallbackFont = typeof adjustFontFallback === "string" ? adjustFontFallback : detectFallbackFont(descriptor.fallback);
|
|
@@ -388,6 +395,58 @@ async function extractFontMetrics(fonts, rootDir) {
|
|
|
388
395
|
return result;
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
// src/ready-gate.ts
|
|
399
|
+
function createReadyGate(options) {
|
|
400
|
+
let ready = false;
|
|
401
|
+
const pendingClients = new Set;
|
|
402
|
+
let timeoutHandle = null;
|
|
403
|
+
function doOpen(currentError) {
|
|
404
|
+
if (ready)
|
|
405
|
+
return;
|
|
406
|
+
ready = true;
|
|
407
|
+
if (timeoutHandle) {
|
|
408
|
+
clearTimeout(timeoutHandle);
|
|
409
|
+
timeoutHandle = null;
|
|
410
|
+
}
|
|
411
|
+
for (const ws of pendingClients) {
|
|
412
|
+
try {
|
|
413
|
+
ws.sendText(JSON.stringify({ type: "connected" }));
|
|
414
|
+
if (currentError) {
|
|
415
|
+
ws.sendText(JSON.stringify({
|
|
416
|
+
type: "error",
|
|
417
|
+
category: currentError.category,
|
|
418
|
+
errors: currentError.errors
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
pendingClients.clear();
|
|
424
|
+
}
|
|
425
|
+
if (options?.timeoutMs) {
|
|
426
|
+
timeoutHandle = setTimeout(() => {
|
|
427
|
+
if (!ready) {
|
|
428
|
+
options.onTimeoutWarning?.();
|
|
429
|
+
doOpen();
|
|
430
|
+
}
|
|
431
|
+
}, options.timeoutMs);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
get isReady() {
|
|
435
|
+
return ready;
|
|
436
|
+
},
|
|
437
|
+
onOpen(ws) {
|
|
438
|
+
if (ready)
|
|
439
|
+
return false;
|
|
440
|
+
pendingClients.add(ws);
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
onClose(ws) {
|
|
444
|
+
pendingClients.delete(ws);
|
|
445
|
+
},
|
|
446
|
+
open: doOpen
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
391
450
|
// src/source-map-resolver.ts
|
|
392
451
|
import { readFileSync } from "fs";
|
|
393
452
|
import { resolve as resolvePath } from "path";
|
|
@@ -953,6 +1012,16 @@ class SSRElement extends SSRNode {
|
|
|
953
1012
|
delete this.attrs.checked;
|
|
954
1013
|
}
|
|
955
1014
|
}
|
|
1015
|
+
get selected() {
|
|
1016
|
+
return "selected" in this.attrs;
|
|
1017
|
+
}
|
|
1018
|
+
set selected(value) {
|
|
1019
|
+
if (value) {
|
|
1020
|
+
this.attrs.selected = "";
|
|
1021
|
+
} else {
|
|
1022
|
+
delete this.attrs.selected;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
956
1025
|
get rows() {
|
|
957
1026
|
return Number(this.attrs.rows) || 0;
|
|
958
1027
|
}
|
|
@@ -1096,19 +1165,35 @@ function installDomShim() {
|
|
|
1096
1165
|
cookie: ""
|
|
1097
1166
|
};
|
|
1098
1167
|
globalThis.document = fakeDocument;
|
|
1168
|
+
const windowStubs = {
|
|
1169
|
+
scrollTo: () => {},
|
|
1170
|
+
scroll: () => {},
|
|
1171
|
+
addEventListener: () => {},
|
|
1172
|
+
removeEventListener: () => {},
|
|
1173
|
+
dispatchEvent: () => true,
|
|
1174
|
+
getComputedStyle: () => ({}),
|
|
1175
|
+
matchMedia: () => ({ matches: false, addListener: () => {}, removeListener: () => {} })
|
|
1176
|
+
};
|
|
1099
1177
|
if (typeof window === "undefined") {
|
|
1100
1178
|
globalThis.window = {
|
|
1101
1179
|
location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
|
|
1102
1180
|
history: {
|
|
1103
1181
|
pushState: () => {},
|
|
1104
1182
|
replaceState: () => {}
|
|
1105
|
-
}
|
|
1183
|
+
},
|
|
1184
|
+
...windowStubs
|
|
1106
1185
|
};
|
|
1107
1186
|
} else {
|
|
1108
|
-
globalThis.window
|
|
1109
|
-
|
|
1187
|
+
const win = globalThis.window;
|
|
1188
|
+
win.location = {
|
|
1189
|
+
...win.location || {},
|
|
1110
1190
|
pathname: ssrStorage.getStore()?.url || "/"
|
|
1111
1191
|
};
|
|
1192
|
+
for (const [key, val] of Object.entries(windowStubs)) {
|
|
1193
|
+
if (typeof win[key] !== "function") {
|
|
1194
|
+
win[key] = val;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1112
1197
|
}
|
|
1113
1198
|
globalThis.Node = SSRNode;
|
|
1114
1199
|
globalThis.HTMLElement = SSRElement;
|
|
@@ -1233,6 +1318,9 @@ function serializeToHtml(node) {
|
|
|
1233
1318
|
return node.html;
|
|
1234
1319
|
}
|
|
1235
1320
|
const { tag, attrs, children } = node;
|
|
1321
|
+
if (tag === "fragment") {
|
|
1322
|
+
return children.map((child) => serializeToHtml(child)).join("");
|
|
1323
|
+
}
|
|
1236
1324
|
const attrStr = serializeAttrs(attrs);
|
|
1237
1325
|
if (VOID_ELEMENTS.has(tag)) {
|
|
1238
1326
|
return `<${tag}${attrStr}>`;
|
|
@@ -1309,6 +1397,9 @@ function renderToStream(tree, options) {
|
|
|
1309
1397
|
return serializeToHtml(placeholder);
|
|
1310
1398
|
}
|
|
1311
1399
|
const { tag, attrs, children } = node;
|
|
1400
|
+
if (tag === "fragment") {
|
|
1401
|
+
return children.map((child) => walkAndSerialize(child)).join("");
|
|
1402
|
+
}
|
|
1312
1403
|
const isRawText = RAW_TEXT_ELEMENTS.has(tag);
|
|
1313
1404
|
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr2(v)}"`).join("");
|
|
1314
1405
|
if (VOID_ELEMENTS.has(tag)) {
|
|
@@ -1604,15 +1695,95 @@ function escapeAttr3(s) {
|
|
|
1604
1695
|
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1605
1696
|
}
|
|
1606
1697
|
|
|
1698
|
+
// src/upstream-watcher.ts
|
|
1699
|
+
import { existsSync, lstatSync, readdirSync, realpathSync, watch } from "fs";
|
|
1700
|
+
import { join as join3 } from "path";
|
|
1701
|
+
function resolveWorkspacePackages(projectRoot, filter) {
|
|
1702
|
+
const results = [];
|
|
1703
|
+
const packageNames = filter === true ? discoverVertzPackages(projectRoot) : parsePackageNames(filter);
|
|
1704
|
+
for (const { scope, name } of packageNames) {
|
|
1705
|
+
const nmPath = join3(projectRoot, "node_modules", scope, name);
|
|
1706
|
+
if (!existsSync(nmPath))
|
|
1707
|
+
continue;
|
|
1708
|
+
const stat = lstatSync(nmPath);
|
|
1709
|
+
if (!stat.isSymbolicLink())
|
|
1710
|
+
continue;
|
|
1711
|
+
const realPath = realpathSync(nmPath);
|
|
1712
|
+
const distPath = join3(realPath, "dist");
|
|
1713
|
+
if (!existsSync(distPath))
|
|
1714
|
+
continue;
|
|
1715
|
+
const fullName = scope ? `${scope}/${name}` : name;
|
|
1716
|
+
results.push({ name: fullName, distPath });
|
|
1717
|
+
}
|
|
1718
|
+
return results;
|
|
1719
|
+
}
|
|
1720
|
+
function discoverVertzPackages(projectRoot) {
|
|
1721
|
+
const scopeDir = join3(projectRoot, "node_modules", "@vertz");
|
|
1722
|
+
if (!existsSync(scopeDir))
|
|
1723
|
+
return [];
|
|
1724
|
+
return readdirSync(scopeDir).filter((entry) => !entry.startsWith(".")).map((entry) => ({ scope: "@vertz", name: entry }));
|
|
1725
|
+
}
|
|
1726
|
+
function parsePackageNames(names) {
|
|
1727
|
+
return names.map((pkg) => {
|
|
1728
|
+
const match = pkg.match(/^(@[^/]+)\/(.+)$/);
|
|
1729
|
+
if (match?.[1] && match[2])
|
|
1730
|
+
return { scope: match[1], name: match[2] };
|
|
1731
|
+
return { scope: "", name: pkg };
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
function createUpstreamWatcher(options) {
|
|
1735
|
+
const { projectRoot, watchDeps, onDistChanged, debounceMs = 1000, persistent = false } = options;
|
|
1736
|
+
const packages = resolveWorkspacePackages(projectRoot, watchDeps);
|
|
1737
|
+
const watchers = [];
|
|
1738
|
+
const debounceTimers = new Map;
|
|
1739
|
+
let closed = false;
|
|
1740
|
+
for (const pkg of packages) {
|
|
1741
|
+
try {
|
|
1742
|
+
const watcher = watch(pkg.distPath, { recursive: true, persistent }, () => {
|
|
1743
|
+
if (closed)
|
|
1744
|
+
return;
|
|
1745
|
+
const existing = debounceTimers.get(pkg.name);
|
|
1746
|
+
if (existing)
|
|
1747
|
+
clearTimeout(existing);
|
|
1748
|
+
debounceTimers.set(pkg.name, setTimeout(() => {
|
|
1749
|
+
debounceTimers.delete(pkg.name);
|
|
1750
|
+
if (!closed) {
|
|
1751
|
+
onDistChanged(pkg.name);
|
|
1752
|
+
}
|
|
1753
|
+
}, debounceMs));
|
|
1754
|
+
});
|
|
1755
|
+
watcher.on("error", () => {});
|
|
1756
|
+
watchers.push(watcher);
|
|
1757
|
+
} catch {}
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
packages,
|
|
1761
|
+
close() {
|
|
1762
|
+
closed = true;
|
|
1763
|
+
for (const w of watchers) {
|
|
1764
|
+
try {
|
|
1765
|
+
w.close();
|
|
1766
|
+
} catch {}
|
|
1767
|
+
}
|
|
1768
|
+
watchers.length = 0;
|
|
1769
|
+
for (const timer of debounceTimers.values()) {
|
|
1770
|
+
clearTimeout(timer);
|
|
1771
|
+
}
|
|
1772
|
+
debounceTimers.clear();
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1607
1777
|
// src/bun-dev-server.ts
|
|
1608
1778
|
function detectFaviconTag(projectRoot) {
|
|
1609
1779
|
const faviconPath = resolve(projectRoot, "public", "favicon.svg");
|
|
1610
|
-
return
|
|
1780
|
+
return existsSync2(faviconPath) ? '<link rel="icon" type="image/svg+xml" href="/favicon.svg">' : "";
|
|
1611
1781
|
}
|
|
1612
1782
|
var STALE_GRAPH_PATTERNS = [
|
|
1613
1783
|
/Export named ['"].*['"] not found in module/i,
|
|
1614
1784
|
/No matching export in ['"].*['"] for import/i,
|
|
1615
|
-
/does not provide an export named/i
|
|
1785
|
+
/does not provide an export named/i,
|
|
1786
|
+
/Failed to resolve module specifier/i
|
|
1616
1787
|
];
|
|
1617
1788
|
function isStaleGraphError(message) {
|
|
1618
1789
|
return STALE_GRAPH_PATTERNS.some((pattern) => pattern.test(message));
|
|
@@ -1712,7 +1883,7 @@ function buildErrorChannelScript(editor) {
|
|
|
1712
1883
|
"V._hadClientError=false;",
|
|
1713
1884
|
"V._needsReload=false;",
|
|
1714
1885
|
"V._restarting=false;",
|
|
1715
|
-
`V.isStaleGraph=function(m){return/Export named ['"].*['"] not found in module/i.test(m)||/No matching export in ['"].*['"] for import/i.test(m)||/does not provide an export named/i.test(m)};`,
|
|
1886
|
+
`V.isStaleGraph=function(m){return/Export named ['"].*['"] not found in module/i.test(m)||/No matching export in ['"].*['"] for import/i.test(m)||/does not provide an export named/i.test(m)||/Failed to resolve module specifier/i.test(m)};`,
|
|
1716
1887
|
"V._canAutoRestart=function(){",
|
|
1717
1888
|
"var raw=sessionStorage.getItem('__vertz_auto_restart');",
|
|
1718
1889
|
"var ts;try{ts=raw?JSON.parse(raw):[]}catch(e){ts=[]}",
|
|
@@ -1884,11 +2055,13 @@ function generateSSRPageHtml({
|
|
|
1884
2055
|
scriptTag,
|
|
1885
2056
|
editor = "vscode",
|
|
1886
2057
|
headTags = "",
|
|
1887
|
-
sessionScript = ""
|
|
2058
|
+
sessionScript = "",
|
|
2059
|
+
htmlDataTheme
|
|
1888
2060
|
}) {
|
|
1889
2061
|
const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
|
|
2062
|
+
const htmlAttrs = htmlDataTheme ? ` data-theme="${htmlDataTheme}"` : "";
|
|
1890
2063
|
return `<!doctype html>
|
|
1891
|
-
<html lang="en">
|
|
2064
|
+
<html lang="en"${htmlAttrs}>
|
|
1892
2065
|
<head>
|
|
1893
2066
|
<meta charset="UTF-8" />
|
|
1894
2067
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -1984,7 +2157,10 @@ function createBunDevServer(options) {
|
|
|
1984
2157
|
logRequests = true,
|
|
1985
2158
|
editor: editorOption,
|
|
1986
2159
|
headTags: headTagsOption = "",
|
|
1987
|
-
sessionResolver
|
|
2160
|
+
sessionResolver,
|
|
2161
|
+
watchDeps,
|
|
2162
|
+
themeFromRequest,
|
|
2163
|
+
onRestartNeeded
|
|
1988
2164
|
} = options;
|
|
1989
2165
|
const faviconTag = detectFaviconTag(projectRoot);
|
|
1990
2166
|
const headTags = [faviconTag, headTagsOption].filter(Boolean).join(`
|
|
@@ -2001,6 +2177,7 @@ function createBunDevServer(options) {
|
|
|
2001
2177
|
let srcWatcherRef = null;
|
|
2002
2178
|
let refreshTimeout = null;
|
|
2003
2179
|
let stopped = false;
|
|
2180
|
+
let ssrFallback = false;
|
|
2004
2181
|
const wsClients = new Set;
|
|
2005
2182
|
let currentError = null;
|
|
2006
2183
|
const sourceMapResolver = createSourceMapResolver(projectRoot);
|
|
@@ -2216,7 +2393,7 @@ function createBunDevServer(options) {
|
|
|
2216
2393
|
}
|
|
2217
2394
|
};
|
|
2218
2395
|
const setupOpenAPIWatcher = () => {
|
|
2219
|
-
if (!openapi || !
|
|
2396
|
+
if (!openapi || !existsSync2(openapi.specPath))
|
|
2220
2397
|
return;
|
|
2221
2398
|
cachedSpec = loadOpenAPISpec();
|
|
2222
2399
|
if (cachedSpec === null)
|
|
@@ -2224,7 +2401,7 @@ function createBunDevServer(options) {
|
|
|
2224
2401
|
try {
|
|
2225
2402
|
const specDir = dirname(openapi.specPath);
|
|
2226
2403
|
const specFile = openapi.specPath.split("/").pop() || "openapi.json";
|
|
2227
|
-
specWatcher =
|
|
2404
|
+
specWatcher = watch2(specDir, { persistent: false }, (eventType, filename) => {
|
|
2228
2405
|
if (filename === specFile && (eventType === "change" || eventType === "rename")) {
|
|
2229
2406
|
if (logRequests) {
|
|
2230
2407
|
console.log("[Server] OpenAPI spec file changed, reloading...");
|
|
@@ -2242,7 +2419,7 @@ function createBunDevServer(options) {
|
|
|
2242
2419
|
headers: { "Content-Type": "application/json" }
|
|
2243
2420
|
});
|
|
2244
2421
|
}
|
|
2245
|
-
if (openapi &&
|
|
2422
|
+
if (openapi && existsSync2(openapi.specPath)) {
|
|
2246
2423
|
cachedSpec = loadOpenAPISpec();
|
|
2247
2424
|
if (cachedSpec) {
|
|
2248
2425
|
return new Response(JSON.stringify(cachedSpec), {
|
|
@@ -2255,6 +2432,42 @@ function createBunDevServer(options) {
|
|
|
2255
2432
|
let isRestarting = false;
|
|
2256
2433
|
let pluginsRegistered = false;
|
|
2257
2434
|
let stableUpdateManifest = null;
|
|
2435
|
+
let upstreamWatcherRef = null;
|
|
2436
|
+
let pendingDistRestart = false;
|
|
2437
|
+
let restartFn = null;
|
|
2438
|
+
if (watchDeps) {
|
|
2439
|
+
upstreamWatcherRef = createUpstreamWatcher({
|
|
2440
|
+
projectRoot,
|
|
2441
|
+
watchDeps,
|
|
2442
|
+
onDistChanged: (pkgName) => {
|
|
2443
|
+
if (!restartFn || stopped)
|
|
2444
|
+
return;
|
|
2445
|
+
if (logRequests) {
|
|
2446
|
+
console.log(`[Server] Upstream package rebuilt: ${pkgName} \u2014 restarting...`);
|
|
2447
|
+
}
|
|
2448
|
+
if (isRestarting) {
|
|
2449
|
+
pendingDistRestart = true;
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
restartFn().then(() => {
|
|
2453
|
+
if (pendingDistRestart && restartFn && !stopped) {
|
|
2454
|
+
pendingDistRestart = false;
|
|
2455
|
+
restartFn().catch((err) => {
|
|
2456
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2457
|
+
console.error(`[Server] Pending upstream restart failed: ${msg}`);
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
}).catch((err) => {
|
|
2461
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2462
|
+
console.error(`[Server] Upstream restart failed: ${msg}`);
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
if (logRequests && upstreamWatcherRef.packages.length > 0) {
|
|
2467
|
+
const names = upstreamWatcherRef.packages.map((p) => p.name).join(", ");
|
|
2468
|
+
console.log(`[Server] Watching upstream packages: ${names}`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2258
2471
|
async function start() {
|
|
2259
2472
|
const { plugin } = await Promise.resolve(globalThis.Bun);
|
|
2260
2473
|
const { createVertzBunPlugin } = await import("./bun-plugin/index.js");
|
|
@@ -2278,8 +2491,8 @@ function createBunDevServer(options) {
|
|
|
2278
2491
|
}
|
|
2279
2492
|
if (!pluginsRegistered) {
|
|
2280
2493
|
const { plugin: serverPlugin, updateManifest } = createVertzBunPlugin({
|
|
2281
|
-
hmr:
|
|
2282
|
-
fastRefresh:
|
|
2494
|
+
hmr: true,
|
|
2495
|
+
fastRefresh: true,
|
|
2283
2496
|
logger,
|
|
2284
2497
|
diagnostics
|
|
2285
2498
|
});
|
|
@@ -2290,16 +2503,34 @@ function createBunDevServer(options) {
|
|
|
2290
2503
|
const updateServerManifest = stableUpdateManifest;
|
|
2291
2504
|
let ssrMod;
|
|
2292
2505
|
try {
|
|
2293
|
-
|
|
2506
|
+
if (isRestarting) {
|
|
2507
|
+
mkdirSync(devDir, { recursive: true });
|
|
2508
|
+
const ssrBootPath = resolve(devDir, "ssr-reload-entry.ts");
|
|
2509
|
+
const ts = Date.now();
|
|
2510
|
+
writeFileSync2(ssrBootPath, `export * from '${entryPath}';
|
|
2511
|
+
`);
|
|
2512
|
+
ssrMod = await import(`${ssrBootPath}?t=${ts}`);
|
|
2513
|
+
} else {
|
|
2514
|
+
ssrMod = await import(entryPath);
|
|
2515
|
+
}
|
|
2516
|
+
ssrFallback = false;
|
|
2294
2517
|
if (logRequests) {
|
|
2295
2518
|
console.log("[Server] SSR module loaded");
|
|
2296
2519
|
}
|
|
2297
2520
|
} catch (e) {
|
|
2298
2521
|
console.error("[Server] Failed to load SSR module:", e);
|
|
2299
2522
|
if (isRestarting) {
|
|
2300
|
-
|
|
2523
|
+
ssrFallback = true;
|
|
2524
|
+
ssrMod = {};
|
|
2525
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
2526
|
+
const errStack = e instanceof Error ? e.stack : undefined;
|
|
2527
|
+
const { message: _, ...loc } = errStack ? parseSourceFromStack(errStack) : { message: "" };
|
|
2528
|
+
queueMicrotask(() => {
|
|
2529
|
+
broadcastError("ssr", [{ message: errMsg, ...loc, stack: errStack }]);
|
|
2530
|
+
});
|
|
2531
|
+
} else {
|
|
2532
|
+
process.exit(1);
|
|
2301
2533
|
}
|
|
2302
|
-
process.exit(1);
|
|
2303
2534
|
}
|
|
2304
2535
|
let fontFallbackMetrics;
|
|
2305
2536
|
if (ssrMod.theme?.fonts) {
|
|
@@ -2328,6 +2559,12 @@ if (import.meta.hot) import.meta.hot.accept();
|
|
|
2328
2559
|
setupOpenAPIWatcher();
|
|
2329
2560
|
let bundledScriptUrl = null;
|
|
2330
2561
|
let hmrBootstrapScript = null;
|
|
2562
|
+
const readyGate = createReadyGate({
|
|
2563
|
+
timeoutMs: 5000,
|
|
2564
|
+
onTimeoutWarning: () => {
|
|
2565
|
+
console.warn("[Server] HMR asset discovery timed out \u2014 unblocking clients");
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2331
2568
|
const routes = {
|
|
2332
2569
|
"/__vertz_hmr": hmrShellModule
|
|
2333
2570
|
};
|
|
@@ -2471,6 +2708,7 @@ data: {}
|
|
|
2471
2708
|
if (logRequests) {
|
|
2472
2709
|
console.log(`[Server] SSR: ${pathname}`);
|
|
2473
2710
|
}
|
|
2711
|
+
const ssrTheme = themeFromRequest?.(request) ?? undefined;
|
|
2474
2712
|
try {
|
|
2475
2713
|
const interceptor = apiHandler ? createFetchInterceptor({
|
|
2476
2714
|
apiHandler,
|
|
@@ -2534,19 +2772,22 @@ data: {}
|
|
|
2534
2772
|
headers: { Location: result.redirect.to }
|
|
2535
2773
|
});
|
|
2536
2774
|
}
|
|
2775
|
+
const bodyHtml = ssrTheme ? result.html.replace(/data-theme="[^"]*"/, `data-theme="${ssrTheme}"`) : result.html;
|
|
2537
2776
|
const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
|
|
2538
2777
|
const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
|
|
2539
2778
|
`);
|
|
2540
2779
|
const html = generateSSRPageHtml({
|
|
2541
2780
|
title,
|
|
2542
2781
|
css: result.css,
|
|
2543
|
-
bodyHtml
|
|
2782
|
+
bodyHtml,
|
|
2544
2783
|
ssrData: result.ssrData,
|
|
2545
2784
|
scriptTag,
|
|
2546
2785
|
editor,
|
|
2547
2786
|
headTags: combinedHeadTags,
|
|
2548
|
-
sessionScript
|
|
2787
|
+
sessionScript,
|
|
2788
|
+
htmlDataTheme: ssrTheme
|
|
2549
2789
|
});
|
|
2790
|
+
clearError();
|
|
2550
2791
|
return new Response(html, {
|
|
2551
2792
|
status: 200,
|
|
2552
2793
|
headers: {
|
|
@@ -2570,7 +2811,8 @@ data: {}
|
|
|
2570
2811
|
ssrData: [],
|
|
2571
2812
|
scriptTag,
|
|
2572
2813
|
editor,
|
|
2573
|
-
headTags
|
|
2814
|
+
headTags,
|
|
2815
|
+
htmlDataTheme: ssrTheme
|
|
2574
2816
|
});
|
|
2575
2817
|
return new Response(fallbackHtml, {
|
|
2576
2818
|
status: 200,
|
|
@@ -2586,13 +2828,15 @@ data: {}
|
|
|
2586
2828
|
wsClients.add(ws);
|
|
2587
2829
|
diagnostics.recordWebSocketChange(wsClients.size);
|
|
2588
2830
|
logger.log("ws", "client-connected", { total: wsClients.size });
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2831
|
+
if (!readyGate.onOpen(ws)) {
|
|
2832
|
+
ws.sendText(JSON.stringify({ type: "connected" }));
|
|
2833
|
+
if (currentError) {
|
|
2834
|
+
ws.sendText(JSON.stringify({
|
|
2835
|
+
type: "error",
|
|
2836
|
+
category: currentError.category,
|
|
2837
|
+
errors: currentError.errors
|
|
2838
|
+
}));
|
|
2839
|
+
}
|
|
2596
2840
|
}
|
|
2597
2841
|
},
|
|
2598
2842
|
message(ws, msg) {
|
|
@@ -2684,6 +2928,7 @@ data: {}
|
|
|
2684
2928
|
},
|
|
2685
2929
|
close(ws) {
|
|
2686
2930
|
wsClients.delete(ws);
|
|
2931
|
+
readyGate.onClose(ws);
|
|
2687
2932
|
diagnostics.recordWebSocketChange(wsClients.size);
|
|
2688
2933
|
}
|
|
2689
2934
|
},
|
|
@@ -2695,7 +2940,13 @@ data: {}
|
|
|
2695
2940
|
if (logRequests) {
|
|
2696
2941
|
console.log(`[Server] SSR+HMR dev server running at http://${host}:${server.port}`);
|
|
2697
2942
|
}
|
|
2698
|
-
|
|
2943
|
+
try {
|
|
2944
|
+
await discoverHMRAssets();
|
|
2945
|
+
} finally {
|
|
2946
|
+
if (!readyGate.isReady) {
|
|
2947
|
+
readyGate.open(currentError);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2699
2950
|
async function discoverHMRAssets() {
|
|
2700
2951
|
try {
|
|
2701
2952
|
const res = await fetch(`http://${host}:${server?.port}/__vertz_hmr`);
|
|
@@ -2720,8 +2971,8 @@ data: {}
|
|
|
2720
2971
|
}
|
|
2721
2972
|
const srcDir = resolve(projectRoot, "src");
|
|
2722
2973
|
stopped = false;
|
|
2723
|
-
if (
|
|
2724
|
-
srcWatcherRef =
|
|
2974
|
+
if (existsSync2(srcDir)) {
|
|
2975
|
+
srcWatcherRef = watch2(srcDir, { recursive: true }, (_event, filename) => {
|
|
2725
2976
|
if (!filename)
|
|
2726
2977
|
return;
|
|
2727
2978
|
if (refreshTimeout)
|
|
@@ -2793,6 +3044,19 @@ data: {}
|
|
|
2793
3044
|
}
|
|
2794
3045
|
if (stopped)
|
|
2795
3046
|
return;
|
|
3047
|
+
if (ssrFallback) {
|
|
3048
|
+
if (onRestartNeeded) {
|
|
3049
|
+
if (logRequests) {
|
|
3050
|
+
console.log("[Server] SSR in fallback mode \u2014 requesting process restart");
|
|
3051
|
+
}
|
|
3052
|
+
await devServer.stop();
|
|
3053
|
+
onRestartNeeded();
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
if (logRequests) {
|
|
3057
|
+
console.log("[Server] SSR in fallback mode \u2014 attempting re-import (best effort)");
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
2796
3060
|
const cacheCleared = clearSSRRequireCache();
|
|
2797
3061
|
logger.log("watcher", "cache-cleared", { entries: cacheCleared });
|
|
2798
3062
|
const ssrWrapperPath = resolve(devDir, "ssr-reload-entry.ts");
|
|
@@ -2803,6 +3067,7 @@ data: {}
|
|
|
2803
3067
|
try {
|
|
2804
3068
|
const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
|
|
2805
3069
|
ssrMod = freshMod;
|
|
3070
|
+
ssrFallback = false;
|
|
2806
3071
|
if (freshMod.theme?.fonts) {
|
|
2807
3072
|
try {
|
|
2808
3073
|
fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
|
|
@@ -2828,6 +3093,7 @@ data: {}
|
|
|
2828
3093
|
try {
|
|
2829
3094
|
const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
|
|
2830
3095
|
ssrMod = freshMod;
|
|
3096
|
+
ssrFallback = false;
|
|
2831
3097
|
if (freshMod.theme?.fonts) {
|
|
2832
3098
|
try {
|
|
2833
3099
|
fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
|
|
@@ -2848,6 +3114,7 @@ data: {}
|
|
|
2848
3114
|
logger.log("watcher", "ssr-reload", { status: "failed", error: errMsg });
|
|
2849
3115
|
const { message: _m, ...loc2 } = errStack ? parseSourceFromStack(errStack) : { message: "" };
|
|
2850
3116
|
broadcastError("ssr", [{ message: errMsg, ...loc2, stack: errStack }]);
|
|
3117
|
+
ssrFallback = true;
|
|
2851
3118
|
}
|
|
2852
3119
|
}
|
|
2853
3120
|
}, 100);
|
|
@@ -2880,6 +3147,10 @@ data: {}
|
|
|
2880
3147
|
server.stop(true);
|
|
2881
3148
|
server = null;
|
|
2882
3149
|
}
|
|
3150
|
+
if (upstreamWatcherRef) {
|
|
3151
|
+
upstreamWatcherRef.close();
|
|
3152
|
+
upstreamWatcherRef = null;
|
|
3153
|
+
}
|
|
2883
3154
|
},
|
|
2884
3155
|
async restart() {
|
|
2885
3156
|
if (isRestarting) {
|
|
@@ -2910,6 +3181,7 @@ data: {}
|
|
|
2910
3181
|
lastBroadcastedError = "";
|
|
2911
3182
|
lastChangedFile = "";
|
|
2912
3183
|
clearGraceUntil = 0;
|
|
3184
|
+
ssrFallback = false;
|
|
2913
3185
|
terminalDedup.reset();
|
|
2914
3186
|
clearSSRRequireCache();
|
|
2915
3187
|
sourceMapResolver.invalidate();
|
|
@@ -2939,6 +3211,7 @@ data: {}
|
|
|
2939
3211
|
isRestarting = false;
|
|
2940
3212
|
}
|
|
2941
3213
|
};
|
|
3214
|
+
restartFn = () => devServer.restart();
|
|
2942
3215
|
return devServer;
|
|
2943
3216
|
}
|
|
2944
3217
|
export {
|