@vertz/ui-server 0.2.14 → 0.2.16

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.
@@ -1,4 +1,8 @@
1
1
  // @bun
2
+ import {
3
+ imageContentType,
4
+ isValidImageName
5
+ } from "./shared/chunk-gggnhyqj.js";
2
6
  import {
3
7
  __require
4
8
  } from "./shared/chunk-eb80r8e8.js";
@@ -40,6 +44,53 @@ function createDebugLogger(logDir) {
40
44
  };
41
45
  }
42
46
 
47
+ // src/dev-image-proxy.ts
48
+ function jsonError(error, status) {
49
+ return new Response(JSON.stringify({ error, status }), {
50
+ status,
51
+ headers: { "Content-Type": "application/json" }
52
+ });
53
+ }
54
+ var FETCH_TIMEOUT_MS = 1e4;
55
+ async function handleDevImageProxy(request) {
56
+ const url = new URL(request.url);
57
+ const sourceUrl = url.searchParams.get("url");
58
+ if (!sourceUrl) {
59
+ return jsonError('Missing required "url" parameter', 400);
60
+ }
61
+ let parsed;
62
+ try {
63
+ parsed = new URL(sourceUrl);
64
+ } catch {
65
+ return jsonError('Invalid "url" parameter', 400);
66
+ }
67
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
68
+ return jsonError("Only HTTP(S) URLs are allowed", 400);
69
+ }
70
+ let response;
71
+ try {
72
+ response = await fetch(sourceUrl, {
73
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
74
+ });
75
+ } catch (error) {
76
+ if (error instanceof DOMException && error.name === "TimeoutError") {
77
+ return jsonError("Image fetch timed out", 504);
78
+ }
79
+ return jsonError("Failed to fetch image", 502);
80
+ }
81
+ if (!response.ok) {
82
+ return jsonError(`Upstream returned status ${response.status}`, 502);
83
+ }
84
+ const contentType = response.headers.get("Content-Type") ?? "application/octet-stream";
85
+ return new Response(response.body, {
86
+ status: 200,
87
+ headers: {
88
+ "Content-Type": contentType,
89
+ "Cache-Control": "no-cache"
90
+ }
91
+ });
92
+ }
93
+
43
94
  // src/diagnostics-collector.ts
