@vertz/ui-server 0.2.19 → 0.2.21

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.
@@ -25,6 +25,14 @@ interface SSRSessionInfo {
25
25
  * Returns null when no valid session exists (expired, missing, or invalid cookie).
26
26
  */
27
27
  type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
28
+ /**
29
+ * Detect `public/favicon.svg` and return a `<link>` tag for it.
30
+ * Returns empty string when the file does not exist.
31
+ *
32
+ * Detection runs once at server startup — adding or removing the file
33
+ * requires a dev server restart (consistent with production build).
34
+ */
35
+ declare function detectFaviconTag(projectRoot: string): string;
28
36
  interface BunDevServerOptions {
29
37
  /** SSR entry module (e.g., './src/app.tsx') */
30
38
  entry: string;
@@ -75,6 +83,7 @@ interface ErrorDetail {
75
83
  stack?: string;
76
84
  }
77
85
  type ErrorCategory = "build" | "resolve" | "runtime" | "ssr";
86
+ declare function isStaleGraphError(message: string): boolean;
78
87
  /** A resolved stack frame for terminal logging. */
79
88
  interface TerminalStackFrame {
80
89
  functionName: string | null;
@@ -100,6 +109,13 @@ declare function createRuntimeErrorDeduplicator(): {
100
109
  interface BunDevServer {
101
110
  start(): Promise<void>;
102
111
  stop(): Promise<void>;
112
+ /**
113
+ * Soft-restart the dev server: stops Bun.serve(), clears all caches,
114
+ * creates a fresh Bun.serve() with a clean HMR module graph.
115
+ * Broadcasts { type: 'restarting' } to clients before stopping.
116
+ * Skips one-time setup (plugin registration, console.error patching).
117
+ */
118
+ restart(): Promise<void>;
103
119
  /** Broadcast an error to all connected WebSocket clients. */
104
120
  broadcastError(category: ErrorCategory, errors: ErrorDetail[]): void;
105
121
  /** Clear current error and notify all connected WebSocket clients.
@@ -177,4 +193,4 @@ declare function clearSSRRequireCache(): number;
177
193
  * SSR is always on. HMR always works. No mode toggle needed.
178
194
  */
179
195
  declare function createBunDevServer(options: BunDevServerOptions): BunDevServer;
180
- export { parseHMRAssets, generateSSRPageHtml, formatTerminalRuntimeError, createRuntimeErrorDeduplicator, createFetchInterceptor, createBunDevServer, clearSSRRequireCache, buildScriptTag, SSRPageHtmlOptions, HMRAssets, FetchInterceptorOptions, ErrorDetail, ErrorCategory, BunDevServerOptions, BunDevServer };
196
+ export { parseHMRAssets, isStaleGraphError, generateSSRPageHtml, formatTerminalRuntimeError, detectFaviconTag, createRuntimeErrorDeduplicator, createFetchInterceptor, createBunDevServer, clearSSRRequireCache, buildScriptTag, SSRPageHtmlOptions, HMRAssets, FetchInterceptorOptions, ErrorDetail, ErrorCategory, BunDevServerOptions, BunDevServer };
@@ -611,6 +611,9 @@ class SSRComment extends SSRNode {
611
611
  }
612
612
  }
613
613
 
614
+ // src/dom-shim/ssr-element.ts
615
+ import { __styleStr } from "@vertz/ui/internals";
616
+
614
617
  // src/types.ts
615
618
  function rawHtml(html) {
616
619
  return { __raw: true, html };
@@ -654,6 +657,52 @@ class SSRDocumentFragment extends SSRNode {
654
657
  }
655
658
 
656
659
  // src/dom-shim/ssr-element.ts
660
+ function camelToKebab(str) {
661
+ return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
662
+ }
663
+ function kebabToCamel(str) {
664
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
665
+ }
666
+ function createDatasetProxy(element) {
667
+ return new Proxy({}, {
668
+ set(_target, prop, value) {
669
+ if (typeof prop === "string") {
670
+ element.setAttribute(`data-${camelToKebab(prop)}`, String(value));
671
+ }
672
+ return true;
673
+ },
674
+ get(_target, prop) {
675
+ if (typeof prop === "string") {
676
+ return element.getAttribute(`data-${camelToKebab(prop)}`) ?? undefined;
677
+ }
678
+ return;
679
+ },
680
+ has(_target, prop) {
681
+ if (typeof prop === "string") {
682
+ return element.getAttribute(`data-${camelToKebab(prop)}`) !== null;
683
+ }
684
+ return false;
685
+ },
686
+ deleteProperty(_target, prop) {
687
+ if (typeof prop === "string") {
688
+ element.removeAttribute(`data-${camelToKebab(prop)}`);
689
+ }
690
+ return true;
691
+ },
692
+ ownKeys() {
693
+ return Object.keys(element.attrs).filter((k) => k.startsWith("data-")).map((k) => kebabToCamel(k.slice(5)));
694
+ },
695
+ getOwnPropertyDescriptor(_target, prop) {
696
+ if (typeof prop === "string") {
697
+ const val = element.getAttribute(`data-${camelToKebab(prop)}`);
698
+ if (val !== null) {
699
+ return { configurable: true, enumerable: true, value: val };
700
+ }
701
+ }
702
+ return;
703
+ }
704
+ });
705
+ }
657
706
  function createStyleProxy(element) {
658
707
  const styles = {};
659
708
  return new Proxy(styles, {
@@ -685,16 +734,27 @@ class SSRElement extends SSRNode {
685
734
  _textContent = null;
686
735
  _innerHTML = null;
687
736
  style;
737
+ dataset;
688
738
  constructor(tag) {
689
739
  super();
690
740
  this.tag = tag;
691
741
  this.style = createStyleProxy(this);
742
+ this.dataset = createDatasetProxy(this);
692
743
  }
693
744
  setAttribute(name, value) {
694
- if (name === "class") {
745
+ if (name === "style" && typeof value === "object" && value !== null) {
746
+ this.attrs.style = __styleStr(value);
747
+ for (const [k, v] of Object.entries(value)) {
748
+ if (v != null)
749
+ this.style[k] = String(v);
750
+ }
751
+ return;
752
+ }
753
+ const attrName = name === "className" ? "class" : name;
754
+ if (attrName === "class") {
695
755
  this._classList = new Set(value.split(/\s+/).filter(Boolean));
696
756
  }
697
- this.attrs[name] = value;
757
+ this.attrs[attrName] = value;
698
758
  }
699
759
  getAttribute(name) {
700
760
  return this.attrs[name] ?? null;
@@ -1347,7 +1407,8 @@ async function ssrRenderToString(module, url, options) {
1347
1407
  css,
1348
1408
  ssrData,
1349
1409
  headTags: themePreloadTags,
1350
- discoveredRoutes: ctx.discoveredRoutes
1410
+ discoveredRoutes: ctx.discoveredRoutes,
1411
+ matchedRoutePatterns: ctx.matchedRoutePatterns
1351
1412
  };
1352
1413
  } finally {
1353
1414
  clearGlobalSSRTimeout();
@@ -1464,6 +1525,18 @@ function escapeAttr3(s) {
1464
1525
  }
1465
1526
 
1466
1527
  // src/bun-dev-server.ts
1528
+ function detectFaviconTag(projectRoot) {
1529
+ const faviconPath = resolve(projectRoot, "public", "favicon.svg");
1530
+ return existsSync(faviconPath) ? '<link rel="icon" type="image/svg+xml" href="/favicon.svg">' : "";
1531
+ }
1532
+ var STALE_GRAPH_PATTERNS = [
1533
+ /Export named ['"].*['"] not found in module/i,
1534
+ /No matching export in ['"].*['"] for import/i,
1535
+ /does not provide an export named/i
1536
+ ];
1537
+ function isStaleGraphError(message) {
1538
+ return STALE_GRAPH_PATTERNS.some((pattern) => pattern.test(message));
1539
+ }
1467
1540
  var MAX_TERMINAL_STACK_FRAMES = 5;
1468
1541
  function formatTerminalRuntimeError(errors, parsedStack) {
1469
1542
  const primary = errors[0];
@@ -1558,6 +1631,24 @@ function buildErrorChannelScript(editor) {
1558
1631
  "V._src=null;",
1559
1632
  "V._hadClientError=false;",
1560
1633
  "V._needsReload=false;",
1634
+ "V._restarting=false;",
1635
+ `V.isStaleGraph=function(m){return/Export named ['"].*['"] not found in module/i.test(m)||/No matching export in ['"].*['"] for import/i.test(m)||/does not provide an export named/i.test(m)};`,
1636
+ "V._canAutoRestart=function(){",
1637
+ "var raw=sessionStorage.getItem('__vertz_auto_restart');",
1638
+ "var ts;try{ts=raw?JSON.parse(raw):[]}catch(e){ts=[]}",
1639
+ "var now=Date.now();",
1640
+ "ts=ts.filter(function(t){return now-t<10000});",
1641
+ "return ts.length<3};",
1642
+ "V._autoRestart=function(){",
1643
+ "if(V._restarting)return;",
1644
+ "var raw=sessionStorage.getItem('__vertz_auto_restart');",
1645
+ "var ts;try{ts=raw?JSON.parse(raw):[]}catch(e){ts=[]}",
1646
+ "var now=Date.now();",
1647
+ "ts=ts.filter(function(t){return now-t<10000});",
1648
+ "if(ts.length>=3)return;",
1649
+ "ts.push(now);",
1650
+ "sessionStorage.setItem('__vertz_auto_restart',JSON.stringify(ts));",
1651
+ "if(V._ws&&V._ws.readyState===1){V._ws.send(JSON.stringify({type:'restart'}))}};",
1561
1652
  'var rts=sessionStorage.getItem("__vertz_recovering");',
1562
1653
  "V._recovering=rts&&(Date.now()-Number(rts)<10000);",
1563
1654
  'if(V._recovering)sessionStorage.removeItem("__vertz_recovering");',
@@ -1598,7 +1689,7 @@ function buildErrorChannelScript(editor) {
1598
1689
  `var link=href?'<a href="'+href+'" style="color:var(--ve-link);text-decoration:underline;text-underline-offset:2px">'+loc+'</a>':'<span>'+loc+'</span>';`,
1599
1690
  `return'<div style="font-size:11px;color:'+color+';margin:1px 0;font-family:ui-monospace,monospace">'+V.esc(name)+' '+link+'</div>'};`,
1600
1691
  "V.removeOverlay=function(){V._src=null;var e=document.getElementById('__vertz_error');if(e)e.remove();" + "var d=document.getElementById('__vertz_error_data');if(d)d.remove()};",
1601
- "V.showOverlay=function(t,body,payload,src){",
1692
+ "V.showOverlay=function(t,body,payload,src,restartable){",
1602
1693
  "V.removeOverlay();",
1603
1694
  "V._src=src||'ws';",
1604
1695
  "var d=document,c=d.createElement('div');",
@@ -1617,12 +1708,16 @@ function buildErrorChannelScript(editor) {
1617
1708
  "--ve-error:hsl(0 72% 65%);--ve-link:hsl(217 91% 70%);--ve-border:hsl(0 0% 18%);",
1618
1709
  "--ve-code:hsl(36 80% 65%);--ve-code-bg:hsl(0 0% 11%);--ve-btn:hsl(0 0% 93%);--ve-btn-fg:hsl(0 0% 7%)}}';",
1619
1710
  "d.head.appendChild(st);",
1711
+ `var btns=restartable?'<div style="display:flex;gap:6px">'`,
1712
+ `+'<button id="__vertz_restart" style="background:var(--ve-btn);color:var(--ve-btn-fg);border:none;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer;font-weight:500">Restart Server</button>'`,
1713
+ `+'<button id="__vertz_retry" style="background:transparent;color:var(--ve-muted);border:1px solid var(--ve-border);border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer">Retry</button></div>'`,
1714
+ `:'<button id="__vertz_retry" style="background:var(--ve-btn);color:var(--ve-btn-fg);border:none;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer;font-weight:500">Retry</button>';`,
1620
1715
  `c.innerHTML='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">'`,
1621
1716
  `+'<span style="font-size:13px;font-weight:600;color:var(--ve-error)">'+V.esc(t)+'</span>'`,
1622
- `+'<button id="__vertz_retry" style="background:var(--ve-btn);color:var(--ve-btn-fg);border:none;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer;font-weight:500">Retry</button>'`,
1623
- "+'</div>'+body;",
1717
+ "+btns+'</div>'+body;",
1624
1718
  "(d.body||d.documentElement).appendChild(c);",
1625
1719
  "d.getElementById('__vertz_retry').onclick=function(){location.reload()};",
1720
+ "var rb=d.getElementById('__vertz_restart');if(rb){rb.onclick=function(){if(V._ws&&V._ws.readyState===1){V._ws.send(JSON.stringify({type:'restart'}))}}};",
1626
1721
  "if(payload){var s=d.createElement('script');s.type='application/json';s.id='__vertz_error_data';s.textContent=JSON.stringify(payload);(d.body||d.documentElement).appendChild(s)}};",
1627
1722
  "var delay=1000,maxDelay=30000;",
1628
1723
  "function connect(){",
@@ -1633,16 +1728,30 @@ function buildErrorChannelScript(editor) {
1633
1728
  "try{var m=JSON.parse(e.data);",
1634
1729
  "if(m.type==='error'){",
1635
1730
  "if(V._recovering)return;",
1636
- "V.showOverlay(m.category==='build'?'Build failed':m.category==='ssr'?'SSR error':m.category==='resolve'?'Module not found':'Runtime error',V.formatErrors(m.errors)+V.formatStack(m.parsedStack),m,'ws')}",
1731
+ "var sg=m.errors&&m.errors.some(function(e){return V.isStaleGraph(e.message)});",
1732
+ "V.showOverlay(m.category==='build'?'Build failed':m.category==='ssr'?'SSR error':m.category==='resolve'?'Module not found':'Runtime error',V.formatErrors(m.errors)+V.formatStack(m.parsedStack),m,'ws',sg)}",
1637
1733
  "else if(m.type==='clear'){",
1638
1734
  "if(V._needsReload){V._needsReload=false;V.removeOverlay();sessionStorage.setItem('__vertz_recovering',String(Date.now()));_reload();return}",
1639
1735
  "var a=document.getElementById('app');",
1640
1736
  "if(!a||a.innerHTML.length<50){V.removeOverlay();sessionStorage.setItem('__vertz_recovering',String(Date.now()));_reload();return}",
1641
1737
  "if(V._hadClientError)return;",
1642
1738
  "V.removeOverlay()}",
1643
- "else if(m.type==='connected'){delay=1000}",
1739
+ "else if(m.type==='restarting'){",
1740
+ "V._restarting=true;",
1741
+ "V.removeOverlay();",
1742
+ "V._src='ws';",
1743
+ "sessionStorage.removeItem('__vertz_reload_count');sessionStorage.removeItem('__vertz_reload_ts');",
1744
+ "var d2=document,c2=d2.createElement('div');c2.id='__vertz_error';",
1745
+ "c2.style.cssText='position:fixed;bottom:16px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#1a1a1a;color:#fff;border-radius:8px;padding:14px 20px;font-family:ui-sans-serif,system-ui,sans-serif;font-size:13px;box-shadow:0 4px 24px rgba(0,0,0,0.2)';",
1746
+ "c2.textContent='Restarting dev server\\u2026';",
1747
+ "(d2.body||d2.documentElement).appendChild(c2);",
1748
+ "V._restartTimer=setTimeout(function(){var el=d2.getElementById('__vertz_error');if(el){el.textContent='Restart timed out. Try restarting manually (Ctrl+C and re-run).'}V._restarting=false},10000)}",
1749
+ "else if(m.type==='connected'){delay=1000;",
1750
+ "var restartEl=document.getElementById('__vertz_error');",
1751
+ "var timedOut=restartEl&&restartEl.textContent&&restartEl.textContent.indexOf('timed out')!==-1;",
1752
+ "if(V._restarting||timedOut){V._restarting=false;if(V._restartTimer){clearTimeout(V._restartTimer);V._restartTimer=null}_reload()}}",
1644
1753
  "}catch(ex){}};",
1645
- "ws.onclose=function(){V._ws=null;setTimeout(function(){delay=Math.min(delay*2,maxDelay);connect()},delay)};",
1754
+ "ws.onclose=function(){V._ws=null;var d3=V._restarting?100:delay;setTimeout(function(){if(!V._restarting){delay=Math.min(delay*2,maxDelay)}connect()},d3)};",
1646
1755
  "ws.onerror=function(){ws.close()}}",
1647
1756
  "connect();",
1648
1757
  "V._sendResolveStack=function(stack,msg){",
@@ -1652,8 +1761,11 @@ function buildErrorChannelScript(editor) {
1652
1761
  "if(V._recovering&&a&&a.innerHTML.length>50)return;",
1653
1762
  "if(V._recovering)V._recovering=false;",
1654
1763
  "V._hadClientError=true;",
1655
- "V.showOverlay(title,V.formatErrors(errors),payload,'client')}",
1764
+ "var sg2=errors&&errors.some(function(e){return V.isStaleGraph(e.message)});",
1765
+ "V.showOverlay(title,V.formatErrors(errors),payload,'client',sg2);",
1766
+ "if(sg2){V._autoRestart()}}",
1656
1767
  "if(V._recovering){setTimeout(function(){V._recovering=false},5000)}",
1768
+ "setTimeout(function(){sessionStorage.removeItem('__vertz_auto_restart')},5000);",
1657
1769
  "window.addEventListener('error',function(e){",
1658
1770
  "var msg=e.message||String(e.error);",
1659
1771
  "var stk=e.error&&e.error.stack;",
@@ -1671,7 +1783,7 @@ function buildErrorChannelScript(editor) {
1671
1783
  "var t=Array.prototype.join.call(arguments,' ');",
1672
1784
  "var hmr=t.match(/\\[vertz-hmr\\] Error re-mounting (\\w+): ([\\s\\S]*?)(?:\\n\\s+at |$)/);",
1673
1785
  "if(hmr){hmrErr=true;V._hadClientError=true;",
1674
- "V.showOverlay('Runtime error',V.formatErrors([{message:hmr[2].split('\\n')[0]}]),{type:'error',category:'runtime',errors:[{message:hmr[2].split('\\n')[0]}]},'client')}",
1786
+ "var hmrMsg=hmr[2].split('\\n')[0];var hmrSg=V.isStaleGraph(hmrMsg);V.showOverlay('Runtime error',V.formatErrors([{message:hmrMsg}]),{type:'error',category:'runtime',errors:[{message:hmrMsg}]},'client',hmrSg);if(hmrSg){V._autoRestart()}}",
1675
1787
  "origCE.apply(console,arguments)};",
1676
1788
  "console.log=function(){",
1677
1789
  "var t=Array.prototype.join.call(arguments,' ');",
@@ -1791,9 +1903,12 @@ function createBunDevServer(options) {
1791
1903
  projectRoot = process.cwd(),
1792
1904
  logRequests = true,
1793
1905
  editor: editorOption,
1794
- headTags = "",
1906
+ headTags: headTagsOption = "",
1795
1907
  sessionResolver
1796
1908
  } = options;
1909
+ const faviconTag = detectFaviconTag(projectRoot);
1910
+ const headTags = [faviconTag, headTagsOption].filter(Boolean).join(`
1911
+ `);
1797
1912
  const editor = detectEditor(editorOption);
1798
1913
  if (apiHandler) {
1799
1914
  installFetchProxy();
@@ -1812,6 +1927,16 @@ function createBunDevServer(options) {
1812
1927
  let clearGraceUntil = 0;
1813
1928
  let runtimeDebounceTimer = null;
1814
1929
  let pendingRuntimeError = null;
1930
+ const autoRestartTimestamps = [];
1931
+ const AUTO_RESTART_CAP = 3;
1932
+ const AUTO_RESTART_WINDOW_MS = 1e4;
1933
+ function canAutoRestart() {
1934
+ const now = Date.now();
1935
+ while (autoRestartTimestamps.length > 0 && now - (autoRestartTimestamps[0] ?? 0) > AUTO_RESTART_WINDOW_MS) {
1936
+ autoRestartTimestamps.shift();
1937
+ }
1938
+ return autoRestartTimestamps.length < AUTO_RESTART_CAP;
1939
+ }
1815
1940
  function broadcastError(category, errors) {
1816
1941
  if (currentError?.category === "build" && category !== "build") {
1817
1942
  return;
@@ -1820,6 +1945,26 @@ function createBunDevServer(options) {
1820
1945
  return;
1821
1946
  }
1822
1947
  if (category === "runtime") {
1948
+ if (errors.some((e) => isStaleGraphError(e.message ?? ""))) {
1949
+ currentError = { category: "runtime", errors };
1950
+ const errMsg = JSON.stringify({ type: "error", category: "runtime", errors });
1951
+ for (const ws of wsClients) {
1952
+ ws.sendText(errMsg);
1953
+ }
1954
+ if (!isRestarting && canAutoRestart()) {
1955
+ autoRestartTimestamps.push(Date.now());
1956
+ if (logRequests) {
1957
+ const truncated = errors[0]?.message?.slice(0, 80) ?? "";
1958
+ console.log(`[Server] Stale graph detected: ${truncated}`);
1959
+ }
1960
+ devServer.restart();
1961
+ } else if (!isRestarting && !canAutoRestart()) {
1962
+ if (logRequests) {
1963
+ console.log("[Server] Auto-restart cap reached (3 in 10s), waiting for manual restart");
1964
+ }
1965
+ }
1966
+ return;
1967
+ }
1823
1968
  if (!pendingRuntimeError || errors.some((e) => e.file)) {
1824
1969
  pendingRuntimeError = errors;
1825
1970
  }
@@ -2027,32 +2172,42 @@ function createBunDevServer(options) {
2027
2172
  }
2028
2173
  return new Response("OpenAPI spec not found", { status: 404 });
2029
2174
  };
2175
+ let isRestarting = false;
2176
+ let pluginsRegistered = false;
2177
+ let stableUpdateManifest = null;
2030
2178
  async function start() {
2031
2179
  const { plugin } = await Promise.resolve(globalThis.Bun);
2032
2180
  const { createVertzBunPlugin } = await import("./bun-plugin/index.js");
2033
2181
  const entryPath = resolve(projectRoot, entry);
2034
2182
  const rawClientSrc = clientEntryOption ?? entry;
2035
2183
  const clientSrc = rawClientSrc.replace(/^\.\//, "/");
2036
- plugin({
2037
- name: "vertz-ssr-jsx-swap",
2038
- setup(build) {
2039
- build.onResolve({ filter: /^@vertz\/ui\/jsx-runtime$/ }, () => ({
2040
- path: "@vertz/ui-server/jsx-runtime",
2041
- external: false
2042
- }));
2043
- build.onResolve({ filter: /^@vertz\/ui\/jsx-dev-runtime$/ }, () => ({
2044
- path: "@vertz/ui-server/jsx-runtime",
2045
- external: false
2046
- }));
2047
- }
2048
- });
2049
- const { plugin: serverPlugin, updateManifest: updateServerManifest } = createVertzBunPlugin({
2050
- hmr: false,
2051
- fastRefresh: false,
2052
- logger,
2053
- diagnostics
2054
- });
2055
- plugin(serverPlugin);
2184
+ if (!pluginsRegistered) {
2185
+ plugin({
2186
+ name: "vertz-ssr-jsx-swap",
2187
+ setup(build) {
2188
+ build.onResolve({ filter: /^@vertz\/ui\/jsx-runtime$/ }, () => ({
2189
+ path: "@vertz/ui-server/jsx-runtime",
2190
+ external: false
2191
+ }));
2192
+ build.onResolve({ filter: /^@vertz\/ui\/jsx-dev-runtime$/ }, () => ({
2193
+ path: "@vertz/ui-server/jsx-runtime",
2194
+ external: false
2195
+ }));
2196
+ }
2197
+ });
2198
+ }
2199
+ if (!pluginsRegistered) {
2200
+ const { plugin: serverPlugin, updateManifest } = createVertzBunPlugin({
2201
+ hmr: false,
2202
+ fastRefresh: false,
2203
+ logger,
2204
+ diagnostics
2205
+ });
2206
+ plugin(serverPlugin);
2207
+ stableUpdateManifest = updateManifest;
2208
+ }
2209
+ pluginsRegistered = true;
2210
+ const updateServerManifest = stableUpdateManifest;
2056
2211
  let ssrMod;
2057
2212
  try {
2058
2213
  ssrMod = await import(entryPath);
@@ -2061,6 +2216,9 @@ function createBunDevServer(options) {
2061
2216
  }
2062
2217
  } catch (e) {
2063
2218
  console.error("[Server] Failed to load SSR module:", e);
2219
+ if (isRestarting) {
2220
+ throw e;
2221
+ }
2064
2222
  process.exit(1);
2065
2223
  }
2066
2224
  let fontFallbackMetrics;
@@ -2265,6 +2423,18 @@ data: {}
2265
2423
  console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
2266
2424
  }
2267
2425
  }
2426
+ if (ssrAuth && apiHandler) {
2427
+ try {
2428
+ const origin = `http://${host}:${server?.port}`;
2429
+ const provRes = await apiHandler(new Request(`${origin}${skipSSRPaths[0]}auth/providers`));
2430
+ if (provRes.ok) {
2431
+ const providers = await provRes.json();
2432
+ ssrAuth.providers = providers;
2433
+ sessionScript += `
2434
+ <script>window.__VERTZ_PROVIDERS__=${safeSerialize(providers)};</script>`;
2435
+ }
2436
+ } catch {}
2437
+ }
2268
2438
  const doRender = async () => {
2269
2439
  logger.log("ssr", "render-start", { url: pathname });
2270
2440
  const ssrStart = performance.now();
@@ -2348,7 +2518,9 @@ data: {}
2348
2518
  message(ws, msg) {
2349
2519
  try {
2350
2520
  const data = JSON.parse(typeof msg === "string" ? msg : new TextDecoder().decode(msg));
2351
- if (data.type === "ping") {
2521
+ if (data.type === "restart") {
2522
+ devServer.restart();
2523
+ } else if (data.type === "ping") {
2352
2524
  ws.sendText(JSON.stringify({ type: "pong" }));
2353
2525
  } else if (data.type === "resolve-stack" && data.stack) {
2354
2526
  const selfFetch = async (url) => {
@@ -2602,7 +2774,7 @@ data: {}
2602
2774
  });
2603
2775
  }
2604
2776
  }
2605
- return {
2777
+ const devServer = {
2606
2778
  start,
2607
2779
  broadcastError,
2608
2780
  clearError,
@@ -2628,13 +2800,73 @@ data: {}
2628
2800
  server.stop(true);
2629
2801
  server = null;
2630
2802
  }
2803
+ },
2804
+ async restart() {
2805
+ if (isRestarting) {
2806
+ if (logRequests) {
2807
+ console.log("[Server] Restart already in progress, skipping");
2808
+ }
2809
+ return;
2810
+ }
2811
+ isRestarting = true;
2812
+ if (logRequests) {
2813
+ console.log("[Server] Restarting dev server...");
2814
+ }
2815
+ const restartMsg = JSON.stringify({ type: "restarting" });
2816
+ for (const ws of wsClients) {
2817
+ try {
2818
+ ws.sendText(restartMsg);
2819
+ } catch {}
2820
+ }
2821
+ await devServer.stop();
2822
+ wsClients.clear();
2823
+ currentError = null;
2824
+ if (runtimeDebounceTimer) {
2825
+ clearTimeout(runtimeDebounceTimer);
2826
+ runtimeDebounceTimer = null;
2827
+ }
2828
+ pendingRuntimeError = null;
2829
+ lastBuildError = "";
2830
+ lastBroadcastedError = "";
2831
+ lastChangedFile = "";
2832
+ clearGraceUntil = 0;
2833
+ terminalDedup.reset();
2834
+ clearSSRRequireCache();
2835
+ sourceMapResolver.invalidate();
2836
+ const retryDelays = [100, 200, 500];
2837
+ let lastErr;
2838
+ for (let attempt = 0;attempt < retryDelays.length; attempt++) {
2839
+ await new Promise((r) => setTimeout(r, retryDelays[attempt]));
2840
+ try {
2841
+ await start();
2842
+ if (logRequests) {
2843
+ console.log(`[Server] Dev server restarted on port ${port}`);
2844
+ }
2845
+ lastErr = null;
2846
+ break;
2847
+ } catch (e) {
2848
+ lastErr = e;
2849
+ if (logRequests) {
2850
+ const errMsg = e instanceof Error ? e.message : String(e);
2851
+ console.log(`[Server] Restart attempt ${attempt + 1} failed: ${errMsg}`);
2852
+ }
2853
+ }
2854
+ }
2855
+ if (lastErr) {
2856
+ const errMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
2857
+ console.error(`[Server] Restart failed after ${retryDelays.length} attempts: ${errMsg}`);
2858
+ }
2859
+ isRestarting = false;
2631
2860
  }
2632
2861
  };
2862
+ return devServer;
2633
2863
  }
2634
2864
  export {
2635
2865
  parseHMRAssets,
2866
+ isStaleGraphError,
2636
2867
  generateSSRPageHtml,
2637
2868
  formatTerminalRuntimeError,
2869
+ detectFaviconTag,
2638
2870
  createRuntimeErrorDeduplicator,
2639
2871
  createFetchInterceptor,
2640
2872
  createBunDevServer,
@@ -607,6 +607,7 @@ function extractStaticProps(element, sourceFile) {
607
607
  if (alt === null)
608
608
  return null;
609
609
  break;
610
+ case "className":
610
611
  case "class":
611
612
  if (value) {
612
613
  className = extractStaticString(value, sourceFile) ?? undefined;
@@ -63,8 +63,9 @@ declare class SSRElement extends SSRNode {
63
63
  display: string;
64
64
  [key: string]: any;
65
65
  };
66
+ dataset: DOMStringMap;
66
67
  constructor(tag: string);
67
- setAttribute(name: string, value: string): void;
68
+ setAttribute(name: string, value: string | Record<string, any>): void;
68
69
  getAttribute(name: string): string | null;
69
70
  removeAttribute(name: string): void;
70
71
  appendChild(child: SSRElement | SSRTextNode | SSRComment | SSRDocumentFragment): void;
@@ -7,7 +7,7 @@ import {
7
7
  installDomShim,
8
8
  removeDomShim,
9
9
  toVNode
10
- } from "../shared/chunk-9jjdzz8c.js";
10
+ } from "../shared/chunk-8matma4a.js";
11
11
  export {
12
12
  toVNode,
13
13
  removeDomShim,
package/dist/index.d.ts CHANGED
@@ -374,7 +374,7 @@ declare function clearGlobalSSRTimeout(): void;
374
374
  */
375
375
  declare function getGlobalSSRTimeout(): number | undefined;
376
376
  import { FontFallbackMetrics as FontFallbackMetrics4 } from "@vertz/ui";
377
- import { FontFallbackMetrics as FontFallbackMetrics3, Theme as Theme2 } from "@vertz/ui";
377
+ import { CompiledRoute, FontFallbackMetrics as FontFallbackMetrics3, Theme as Theme2 } from "@vertz/ui";
378
378
  import { SSRAuth as SSRAuth_jq1nwm } from "@vertz/ui/internals";
379
379
  interface SSRModule {
380
380
  default?: () => unknown;
@@ -390,6 +390,8 @@ interface SSRModule {
390
390
  * SSR renderer. Export `getInjectedCSS` from @vertz/ui in the app entry.
391
391
  */
392
392
  getInjectedCSS?: () => string[];
393
+ /** Compiled routes exported from the app for build-time SSG with generateParams. */
394
+ routes?: CompiledRoute[];
393
395
  }
394
396
  interface SSRRenderResult {
395
397
  html: string;
@@ -402,6 +404,8 @@ interface SSRRenderResult {
402
404
  headTags: string;
403
405
  /** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
404
406
  discoveredRoutes?: string[];
407
+ /** Route patterns that matched the current URL (for per-route modulepreload). */
408
+ matchedRoutePatterns?: string[];
405
409
  /** Set when ProtectedRoute writes a redirect during SSR. Server should return 302. */
406
410
  redirect?: {
407
411
  to: string;
@@ -500,6 +504,13 @@ interface SSRHandlerOptions {
500
504
  fallbackMetrics?: Record<string, FontFallbackMetrics4>;
501
505
  /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
502
506
  modulepreload?: string[];
507
+ /**
508
+ * Route chunk manifest for per-route modulepreload injection.
509
+ * When provided, only chunks for the matched route are preloaded instead of all chunks.
510
+ */
511
+ routeChunkManifest?: {
512
+ routes: Record<string, string[]>;
513
+ };
503
514
  /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
504
515
  cacheControl?: string;
505
516
  /**
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  ssrDiscoverQueries,
20
20
  ssrRenderToString,
21
21
  streamToString
22
- } from "./shared/chunk-c5ee9yf1.js";
22
+ } from "./shared/chunk-vyjj02de.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-9jjdzz8c.js";
34
+ } from "./shared/chunk-8matma4a.js";
35
35
 
36
36
  // src/asset-pipeline.ts
37
37
  function renderAssetTags(assets) {
@@ -29,13 +29,18 @@ function jsx(tag, props) {
29
29
  }
30
30
  const { children, ...attrs } = props || {};
31
31
  const serializableAttrs = {};
32
+ const resolvedClass = unwrapSignal(attrs.className) ?? unwrapSignal(attrs.class);
32
33
  for (const [key, rawValue] of Object.entries(attrs)) {
33
34
  if (key.startsWith("on") && typeof rawValue === "function") {
34
35
  continue;
35
36
  }
36
37
  const value = unwrapSignal(rawValue);
37
- if (key === "class" && value != null) {
38
- serializableAttrs.class = String(value);
38
+ if (key === "className" || key === "class") {
39
+ if (key === "class" && attrs.className != null)
40
+ continue;
41
+ if (resolvedClass != null) {
42
+ serializableAttrs.class = String(resolvedClass);
43
+ }
39
44
  continue;
40
45
  }
41
46
  if (key === "style" && value != null) {
@@ -61,6 +61,9 @@ class SSRComment extends SSRNode {
61
61
  }
62
62
  }
63
63
 
64
+ // src/dom-shim/ssr-element.ts
65
+ import { __styleStr } from "@vertz/ui/internals";
66
+
64
67
  // src/types.ts
65
68
  function rawHtml(html) {
66
69
  return { __raw: true, html };
@@ -104,6 +107,52 @@ class SSRDocumentFragment extends SSRNode {
104
107
  }
105
108
 
106
109
  // src/dom-shim/ssr-element.ts
110
+ function camelToKebab(str) {
111
+ return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
112
+ }
113
+ function kebabToCamel(str) {
114
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
115
+ }
116
+ function createDatasetProxy(element) {
117
+ return new Proxy({}, {
118
+ set(_target, prop, value) {
119
+ if (typeof prop === "string") {
120
+ element.setAttribute(`data-${camelToKebab(prop)}`, String(value));
121
+ }
122
+ return true;
123
+ },
124
+ get(_target, prop) {
125
+ if (typeof prop === "string") {
126
+ return element.getAttribute(`data-${camelToKebab(prop)}`) ?? undefined;
127
+ }
128
+ return;
129
+ },
130
+ has(_target, prop) {
131
+ if (typeof prop === "string") {
132
+ return element.getAttribute(`data-${camelToKebab(prop)}`) !== null;
133
+ }
134
+ return false;
135
+ },
136
+ deleteProperty(_target, prop) {
137
+ if (typeof prop === "string") {
138
+ element.removeAttribute(`data-${camelToKebab(prop)}`);
139
+ }
140
+ return true;
141
+ },
142
+ ownKeys() {
143
+ return Object.keys(element.attrs).filter((k) => k.startsWith("data-")).map((k) => kebabToCamel(k.slice(5)));
144
+ },
145
+ getOwnPropertyDescriptor(_target, prop) {
146
+ if (typeof prop === "string") {
147
+ const val = element.getAttribute(`data-${camelToKebab(prop)}`);
148
+ if (val !== null) {
149
+ return { configurable: true, enumerable: true, value: val };
150
+ }
151
+ }
152
+ return;
153
+ }
154
+ });
155
+ }
107
156
  function createStyleProxy(element) {
108
157
  const styles = {};
109
158
  return new Proxy(styles, {
@@ -135,16 +184,27 @@ class SSRElement extends SSRNode {
135
184
  _textContent = null;
136
185
  _innerHTML = null;
137
186
  style;
187
+ dataset;
138
188
  constructor(tag) {
139
189
  super();
140
190
  this.tag = tag;
141
191
  this.style = createStyleProxy(this);
192
+ this.dataset = createDatasetProxy(this);
142
193
  }
143
194
  setAttribute(name, value) {
144
- if (name === "class") {
195
+ if (name === "style" && typeof value === "object" && value !== null) {
196
+ this.attrs.style = __styleStr(value);
197
+ for (const [k, v] of Object.entries(value)) {
198
+ if (v != null)
199
+ this.style[k] = String(v);
200
+ }
201
+ return;
202
+ }
203
+ const attrName = name === "className" ? "class" : name;
204
+ if (attrName === "class") {
145
205
  this._classList = new Set(value.split(/\s+/).filter(Boolean));
146
206
  }
147
- this.attrs[name] = value;
207
+ this.attrs[attrName] = value;
148
208
  }
149
209
  getAttribute(name) {
150
210
  return this.attrs[name] ?? null;
@@ -6,7 +6,7 @@ import {
6
6
  setGlobalSSRTimeout,
7
7
  ssrStorage,
8
8
  toVNode
9
- } from "./chunk-9jjdzz8c.js";
9
+ } from "./chunk-8matma4a.js";
10
10
 
11
11
  // src/html-serializer.ts
12
12
  var VOID_ELEMENTS = new Set([
@@ -330,7 +330,8 @@ async function ssrRenderToString(module, url, options) {
330
330
  css,
331
331
  ssrData,
332
332
  headTags: themePreloadTags,
333
- discoveredRoutes: ctx.discoveredRoutes
333
+ discoveredRoutes: ctx.discoveredRoutes,
334
+ matchedRoutePatterns: ctx.matchedRoutePatterns
334
335
  };
335
336
  } finally {
336
337
  clearGlobalSSRTimeout();
@@ -536,7 +537,7 @@ function injectIntoTemplate(options) {
536
537
  if (template.includes("<!--ssr-outlet-->")) {
537
538
  html = template.replace("<!--ssr-outlet-->", appHtml);
538
539
  } else {
539
- html = template.replace(/(<div[^>]*id="app"[^>]*>)([\s\S]*?)(<\/div>)/, `$1${appHtml}$3`);
540
+ html = replaceAppDivContent(template, appHtml);
540
541
  }
541
542
  if (headTags) {
542
543
  html = html.replace("</head>", `${headTags}
@@ -560,6 +561,37 @@ function injectIntoTemplate(options) {
560
561
  }
561
562
  return html;
562
563
  }
564
+ function replaceAppDivContent(template, appHtml) {
565
+ const openMatch = template.match(/<div[^>]*id="app"[^>]*>/);
566
+ if (!openMatch || openMatch.index == null)
567
+ return template;
568
+ const openTag = openMatch[0];
569
+ const contentStart = openMatch.index + openTag.length;
570
+ let depth = 1;
571
+ let i = contentStart;
572
+ const len = template.length;
573
+ while (i < len && depth > 0) {
574
+ if (template[i] === "<") {
575
+ if (template.startsWith("</div>", i)) {
576
+ depth--;
577
+ if (depth === 0)
578
+ break;
579
+ i += 6;
580
+ } else if (template.startsWith("<div", i) && /^<div[\s>]/.test(template.slice(i, i + 5))) {
581
+ depth++;
582
+ i += 4;
583
+ } else {
584
+ i++;
585
+ }
586
+ } else {
587
+ i++;
588
+ }
589
+ }
590
+ if (depth !== 0) {
591
+ return template;
592
+ }
593
+ return template.slice(0, contentStart) + appHtml + template.slice(i);
594
+ }
563
595
 
564
596
  // src/ssr-handler.ts
565
597
  import { compileTheme as compileTheme2 } from "@vertz/ui";
@@ -595,6 +627,7 @@ function createSSRHandler(options) {
595
627
  nonce,
596
628
  fallbackMetrics,
597
629
  modulepreload,
630
+ routeChunkManifest,
598
631
  cacheControl,
599
632
  sessionResolver
600
633
  } = options;
@@ -646,7 +679,7 @@ function createSSRHandler(options) {
646
679
  console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
647
680
  }
648
681
  }
649
- return handleHTMLRequest(module, template, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript, ssrAuth);
682
+ return handleHTMLRequest(module, template, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth);
650
683
  };
651
684
  }
652
685
  async function handleNavRequest(module, url, ssrTimeout) {
@@ -672,7 +705,7 @@ data: {}
672
705
  });
673
706
  }
674
707
  }
675
- async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript, ssrAuth) {
708
+ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, staticModulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth) {
676
709
  try {
677
710
  const result = await ssrRenderToString(module, url, { ssrTimeout, fallbackMetrics, ssrAuth });
678
711
  if (result.redirect) {
@@ -681,6 +714,21 @@ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallb
681
714
  headers: { Location: result.redirect.to }
682
715
  });
683
716
  }
717
+ let modulepreloadTags = staticModulepreloadTags;
718
+ if (routeChunkManifest && result.matchedRoutePatterns?.length) {
719
+ const chunkPaths = new Set;
720
+ for (const pattern of result.matchedRoutePatterns) {
721
+ const chunks = routeChunkManifest.routes[pattern];
722
+ if (chunks) {
723
+ for (const chunk of chunks) {
724
+ chunkPaths.add(chunk);
725
+ }
726
+ }
727
+ }
728
+ if (chunkPaths.size > 0) {
729
+ modulepreloadTags = buildModulepreloadTags([...chunkPaths]);
730
+ }
731
+ }
684
732
  const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
685
733
  `);
686
734
  const html = injectIntoTemplate({
@@ -1,5 +1,5 @@
1
- import { CompiledRoute } from "@vertz/ui";
2
- import { FontFallbackMetrics, Theme } from "@vertz/ui";
1
+ import { CompiledRoute as CompiledRoute2 } from "@vertz/ui";
2
+ import { CompiledRoute, FontFallbackMetrics, Theme } from "@vertz/ui";
3
3
  import { SSRAuth as SSRAuth_jq1nwm } from "@vertz/ui/internals";
4
4
  interface SSRModule {
5
5
  default?: () => unknown;
@@ -15,6 +15,8 @@ interface SSRModule {
15
15
  * SSR renderer. Export `getInjectedCSS` from @vertz/ui in the app entry.
16
16
  */
17
17
  getInjectedCSS?: () => string[];
18
+ /** Compiled routes exported from the app for build-time SSG with generateParams. */
19
+ routes?: CompiledRoute[];
18
20
  }
19
21
  interface SSRRenderResult {
20
22
  html: string;
@@ -27,6 +29,8 @@ interface SSRRenderResult {
27
29
  headTags: string;
28
30
  /** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
29
31
  discoveredRoutes?: string[];
32
+ /** Route patterns that matched the current URL (for per-route modulepreload). */
33
+ matchedRoutePatterns?: string[];
30
34
  /** Set when ProtectedRoute writes a redirect during SSR. Server should return 302. */
31
35
  redirect?: {
32
36
  to: string;
@@ -88,7 +92,7 @@ declare function discoverRoutes(module: SSRModule): Promise<string[]>;
88
92
  * - Routes with `*` wildcard
89
93
  * - Routes with `prerender: false` (looked up in compiledRoutes)
90
94
  */
91
- declare function filterPrerenderableRoutes(patterns: string[], compiledRoutes?: CompiledRoute[]): string[];
95
+ declare function filterPrerenderableRoutes(patterns: string[], compiledRoutes?: CompiledRoute2[]): string[];
92
96
  /**
93
97
  * Pre-render a list of routes into complete HTML strings.
94
98
  *
@@ -110,6 +114,16 @@ declare function prerenderRoutes(module: SSRModule, template: string, options: P
110
114
  * pages (like /manifesto) ship zero JavaScript.
111
115
  */
112
116
  declare function stripScriptsFromStaticHTML(html: string): string;
117
+ /**
118
+ * Collect all paths that should be pre-rendered from compiled routes.
119
+ *
120
+ * Walks the route tree and collects:
121
+ * - Static routes with `prerender: true`
122
+ * - Dynamic routes with `generateParams` (expanded to concrete paths)
123
+ * - Skips routes with `prerender: false`
124
+ * - Routes without `prerender` or `generateParams` are not collected
125
+ */
126
+ declare function collectPrerenderPaths(routes: CompiledRoute2[], prefix?: string): Promise<string[]>;
113
127
  import { FontFallbackMetrics as FontFallbackMetrics2 } from "@vertz/ui";
114
128
  import { AccessSet } from "@vertz/ui/auth";
115
129
  interface SessionData {
@@ -167,6 +181,13 @@ interface SSRHandlerOptions {
167
181
  fallbackMetrics?: Record<string, FontFallbackMetrics2>;
168
182
  /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
169
183
  modulepreload?: string[];
184
+ /**
185
+ * Route chunk manifest for per-route modulepreload injection.
186
+ * When provided, only chunks for the matched route are preloaded instead of all chunks.
187
+ */
188
+ routeChunkManifest?: {
189
+ routes: Record<string, string[]>;
190
+ };
170
191
  /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
171
192
  cacheControl?: string;
172
193
  /**
@@ -197,4 +218,4 @@ interface InjectIntoTemplateOptions {
197
218
  * injects CSS before </head>, and ssrData before </body>.
198
219
  */
199
220
  declare function injectIntoTemplate(options: InjectIntoTemplateOptions): string;
200
- export { stripScriptsFromStaticHTML, ssrRenderToString, ssrDiscoverQueries, prerenderRoutes, injectIntoTemplate, filterPrerenderableRoutes, discoverRoutes, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult, PrerenderResult, PrerenderOptions };
221
+ export { stripScriptsFromStaticHTML, ssrRenderToString, ssrDiscoverQueries, prerenderRoutes, injectIntoTemplate, filterPrerenderableRoutes, discoverRoutes, createSSRHandler, collectPrerenderPaths, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult, PrerenderResult, PrerenderOptions };
package/dist/ssr/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  injectIntoTemplate,
4
4
  ssrDiscoverQueries,
5
5
  ssrRenderToString
6
- } from "../shared/chunk-c5ee9yf1.js";
7
- import"../shared/chunk-9jjdzz8c.js";
6
+ } from "../shared/chunk-vyjj02de.js";
7
+ import"../shared/chunk-8matma4a.js";
8
8
 
9
9
  // src/prerender.ts
10
10
  async function discoverRoutes(module) {
@@ -56,6 +56,36 @@ function stripScriptsFromStaticHTML(html) {
56
56
  result = result.replace(/<link\b[^>]*\brel=["']modulepreload["'][^>]*\/?>/gi, "");
57
57
  return result;
58
58
  }
59
+ async function collectPrerenderPaths(routes, prefix = "") {
60
+ const paths = [];
61
+ for (const route of routes) {
62
+ const fullPattern = joinPatterns(prefix, route.pattern);
63
+ const optedOut = route.prerender === false;
64
+ if (!optedOut) {
65
+ if (route.generateParams) {
66
+ const paramSets = await route.generateParams();
67
+ for (const params of paramSets) {
68
+ let path = fullPattern;
69
+ for (const [key, value] of Object.entries(params)) {
70
+ path = path.replaceAll(`:${key}`, value);
71
+ }
72
+ const unreplaced = path.match(/:(\w+)/);
73
+ if (unreplaced) {
74
+ throw new Error(`generateParams for "${fullPattern}" returned params missing key "${unreplaced[1]}"`);
75
+ }
76
+ paths.push(path);
77
+ }
78
+ } else if (route.prerender === true) {
79
+ paths.push(fullPattern);
80
+ }
81
+ }
82
+ if (route.children) {
83
+ const childPaths = await collectPrerenderPaths(route.children, fullPattern);
84
+ paths.push(...childPaths);
85
+ }
86
+ }
87
+ return paths;
88
+ }
59
89
  function findCompiledRoute(routes, pattern, prefix = "") {
60
90
  for (const route of routes) {
61
91
  const fullPattern = joinPatterns(prefix, route.pattern);
@@ -84,5 +114,6 @@ export {
84
114
  injectIntoTemplate,
85
115
  filterPrerenderableRoutes,
86
116
  discoverRoutes,
87
- createSSRHandler
117
+ createSSRHandler,
118
+ collectPrerenderPaths
88
119
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/ui-server",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
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.18",
62
- "@vertz/ui": "^0.2.18",
63
- "@vertz/ui-compiler": "^0.2.18",
61
+ "@vertz/core": "^0.2.20",
62
+ "@vertz/ui": "^0.2.20",
63
+ "@vertz/ui-compiler": "^0.2.20",
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.18",
69
+ "@vertz/codegen": "^0.2.20",
70
70
  "@vertz/ui-auth": "^0.2.18",
71
71
  "bun-types": "^1.3.10",
72
72
  "bunup": "^0.16.31",