@vertz/ui-server 0.2.15 → 0.2.17

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,3 +1,30 @@
1
+ import { AccessSet } from "@vertz/ui/auth";
2
+ interface SessionData {
3
+ user: {
4
+ id: string;
5
+ email: string;
6
+ role: string;
7
+ [key: string]: unknown;
8
+ };
9
+ /** Unix timestamp in milliseconds (JWT exp * 1000). */
10
+ expiresAt: number;
11
+ }
12
+ /** Resolved session data for SSR injection. */
13
+ interface SSRSessionInfo {
14
+ session: SessionData;
15
+ /**
16
+ * Access set from JWT acl claim.
17
+ * - Present (object): inline access set (no overflow)
18
+ * - null: access control is configured but the set overflowed the JWT
19
+ * - undefined: access control is not configured
20
+ */
21
+ accessSet?: AccessSet | null;
22
+ }
23
+ /**
24
+ * Callback that extracts session data from a request.
25
+ * Returns null when no valid session exists (expired, missing, or invalid cookie).
26
+ */
27
+ type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
1
28
  interface BunDevServerOptions {
2
29
  /** SSR entry module (e.g., './src/app.tsx') */
3
30
  entry: string;
@@ -31,6 +58,12 @@ interface BunDevServerOptions {
31
58
  editor?: string;
32
59
  /** Extra HTML tags to inject into the <head> (e.g., font preloads, meta tags). */
33
60
  headTags?: string;
61
+ /**
62
+ * Resolves session data from request cookies for SSR injection.
63
+ * When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
64
+ * optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
65
+ */
66
+ sessionResolver?: SessionResolver;
34
67
  }
35
68
  interface ErrorDetail {
36
69
  message: string;
@@ -96,11 +129,13 @@ interface SSRPageHtmlOptions {
96
129
  scriptTag: string;
97
130
  editor?: string;
98
131
  headTags?: string;
132
+ /** Pre-built session + access set script tags for SSR injection. */
133
+ sessionScript?: string;
99
134
  }
100
135
  /**
101
136
  * Generate a full SSR HTML page with the given content, CSS, SSR data, and script tag.
102
137
  */
103
- declare function generateSSRPageHtml({ title, css, bodyHtml, ssrData, scriptTag, editor, headTags }: SSRPageHtmlOptions): string;
138
+ declare function generateSSRPageHtml({ title, css, bodyHtml, ssrData, scriptTag, editor, headTags, sessionScript }: SSRPageHtmlOptions): string;
104
139
  interface FetchInterceptorOptions {
105
140
  apiHandler: (req: Request) => Promise<Response>;
106
141
  origin: string;
@@ -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) {
@@ -1062,19 +1268,57 @@ async function ssrRenderToString(module, url, options) {
1062
1268
  const ssrTimeout = options?.ssrTimeout ?? 300;
1063
1269
  ensureDomShim();
1064
1270
  const ctx = createRequestContext(normalizedUrl);
1271
+ if (options?.ssrAuth) {
1272
+ ctx.ssrAuth = options.ssrAuth;
1273
+ }
1065
1274
  return ssrStorage.run(ctx, async () => {
1066
1275
  try {
1067
1276
  setGlobalSSRTimeout(ssrTimeout);
1068
1277
  const createApp = resolveAppFactory(module);
1069
1278
  let themeCss = "";
1279
+ let themePreloadTags = "";
1070
1280
  if (module.theme) {
1071
1281
  try {
1072
- themeCss = compileTheme(module.theme).css;
1282
+ const compiled = compileTheme(module.theme, {
1283
+ fallbackMetrics: options?.fallbackMetrics
1284
+ });
1285
+ themeCss = compiled.css;
1286
+ themePreloadTags = compiled.preloadTags;
1073
1287
  } catch (e) {
1074
1288
  console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
1075
1289
  }
1076
1290
  }
1077
1291
  createApp();
1292
+ if (ctx.ssrRedirect) {
1293
+ return {
1294
+ html: "",
1295
+ css: "",
1296
+ ssrData: [],
1297
+ headTags: "",
1298
+ redirect: ctx.ssrRedirect
1299
+ };
1300
+ }
1301
+ const store = ssrStorage.getStore();
1302
+ if (store) {
1303
+ if (store.pendingRouteComponents?.size) {
1304
+ const entries = Array.from(store.pendingRouteComponents.entries());
1305
+ const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
1306
+ promise.then((mod) => ({ route, factory: mod.default })),
1307
+ new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
1308
+ ])));
1309
+ store.resolvedComponents = new Map;
1310
+ for (const result of results) {
1311
+ if (result.status === "fulfilled") {
1312
+ const { route, factory } = result.value;
1313
+ store.resolvedComponents.set(route, factory);
1314
+ }
1315
+ }
1316
+ store.pendingRouteComponents = undefined;
1317
+ }
1318
+ if (!store.resolvedComponents) {
1319
+ store.resolvedComponents = new Map;
1320
+ }
1321
+ }
1078
1322
  const queries = getSSRQueries();
1079
1323
  const resolvedQueries = [];
1080
1324
  if (queries.length > 0) {
@@ -1086,7 +1330,6 @@ async function ssrRenderToString(module, url, options) {
1086
1330
  }),
1087
1331
  new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
1088
1332
  ])));
