@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.
@@ -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;
@@ -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
- return join2(rootDir, cleaned);
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.location = {
1109
- ...globalThis.window.location || {},
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 existsSync(faviconPath) ? '<link rel="icon" type="image/svg+xml" href="/favicon.svg">' : "";
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 || !existsSync(openapi.specPath))
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 = watch(specDir, { persistent: false }, (eventType, filename) => {
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 && existsSync(openapi.specPath)) {
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: false,
2282
- fastRefresh: false,
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
- ssrMod = await import(entryPath);
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
- throw e;
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: result.html,
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
- ws.sendText(JSON.stringify({ type: "connected" }));
2590
- if (currentError) {
2591
- ws.sendText(JSON.stringify({
2592
- type: "error",
2593
- category: currentError.category,
2594
- errors: currentError.errors
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
- await discoverHMRAssets();
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 (existsSync(srcDir)) {
2724
- srcWatcherRef = watch(srcDir, { recursive: true }, (_event, filename) => {
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 {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  captureDOMState,
4
4
  restoreDOMState
5
- } from "../shared/chunk-2qsqp9xj.js";
5
+ } from "../shared/chunk-eenfpa59.js";
6
6
  import"../shared/chunk-eb80r8e8.js";
7
7
  export {
8
8
  restoreDOMState,