@vertz/ui-server 0.2.23 → 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.
@@ -82,6 +82,22 @@ interface BunDevServerOptions {
82
82
  * @default false
83
83
  */
84
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;
85
101
  }
86
102
  interface ErrorDetail {
87
103
  message: string;
@@ -157,11 +173,13 @@ interface SSRPageHtmlOptions {
157
173
  headTags?: string;
158
174
  /** Pre-built session + access set script tags for SSR injection. */
159
175
  sessionScript?: string;
176
+ /** Theme value for `data-theme` attribute on `<html>`. */
177
+ htmlDataTheme?: string;
160
178
  }
161
179
  /**
162
180
  * Generate a full SSR HTML page with the given content, CSS, SSR data, and script tag.
163
181
  */
164
- 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;
165
183
  interface FetchInterceptorOptions {
166
184
  apiHandler: (req: Request) => Promise<Response>;
167
185
  origin: string;
@@ -395,6 +395,58 @@ async function extractFontMetrics(fonts, rootDir) {
395
395
  return result;
396
396
  }
397
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
+
398
450
  // src/source-map-resolver.ts
399
451
  import { readFileSync } from "fs";
400
452
  import { resolve as resolvePath } from "path";
@@ -1113,19 +1165,35 @@ function installDomShim() {
1113
1165
  cookie: ""
1114
1166
  };
1115
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
+ };
1116
1177
  if (typeof window === "undefined") {
1117
1178
  globalThis.window = {
1118
1179
  location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
1119
1180
  history: {
1120
1181
  pushState: () => {},
1121
1182
  replaceState: () => {}
1122
- }
1183
+ },
1184
+ ...windowStubs
1123
1185
  };
1124
1186
  } else {
1125
- globalThis.window.location = {
1126
- ...globalThis.window.location || {},
1187
+ const win = globalThis.window;
1188
+ win.location = {
1189
+ ...win.location || {},
1127
1190
  pathname: ssrStorage.getStore()?.url || "/"
1128
1191
  };
1192
+ for (const [key, val] of Object.entries(windowStubs)) {
1193
+ if (typeof win[key] !== "function") {
1194
+ win[key] = val;
1195
+ }
1196
+ }
1129
1197
  }
1130
1198
  globalThis.Node = SSRNode;
1131
1199
  globalThis.HTMLElement = SSRElement;
@@ -1250,6 +1318,9 @@ function serializeToHtml(node) {
1250
1318
  return node.html;
1251
1319
  }
1252
1320
  const { tag, attrs, children } = node;
1321
+ if (tag === "fragment") {
1322
+ return children.map((child) => serializeToHtml(child)).join("");
1323
+ }
1253
1324
  const attrStr = serializeAttrs(attrs);
1254
1325
  if (VOID_ELEMENTS.has(tag)) {
1255
1326
  return `<${tag}${attrStr}>`;
@@ -1326,6 +1397,9 @@ function renderToStream(tree, options) {
1326
1397
  return serializeToHtml(placeholder);
1327
1398
  }
1328
1399
  const { tag, attrs, children } = node;
1400
+ if (tag === "fragment") {
1401
+ return children.map((child) => walkAndSerialize(child)).join("");
1402
+ }
1329
1403
  const isRawText = RAW_TEXT_ELEMENTS.has(tag);
1330
1404
  const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr2(v)}"`).join("");
1331
1405
  if (VOID_ELEMENTS.has(tag)) {
@@ -1981,11 +2055,13 @@ function generateSSRPageHtml({
1981
2055
  scriptTag,
1982
2056
  editor = "vscode",
1983
2057
  headTags = "",
1984
- sessionScript = ""
2058
+ sessionScript = "",
2059
+ htmlDataTheme
1985
2060
  }) {
1986
2061
  const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
2062
+ const htmlAttrs = htmlDataTheme ? ` data-theme="${htmlDataTheme}"` : "";
1987
2063
  return `<!doctype html>
1988
- <html lang="en">
2064
+ <html lang="en"${htmlAttrs}>
1989
2065
  <head>
1990
2066
  <meta charset="UTF-8" />
1991
2067
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -2082,7 +2158,9 @@ function createBunDevServer(options) {
2082
2158
  editor: editorOption,
2083
2159
  headTags: headTagsOption = "",
2084
2160
  sessionResolver,
2085
- watchDeps
2161
+ watchDeps,
2162
+ themeFromRequest,
2163
+ onRestartNeeded
2086
2164
  } = options;
2087
2165
  const faviconTag = detectFaviconTag(projectRoot);
2088
2166
  const headTags = [faviconTag, headTagsOption].filter(Boolean).join(`
@@ -2099,6 +2177,7 @@ function createBunDevServer(options) {
2099
2177
  let srcWatcherRef = null;
2100
2178
  let refreshTimeout = null;
2101
2179
  let stopped = false;
2180
+ let ssrFallback = false;
2102
2181
  const wsClients = new Set;
2103
2182
  let currentError = null;
2104
2183
  const sourceMapResolver = createSourceMapResolver(projectRoot);
@@ -2412,8 +2491,8 @@ function createBunDevServer(options) {
2412
2491
  }
2413
2492
  if (!pluginsRegistered) {
2414
2493
  const { plugin: serverPlugin, updateManifest } = createVertzBunPlugin({
2415
- hmr: false,
2416
- fastRefresh: false,
2494
+ hmr: true,
2495
+ fastRefresh: true,
2417
2496
  logger,
2418
2497
  diagnostics
2419
2498
  });
@@ -2424,16 +2503,34 @@ function createBunDevServer(options) {
2424
2503
  const updateServerManifest = stableUpdateManifest;
2425
2504
  let ssrMod;
2426
2505
  try {
2427
- 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;
2428
2517
  if (logRequests) {
2429
2518
  console.log("[Server] SSR module loaded");
2430
2519
  }
2431
2520
  } catch (e) {
2432
2521
  console.error("[Server] Failed to load SSR module:", e);
2433
2522
  if (isRestarting) {
2434
- 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);
2435
2533
  }
2436
- process.exit(1);
2437
2534
  }
2438
2535
  let fontFallbackMetrics;
2439
2536
  if (ssrMod.theme?.fonts) {
@@ -2462,6 +2559,12 @@ if (import.meta.hot) import.meta.hot.accept();
2462
2559
  setupOpenAPIWatcher();
2463
2560
  let bundledScriptUrl = null;
2464
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
+ });
2465
2568
  const routes = {
2466
2569
  "/__vertz_hmr": hmrShellModule
2467
2570
  };
@@ -2605,6 +2708,7 @@ data: {}
2605
2708
  if (logRequests) {
2606
2709
  console.log(`[Server] SSR: ${pathname}`);
2607
2710
  }
2711
+ const ssrTheme = themeFromRequest?.(request) ?? undefined;
2608
2712
  try {
2609
2713
  const interceptor = apiHandler ? createFetchInterceptor({
2610
2714
  apiHandler,
@@ -2668,19 +2772,22 @@ data: {}
2668
2772
  headers: { Location: result.redirect.to }
2669
2773
  });
2670
2774
  }
2775
+ const bodyHtml = ssrTheme ? result.html.replace(/data-theme="[^"]*"/, `data-theme="${ssrTheme}"`) : result.html;
2671
2776
  const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
2672
2777
  const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
2673
2778
  `);
2674
2779
  const html = generateSSRPageHtml({
2675
2780
  title,
2676
2781
  css: result.css,
2677
- bodyHtml: result.html,
2782
+ bodyHtml,
2678
2783
  ssrData: result.ssrData,
2679
2784
  scriptTag,
2680
2785
  editor,
2681
2786
  headTags: combinedHeadTags,
2682
- sessionScript
2787
+ sessionScript,
2788
+ htmlDataTheme: ssrTheme
2683
2789
  });
2790
+ clearError();
2684
2791
  return new Response(html, {
2685
2792
  status: 200,
2686
2793
  headers: {
@@ -2704,7 +2811,8 @@ data: {}
2704
2811
  ssrData: [],
2705
2812
  scriptTag,
2706
2813
  editor,
2707
- headTags
2814
+ headTags,
2815
+ htmlDataTheme: ssrTheme
2708
2816
  });
2709
2817
  return new Response(fallbackHtml, {
2710
2818
  status: 200,
@@ -2720,13 +2828,15 @@ data: {}
2720
2828
  wsClients.add(ws);
2721
2829
  diagnostics.recordWebSocketChange(wsClients.size);
2722
2830
  logger.log("ws", "client-connected", { total: wsClients.size });
2723
- ws.sendText(JSON.stringify({ type: "connected" }));
2724
- if (currentError) {
2725
- ws.sendText(JSON.stringify({
2726
- type: "error",
2727
- category: currentError.category,
2728
- errors: currentError.errors
2729
- }));
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
+ }
2730
2840
  }
2731
2841
  },
2732
2842
  message(ws, msg) {
@@ -2818,6 +2928,7 @@ data: {}
2818
2928
  },
2819
2929
  close(ws) {
2820
2930
  wsClients.delete(ws);
2931
+ readyGate.onClose(ws);
2821
2932
  diagnostics.recordWebSocketChange(wsClients.size);
2822
2933
  }
2823
2934
  },
@@ -2829,7 +2940,13 @@ data: {}
2829
2940
  if (logRequests) {
2830
2941
  console.log(`[Server] SSR+HMR dev server running at http://${host}:${server.port}`);
2831
2942
  }
2832
- await discoverHMRAssets();
2943
+ try {
2944
+ await discoverHMRAssets();
2945
+ } finally {
2946
+ if (!readyGate.isReady) {
2947
+ readyGate.open(currentError);
2948
+ }
2949
+ }
2833
2950
  async function discoverHMRAssets() {
2834
2951
  try {
2835
2952
  const res = await fetch(`http://${host}:${server?.port}/__vertz_hmr`);
@@ -2927,6 +3044,19 @@ data: {}
2927
3044
  }
2928
3045
  if (stopped)
2929
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
+ }
2930
3060
  const cacheCleared = clearSSRRequireCache();
2931
3061
  logger.log("watcher", "cache-cleared", { entries: cacheCleared });
2932
3062
  const ssrWrapperPath = resolve(devDir, "ssr-reload-entry.ts");
@@ -2937,6 +3067,7 @@ data: {}
2937
3067
  try {
2938
3068
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2939
3069
  ssrMod = freshMod;
3070
+ ssrFallback = false;
2940
3071
  if (freshMod.theme?.fonts) {
2941
3072
  try {
2942
3073
  fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
@@ -2962,6 +3093,7 @@ data: {}
2962
3093
  try {
2963
3094
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2964
3095
  ssrMod = freshMod;
3096
+ ssrFallback = false;
2965
3097
  if (freshMod.theme?.fonts) {
2966
3098
  try {
2967
3099
  fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
@@ -2982,6 +3114,7 @@ data: {}
2982
3114
  logger.log("watcher", "ssr-reload", { status: "failed", error: errMsg });
2983
3115
  const { message: _m, ...loc2 } = errStack ? parseSourceFromStack(errStack) : { message: "" };
2984
3116
  broadcastError("ssr", [{ message: errMsg, ...loc2, stack: errStack }]);
3117
+ ssrFallback = true;
2985
3118
  }
2986
3119
  }
2987
3120
  }, 100);
@@ -3048,6 +3181,7 @@ data: {}
3048
3181
  lastBroadcastedError = "";
3049
3182
  lastChangedFile = "";
3050
3183
  clearGraceUntil = 0;
3184
+ ssrFallback = false;
3051
3185
  terminalDedup.reset();
3052
3186
  clearSSRRequireCache();
3053
3187
  sourceMapResolver.invalidate();
@@ -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,
@@ -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
 
8
8
  // src/bun-plugin/fast-refresh-runtime.ts
@@ -40,9 +40,10 @@ function __$refreshReg(moduleId, name, factory, hash) {
40
40
  return;
41
41
  existing.factory = factory;
42
42
  existing.hash = hash;
43
+ existing.dirty = true;
43
44
  dirtyModules.add(moduleId);
44
45
  } else {
45
- mod.set(name, { factory, instances: [], hash });
46
+ mod.set(name, { factory, instances: [], hash, dirty: false });
46
47
  }
47
48
  }
48
49
  function __$refreshTrack(moduleId, name, element, args, cleanups, contextScope, signals = []) {
@@ -68,6 +69,9 @@ function __$refreshPerform(moduleId) {
68
69
  return;
69
70
  performingRefresh = true;
70
71
  for (const [name, record] of mod) {
72
+ if (!record.dirty)
73
+ continue;
74
+ record.dirty = false;
71
75
  const { factory, instances } = record;
72
76
  const updatedInstances = [];
73
77
  for (const instance of instances) {
@@ -22,7 +22,7 @@ import {
22
22
  transformRouteSplitting
23
23
  } from "@vertz/ui-compiler";
24
24
  import MagicString3 from "magic-string";
25
- import { Project as Project2, ts as ts4 } from "ts-morph";
25
+ import { Project as Project2, ts as ts5 } from "ts-morph";
26
26
 
27
27
  // src/bun-plugin/context-stable-ids.ts
28
28
  import { ts } from "ts-morph";
@@ -72,19 +72,18 @@ function loadEntitySchema(schemaPath) {
72
72
  }
73
73
 
74
74
  // src/bun-plugin/fast-refresh-codegen.ts
75
- function generateRefreshPreamble(moduleId, contentHash) {
75
+ function generateRefreshPreamble(moduleId) {
76
76
  const escapedId = moduleId.replace(/['\\]/g, "\\$&");
77
- let code = `const __$fr = globalThis[Symbol.for('vertz:fast-refresh')];
78
- ` + `const { __$refreshReg, __$refreshTrack, __$refreshPerform, ` + `pushScope: __$pushScope, popScope: __$popScope, ` + `_tryOnCleanup: __$tryCleanup, runCleanups: __$runCleanups, ` + `getContextScope: __$getCtx, setContextScope: __$setCtx, ` + `startSignalCollection: __$startSigCol, stopSignalCollection: __$stopSigCol } = __$fr;
77
+ const noop = "() => {}";
78
+ const noopArr = "() => []";
79
+ const noopNull = "() => null";
80
+ const noopPassthrough = "(_m, _n, el) => el";
81
+ return `const __$fr = globalThis[Symbol.for('vertz:fast-refresh')] ?? {};
82
+ ` + `const { ` + `__$refreshReg = ${noop}, ` + `__$refreshTrack = ${noopPassthrough}, ` + `__$refreshPerform = ${noop}, ` + `pushScope: __$pushScope = ${noopArr}, ` + `popScope: __$popScope = ${noop}, ` + `_tryOnCleanup: __$tryCleanup = ${noop}, ` + `runCleanups: __$runCleanups = ${noop}, ` + `getContextScope: __$getCtx = ${noopNull}, ` + `setContextScope: __$setCtx = ${noopNull}, ` + `startSignalCollection: __$startSigCol = ${noop}, ` + `stopSignalCollection: __$stopSigCol = ${noopArr} } = __$fr;
79
83
  ` + `const __$moduleId = '${escapedId}';
80
84
  `;
81
- if (contentHash) {
82
- code += `const __$moduleHash = '${contentHash}';
83
- `;
84
- }
85
- return code;
86
85
  }
87
- function generateRefreshWrapper(componentName) {
86
+ function generateRefreshWrapper(componentName, componentHash) {
88
87
  return `
89
88
  const __$orig_${componentName} = ${componentName};
90
89
  ` + `${componentName} = function(...__$args) {
@@ -99,20 +98,22 @@ const __$orig_${componentName} = ${componentName};
99
98
  ` + ` }
100
99
  ` + ` return __$refreshTrack(__$moduleId, '${componentName}', __$ret, __$args, __$scope, __$ctx, __$sigs);
101
100
  ` + `};
102
- ` + `__$refreshReg(__$moduleId, '${componentName}', ${componentName}, __$moduleHash);
101
+ ` + `__$refreshReg(__$moduleId, '${componentName}', ${componentName}, '${componentHash}');
103
102
  `;
104
103
  }
105
104
  function generateRefreshPerform() {
106
105
  return `__$refreshPerform(__$moduleId);
107
106
  `;
108
107
  }
109
- function generateRefreshCode(moduleId, components, contentHash) {
108
+ function generateRefreshCode(moduleId, components, source) {
110
109
  if (components.length === 0)
111
110
  return null;
112
- const preamble = generateRefreshPreamble(moduleId, contentHash);
111
+ const preamble = generateRefreshPreamble(moduleId);
113
112
  let epilogue = "";
114
113
  for (const comp of components) {
115
- epilogue += generateRefreshWrapper(comp.name);
114
+ const body = source.slice(comp.bodyStart, comp.bodyEnd);
115
+ const hash = Bun.hash(body).toString(36);
116
+ epilogue += generateRefreshWrapper(comp.name, hash);
116
117
  }
117
118
  epilogue += generateRefreshPerform();
118
119
  return { preamble, epilogue };
@@ -267,6 +268,8 @@ function resolveCrossFileFields(filePath, propFlows, options) {
267
268
  if (childFields.hasOpaqueAccess) {
268
269
  hasOpaqueAccess = true;
269
270
  }
271
+ } else {
272
+ hasOpaqueAccess = true;
270
273
  }
271
274
  }
272
275
  return { fields, hasOpaqueAccess };
@@ -274,9 +277,33 @@ function resolveCrossFileFields(filePath, propFlows, options) {
274
277
 
275
278
  // src/bun-plugin/field-selection-manifest.ts
276
279
  import { analyzeComponentPropFields } from "@vertz/ui-compiler";
280
+ import { ts as ts2 } from "ts-morph";
281
+ var RE_EXPORT_PATTERN = /export\s+(?:\{|\*)\s*.*?\bfrom\b/;
282
+ function parseReExports(sourceText, filePath) {
283
+ if (!RE_EXPORT_PATTERN.test(sourceText))
284
+ return [];
285
+ const sourceFile = ts2.createSourceFile(filePath, sourceText, ts2.ScriptTarget.Latest, true);
286
+ const reExports = [];
287
+ for (const stmt of sourceFile.statements) {
288
+ if (!ts2.isExportDeclaration(stmt) || !stmt.moduleSpecifier)
289
+ continue;
290
+ const source = stmt.moduleSpecifier.getText(sourceFile).replace(/^['"]|['"]$/g, "");
291
+ if (!stmt.exportClause) {
292
+ reExports.push({ name: "*", originalName: "*", source });
293
+ } else if (ts2.isNamedExports(stmt.exportClause)) {
294
+ for (const el of stmt.exportClause.elements) {
295
+ const exportedName = el.name.getText(sourceFile);
296
+ const originalName = el.propertyName ? el.propertyName.getText(sourceFile) : exportedName;
297
+ reExports.push({ name: exportedName, originalName, source });
298
+ }
299
+ }
300
+ }
301
+ return reExports;
302
+ }
277
303
 
278
304
  class FieldSelectionManifest {
279
305
  fileComponents = new Map;
306
+ fileReExports = new Map;
280
307
  importResolver = () => {
281
308
  return;
282
309
  };
@@ -287,30 +314,68 @@ class FieldSelectionManifest {
287
314
  registerFile(filePath, sourceText) {
288
315
  const components = analyzeComponentPropFields(filePath, sourceText);
289
316
  this.fileComponents.set(filePath, components);
317
+ const reExports = parseReExports(sourceText, filePath);
318
+ this.fileReExports.set(filePath, reExports);
290
319
  this.resolvedCache.clear();
291
320
  }
292
321
  updateFile(filePath, sourceText) {
293
322
  const oldComponents = this.fileComponents.get(filePath);
294
323
  const newComponents = analyzeComponentPropFields(filePath, sourceText);
295
- const changed = !componentsEqual(oldComponents, newComponents);
296
- if (changed) {
324
+ const componentsChanged = !componentsEqual(oldComponents, newComponents);
325
+ if (componentsChanged) {
297
326
  this.fileComponents.set(filePath, newComponents);
327
+ }
328
+ const oldReExports = this.fileReExports.get(filePath);
329
+ const newReExports = parseReExports(sourceText, filePath);
330
+ const reExportsChanged = !reExportsEqual(oldReExports, newReExports);
331
+ if (reExportsChanged) {
332
+ this.fileReExports.set(filePath, newReExports);
333
+ }
334
+ const changed = componentsChanged || reExportsChanged;
335
+ if (changed) {
298
336
  this.resolvedCache.clear();
299
337
  }
300
338
  return { changed };
301
339
  }
302
340
  deleteFile(filePath) {
303
341
  this.fileComponents.delete(filePath);
342
+ this.fileReExports.delete(filePath);
304
343
  this.resolvedCache.clear();
305
344
  }
306
345
  getComponentPropFields(filePath, componentName, propName) {
307
346
  const components = this.fileComponents.get(filePath);
308
- if (!components)
347
+ if (components) {
348
+ const component = components.find((c) => c.componentName === componentName);
349
+ if (component)
350
+ return component.props[propName];
351
+ }
352
+ return this.followReExports(filePath, componentName, propName, new Set);
353
+ }
354
+ followReExports(filePath, componentName, propName, visited) {
355
+ if (visited.has(filePath))
309
356
  return;
310
- const component = components.find((c) => c.componentName === componentName);
311
- if (!component)
357
+ visited.add(filePath);
358
+ const reExports = this.fileReExports.get(filePath);
359
+ if (!reExports)
312
360
  return;
313
- return component.props[propName];
361
+ for (const re of reExports) {
362
+ if (re.name !== componentName && re.name !== "*")
363
+ continue;
364
+ const targetPath = this.importResolver(re.source, filePath);
365
+ if (!targetPath)
366
+ continue;
367
+ const targetName = re.name === "*" ? componentName : re.originalName;
368
+ const targetComponents = this.fileComponents.get(targetPath);
369
+ if (targetComponents) {
370
+ const component = targetComponents.find((c) => c.componentName === targetName);
371
+ if (component)
372
+ return component.props[propName];
373
+ }
374
+ const result = this.followReExports(targetPath, targetName, propName, visited);
375
+ if (result)
376
+ return result;
377
+ }
378
+ return;
314
379
  }
315
380
  getResolvedPropFields(filePath, componentName, propName) {
316
381
  const cacheKey = `${filePath}::${componentName}::${propName}`;
@@ -383,10 +448,37 @@ function componentsEqual(a, b) {
383
448
  if (aFieldsSorted[k] !== bFieldsSorted[k])
384
449
  return false;
385
450
  }
451
+ if (aProp.forwarded.length !== bProp.forwarded.length)
452
+ return false;
453
+ for (let f = 0;f < aProp.forwarded.length; f++) {
454
+ const af = aProp.forwarded[f];
455
+ const bf = bProp.forwarded[f];
456
+ if (af.componentName !== bf.componentName)
457
+ return false;
458
+ if (af.importSource !== bf.importSource)
459
+ return false;
460
+ if (af.propName !== bf.propName)
461
+ return false;
462
+ }
386
463
  }
387
464
  }
388
465
  return true;
389
466
  }
467
+ function reExportsEqual(a, b) {
468
+ if (!a)
469
+ return b.length === 0;
470
+ if (a.length !== b.length)
471
+ return false;
472
+ for (let i = 0;i < a.length; i++) {
473
+ if (a[i].name !== b[i].name)
474
+ return false;
475
+ if (a[i].originalName !== b[i].originalName)
476
+ return false;
477
+ if (a[i].source !== b[i].source)
478
+ return false;
479
+ }
480
+ return true;
481
+ }
390
482
 
391
483
  // src/bun-plugin/file-path-hash.ts
392
484
  function filePathHash(filePath) {
@@ -461,7 +553,7 @@ async function processImage(opts) {
461
553
 
462
554
  // src/bun-plugin/image-transform.ts
463
555
  import MagicString2 from "magic-string";
464
- import { Project, ts as ts2 } from "ts-morph";
556
+ import { Project, ts as ts3 } from "ts-morph";
465
557
  function escapeAttr(value) {
466
558
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
467
559
  }
@@ -535,13 +627,13 @@ function findImageImportName(source) {
535
627
  function findImageJsxElements(sourceFile, localName) {
536
628
  const results = [];
537
629
  function visit(node) {
538
- if (ts2.isJsxSelfClosingElement(node)) {
630
+ if (ts3.isJsxSelfClosingElement(node)) {
539
631
  const tagName = node.tagName.getText(sourceFile.compilerNode);
540
632
  if (tagName === localName) {
541
633
  results.push(node);
542
634
  }
543
635
  }
544
- ts2.forEachChild(node, visit);
636
+ ts3.forEachChild(node, visit);
545
637
  }
546
638
  visit(sourceFile.compilerNode);
547
639
  return results;
@@ -549,7 +641,7 @@ function findImageJsxElements(sourceFile, localName) {
549
641
  function extractStaticProps(element, sourceFile) {
550
642
  const attrs = element.attributes;
551
643
  for (const attr of attrs.properties) {
552
- if (ts2.isJsxSpreadAttribute(attr))
644
+ if (ts3.isJsxSpreadAttribute(attr))
553
645
  return null;
554
646
  }
555
647
  let src = null;
@@ -582,7 +674,7 @@ function extractStaticProps(element, sourceFile) {
582
674
  "fit"
583
675
  ]);
584
676
  for (const attr of attrs.properties) {
585
- if (!ts2.isJsxAttribute(attr))
677
+ if (!ts3.isJsxAttribute(attr))
586
678
  continue;
587
679
  const name = attr.name.getText(sourceFile.compilerNode);
588
680
  const value = attr.initializer;
@@ -686,15 +778,15 @@ function extractStaticProps(element, sourceFile) {
686
778
  function extractStaticString(value, _sourceFile) {
687
779
  if (!value)
688
780
  return null;
689
- if (ts2.isStringLiteral(value)) {
781
+ if (ts3.isStringLiteral(value)) {
690
782
  return value.text;
691
783
  }
692
- if (ts2.isJsxExpression(value) && value.expression) {
784
+ if (ts3.isJsxExpression(value) && value.expression) {
693
785
  const expr = value.expression;
694
- if (ts2.isStringLiteral(expr)) {
786
+ if (ts3.isStringLiteral(expr)) {
695
787
  return expr.text;
696
788
  }
697
- if (ts2.isNoSubstitutionTemplateLiteral(expr)) {
789
+ if (ts3.isNoSubstitutionTemplateLiteral(expr)) {
698
790
  return expr.text;
699
791
  }
700
792
  }
@@ -703,9 +795,9 @@ function extractStaticString(value, _sourceFile) {
703
795
  function extractStaticNumber(value, _sourceFile) {
704
796
  if (!value)
705
797
  return null;
706
- if (ts2.isJsxExpression(value) && value.expression) {
798
+ if (ts3.isJsxExpression(value) && value.expression) {
707
799
  const expr = value.expression;
708
- if (ts2.isNumericLiteral(expr)) {
800
+ if (ts3.isNumericLiteral(expr)) {
709
801
  return Number(expr.text);
710
802
  }
711
803
  }
@@ -714,18 +806,18 @@ function extractStaticNumber(value, _sourceFile) {
714
806
  function extractStaticBoolean(value, _sourceFile) {
715
807
  if (!value)
716
808
  return null;
717
- if (ts2.isJsxExpression(value) && value.expression) {
809
+ if (ts3.isJsxExpression(value) && value.expression) {
718
810
  const expr = value.expression;
719
- if (expr.kind === ts2.SyntaxKind.TrueKeyword)
811
+ if (expr.kind === ts3.SyntaxKind.TrueKeyword)
720
812
  return true;
721
- if (expr.kind === ts2.SyntaxKind.FalseKeyword)
813
+ if (expr.kind === ts3.SyntaxKind.FalseKeyword)
722
814
  return false;
723
815
  }
724
816
  return null;
725
817
  }
726
818
 
727
819
  // src/bun-plugin/island-id-inject.ts
728
- import { ts as ts3 } from "ts-morph";
820
+ import { ts as ts4 } from "ts-morph";
729
821
  function injectIslandIds(source, sourceFile, relFilePath) {
730
822
  const originalSource = source.original;
731
823
  if (!originalSource.includes("<Island") && !originalSource.includes("Island")) {
@@ -759,20 +851,20 @@ function findIslandImportName(source) {
759
851
  function findIslandJsxElements(sourceFile, localName) {
760
852
  const results = [];
761
853
  function visit(node) {
762
- if (ts3.isJsxSelfClosingElement(node)) {
854
+ if (ts4.isJsxSelfClosingElement(node)) {
763
855
  const tagName = node.tagName.getText(sourceFile.compilerNode);
764
856
  if (tagName === localName) {
765
857
  results.push(node);
766
858
  }
767
859
  }
768
- ts3.forEachChild(node, visit);
860
+ ts4.forEachChild(node, visit);
769
861
  }
770
862
  visit(sourceFile.compilerNode);
771
863
  return results;
772
864
  }
773
865
  function hasIdProp(element, sourceFile) {
774
866
  for (const attr of element.attributes.properties) {
775
- if (ts3.isJsxAttribute(attr)) {
867
+ if (ts4.isJsxAttribute(attr)) {
776
868
  const name = attr.name.getText(sourceFile.compilerNode);
777
869
  if (name === "id")
778
870
  return true;
@@ -782,7 +874,7 @@ function hasIdProp(element, sourceFile) {
782
874
  }
783
875
  function extractComponentName(element, sourceFile) {
784
876
  for (const attr of element.attributes.properties) {
785
- if (!ts3.isJsxAttribute(attr))
877
+ if (!ts4.isJsxAttribute(attr))
786
878
  continue;
787
879
  const name = attr.name.getText(sourceFile.compilerNode);
788
880
  if (name !== "component")
@@ -790,8 +882,8 @@ function extractComponentName(element, sourceFile) {
790
882
  const value = attr.initializer;
791
883
  if (!value)
792
884
  return null;
793
- if (ts3.isJsxExpression(value) && value.expression) {
794
- if (ts3.isIdentifier(value.expression)) {
885
+ if (ts4.isJsxExpression(value) && value.expression) {
886
+ if (ts4.isIdentifier(value.expression)) {
795
887
  return value.expression.text;
796
888
  }
797
889
  }
@@ -896,7 +988,7 @@ function createVertzBunPlugin(options) {
896
988
  fieldSelectionManifest.setImportResolver(fieldSelectionResolveImport);
897
989
  let fieldSelectionFileCount = 0;
898
990
  for (const [filePath] of manifests) {
899
- if (filePath.endsWith(".tsx")) {
991
+ if (filePath.endsWith(".tsx") || filePath.endsWith(".ts")) {
900
992
  try {
901
993
  const sourceText = readFileSync3(filePath, "utf-8");
902
994
  fieldSelectionManifest.registerFile(filePath, sourceText);
@@ -953,7 +1045,7 @@ function createVertzBunPlugin(options) {
953
1045
  const hydrationProject = new Project2({
954
1046
  useInMemoryFileSystem: true,
955
1047
  compilerOptions: {
956
- jsx: ts4.JsxEmit.Preserve,
1048
+ jsx: ts5.JsxEmit.Preserve,
957
1049
  strict: true
958
1050
  }
959
1051
  });
@@ -1066,8 +1158,7 @@ function createVertzBunPlugin(options) {
1066
1158
  let refreshEpilogue = "";
1067
1159
  if (fastRefresh) {
1068
1160
  const components = componentAnalyzer.analyze(hydrationSourceFile);
1069
- const contentHash = Bun.hash(source).toString(36);
1070
- const refreshCode = generateRefreshCode(args.path, components, contentHash);
1161
+ const refreshCode = generateRefreshCode(args.path, components, source);
1071
1162
  if (refreshCode) {
1072
1163
  refreshPreamble = refreshCode.preamble;
1073
1164
  refreshEpilogue = refreshCode.epilogue;
@@ -1094,7 +1185,7 @@ function createVertzBunPlugin(options) {
1094
1185
  }
1095
1186
  if (hmr) {
1096
1187
  contents += `
1097
- import.meta.hot.accept();
1188
+ if (import.meta.hot) import.meta.hot.accept();
1098
1189
  `;
1099
1190
  }
1100
1191
  contents += sourceMapComment;
@@ -1164,7 +1255,7 @@ import.meta.hot.accept();
1164
1255
  if (changed) {
1165
1256
  manifestsRecord = null;
1166
1257
  }
1167
- if (filePath.endsWith(".tsx")) {
1258
+ if (filePath.endsWith(".tsx") || filePath.endsWith(".ts")) {
1168
1259
  fieldSelectionManifest.updateFile(filePath, sourceText);
1169
1260
  }
1170
1261
  if (logger?.isEnabled("manifest")) {
@@ -7,7 +7,7 @@ import {
7
7
  installDomShim,
8
8
  removeDomShim,
9
9
  toVNode
10
- } from "../shared/chunk-bm16zy8d.js";
10
+ } from "../shared/chunk-zs75v8qj.js";
11
11
  export {
12
12
  toVNode,
13
13
  removeDomShim,
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  ssrDiscoverQueries,
20
20
  ssrRenderToString,
21
21
  streamToString
22
- } from "./shared/chunk-5cny4vzm.js";
22
+ } from "./shared/chunk-g0zqrb60.js";
23
23
  import {
24
24
  clearGlobalSSRTimeout,
25
25
  createSSRAdapter,
@@ -31,7 +31,7 @@ import {
31
31
  registerSSRQuery,
32
32
  setGlobalSSRTimeout,
33
33
  ssrStorage
34
- } from "./shared/chunk-bm16zy8d.js";
34
+ } from "./shared/chunk-zs75v8qj.js";
35
35
 
36
36
  // src/asset-pipeline.ts
37
37
  function renderAssetTags(assets) {
@@ -7,17 +7,28 @@ function captureDOMState(element) {
7
7
  scrollPositions: captureScrollPositions(element)
8
8
  };
9
9
  }
10
+ function formFieldKey(el, index) {
11
+ const name = el.getAttribute("name");
12
+ if (name)
13
+ return `name:${name}`;
14
+ const id = el.getAttribute("id");
15
+ if (id)
16
+ return `id:${id}`;
17
+ const placeholder = el.getAttribute("placeholder");
18
+ if (placeholder)
19
+ return `placeholder:${placeholder}`;
20
+ return `pos:${el.tagName.toLowerCase()}:${index}`;
21
+ }
10
22
  function captureFormFields(element) {
11
23
  const fields = new Map;
12
24
  const inputs = element.querySelectorAll("input, textarea, select");
13
- for (const el of inputs) {
14
- const name = el.getAttribute("name");
15
- if (!name)
16
- continue;
25
+ for (let i = 0;i < inputs.length; i++) {
26
+ const el = inputs[i];
17
27
  const type = el.type ?? "";
18
28
  if (type === "file")
19
29
  continue;
20
- fields.set(name, {
30
+ const key = formFieldKey(el, i);
31
+ fields.set(key, {
21
32
  value: el.value ?? "",
22
33
  checked: el.checked ?? false,
23
34
  selectedIndex: el.selectedIndex ?? -1,
@@ -87,11 +98,10 @@ function restoreFormFields(element, fields) {
87
98
  if (fields.size === 0)
88
99
  return;
89
100
  const inputs = element.querySelectorAll("input, textarea, select");
90
- for (const el of inputs) {
91
- const name = el.getAttribute("name");
92
- if (!name)
93
- continue;
94
- const saved = fields.get(name);
101
+ for (let i = 0;i < inputs.length; i++) {
102
+ const el = inputs[i];
103
+ const key = formFieldKey(el, i);
104
+ const saved = fields.get(key);
95
105
  if (!saved)
96
106
  continue;
97
107
  if (saved.type === "file")
@@ -6,7 +6,7 @@ import {
6
6
  setGlobalSSRTimeout,
7
7
  ssrStorage,
8
8
  toVNode
9
- } from "./chunk-bm16zy8d.js";
9
+ } from "./chunk-zs75v8qj.js";
10
10
 
11
11
  // src/html-serializer.ts
12
12
  var VOID_ELEMENTS = new Set([
@@ -51,6 +51,9 @@ function serializeToHtml(node) {
51
51
  return node.html;
52
52
  }
53
53
  const { tag, attrs, children } = node;
54
+ if (tag === "fragment") {
55
+ return children.map((child) => serializeToHtml(child)).join("");
56
+ }
54
57
  const attrStr = serializeAttrs(attrs);
55
58
  if (VOID_ELEMENTS.has(tag)) {
56
59
  return `<${tag}${attrStr}>`;
@@ -141,6 +144,9 @@ function renderToStream(tree, options) {
141
144
  return serializeToHtml(placeholder);
142
145
  }
143
146
  const { tag, attrs, children } = node;
147
+ if (tag === "fragment") {
148
+ return children.map((child) => walkAndSerialize(child)).join("");
149
+ }
144
150
  const isRawText = RAW_TEXT_ELEMENTS.has(tag);
145
151
  const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
146
152
  if (VOID_ELEMENTS.has(tag)) {
@@ -568,19 +568,35 @@ function installDomShim() {
568
568
  cookie: ""
569
569
  };
570
570
  globalThis.document = fakeDocument;
571
+ const windowStubs = {
572
+ scrollTo: () => {},
573
+ scroll: () => {},
574
+ addEventListener: () => {},
575
+ removeEventListener: () => {},
576
+ dispatchEvent: () => true,
577
+ getComputedStyle: () => ({}),
578
+ matchMedia: () => ({ matches: false, addListener: () => {}, removeListener: () => {} })
579
+ };
571
580
  if (typeof window === "undefined") {
572
581
  globalThis.window = {
573
582
  location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
574
583
  history: {
575
584
  pushState: () => {},
576
585
  replaceState: () => {}
577
- }
586
+ },
587
+ ...windowStubs
578
588
  };
579
589
  } else {
580
- globalThis.window.location = {
581
- ...globalThis.window.location || {},
590
+ const win = globalThis.window;
591
+ win.location = {
592
+ ...win.location || {},
582
593
  pathname: ssrStorage.getStore()?.url || "/"
583
594
  };
595
+ for (const [key, val] of Object.entries(windowStubs)) {
596
+ if (typeof win[key] !== "function") {
597
+ win[key] = val;
598
+ }
599
+ }
584
600
  }
585
601
  globalThis.Node = SSRNode;
586
602
  globalThis.HTMLElement = SSRElement;
package/dist/ssr/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  injectIntoTemplate,
4
4
  ssrDiscoverQueries,
5
5
  ssrRenderToString
6
- } from "../shared/chunk-5cny4vzm.js";
7
- import"../shared/chunk-bm16zy8d.js";
6
+ } from "../shared/chunk-g0zqrb60.js";
7
+ import"../shared/chunk-zs75v8qj.js";
8
8
 
9
9
  // src/prerender.ts
10
10
  async function discoverRoutes(module) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/ui-server",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz UI server-side rendering runtime",
@@ -58,15 +58,15 @@
58
58
  "@ampproject/remapping": "^2.3.0",
59
59
  "@capsizecss/unpack": "^4.0.0",
60
60
  "@jridgewell/trace-mapping": "^0.3.31",
61
- "@vertz/core": "^0.2.22",
62
- "@vertz/ui": "^0.2.22",
63
- "@vertz/ui-compiler": "^0.2.22",
61
+ "@vertz/core": "^0.2.23",
62
+ "@vertz/ui": "^0.2.23",
63
+ "@vertz/ui-compiler": "^0.2.23",
64
64
  "magic-string": "^0.30.0",
65
65
  "sharp": "^0.34.5",
66
66
  "ts-morph": "^27.0.2"
67
67
  },
68
68
  "devDependencies": {
69
- "@vertz/codegen": "^0.2.22",
69
+ "@vertz/codegen": "^0.2.23",
70
70
  "@vertz/ui-auth": "^0.2.19",
71
71
  "bun-types": "^1.3.10",
72
72
  "bunup": "^0.16.31",