1089
- const store = ssrStorage.getStore();
1090
1333
  if (store)
1091
1334
  store.queries = [];
1092
1335
  }
@@ -1099,7 +1342,13 @@ async function ssrRenderToString(module, url, options) {
1099
1342
  key,
1100
1343
  data: JSON.parse(JSON.stringify(data))
1101
1344
  })) : [];
1102
- return { html, css, ssrData };
1345
+ return {
1346
+ html,
1347
+ css,
1348
+ ssrData,
1349
+ headTags: themePreloadTags,
1350
+ discoveredRoutes: ctx.discoveredRoutes
1351
+ };
1103
1352
  } finally {
1104
1353
  clearGlobalSSRTimeout();
1105
1354
  }
@@ -1203,6 +1452,17 @@ data: ${safeSerialize(entry)}
1203
1452
  });
1204
1453
  }
1205
1454
 
1455
+ // src/ssr-session.ts
1456
+ function createSessionScript(session, nonce) {
1457
+ const json = JSON.stringify(session);
1458
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1459
+ const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
1460
+ return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
1461
+ }
1462
+ function escapeAttr3(s) {
1463
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
1464
+ }
1465
+
1206
1466
  // src/bun-dev-server.ts
1207
1467
  var MAX_TERMINAL_STACK_FRAMES = 5;
