@vertz/ui-server 0.2.23 → 0.2.25

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.
@@ -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";
@@ -540,6 +592,47 @@ function createSourceMapResolver(projectRoot) {
540
592
  };
541
593
  }
542
594
 
595
+ // src/ssr-access-evaluator.ts
596
+ function toPrefetchSession(ssrAuth) {
597
+ if (!ssrAuth || ssrAuth.status !== "authenticated" || !ssrAuth.user) {
598
+ return { status: "unauthenticated" };
599
+ }
600
+ const roles = ssrAuth.user.role ? [ssrAuth.user.role] : undefined;
601
+ return {
602
+ status: "authenticated",
603
+ roles,
604
+ tenantId: ssrAuth.user.tenantId
605
+ };
606
+ }
607
+ function evaluateAccessRule(rule, session) {
608
+ switch (rule.type) {
609
+ case "public":
610
+ return true;
611
+ case "authenticated":
612
+ return session.status === "authenticated";
613
+ case "role":
614
+ if (session.status !== "authenticated")
615
+ return false;
616
+ return session.roles?.some((r) => rule.roles.includes(r)) === true;
617
+ case "entitlement":
618
+ if (session.status !== "authenticated")
619
+ return false;
620
+ return session.entitlements?.[rule.value] === true;
621
+ case "where":
622
+ return true;
623
+ case "fva":
624
+ return session.status === "authenticated";
625
+ case "deny":
626
+ return false;
627
+ case "all":
628
+ return rule.rules.every((r) => evaluateAccessRule(r, session));
629
+ case "any":
630
+ return rule.rules.some((r) => evaluateAccessRule(r, session));
631
+ default:
632
+ return false;
633
+ }
634
+ }
635
+
543
636
  // src/ssr-access-set.ts
