@vertz/ui-server 0.2.24 → 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.
@@ -592,6 +592,47 @@ function createSourceMapResolver(projectRoot) {
592
592
  };
593
593
  }
594
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
+
595
636
  // src/ssr-access-set.ts
596
637
  function createAccessSetScript(accessSet, nonce) {
597
638
  const json = JSON.stringify(accessSet);
@@ -603,6 +644,124 @@ function escapeAttr(s) {
603
644
  return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
604
645
  }
605
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
+
606
765
  // src/ssr-render.ts
607
766
  import { compileTheme } from "@vertz/ui";
608
767
  import { EntityStore, MemoryCache, QueryEnvelopeStore } from "@vertz/ui/internals";
@@ -612,6 +771,7 @@ import { setAdapter } from "@vertz/ui/internals";
612
771
 
613
772
  // src/dom-shim/ssr-node.ts
614
773
  class SSRNode {
774
+ nodeType = 1;
615
775
  childNodes = [];
616
776
  parentNode = null;
617
777
  get firstChild() {
@@ -657,6 +817,7 @@ class SSRNode {
657
817
 
658
818
  // src/dom-shim/ssr-comment.ts
659
819
  class SSRComment extends SSRNode {
820
+ nodeType = 8;
660
821
  text;
661
822
  constructor(text) {
662
823
  super();
@@ -680,6 +841,7 @@ function rawHtml(html) {
680
841
 
681
842
  // src/dom-shim/ssr-text-node.ts
682
843
  class SSRTextNode extends SSRNode {
844
+ nodeType = 3;
683
845
  text;
684
846
  constructor(text) {
685
847
  super();
@@ -695,6 +857,7 @@ class SSRTextNode extends SSRNode {
695
857
 
696
858
  // src/dom-shim/ssr-fragment.ts
697
859
  class SSRDocumentFragment extends SSRNode {
860
+ nodeType = 11;
698
861
  children = [];
699
862
  appendChild(child) {
700
863
  if (child instanceof SSRTextNode) {
@@ -1695,6 +1858,426 @@ function escapeAttr3(s) {
1695
1858
  return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
1696
1859
  }
1697
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
+
1698
2281
  // src/upstream-watcher.ts
1699
2282
  import { existsSync, lstatSync, readdirSync, realpathSync, watch } from "fs";
1700
2283
  import { join as join3 } from "path";
@@ -2540,6 +3123,53 @@ function createBunDevServer(options) {
2540
3123
  console.warn("[Server] Failed to extract font metrics:", e);
2541
3124
  }
2542
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
+ }
2543
3173
  mkdirSync(devDir, { recursive: true });
2544
3174
  const frInitPath = resolve(devDir, "fast-refresh-init.ts");
2545
3175
  writeFileSync2(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
@@ -2630,6 +3260,12 @@ if (import.meta.hot) import.meta.hot.accept();
2630
3260
  if (pathname === "/__vertz_diagnostics") {
2631
3261
  return Response.json(diagnostics.getSnapshot());
2632
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
+ }
2633
3269
  if (pathname === "/_vertz/image") {
2634
3270
  return handleDevImageProxy(request);
2635
3271
  }
@@ -2756,10 +3392,12 @@ data: {}
2756
3392
  const doRender = async () => {
2757
3393
  logger.log("ssr", "render-start", { url: pathname });
2758
3394
  const ssrStart = performance.now();
2759
- const result = await ssrRenderToString(ssrMod, pathname + url.search, {
3395
+ const result = await ssrRenderSinglePass(ssrMod, pathname + url.search, {
2760
3396
  ssrTimeout: 300,
2761
3397
  fallbackMetrics: fontFallbackMetrics,
2762
- ssrAuth
3398
+ ssrAuth,
3399
+ manifest: prefetchManager?.getSSRManifest(),
3400
+ prefetchSession: toPrefetchSession(ssrAuth)
2763
3401
  });
2764
3402
  logger.log("ssr", "render-done", {
2765
3403
  url: pathname,
@@ -2969,7 +3607,6 @@ data: {}
2969
3607
  console.warn("[Server] Could not discover HMR bundled URL:", e);
2970
3608
  }
2971
3609
  }
2972
- const srcDir = resolve(projectRoot, "src");
2973
3610
  stopped = false;
2974
3611
  if (existsSync2(srcDir)) {
2975
3612
  srcWatcherRef = watch2(srcDir, { recursive: true }, (_event, filename) => {
@@ -3040,6 +3677,16 @@ data: {}
3040
3677
  const { changed } = updateServerManifest(changedFilePath, source);
3041
3678
  const manifestDurationMs = Math.round(performance.now() - manifestStartMs);
3042
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
+ }
3043
3690
  } catch {}
3044
3691
  }
3045
3692
  if (stopped)