1208
1468
  function formatTerminalRuntimeError(errors, parsedStack) {
@@ -1431,7 +1691,8 @@ function generateSSRPageHtml({
1431
1691
  ssrData,
1432
1692
  scriptTag,
1433
1693
  editor = "vscode",
1434
- headTags = ""
1694
+ headTags = "",
1695
+ sessionScript = ""
1435
1696
  }) {
1436
1697
  const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
1437
1698
  return `<!doctype html>
@@ -1447,6 +1708,7 @@ function generateSSRPageHtml({
1447
1708
  </head>
1448
1709
  <body>
1449
1710
  <div id="app">${bodyHtml}</div>
1711
+ ${sessionScript}
1450
1712
  ${ssrDataScript}
1451
1713
  ${scriptTag}
1452
1714
  </body>
@@ -1529,7 +1791,8 @@ function createBunDevServer(options) {
1529
1791
  projectRoot = process.cwd(),
1530
1792
  logRequests = true,
1531
1793
  editor: editorOption,
1532
- headTags = ""
1794
+ headTags = "",
1795
+ sessionResolver
1533
1796
  } = options;
1534
1797
  const editor = detectEditor(editorOption);
1535
1798
  if (apiHandler) {
@@ -1783,7 +2046,7 @@ function createBunDevServer(options) {
1783
2046
  }));
1784
2047
  }
1785
2048
  });
1786
- const { plugin: serverPlugin } = createVertzBunPlugin({
2049
+ const { plugin: serverPlugin, updateManifest: updateServerManifest } = createVertzBunPlugin({
1787
2050
  hmr: false,
1788
2051
  fastRefresh: false,
1789
2052
  logger,
@@ -1800,6 +2063,14 @@ function createBunDevServer(options) {
1800
2063
  console.error("[Server] Failed to load SSR module:", e);
1801
2064
  process.exit(1);
1802
2065
  }
2066
+ let fontFallbackMetrics;
2067
+ if (ssrMod.theme?.fonts) {
2068
+ try {
2069
+ fontFallbackMetrics = await extractFontMetrics(ssrMod.theme.fonts, projectRoot);
2070
+ } catch (e) {
2071
+ console.warn("[Server] Failed to extract font metrics:", e);
2072
+ }
2073
+ }
1803
2074
  mkdirSync(devDir, { recursive: true });
1804
2075
  const frInitPath = resolve(devDir, "fast-refresh-init.ts");
1805
2076
  writeFileSync2(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
@@ -1884,6 +2155,31 @@ if (import.meta.hot) import.meta.hot.accept();
1884
2155
  if (pathname === "/__vertz_diagnostics") {
1885
2156
  return Response.json(diagnostics.getSnapshot());
1886
2157
  }
2158
+ if (pathname === "/_vertz/image") {
2159
+ return handleDevImageProxy(request);
2160
+ }
2161
+ if (pathname.startsWith("/__vertz_img/")) {
2162
+ const imgName = pathname.slice("/__vertz_img/".length);
2163
+ if (!isValidImageName(imgName)) {
2164
+ return new Response("Forbidden", { status: 403 });
2165
+ }
2166
+ const imagesDir = resolve(projectRoot, ".vertz", "images");
2167
+ const imgPath = resolve(imagesDir, imgName);
2168
+ if (!imgPath.startsWith(imagesDir)) {
2169
+ return new Response("Forbidden", { status: 403 });
2170
+ }
2171
+ const file = Bun.file(imgPath);
2172
+ if (await file.exists()) {
2173
+ const ext = imgName.split(".").pop();
2174
+ return new Response(file, {
2175
+ headers: {
2176
+ "Content-Type": imageContentType(ext),
2177
+ "Cache-Control": "public, max-age=31536000, immutable"
2178
+ }
2179
+ });
2180
+ }
2181
+ return new Response("Not Found", { status: 404 });
2182
+ }
1887
2183
  if (openapi && request.method === "GET" && pathname === "/api/openapi.json") {
1888
2184
  return serveOpenAPISpec();
1889
2185
  }
@@ -1944,16 +2240,53 @@ data: {}
1944
2240
  skipSSRPaths,
1945
2241
  originalFetch: globalThis.fetch
1946
2242
  }) : null;
2243
+ let sessionScript = "";
2244
+ let ssrAuth;
2245
+ if (sessionResolver) {
2246
+ try {
2247
+ const sessionResult = await sessionResolver(request);
2248
+ if (sessionResult) {
2249
+ ssrAuth = {
2250
+ status: "authenticated",
2251
+ user: sessionResult.session.user,
2252
+ expiresAt: sessionResult.session.expiresAt
2253
+ };
2254
+ const scripts = [];
2255
+ scripts.push(createSessionScript(sessionResult.session));
2256
+ if (sessionResult.accessSet != null) {
2257
+ scripts.push(createAccessSetScript(sessionResult.accessSet));
2258
+ }
2259
+ sessionScript = scripts.join(`
2260
+ `);
2261
+ } else {
2262
+ ssrAuth = { status: "unauthenticated" };
2263
+ }
2264
+ } catch (resolverErr) {
2265
+ console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
2266
+ }
2267
+ }
1947
2268
  const doRender = async () => {
1948
2269
  logger.log("ssr", "render-start", { url: pathname });
1949
2270
  const ssrStart = performance.now();
1950
- const result = await ssrRenderToString(ssrMod, pathname, { ssrTimeout: 300 });
2271
+ const result = await ssrRenderToString(ssrMod, pathname + url.search, {
2272
+ ssrTimeout: 300,
2273
+ fallbackMetrics: fontFallbackMetrics,
2274
+ ssrAuth
2275
+ });
1951
2276
  logger.log("ssr", "render-done", {
1952
2277
  url: pathname,
1953
2278
  durationMs: Math.round(performance.now() - ssrStart),
1954
2279
  htmlBytes: result.html.length
1955
2280
  });
2281
+ if (result.redirect) {
2282
+ return new Response(null, {
2283
+ status: 302,
2284
+ headers: { Location: result.redirect.to }
2285
+ });
2286
+ }
1956
2287
  const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
2288
+ const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
2289
+ `);
1957
2290
  const html = generateSSRPageHtml({
1958
2291
  title,
1959
2292
  css: result.css,
@@ -1961,7 +2294,8 @@ data: {}
1961
2294
  ssrData: result.ssrData,
1962
2295
  scriptTag,
1963
2296
  editor,
1964
- headTags
2297
+ headTags: combinedHeadTags,
2298
+ sessionScript
1965
2299
  });
1966
2300
  return new Response(html, {
1967
2301
  status: 200,
@@ -2193,6 +2527,18 @@ data: {}
2193
2527
  clearErrorForFileChange();
2194
2528
  }
2195
2529
  } catch {}
2530
+ if (stopped)
2531
+ return;
2532
+ if (filename.endsWith(".ts") || filename.endsWith(".tsx")) {
2533
+ const changedFilePath = resolve(srcDir, filename);
2534
+ try {
2535
+ const manifestStartMs = performance.now();
2536
+ const source = await Bun.file(changedFilePath).text();
2537
+ const { changed } = updateServerManifest(changedFilePath, source);
2538
+ const manifestDurationMs = Math.round(performance.now() - manifestStartMs);
2539
+ diagnostics.recordManifestUpdate(lastChangedFile, changed, manifestDurationMs);
2540
+ } catch {}
2541
+ }
2196
2542
  if (stopped)
2197
2543
  return;
2198
2544
  const cacheCleared = clearSSRRequireCache();
@@ -2205,6 +2551,11 @@ data: {}
2205
2551
  try {
2206
2552
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2207
2553
  ssrMod = freshMod;
2554
+ if (freshMod.theme?.fonts) {
2555
+ try {
2556
+ fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
2557
+ } catch {}
2558
+ }
2208
2559
  const durationMs = Math.round(performance.now() - ssrReloadStart);
2209
2560
  diagnostics.recordSSRReload(true, durationMs);
2210
2561
  logger.log("watcher", "ssr-reload", { status: "ok", durationMs });
@@ -2225,6 +2576,11 @@ data: {}
2225
2576
  try {
2226
2577
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2227
2578
  ssrMod = freshMod;
2579
+ if (freshMod.theme?.fonts) {
2580
+ try {
2581
+ fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
2582
+ } catch {}
2583
+ }
2228
2584
  const durationMs = Math.round(performance.now() - ssrReloadStart);
2229
2585
  diagnostics.recordSSRReload(true, durationMs);
2230
2586
  logger.log("watcher", "ssr-reload", { status: "ok", durationMs, retry: true });