44
95
  class DiagnosticsCollector {
45
96
  startTime = Date.now();
@@ -59,6 +110,10 @@ class DiagnosticsCollector {
59
110
  manifestFileCount = 0;
60
111
  manifestDurationMs = 0;
61
112
  manifestWarnings = [];
113
+ manifestHmrUpdateCount = 0;
114
+ manifestLastHmrUpdate = null;
115
+ manifestLastHmrFile = null;
116
+ manifestLastHmrChanged = null;
62
117
  errorCurrent = null;
63
118
  errorLastCategory = null;
64
119
  errorLastMessage = null;
@@ -67,6 +122,10 @@ class DiagnosticsCollector {
67
122
  watcherLastChangeTime = null;
68
123
  static MAX_RUNTIME_ERRORS = 10;
69
124
  runtimeErrorsBuffer = [];
125
+ fieldSelectionManifestFileCount = 0;
126
+ fieldSelectionEntries = new Map;
127
+ static MAX_FIELD_MISSES = 50;
128
+ fieldMissesBuffer = [];
70
129
  recordPluginConfig(filter, hmr, fastRefresh) {
71
130
  this.pluginFilter = filter;
72
131
  this.pluginHmr = hmr;
@@ -94,6 +153,12 @@ class DiagnosticsCollector {
94
153
  this.manifestDurationMs = durationMs;
95
154
  this.manifestWarnings = warnings;
96
155
  }
156
+ recordManifestUpdate(file, changed, _durationMs) {
157
+ this.manifestHmrUpdateCount++;
158
+ this.manifestLastHmrUpdate = new Date().toISOString();
159
+ this.manifestLastHmrFile = file;
160
+ this.manifestLastHmrChanged = changed;
161
+ }
97
162
  recordHMRAssets(bundledScriptUrl, bootstrapDiscovered) {
98
163
  this.hmrBundledScriptUrl = bundledScriptUrl;
99
164
  this.hmrBootstrapDiscovered = bootstrapDiscovered;
@@ -126,6 +191,24 @@ class DiagnosticsCollector {
126
191
  clearRuntimeErrors() {
127
192
  this.runtimeErrorsBuffer = [];
128
193
  }
194
+ recordFieldSelectionManifest(fileCount) {
195
+ this.fieldSelectionManifestFileCount = fileCount;
196
+ }
197
+ recordFieldSelection(file, entry) {
198
+ this.fieldSelectionEntries.set(file, entry);
199
+ }
200
+ recordFieldMiss(type, id, field, querySource) {
201
+ this.fieldMissesBuffer.push({
202
+ type,
203
+ id,
204
+ field,
205
+ querySource,
206
+ timestamp: new Date().toISOString()
207
+ });
208
+ if (this.fieldMissesBuffer.length > DiagnosticsCollector.MAX_FIELD_MISSES) {
209
+ this.fieldMissesBuffer = this.fieldMissesBuffer.slice(this.fieldMissesBuffer.length - DiagnosticsCollector.MAX_FIELD_MISSES);
210
+ }
211
+ }
129
212
  getSnapshot() {
130
213
  return {
131
214
  status: "ok",
@@ -152,7 +235,11 @@ class DiagnosticsCollector {
152
235
  manifest: {
153
236
  fileCount: this.manifestFileCount,
154
237
  durationMs: this.manifestDurationMs,
155
- warnings: [...this.manifestWarnings]
238
+ warnings: [...this.manifestWarnings],
239
+ hmrUpdateCount: this.manifestHmrUpdateCount,
240
+ lastHmrUpdate: this.manifestLastHmrUpdate,
241
+ lastHmrFile: this.manifestLastHmrFile,
242
+ lastHmrChanged: this.manifestLastHmrChanged
156
243
  },
157
244
  errors: {
158
245
  current: this.errorCurrent,
@@ -166,7 +253,12 @@ class DiagnosticsCollector {
166
253
  lastChangedFile: this.watcherLastChangedFile,
167
254
  lastChangeTime: this.watcherLastChangeTime
168
255
  },
169
- runtimeErrors: [...this.runtimeErrorsBuffer]
256
+ runtimeErrors: [...this.runtimeErrorsBuffer],
257
+ fieldSelection: {
258
+ manifestFileCount: this.fieldSelectionManifestFileCount,
259
+ entries: Object.fromEntries(this.fieldSelectionEntries),
260
+ misses: [...this.fieldMissesBuffer]
261
+ }
170
262
  };
171
263
  }
172
264
  }
@@ -193,6 +285,109 @@ function runWithScopedFetch(interceptor, fn) {
193
285
  return fetchScope.run(interceptor, fn);
194
286
  }
195
287
 
288
+ // src/font-metrics.ts
289
+ import { readFile } from "fs/promises";
290
+ import { join as join2 } from "path";
291
+ import { fromBuffer } from "@capsizecss/unpack";
292
+ var SYSTEM_FONT_METRICS = {
293
+ Arial: {
294
+ ascent: 1854,
295
+ descent: -434,
296
+ lineGap: 67,
297
+ unitsPerEm: 2048,
298
+ xWidthAvg: 904
299
+ },
300
+ "Times New Roman": {
301
+ ascent: 1825,
302
+ descent: -443,
303
+ lineGap: 87,
304
+ unitsPerEm: 2048,
305
+ xWidthAvg: 819
306
+ },
307
+ "Courier New": {
308
+ ascent: 1705,
309
+ descent: -615,
310
+ lineGap: 0,
311
+ unitsPerEm: 2048,
312
+ xWidthAvg: 1229
313
+ }
314
+ };
315
+ function detectFallbackFont(fallback) {
316
+ for (const f of fallback) {
317
+ const lower = f.toLowerCase();
318
+ if (lower === "sans-serif" || lower === "system-ui")
319
+ return "Arial";
320
+ if (lower === "serif")
321
+ return "Times New Roman";
322
+ if (lower === "monospace")
323
+ return "Courier New";
324
+ }
325
+ return "Arial";
326
+ }
327
+ function formatPercent(value) {
328
+ return `${(value * 100).toFixed(2)}%`;
329
+ }
330
+ function computeFallbackMetrics(fontMetrics, fallbackFont) {
331
+ const systemMetrics = SYSTEM_FONT_METRICS[fallbackFont];
332
+ const fontNormalizedWidth = fontMetrics.xWidthAvg / fontMetrics.unitsPerEm;
333
+ const systemNormalizedWidth = systemMetrics.xWidthAvg / systemMetrics.unitsPerEm;
334
+ const sizeAdjust = fontNormalizedWidth / systemNormalizedWidth;
335
+ const ascentOverride = fontMetrics.ascent / (fontMetrics.unitsPerEm * sizeAdjust);
336
+ const descentOverride = Math.abs(fontMetrics.descent) / (fontMetrics.unitsPerEm * sizeAdjust);
337
+ const lineGapOverride = fontMetrics.lineGap / (fontMetrics.unitsPerEm * sizeAdjust);
338
+ return {
339
+ ascentOverride: formatPercent(ascentOverride),
340
+ descentOverride: formatPercent(descentOverride),
341
+ lineGapOverride: formatPercent(lineGapOverride),
342
+ sizeAdjust: formatPercent(sizeAdjust),
343
+ fallbackFont
344
+ };
345
+ }
346
+ function getPrimarySrcPath(descriptor) {
347
+ const { src } = descriptor;
348
+ if (!src)
349
+ return null;
350
+ if (typeof src === "string")
351
+ return src;
352
+ const first = src[0];
353
+ if (first)
354
+ return first.path;
355
+ return null;
356
+ }
357
+ function resolveFilePath(urlPath, rootDir) {
358
+ const cleaned = urlPath.startsWith("/") ? urlPath.slice(1) : urlPath;
359
+ return join2(rootDir, cleaned);
360
+ }
361
+ async function extractFontMetrics(fonts, rootDir) {
362
+ const result = {};
363
+ for (const [key, descriptor] of Object.entries(fonts)) {
364
+ const adjustFontFallback = descriptor.adjustFontFallback ?? true;
365
+ if (adjustFontFallback === false)
366
+ continue;
367
+ const srcPath = getPrimarySrcPath(descriptor);
368
+ if (!srcPath)
369
+ continue;
370
+ if (!srcPath.toLowerCase().endsWith(".woff2"))
371
+ continue;
372
+ try {
373
+ const filePath = resolveFilePath(srcPath, rootDir);
374
+ const buffer = await readFile(filePath);
375
+ const metrics = await fromBuffer(buffer);
376
+ const fallbackFont = typeof adjustFontFallback === "string" ? adjustFontFallback : detectFallbackFont(descriptor.fallback);
377
+ result[key] = computeFallbackMetrics({
378
+ ascent: metrics.ascent,
379
+ descent: metrics.descent,
380
+ lineGap: metrics.lineGap,
381
+ unitsPerEm: metrics.unitsPerEm,
382
+ xWidthAvg: metrics.xWidthAvg
383
+ }, fallbackFont);
384
+ } catch (error) {
385
+ console.warn(`[vertz] Failed to extract font metrics for "${key}" from "${srcPath}":`, error instanceof Error ? error.message : error);
386
+ }
387
+ }
388
+ return result;
389
+ }
390
+
196
391
  // src/source-map-resolver.ts
197
392
  import { readFileSync } from "fs";
198
393
  import { resolve as resolvePath } from "path";
@@ -338,6 +533,17 @@ function createSourceMapResolver(projectRoot) {
338
533
  };
339
534
  }
340
535
 
536
+ // src/ssr-access-set.ts
537
+ function createAccessSetScript(accessSet, nonce) {
538
+ const json = JSON.stringify(accessSet);
539
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
540
+ const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
541
+ return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
542
+ }
543
+ function escapeAttr(s) {
544
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
545
+ }
546
+
341
547
  // src/ssr-render.ts
342
548
  import { compileTheme } from "@vertz/ui";
343
549
  import { EntityStore, MemoryCache, QueryEnvelopeStore } from "@vertz/ui/internals";
@@ -745,14 +951,14 @@ function installDomShim() {
745
951
  querySelector: () => null,
746
952
  querySelectorAll: () => [],
747
953
  getElementById: () => null,
954
+ addEventListener: () => {},
955
+ removeEventListener: () => {},
748
956
  cookie: ""
749
957
  };
750
958
  globalThis.document = fakeDocument;
751
959
  if (typeof window === "undefined") {
752
960
  globalThis.window = {
753
961
  location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
754
- addEventListener: () => {},
755
- removeEventListener: () => {},
756
962
  history: {
757
963
  pushState: () => {},
758
964
  replaceState: () => {}
@@ -865,14 +1071,14 @@ var RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
865
1071
  function escapeHtml(text) {
866
1072
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
867
1073
  }
868
- function escapeAttr(value) {
1074
+ function escapeAttr2(value) {
869
1075
  const str = typeof value === "string" ? value : String(value);
870
1076
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
871
1077
  }
872
1078
  function serializeAttrs(attrs) {
873
1079
  const parts = [];
874
1080
  for (const [key, value] of Object.entries(attrs)) {
875
- parts.push(` ${key}="${escapeAttr(value)}"`);
1081
+ parts.push(` ${key}="${escapeAttr2(value)}"`);
876
1082
  }
877
1083
  return parts.join("");
878
1084
  }
@@ -937,7 +1143,7 @@ async function streamToString(stream) {
937
1143
  function createTemplateChunk(slotId, resolvedHtml, nonce) {
938
1144
  const tmplId = `v-tmpl-${slotId}`;
939
1145
  const slotRef = `v-slot-${slotId}`;
940
- const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
1146
+ const nonceAttr = nonce != null ? ` nonce="${escapeAttr2(nonce)}"` : "";
941
1147
  return `<template id="${tmplId}">${resolvedHtml}</template>` + `<script${nonceAttr}>` + `(function(){` + `var s=document.getElementById("${slotRef}");` + `var t=document.getElementById("${tmplId}");` + `if(s&&t){s.replaceWith(t.content.cloneNode(true));t.remove()}` + `})()` + "</script>";
942
1148
  }
943
1149
 
@@ -964,7 +1170,7 @@ function renderToStream(tree, options) {
964
1170
  }
965
1171
  const { tag, attrs, children } = node;
966
1172
  const isRawText = RAW_TEXT_ELEMENTS.has(tag);
967
- const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
1173
+ const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr2(v)}"`).join("");
968
1174
  if (VOID_ELEMENTS.has(tag)) {
969
1175
  return `<${tag}${attrStr}>`;
970
1176
  }
@@ -1020,7 +1226,7 @@ function createRequestContext(url) {
1020
1226
  contextScope: null,
1021
1227
  entityStore: new EntityStore,
1022
1228
  envelopeStore: new QueryEnvelopeStore,
1023
- queryCache: new MemoryCache,
1229
+ queryCache: new MemoryCache({ maxSize: Infinity }),
1024
1230
  inflight: new Map,
1025
1231
  queries: [],
1026
1232
  errors: []
@@ -1041,9 +1247,6 @@ function resolveAppFactory(module) {
1041
1247
  return createApp;
1042
1248
  }
1043
1249
  function collectCSS(themeCss, module) {
1044
- const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
1045
- const globalTags = module.styles ? module.styles.map((s) => `<style data-vertz-css>${s}</style>`).join(`
1046
- `) : "";
1047
1250
  const alreadyIncluded = new Set;
1048
1251
  if (themeCss)
1049
1252
  alreadyIncluded.add(themeCss);
@@ -1052,9 +1255,12 @@ function collectCSS(themeCss, module) {
1052
1255
  alreadyIncluded.add(s);
1053
1256
  }
1054
1257
  const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
1055
- const componentStyles = componentCss.map((s) => `<style data-vertz-css>${s}</style>`).join(`
1056
- `);
1057
- return [themeTag, globalTags, componentStyles].filter(Boolean).join(`
1258
+ const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
1259
+ const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
1260
+ `)}</style>` : "";
1261
+ const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
1262
+ `)}</style>` : "";
1263
+ return [themeTag, globalTag, componentTag].filter(Boolean).join(`
1058
1264
  `);
1059
1265
  }
1060
1266
  async function ssrRenderToString(module, url, options) {
@@ -1067,14 +1273,40 @@ async function ssrRenderToString(module, url, options) {
1067
1273
  setGlobalSSRTimeout(ssrTimeout);
1068
1274
  const createApp = resolveAppFactory(module);
1069
1275
  let themeCss = "";
1276
+ let themePreloadTags = "";
1070
1277
  if (module.theme) {
1071
1278
  try {
1072
- themeCss = compileTheme(module.theme).css;
1279
+ const compiled = compileTheme(module.theme, {
1280
+ fallbackMetrics: options?.fallbackMetrics
1281
+ });
1282
+ themeCss = compiled.css;
1283
+ themePreloadTags = compiled.preloadTags;
1073
1284
  } catch (e) {
1074
1285
  console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
1075
1286
  }
1076
1287
  }
1077
1288
  createApp();
1289
+ const store = ssrStorage.getStore();
1290
+ if (store) {
1291
+ if (store.pendingRouteComponents?.size) {
1292
+ const entries = Array.from(store.pendingRouteComponents.entries());
1293
+ const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
1294
+ promise.then((mod) => ({ route, factory: mod.default })),
1295
+ new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
1296
+ ])));
1297
+ store.resolvedComponents = new Map;
1298
+ for (const result of results) {
1299
+ if (result.status === "fulfilled") {
1300
+ const { route, factory } = result.value;
1301
+ store.resolvedComponents.set(route, factory);
1302
+ }
1303
+ }
1304
+ store.pendingRouteComponents = undefined;
1305
+ }
1306
+ if (!store.resolvedComponents) {
1307
+ store.resolvedComponents = new Map;
1308
+ }
1309
+ }
1078
1310
  const queries = getSSRQueries();
1079
1311
  const resolvedQueries = [];
1080
1312
  if (queries.length > 0) {
@@ -1086,7 +1318,6 @@ async function ssrRenderToString(module, url, options) {
1086
1318
  }),
1087
1319
  new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
1088
1320
  ])));
1089
- const store = ssrStorage.getStore();
1090
1321
  if (store)
1091
1322
  store.queries = [];
1092
1323
  }
@@ -1099,7 +1330,13 @@ async function ssrRenderToString(module, url, options) {
1099
1330
  key,
1100
1331
  data: JSON.parse(JSON.stringify(data))
1101
1332
  })) : [];
1102
- return { html, css, ssrData };
1333
+ return {
1334
+ html,
1335
+ css,
1336
+ ssrData,
1337
+ headTags: themePreloadTags,
1338
+ discoveredRoutes: ctx.discoveredRoutes
1339
+ };
1103
1340
  } finally {
1104
1341
  clearGlobalSSRTimeout();
1105
1342
  }
@@ -1203,6 +1440,17 @@ data: ${safeSerialize(entry)}
1203
1440
  });
1204
1441
  }
1205
1442
 
1443
+ // src/ssr-session.ts
1444
+ function createSessionScript(session, nonce) {
1445
+ const json = JSON.stringify(session);
1446
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1447
+ const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
1448
+ return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
1449
+ }
1450
+ function escapeAttr3(s) {
1451
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
1452
+ }
1453
+
1206
1454
  // src/bun-dev-server.ts
1207
1455
  var MAX_TERMINAL_STACK_FRAMES = 5;
1208
1456
  function formatTerminalRuntimeError(errors, parsedStack) {
@@ -1431,7 +1679,8 @@ function generateSSRPageHtml({
1431
1679
  ssrData,
1432
1680
  scriptTag,
1433
1681
  editor = "vscode",
1434
- headTags = ""
1682
+ headTags = "",
1683
+ sessionScript = ""
1435
1684
  }) {
1436
1685
  const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
1437
1686
  return `<!doctype html>
@@ -1447,6 +1696,7 @@ function generateSSRPageHtml({
1447
1696
  </head>
1448
1697
  <body>
1449
1698
  <div id="app">${bodyHtml}</div>
1699
+ ${sessionScript}
1450
1700
  ${ssrDataScript}
1451
1701
  ${scriptTag}
1452
1702
  </body>
@@ -1509,6 +1759,13 @@ function buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc) {
1509
1759
  }
1510
1760
  return `<script type="module" src="${clientSrc}"></script>`;
1511
1761
  }
1762
+ function clearSSRRequireCache() {
1763
+ const keys = Object.keys(__require.cache);
1764
+ for (const key of keys) {
1765
+ delete __require.cache[key];
1766
+ }
1767
+ return keys.length;
1768
+ }
1512
1769
  function createBunDevServer(options) {
1513
1770
  const {
1514
1771
  entry,
@@ -1522,7 +1779,8 @@ function createBunDevServer(options) {
1522
1779
  projectRoot = process.cwd(),
1523
1780
  logRequests = true,
1524
1781
  editor: editorOption,
1525
- headTags = ""
1782
+ headTags = "",
1783
+ sessionResolver
1526
1784
  } = options;
1527
1785
  const editor = detectEditor(editorOption);
1528
1786
  if (apiHandler) {
@@ -1776,7 +2034,7 @@ function createBunDevServer(options) {
1776
2034
  }));
1777
2035
  }
1778
2036
  });
1779
- const { plugin: serverPlugin } = createVertzBunPlugin({
2037
+ const { plugin: serverPlugin, updateManifest: updateServerManifest } = createVertzBunPlugin({
1780
2038
  hmr: false,
1781
2039
  fastRefresh: false,
1782
2040
  logger,
@@ -1793,6 +2051,14 @@ function createBunDevServer(options) {
1793
2051
  console.error("[Server] Failed to load SSR module:", e);
1794
2052
  process.exit(1);
1795
2053
  }
2054
+ let fontFallbackMetrics;
2055
+ if (ssrMod.theme?.fonts) {
2056
+ try {
2057
+ fontFallbackMetrics = await extractFontMetrics(ssrMod.theme.fonts, projectRoot);
2058
+ } catch (e) {
2059
+ console.warn("[Server] Failed to extract font metrics:", e);
2060
+ }
2061
+ }
1796
2062
  mkdirSync(devDir, { recursive: true });
1797
2063
  const frInitPath = resolve(devDir, "fast-refresh-init.ts");
1798
2064
  writeFileSync2(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
@@ -1877,6 +2143,31 @@ if (import.meta.hot) import.meta.hot.accept();
1877
2143
  if (pathname === "/__vertz_diagnostics") {
1878
2144
  return Response.json(diagnostics.getSnapshot());
1879
2145
  }
2146
+ if (pathname === "/_vertz/image") {
2147
+ return handleDevImageProxy(request);
2148
+ }
2149
+ if (pathname.startsWith("/__vertz_img/")) {
2150
+ const imgName = pathname.slice("/__vertz_img/".length);
2151
+ if (!isValidImageName(imgName)) {
2152
+ return new Response("Forbidden", { status: 403 });
2153
+ }
2154
+ const imagesDir = resolve(projectRoot, ".vertz", "images");
2155
+ const imgPath = resolve(imagesDir, imgName);
2156
+ if (!imgPath.startsWith(imagesDir)) {
2157
+ return new Response("Forbidden", { status: 403 });
2158
+ }
2159
+ const file = Bun.file(imgPath);
2160
+ if (await file.exists()) {
2161
+ const ext = imgName.split(".").pop();
2162
+ return new Response(file, {
2163
+ headers: {
2164
+ "Content-Type": imageContentType(ext),
2165
+ "Cache-Control": "public, max-age=31536000, immutable"
2166
+ }
2167
+ });
2168
+ }
2169
+ return new Response("Not Found", { status: 404 });
2170
+ }
1880
2171
  if (openapi && request.method === "GET" && pathname === "/api/openapi.json") {
1881
2172
  return serveOpenAPISpec();
1882
2173
  }
@@ -1937,16 +2228,38 @@ data: {}
1937
2228
  skipSSRPaths,
1938
2229
  originalFetch: globalThis.fetch
1939
2230
  }) : null;
2231
+ let sessionScript = "";
2232
+ if (sessionResolver) {
2233
+ try {
2234
+ const sessionResult = await sessionResolver(request);
2235
+ if (sessionResult) {
2236
+ const scripts = [];
2237
+ scripts.push(createSessionScript(sessionResult.session));
2238
+ if (sessionResult.accessSet != null) {
2239
+ scripts.push(createAccessSetScript(sessionResult.accessSet));
2240
+ }
2241
+ sessionScript = scripts.join(`
2242
+ `);
2243
+ }
2244
+ } catch (resolverErr) {
2245
+ console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
2246
+ }
2247
+ }
1940
2248
  const doRender = async () => {
1941
2249
  logger.log("ssr", "render-start", { url: pathname });
1942
2250
  const ssrStart = performance.now();
1943
- const result = await ssrRenderToString(ssrMod, pathname, { ssrTimeout: 300 });
2251
+ const result = await ssrRenderToString(ssrMod, pathname, {
2252
+ ssrTimeout: 300,
2253
+ fallbackMetrics: fontFallbackMetrics
2254
+ });
1944
2255
  logger.log("ssr", "render-done", {
1945
2256
  url: pathname,
1946
2257
  durationMs: Math.round(performance.now() - ssrStart),
1947
2258
  htmlBytes: result.html.length
1948
2259
  });
1949
2260
  const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
2261
+ const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
2262
+ `);
1950
2263
  const html = generateSSRPageHtml({
1951
2264
  title,
1952
2265
  css: result.css,
@@ -1954,7 +2267,8 @@ data: {}
1954
2267
  ssrData: result.ssrData,
1955
2268
  scriptTag,
1956
2269
  editor,
1957
- headTags
2270
+ headTags: combinedHeadTags,
2271
+ sessionScript
1958
2272
  });
1959
2273
  return new Response(html, {
1960
2274
  status: 200,
@@ -2188,14 +2502,19 @@ data: {}
2188
2502
  } catch {}
2189
2503
  if (stopped)
2190
2504
  return;
2191
- const cacheKeys = Object.keys(__require.cache);
2192
- let cacheCleared = 0;
2193
- for (const key of cacheKeys) {
2194
- if (key.startsWith(srcDir) || key.startsWith(entryPath)) {
2195
- delete __require.cache[key];
2196
- cacheCleared++;
2197
- }
2505
+ if (filename.endsWith(".ts") || filename.endsWith(".tsx")) {
2506
+ const changedFilePath = resolve(srcDir, filename);
2507
+ try {
2508
+ const manifestStartMs = performance.now();
2509
+ const source = await Bun.file(changedFilePath).text();
2510
+ const { changed } = updateServerManifest(changedFilePath, source);
2511
+ const manifestDurationMs = Math.round(performance.now() - manifestStartMs);
2512
+ diagnostics.recordManifestUpdate(lastChangedFile, changed, manifestDurationMs);
2513
+ } catch {}
2198
2514
  }
2515
+ if (stopped)
2516
+ return;
2517
+ const cacheCleared = clearSSRRequireCache();
2199
2518
  logger.log("watcher", "cache-cleared", { entries: cacheCleared });
2200
2519
  const ssrWrapperPath = resolve(devDir, "ssr-reload-entry.ts");
2201
2520
  mkdirSync(devDir, { recursive: true });
@@ -2205,6 +2524,11 @@ data: {}
2205
2524
  try {
2206
2525
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2207
2526
  ssrMod = freshMod;
2527
+ if (freshMod.theme?.fonts) {
2528
+ try {
2529
+ fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
2530
+ } catch {}
2531
+ }
2208
2532
  const durationMs = Math.round(performance.now() - ssrReloadStart);
2209
2533
  diagnostics.recordSSRReload(true, durationMs);
2210
2534
  logger.log("watcher", "ssr-reload", { status: "ok", durationMs });
@@ -2218,18 +2542,18 @@ data: {}
2218
2542
  await new Promise((r) => setTimeout(r, 500));
2219
2543
  if (stopped)
2220
2544
  return;
2221
- const retryKeys = Object.keys(__require.cache);
2222
- for (const key of retryKeys) {
2223
- if (key.startsWith(srcDir) || key.startsWith(entryPath)) {
2224
- delete __require.cache[key];
2225
- }
2226
- }
2545
+ clearSSRRequireCache();
2227
2546
  mkdirSync(devDir, { recursive: true });
2228
2547
  writeFileSync2(ssrWrapperPath, `export * from '${entryPath}';
2229
2548
  `);
2230
2549
  try {
2231
2550
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2232
2551
  ssrMod = freshMod;
2552
+ if (freshMod.theme?.fonts) {
2553
+ try {
2554
+ fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
2555
+ } catch {}
2556
+ }
2233
2557
  const durationMs = Math.round(performance.now() - ssrReloadStart);
2234
2558
  diagnostics.recordSSRReload(true, durationMs);
2235
2559
  logger.log("watcher", "ssr-reload", { status: "ok", durationMs, retry: true });
@@ -2287,5 +2611,6 @@ export {
2287
2611
  createRuntimeErrorDeduplicator,
2288
2612
  createFetchInterceptor,
2289
2613
  createBunDevServer,
2614
+ clearSSRRequireCache,
2290
2615
  buildScriptTag
2291
2616
  };