544
637
  function createAccessSetScript(accessSet, nonce) {
545
638
  const json = JSON.stringify(accessSet);
@@ -551,6 +644,124 @@ function escapeAttr(s) {
551
644
  return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
552
645
  }
553
646
 
647
+ // src/ssr-prefetch-dev.ts
648
+ import {
649
+ analyzeComponentQueries,
650
+ generatePrefetchManifest
651
+ } from "@vertz/ui-compiler";
652
+ function createPrefetchManifestManager(options) {
653
+ const { routerPath, readFile: readFile2, resolveImport } = options;
654
+ let currentManifest = null;
655
+ let currentSSRManifest;
656
+ let rebuildCount = 0;
657
+ let lastRebuildMs = null;
658
+ let lastRebuildAt = null;
659
+ let fileToRouteIndices = new Map;
660
+ function buildFileIndex(routes) {
661
+ const index = new Map;
662
+ for (let i = 0;i < routes.length; i++) {
663
+ const file = routes[i]?.file;
664
+ if (file) {
665
+ const existing = index.get(file) ?? [];
666
+ existing.push(i);
667
+ index.set(file, existing);
668
+ }
669
+ }
670
+ return index;
671
+ }
672
+ function toSSRManifest(manifest) {
673
+ const routeEntries = {};
674
+ for (const route of manifest.routes) {
675
+ const existing = routeEntries[route.pattern];
676
+ if (existing) {
677
+ existing.queries.push(...route.queries);
678
+ } else {
679
+ routeEntries[route.pattern] = { queries: [...route.queries] };
680
+ }
681
+ }
682
+ return {
683
+ routePatterns: [...new Set(manifest.routes.map((r) => r.pattern))],
684
+ routeEntries
685
+ };
686
+ }
687
+ function fullBuild(routerSourceOverride) {
688
+ const start = performance.now();
689
+ const routerSource = routerSourceOverride ?? readFile2(routerPath);
690
+ if (!routerSource) {
691
+ return;
692
+ }
693
+ try {
694
+ const manifest = generatePrefetchManifest({
695
+ routerSource,
696
+ routerPath,
697
+ readFile: readFile2,
698
+ resolveImport
699
+ });
700
+ currentManifest = manifest;
701
+ currentSSRManifest = toSSRManifest(manifest);
702
+ fileToRouteIndices = buildFileIndex(manifest.routes);
703
+ rebuildCount++;
704
+ lastRebuildMs = Math.round(performance.now() - start);
705
+ lastRebuildAt = new Date().toISOString();
706
+ } catch {}
707
+ }
708
+ function incrementalUpdate(filePath, sourceText) {
709
+ if (!currentManifest)
710
+ return;
711
+ const indices = fileToRouteIndices.get(filePath);
712
+ if (!indices || indices.length === 0)
713
+ return;
714
+ const start = performance.now();
715
+ try {
716
+ const analysis = analyzeComponentQueries(sourceText, filePath);
717
+ const newRoutes = [...currentManifest.routes];
718
+ for (const idx of indices) {
719
+ const existing = newRoutes[idx];
720
+ if (existing) {
721
+ newRoutes[idx] = {
722
+ ...existing,
723
+ queries: analysis.queries,
724
+ params: analysis.params
725
+ };
726
+ }
727
+ }
728
+ const newManifest = {
729
+ ...currentManifest,
730
+ routes: newRoutes,
731
+ generatedAt: new Date().toISOString()
732
+ };
733
+ currentManifest = newManifest;
734
+ currentSSRManifest = toSSRManifest(newManifest);
735
+ rebuildCount++;
736
+ lastRebuildMs = Math.round(performance.now() - start);
737
+ lastRebuildAt = new Date().toISOString();
738
+ } catch {}
739
+ }
740
+ return {
741
+ build() {
742
+ fullBuild();
743
+ },
744
+ onFileChange(filePath, sourceText) {
745
+ if (filePath === routerPath) {
746
+ fullBuild(sourceText);
747
+ } else {
748
+ incrementalUpdate(filePath, sourceText);
749
+ }
750
+ },
751
+ getSSRManifest() {
752
+ return currentSSRManifest;
753
+ },
754
+ getSnapshot() {
755
+ return {
756
+ manifest: currentManifest,
757
+ rebuildCount,
758
+ lastRebuildMs,
759
+ lastRebuildAt
760
+ };
761
+ }
762
+ };
763
+ }
764
+
554
765
  // src/ssr-render.ts
555
766
  import { compileTheme } from "@vertz/ui";
556
767
  import { EntityStore, MemoryCache, QueryEnvelopeStore } from "@vertz/ui/internals";
@@ -560,6 +771,7 @@ import { setAdapter } from "@vertz/ui/internals";
560
771
 
561
772
  // src/dom-shim/ssr-node.ts
562
773
  class SSRNode {
774
+ nodeType = 1;
563
775
  childNodes = [];
564
776
  parentNode = null;
565
777
  get firstChild() {
@@ -605,6 +817,7 @@ class SSRNode {
605
817
 
606
818
  // src/dom-shim/ssr-comment.ts
607
819
  class SSRComment extends SSRNode {
820
+ nodeType = 8;
608
821
  text;
609
822
  constructor(text) {
610
823
  super();
@@ -628,6 +841,7 @@ function rawHtml(html) {
628
841
 
629
842
  // src/dom-shim/ssr-text-node.ts
630
843
  class SSRTextNode extends SSRNode {
844
+ nodeType = 3;
631
845
  text;
632
846
  constructor(text) {
633
847
  super();
@@ -643,6 +857,7 @@ class SSRTextNode extends SSRNode {
643
857
 
644
858
  // src/dom-shim/ssr-fragment.ts
645
859
  class SSRDocumentFragment extends SSRNode {
860
+ nodeType = 11;
646
861
  children = [];
647
862
  appendChild(child) {
648
863
  if (child instanceof SSRTextNode) {
@@ -1113,19 +1328,35 @@ function installDomShim() {
1113
1328
  cookie: ""
1114
1329
  };
1115
1330
  globalThis.document = fakeDocument;
1331
+ const windowStubs = {
1332
+ scrollTo: () => {},
1333
+ scroll: () => {},
1334
+ addEventListener: () => {},
1335
+ removeEventListener: () => {},
1336
+ dispatchEvent: () => true,
1337
+ getComputedStyle: () => ({}),
1338
+ matchMedia: () => ({ matches: false, addListener: () => {}, removeListener: () => {} })
1339
+ };
1116
1340
  if (typeof window === "undefined") {
1117
1341
  globalThis.window = {
1118
1342
  location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
1119
1343
  history: {
1120
1344
  pushState: () => {},
1121
1345
  replaceState: () => {}
1122
- }
1346
+ },
1347
+ ...windowStubs
1123
1348
  };
1124
1349
  } else {
1125
- globalThis.window.location = {
1126
- ...globalThis.window.location || {},
1350
+ const win = globalThis.window;
1351
+ win.location = {
1352
+ ...win.location || {},
1127
1353
  pathname: ssrStorage.getStore()?.url || "/"
1128
1354
  };
1355
+ for (const [key, val] of Object.entries(windowStubs)) {
1356
+ if (typeof win[key] !== "function") {
1357
+ win[key] = val;
1358
+ }
1359
+ }
1129
1360
  }
1130
1361
  globalThis.Node = SSRNode;
1131
1362
  globalThis.HTMLElement = SSRElement;
@@ -1250,6 +1481,9 @@ function serializeToHtml(node) {
1250
1481
  return node.html;
1251
1482
  }
1252
1483
  const { tag, attrs, children } = node;
1484
+ if (tag === "fragment") {
1485
+ return children.map((child) => serializeToHtml(child)).join("");
1486
+ }
1253
1487
  const attrStr = serializeAttrs(attrs);
1254
1488
  if (VOID_ELEMENTS.has(tag)) {
1255
1489
  return `<${tag}${attrStr}>`;
@@ -1326,6 +1560,9 @@ function renderToStream(tree, options) {
1326
1560
  return serializeToHtml(placeholder);
1327
1561
  }
1328
1562
  const { tag, attrs, children } = node;
1563
+ if (tag === "fragment") {
1564
+ return children.map((child) => walkAndSerialize(child)).join("");
1565
+ }
1329
1566
  const isRawText = RAW_TEXT_ELEMENTS.has(tag);
1330
1567
  const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr2(v)}"`).join("");
1331
1568
  if (VOID_ELEMENTS.has(tag)) {
@@ -1621,6 +1858,426 @@ function escapeAttr3(s) {
1621
1858
  return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
1622
1859
  }
1623
1860
 
1861
+ // src/ssr-single-pass.ts
1862
+ import { compileTheme as compileTheme2 } from "@vertz/ui";
1863
+
1864
+ // src/ssr-manifest-prefetch.ts
1865
+ function reconstructDescriptors(queries, routeParams, apiClient) {
1866
+ if (!apiClient)
1867
+ return [];
1868
+ const result = [];
1869
+ for (const query of queries) {
1870
+ const descriptor = reconstructSingle(query, routeParams, apiClient);
1871
+ if (descriptor) {
1872
+ result.push(descriptor);
1873
+ }
1874
+ }
1875
+ return result;
1876
+ }
1877
+ function reconstructSingle(query, routeParams, apiClient) {
1878
+ const { entity, operation } = query;
1879
+ if (!entity || !operation)
1880
+ return;
1881
+ const entitySdk = apiClient[entity];
1882
+ if (!entitySdk)
1883
+ return;
1884
+ const method = entitySdk[operation];
1885
+ if (typeof method !== "function")
1886
+ return;
1887
+ const args = buildFactoryArgs(query, routeParams);
1888
+ if (args === undefined)
1889
+ return;
1890
+ try {
1891
+ const descriptor = method(...args);
1892
+ if (!descriptor || typeof descriptor._key !== "string" || typeof descriptor._fetch !== "function") {
1893
+ return;
1894
+ }
1895
+ return { key: descriptor._key, fetch: descriptor._fetch };
1896
+ } catch {
1897
+ return;
1898
+ }
1899
+ }
1900
+ function buildFactoryArgs(query, routeParams) {
1901
+ const { operation, idParam, queryBindings } = query;
1902
+ if (operation === "get") {
1903
+ if (idParam) {
1904
+ const id = routeParams[idParam];
1905
+ if (!id)
1906
+ return;
1907
+ const options = resolveQueryBindings(queryBindings, routeParams);
1908
+ if (options === undefined && queryBindings)
1909
+ return;
1910
+ return options ? [id, options] : [id];
1911
+ }
1912
+ return;
1913
+ }
1914
+ if (!queryBindings)
1915
+ return [];
1916
+ const resolved = resolveQueryBindings(queryBindings, routeParams);
1917
+ if (resolved === undefined)
1918
+ return;
1919
+ return [resolved];
1920
+ }
1921
+ function resolveQueryBindings(bindings, routeParams) {
1922
+ if (!bindings)
1923
+ return;
1924
+ const resolved = {};
1925
+ if (bindings.where) {
1926
+ const where = {};
1927
+ for (const [key, value] of Object.entries(bindings.where)) {
1928
+ if (value === null)
1929
+ return;
1930
+ if (typeof value === "string" && value.startsWith("$")) {
1931
+ const paramName = value.slice(1);
1932
+ const paramValue = routeParams[paramName];
1933
+ if (!paramValue)
1934
+ return;
1935
+ where[key] = paramValue;
1936
+ } else {
1937
+ where[key] = value;
1938
+ }
1939
+ }
1940
+ resolved.where = where;
1941
+ }
1942
+ if (bindings.select)
1943
+ resolved.select = bindings.select;
1944
+ if (bindings.include)
1945
+ resolved.include = bindings.include;
1946
+ if (bindings.orderBy)
1947
+ resolved.orderBy = bindings.orderBy;
1948
+ if (bindings.limit !== undefined)
1949
+ resolved.limit = bindings.limit;
1950
+ return resolved;
1951
+ }
1952
+
1953
+ // src/ssr-route-matcher.ts
1954
+ function matchUrlToPatterns(url, patterns) {
1955
+ const path = (url.split("?")[0] ?? "").split("#")[0] ?? "";
1956
+ const matches = [];
1957
+ for (const pattern of patterns) {
1958
+ const result = matchPattern(path, pattern);
1959
+ if (result) {
1960
+ matches.push(result);
1961
+ }
1962
+ }
1963
+ matches.sort((a, b) => {
1964
+ const aSegments = a.pattern.split("/").length;
1965
+ const bSegments = b.pattern.split("/").length;
1966
+ return aSegments - bSegments;
1967
+ });
1968
+ return matches;
1969
+ }
1970
+ function matchPattern(path, pattern) {
1971
+ const pathSegments = path.split("/").filter(Boolean);
1972
+ const patternSegments = pattern.split("/").filter(Boolean);
1973
+ if (patternSegments.length > pathSegments.length)
1974
+ return;
1975
+ const params = {};
1976
+ for (let i = 0;i < patternSegments.length; i++) {
1977
+ const seg = patternSegments[i];
1978
+ const val = pathSegments[i];
1979
+ if (seg.startsWith(":")) {
1980
+ params[seg.slice(1)] = val;
1981
+ } else if (seg !== val) {
1982
+ return;
1983
+ }
1984
+ }
1985
+ return { pattern, params };
1986
+ }
1987
+
1988
+ // src/ssr-single-pass.ts
1989
+ async function ssrRenderSinglePass(module, url, options) {
1990
+ if (options?.prefetch === false) {
1991
+ return ssrRenderToString(module, url, options);
1992
+ }
1993
+ const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
1994
+ const ssrTimeout = options?.ssrTimeout ?? 300;
1995
+ ensureDomShim2();
1996
+ const zeroDiscoveryData = attemptZeroDiscovery(normalizedUrl, module, options, ssrTimeout);
1997
+ if (zeroDiscoveryData) {
1998
+ return renderWithPrefetchedData(module, normalizedUrl, zeroDiscoveryData, options);
1999
+ }
2000
+ const discoveryCtx = createRequestContext(normalizedUrl);
2001
+ if (options?.ssrAuth) {
2002
+ discoveryCtx.ssrAuth = options.ssrAuth;
2003
+ }
2004
+ const discoveredData = await ssrStorage.run(discoveryCtx, async () => {
2005
+ try {
2006
+ setGlobalSSRTimeout(ssrTimeout);
2007
+ const createApp = resolveAppFactory2(module);
2008
+ createApp();
2009
+ if (discoveryCtx.ssrRedirect) {
2010
+ return { redirect: discoveryCtx.ssrRedirect };
2011
+ }
2012
+ if (discoveryCtx.pendingRouteComponents?.size) {
2013
+ const entries = Array.from(discoveryCtx.pendingRouteComponents.entries());
2014
+ const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
2015
+ promise.then((mod) => ({ route, factory: mod.default })),
2016
+ new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
2017
+ ])));
2018
+ discoveryCtx.resolvedComponents = new Map;
2019
+ for (const result of results) {
2020
+ if (result.status === "fulfilled") {
2021
+ const { route, factory } = result.value;
2022
+ discoveryCtx.resolvedComponents.set(route, factory);
2023
+ }
2024
+ }
2025
+ discoveryCtx.pendingRouteComponents = undefined;
2026
+ }
2027
+ const queries = getSSRQueries();
2028
+ const eligibleQueries = filterByEntityAccess(queries, options?.manifest?.entityAccess, options?.prefetchSession);
2029
+ const resolvedQueries = [];
2030
+ if (eligibleQueries.length > 0) {
2031
+ await Promise.allSettled(eligibleQueries.map(({ promise, timeout, resolve, key }) => Promise.race([
2032
+ promise.then((data) => {
2033
+ resolve(data);
2034
+ resolvedQueries.push({ key, data });
2035
+ return "resolved";
2036
+ }),
2037
+ new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
2038
+ ])));
2039
+ }
2040
+ return {
2041
+ resolvedQueries,
2042
+ resolvedComponents: discoveryCtx.resolvedComponents
2043
+ };
2044
+ } finally {
2045
+ clearGlobalSSRTimeout();
2046
+ }
2047
+ });
2048
+ if ("redirect" in discoveredData) {
2049
+ return {
2050
+ html: "",
2051
+ css: "",
2052
+ ssrData: [],
2053
+ headTags: "",
2054
+ redirect: discoveredData.redirect
2055
+ };
2056
+ }
2057
+ const renderCtx = createRequestContext(normalizedUrl);
2058
+ if (options?.ssrAuth) {
2059
+ renderCtx.ssrAuth = options.ssrAuth;
2060
+ }
2061
+ for (const { key, data } of discoveredData.resolvedQueries) {
2062
+ renderCtx.queryCache.set(key, data);
2063
+ }
2064
+ renderCtx.resolvedComponents = discoveredData.resolvedComponents ?? new Map;
2065
+ return ssrStorage.run(renderCtx, async () => {
2066
+ try {
2067
+ setGlobalSSRTimeout(ssrTimeout);
2068
+ const createApp = resolveAppFactory2(module);
2069
+ let themeCss = "";
2070
+ let themePreloadTags = "";
2071
+ if (module.theme) {
2072
+ try {
2073
+ const compiled = compileTheme2(module.theme, {
2074
+ fallbackMetrics: options?.fallbackMetrics
2075
+ });
2076
+ themeCss = compiled.css;
2077
+ themePreloadTags = compiled.preloadTags;
2078
+ } catch (e) {
2079
+ console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
2080
+ }
2081
+ }
2082
+ const app = createApp();
2083
+ const vnode = toVNode(app);
2084
+ const stream = renderToStream(vnode);
2085
+ const html = await streamToString(stream);
2086
+ const css = collectCSS2(themeCss, module);
2087
+ const ssrData = discoveredData.resolvedQueries.map(({ key, data }) => ({
2088
+ key,
2089
+ data: JSON.parse(JSON.stringify(data))
2090
+ }));
2091
+ return {
2092
+ html,
2093
+ css,
2094
+ ssrData,
2095
+ headTags: themePreloadTags,
2096
+ discoveredRoutes: renderCtx.discoveredRoutes,
2097
+ matchedRoutePatterns: renderCtx.matchedRoutePatterns
2098
+ };
2099
+ } finally {
2100
+ clearGlobalSSRTimeout();
2101
+ }
2102
+ });
2103
+ }
2104
+ function attemptZeroDiscovery(url, module, options, ssrTimeout) {
2105
+ const manifest = options?.manifest;
2106
+ if (!manifest?.routeEntries || !module.api)
2107
+ return null;
2108
+ const matches = matchUrlToPatterns(url, manifest.routePatterns);
2109
+ if (matches.length === 0)
2110
+ return null;
2111
+ const allQueries = [];
2112
+ let mergedParams = {};
2113
+ for (const match of matches) {
2114
+ const entry = manifest.routeEntries[match.pattern];
2115
+ if (entry) {
2116
+ allQueries.push(...entry.queries);
2117
+ }
2118
+ mergedParams = { ...mergedParams, ...match.params };
2119
+ }
2120
+ if (allQueries.length === 0)
2121
+ return null;
2122
+ const descriptors = reconstructDescriptors(allQueries, mergedParams, module.api);
2123
+ if (descriptors.length === 0)
2124
+ return null;
2125
+ return prefetchFromDescriptors(descriptors, ssrTimeout);
2126
+ }
2127
+ async function prefetchFromDescriptors(descriptors, ssrTimeout) {
2128
+ const resolvedQueries = [];
2129
+ await Promise.allSettled(descriptors.map(({ key, fetch: fetchFn }) => Promise.race([
2130
+ fetchFn().then((result) => {
2131
+ const data = unwrapResult(result);
2132
+ resolvedQueries.push({ key, data });
2133
+ return "resolved";
2134
+ }),
2135
+ new Promise((r) => setTimeout(r, ssrTimeout)).then(() => "timeout")
2136
+ ])));
2137
+ return { resolvedQueries };
2138
+ }
2139
+ function unwrapResult(result) {
2140
+ if (result && typeof result === "object" && "ok" in result && "data" in result) {
2141
+ const r = result;
2142
+ if (r.ok)
2143
+ return r.data;
2144
+ }
2145
+ return result;
2146
+ }
2147
+ async function renderWithPrefetchedData(module, normalizedUrl, prefetchedData, options) {
2148
+ const data = await prefetchedData;
2149
+ const ssrTimeout = options?.ssrTimeout ?? 300;
2150
+ const renderCtx = createRequestContext(normalizedUrl);
2151
+ if (options?.ssrAuth) {
2152
+ renderCtx.ssrAuth = options.ssrAuth;
2153
+ }
2154
+ for (const { key, data: queryData } of data.resolvedQueries) {
2155
+ renderCtx.queryCache.set(key, queryData);
2156
+ }
2157
+ renderCtx.resolvedComponents = new Map;
2158
+ return ssrStorage.run(renderCtx, async () => {
2159
+ try {
2160
+ setGlobalSSRTimeout(ssrTimeout);
2161
+ const createApp = resolveAppFactory2(module);
2162
+ let themeCss = "";
2163
+ let themePreloadTags = "";
2164
+ if (module.theme) {
2165
+ try {
2166
+ const compiled = compileTheme2(module.theme, {
2167
+ fallbackMetrics: options?.fallbackMetrics
2168
+ });
2169
+ themeCss = compiled.css;
2170
+ themePreloadTags = compiled.preloadTags;
2171
+ } catch (e) {
2172
+ console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
2173
+ }
2174
+ }
2175
+ const app = createApp();
2176
+ const vnode = toVNode(app);
2177
+ const stream = renderToStream(vnode);
2178
+ const html = await streamToString(stream);
2179
+ if (renderCtx.ssrRedirect) {
2180
+ return {
2181
+ html: "",
2182
+ css: "",
2183
+ ssrData: [],
2184
+ headTags: "",
2185
+ redirect: renderCtx.ssrRedirect,
2186
+ discoveredRoutes: renderCtx.discoveredRoutes,
2187
+ matchedRoutePatterns: renderCtx.matchedRoutePatterns
2188
+ };
2189
+ }
2190
+ const css = collectCSS2(themeCss, module);
2191
+ const ssrData = data.resolvedQueries.map(({ key, data: d }) => ({
2192
+ key,
2193
+ data: JSON.parse(JSON.stringify(d))
2194
+ }));
2195
+ return {
2196
+ html,
2197
+ css,
2198
+ ssrData,
2199
+ headTags: themePreloadTags,
2200
+ discoveredRoutes: renderCtx.discoveredRoutes,
2201
+ matchedRoutePatterns: renderCtx.matchedRoutePatterns
2202
+ };
2203
+ } finally {
2204
+ clearGlobalSSRTimeout();
2205
+ }
2206
+ });
2207
+ }
2208
+ var domShimInstalled2 = false;
2209
+ function ensureDomShim2() {
2210
+ if (domShimInstalled2 && typeof document !== "undefined")
2211
+ return;
2212
+ domShimInstalled2 = true;
2213
+ installDomShim();
2214
+ }
2215
+ function resolveAppFactory2(module) {
2216
+ const createApp = module.default || module.App;
2217
+ if (typeof createApp !== "function") {
2218
+ throw new Error("App entry must export a default function or named App function");
2219
+ }
2220
+ return createApp;
2221
+ }
2222
+ function filterByEntityAccess(queries, entityAccess, session) {
2223
+ if (!entityAccess || !session)
2224
+ return queries;
2225
+ return queries.filter(({ key }) => {
2226
+ const entity = extractEntityFromKey(key);
2227
+ const method = extractMethodFromKey(key);
2228
+ if (!entity)
2229
+ return true;
2230
+ const entityRules = entityAccess[entity];
2231
+ if (!entityRules)
2232
+ return true;
2233
+ const rule = entityRules[method];
2234
+ if (!rule)
2235
+ return true;
2236
+ return evaluateAccessRule(rule, session);
2237
+ });
2238
+ }
2239
+ function extractEntityFromKey(key) {
2240
+ const pathStart = key.indexOf(":/");
2241
+ if (pathStart === -1)
2242
+ return;
2243
+ const path = key.slice(pathStart + 2);
2244
+ const firstSlash = path.indexOf("/");
2245
+ const questionMark = path.indexOf("?");
2246
+ if (firstSlash === -1 && questionMark === -1)
2247
+ return path;
2248
+ if (firstSlash === -1)
2249
+ return path.slice(0, questionMark);
2250
+ if (questionMark === -1)
2251
+ return path.slice(0, firstSlash);
2252
+ return path.slice(0, Math.min(firstSlash, questionMark));
2253
+ }
2254
+ function extractMethodFromKey(key) {
2255
+ const pathStart = key.indexOf(":/");
2256
+ if (pathStart === -1)
2257
+ return "list";
2258
+ const path = key.slice(pathStart + 2);
2259
+ const cleanPath = path.split("?")[0] ?? "";
2260
+ const segments = cleanPath.split("/").filter(Boolean);
2261
+ return segments.length > 1 ? "get" : "list";
2262
+ }
2263
+ function collectCSS2(themeCss, module) {
2264
+ const alreadyIncluded = new Set;
2265
+ if (themeCss)
2266
+ alreadyIncluded.add(themeCss);
2267
+ if (module.styles) {
2268
+ for (const s of module.styles)
2269
+ alreadyIncluded.add(s);
2270
+ }
2271
+ const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
2272
+ const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
2273
+ const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
2274
+ `)}</style>` : "";
2275
+ const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
2276
+ `)}</style>` : "";
2277
+ return [themeTag, globalTag, componentTag].filter(Boolean).join(`
2278
+ `);
2279
+ }
2280
+
1624
2281
  // src/upstream-watcher.ts
1625
2282
  import { existsSync, lstatSync, readdirSync, realpathSync, watch } from "fs";
1626
2283
  import { join as join3 } from "path";
@@ -1981,11 +2638,13 @@ function generateSSRPageHtml({
1981
2638
  scriptTag,
1982
2639
  editor = "vscode",
1983
2640
  headTags = "",
1984
- sessionScript = ""
2641
+ sessionScript = "",
2642
+ htmlDataTheme
1985
2643
  }) {
1986
2644
  const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
2645
+ const htmlAttrs = htmlDataTheme ? ` data-theme="${htmlDataTheme}"` : "";
1987
2646
  return `<!doctype html>
1988
- <html lang="en">
2647
+ <html lang="en"${htmlAttrs}>
1989
2648
  <head>
1990
2649
  <meta charset="UTF-8" />
1991
2650
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -2082,7 +2741,9 @@ function createBunDevServer(options) {
2082
2741
  editor: editorOption,
2083
2742
  headTags: headTagsOption = "",
2084
2743
  sessionResolver,
2085
- watchDeps
2744
+ watchDeps,
2745
+ themeFromRequest,
2746
+ onRestartNeeded
2086
2747
  } = options;
2087
2748
  const faviconTag = detectFaviconTag(projectRoot);
2088
2749
  const headTags = [faviconTag, headTagsOption].filter(Boolean).join(`
@@ -2099,6 +2760,7 @@ function createBunDevServer(options) {
2099
2760
  let srcWatcherRef = null;
2100
2761
  let refreshTimeout = null;
2101
2762
  let stopped = false;
2763
+ let ssrFallback = false;
2102
2764
  const wsClients = new Set;
2103
2765
  let currentError = null;
2104
2766
  const sourceMapResolver = createSourceMapResolver(projectRoot);
@@ -2412,8 +3074,8 @@ function createBunDevServer(options) {
2412
3074
  }
2413
3075
  if (!pluginsRegistered) {
2414
3076
  const { plugin: serverPlugin, updateManifest } = createVertzBunPlugin({
2415
- hmr: false,
2416
- fastRefresh: false,
3077
+ hmr: true,
3078
+ fastRefresh: true,
2417
3079
  logger,
2418
3080
  diagnostics
2419
3081
  });
@@ -2424,16 +3086,34 @@ function createBunDevServer(options) {
2424
3086
  const updateServerManifest = stableUpdateManifest;
2425
3087
  let ssrMod;
2426
3088
  try {
2427
- ssrMod = await import(entryPath);
3089
+ if (isRestarting) {
3090
+ mkdirSync(devDir, { recursive: true });
3091
+ const ssrBootPath = resolve(devDir, "ssr-reload-entry.ts");
3092
+ const ts = Date.now();
3093
+ writeFileSync2(ssrBootPath, `export * from '${entryPath}';
3094
+ `);
3095
+ ssrMod = await import(`${ssrBootPath}?t=${ts}`);
3096
+ } else {
3097
+ ssrMod = await import(entryPath);
3098
+ }
3099
+ ssrFallback = false;
2428
3100
  if (logRequests) {
2429
3101
  console.log("[Server] SSR module loaded");
2430
3102
  }
2431
3103
  } catch (e) {
2432
3104
  console.error("[Server] Failed to load SSR module:", e);
2433
3105
  if (isRestarting) {
2434
- throw e;
3106
+ ssrFallback = true;
3107
+ ssrMod = {};
3108
+ const errMsg = e instanceof Error ? e.message : String(e);
3109
+ const errStack = e instanceof Error ? e.stack : undefined;
3110
+ const { message: _, ...loc } = errStack ? parseSourceFromStack(errStack) : { message: "" };
3111
+ queueMicrotask(() => {
3112
+ broadcastError("ssr", [{ message: errMsg, ...loc, stack: errStack }]);
3113
+ });
3114
+ } else {
3115
+ process.exit(1);
2435
3116
  }
2436
- process.exit(1);
2437
3117
  }
2438
3118
  let fontFallbackMetrics;
2439
3119
  if (ssrMod.theme?.fonts) {
@@ -2443,6 +3123,53 @@ function createBunDevServer(options) {
2443
3123
  console.warn("[Server] Failed to extract font metrics:", e);
2444
3124
  }
2445
3125
  }
3126
+ let prefetchManager = null;
3127
+ const srcDir = resolve(projectRoot, "src");
3128
+ const routerCandidates = [resolve(srcDir, "router.tsx"), resolve(srcDir, "router.ts")];
3129
+ const routerPath = routerCandidates.find((p) => existsSync2(p));
3130
+ if (routerPath) {
3131
+ prefetchManager = createPrefetchManifestManager({
3132
+ routerPath,
3133
+ readFile: (path) => {
3134
+ try {
3135
+ return readFileSync2(path, "utf-8");
3136
+ } catch {
3137
+ return;
3138
+ }
3139
+ },
3140
+ resolveImport: (specifier, fromFile) => {
3141
+ if (!specifier.startsWith("."))
3142
+ return;
3143
+ const dir = dirname(fromFile);
3144
+ const base = resolve(dir, specifier);
3145
+ for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
3146
+ const candidate = `${base}${ext}`;
3147
+ if (existsSync2(candidate))
3148
+ return candidate;
3149
+ }
3150
+ for (const ext of [".tsx", ".ts"]) {
3151
+ const candidate = resolve(base, `index${ext}`);
3152
+ if (existsSync2(candidate))
3153
+ return candidate;
3154
+ }
3155
+ return;
3156
+ }
3157
+ });
3158
+ try {
3159
+ const buildStart = performance.now();
3160
+ prefetchManager.build();
3161
+ const buildMs = Math.round(performance.now() - buildStart);
3162
+ logger.log("prefetch", "initial-build", { routerPath, durationMs: buildMs });
3163
+ if (logRequests) {
3164
+ const manifest = prefetchManager.getSSRManifest();
3165
+ const routeCount = manifest?.routePatterns.length ?? 0;
3166
+ console.log(`[Server] Prefetch manifest built (${routeCount} routes, ${buildMs}ms)`);
3167
+ }
3168
+ } catch (e) {
3169
+ console.warn("[Server] Failed to build prefetch manifest:", e instanceof Error ? e.message : e);
3170
+ prefetchManager = null;
3171
+ }
3172
+ }
2446
3173
  mkdirSync(devDir, { recursive: true });
2447
3174
  const frInitPath = resolve(devDir, "fast-refresh-init.ts");
2448
3175
  writeFileSync2(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
@@ -2462,6 +3189,12 @@ if (import.meta.hot) import.meta.hot.accept();
2462
3189
  setupOpenAPIWatcher();
2463
3190
  let bundledScriptUrl = null;
2464
3191
  let hmrBootstrapScript = null;
3192
+ const readyGate = createReadyGate({
3193
+ timeoutMs: 5000,
3194
+ onTimeoutWarning: () => {
3195
+ console.warn("[Server] HMR asset discovery timed out \u2014 unblocking clients");
3196
+ }
3197
+ });
2465
3198
  const routes = {
2466
3199
  "/__vertz_hmr": hmrShellModule
2467
3200
  };
@@ -2527,6 +3260,12 @@ if (import.meta.hot) import.meta.hot.accept();
2527
3260
  if (pathname === "/__vertz_diagnostics") {
2528
3261
  return Response.json(diagnostics.getSnapshot());
2529
3262
  }
3263
+ if (pathname === "/__vertz_prefetch_manifest") {
3264
+ if (!prefetchManager) {
3265
+ return Response.json({ error: "No prefetch manifest available (router file not found)" }, { status: 404 });
3266
+ }
3267
+ return Response.json(prefetchManager.getSnapshot());
3268
+ }
2530
3269
  if (pathname === "/_vertz/image") {
2531
3270
  return handleDevImageProxy(request);
2532
3271
  }
@@ -2605,6 +3344,7 @@ data: {}
2605
3344
  if (logRequests) {
2606
3345
  console.log(`[Server] SSR: ${pathname}`);
2607
3346
  }
3347
+ const ssrTheme = themeFromRequest?.(request) ?? undefined;
2608
3348
  try {
2609
3349
  const interceptor = apiHandler ? createFetchInterceptor({
2610
3350
  apiHandler,
@@ -2652,10 +3392,12 @@ data: {}
2652
3392
  const doRender = async () => {
2653
3393
  logger.log("ssr", "render-start", { url: pathname });
2654
3394
  const ssrStart = performance.now();
2655
- const result = await ssrRenderToString(ssrMod, pathname + url.search, {
3395
+ const result = await ssrRenderSinglePass(ssrMod, pathname + url.search, {
2656
3396
  ssrTimeout: 300,
2657
3397
  fallbackMetrics: fontFallbackMetrics,
2658
- ssrAuth
3398
+ ssrAuth,
3399
+ manifest: prefetchManager?.getSSRManifest(),
3400
+ prefetchSession: toPrefetchSession(ssrAuth)
2659
3401
  });
2660
3402
  logger.log("ssr", "render-done", {
2661
3403
  url: pathname,
@@ -2668,19 +3410,22 @@ data: {}
2668
3410
  headers: { Location: result.redirect.to }
2669
3411
  });
2670
3412
  }
3413
+ const bodyHtml = ssrTheme ? result.html.replace(/data-theme="[^"]*"/, `data-theme="${ssrTheme}"`) : result.html;
2671
3414
  const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
2672
3415
  const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
2673
3416
  `);
2674
3417
  const html = generateSSRPageHtml({
2675
3418
  title,
2676
3419
  css: result.css,
2677
- bodyHtml: result.html,
3420
+ bodyHtml,
2678
3421
  ssrData: result.ssrData,
2679
3422
  scriptTag,
2680
3423
  editor,
2681
3424
  headTags: combinedHeadTags,
2682
- sessionScript
3425
+ sessionScript,
3426
+ htmlDataTheme: ssrTheme
2683
3427
  });
3428
+ clearError();
2684
3429
  return new Response(html, {
2685
3430
  status: 200,
2686
3431
  headers: {
@@ -2704,7 +3449,8 @@ data: {}
2704
3449
  ssrData: [],
2705
3450
  scriptTag,
2706
3451
  editor,
2707
- headTags
3452
+ headTags,
3453
+ htmlDataTheme: ssrTheme
2708
3454
  });
2709
3455
  return new Response(fallbackHtml, {
2710
3456
  status: 200,
@@ -2720,13 +3466,15 @@ data: {}
2720
3466
  wsClients.add(ws);
2721
3467
  diagnostics.recordWebSocketChange(wsClients.size);
2722
3468
  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
- }));
3469
+ if (!readyGate.onOpen(ws)) {
3470
+ ws.sendText(JSON.stringify({ type: "connected" }));
3471
+ if (currentError) {
3472
+ ws.sendText(JSON.stringify({
3473
+ type: "error",
3474
+ category: currentError.category,
3475
+ errors: currentError.errors
3476
+ }));
3477
+ }
2730
3478
  }
2731
3479
  },
2732
3480
  message(ws, msg) {
@@ -2818,6 +3566,7 @@ data: {}
2818
3566
  },
2819
3567
  close(ws) {
2820
3568
  wsClients.delete(ws);
3569
+ readyGate.onClose(ws);
2821
3570
  diagnostics.recordWebSocketChange(wsClients.size);
2822
3571
  }
2823
3572
  },
@@ -2829,7 +3578,13 @@ data: {}
2829
3578
  if (logRequests) {
2830
3579
  console.log(`[Server] SSR+HMR dev server running at http://${host}:${server.port}`);
2831
3580
  }
2832
- await discoverHMRAssets();
3581
+ try {
3582
+ await discoverHMRAssets();
3583
+ } finally {
3584
+ if (!readyGate.isReady) {
3585
+ readyGate.open(currentError);
3586
+ }
3587
+ }
2833
3588
  async function discoverHMRAssets() {
2834
3589
  try {
2835
3590
  const res = await fetch(`http://${host}:${server?.port}/__vertz_hmr`);
@@ -2852,7 +3607,6 @@ data: {}
2852
3607
  console.warn("[Server] Could not discover HMR bundled URL:", e);
2853
3608
  }
2854
3609
  }
2855
- const srcDir = resolve(projectRoot, "src");
2856
3610
  stopped = false;
2857
3611
  if (existsSync2(srcDir)) {
2858
3612
  srcWatcherRef = watch2(srcDir, { recursive: true }, (_event, filename) => {
@@ -2923,10 +3677,33 @@ data: {}
2923
3677
  const { changed } = updateServerManifest(changedFilePath, source);
2924
3678
  const manifestDurationMs = Math.round(performance.now() - manifestStartMs);
2925
3679
  diagnostics.recordManifestUpdate(lastChangedFile, changed, manifestDurationMs);
3680
+ if (prefetchManager) {
3681
+ const prefetchStart = performance.now();
3682
+ prefetchManager.onFileChange(changedFilePath, source);
3683
+ const prefetchMs = Math.round(performance.now() - prefetchStart);
3684
+ logger.log("prefetch", "rebuild", {
3685
+ file: lastChangedFile,
3686
+ durationMs: prefetchMs,
3687
+ isRouter: changedFilePath === routerPath
3688
+ });
3689
+ }
2926
3690
  } catch {}
2927
3691
  }
2928
3692
  if (stopped)
2929
3693
  return;
3694
+ if (ssrFallback) {
3695
+ if (onRestartNeeded) {
3696
+ if (logRequests) {
3697
+ console.log("[Server] SSR in fallback mode \u2014 requesting process restart");
3698
+ }
3699
+ await devServer.stop();
3700
+ onRestartNeeded();
3701
+ return;
3702
+ }
3703
+ if (logRequests) {
3704
+ console.log("[Server] SSR in fallback mode \u2014 attempting re-import (best effort)");
3705
+ }
3706
+ }
2930
3707
  const cacheCleared = clearSSRRequireCache();
2931
3708
  logger.log("watcher", "cache-cleared", { entries: cacheCleared });
2932
3709
  const ssrWrapperPath = resolve(devDir, "ssr-reload-entry.ts");
@@ -2937,6 +3714,7 @@ data: {}
2937
3714
  try {
2938
3715
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2939
3716
  ssrMod = freshMod;
3717
+ ssrFallback = false;
2940
3718
  if (freshMod.theme?.fonts) {
2941
3719
  try {
2942
3720
  fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
@@ -2962,6 +3740,7 @@ data: {}
2962
3740
  try {
2963
3741
  const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
2964
3742
  ssrMod = freshMod;
3743
+ ssrFallback = false;
2965
3744
  if (freshMod.theme?.fonts) {
2966
3745
  try {
2967
3746
  fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
@@ -2982,6 +3761,7 @@ data: {}
2982
3761
  logger.log("watcher", "ssr-reload", { status: "failed", error: errMsg });
2983
3762
  const { message: _m, ...loc2 } = errStack ? parseSourceFromStack(errStack) : { message: "" };
2984
3763
  broadcastError("ssr", [{ message: errMsg, ...loc2, stack: errStack }]);
3764
+ ssrFallback = true;
2985
3765
  }
2986
3766
  }
2987
3767
  }, 100);
@@ -3048,6 +3828,7 @@ data: {}
3048
3828
  lastBroadcastedError = "";
3049
3829
  lastChangedFile = "";
3050
3830
  clearGraceUntil = 0;
3831
+ ssrFallback = false;
3051
3832
  terminalDedup.reset();
3052
3833
  clearSSRRequireCache();
3053
3834
  sourceMapResolver.invalidate();