@vertz/ui-server 0.2.15 → 0.2.16

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.
@@ -6,7 +6,7 @@ import {
6
6
  setGlobalSSRTimeout,
7
7
  ssrStorage,
8
8
  toVNode
9
- } from "./chunk-n1arq9xq.js";
9
+ } from "./chunk-9jjdzz8c.js";
10
10
 
11
11
  // src/html-serializer.ts
12
12
  var VOID_ELEMENTS = new Set([
@@ -209,7 +209,7 @@ function createRequestContext(url) {
209
209
  contextScope: null,
210
210
  entityStore: new EntityStore,
211
211
  envelopeStore: new QueryEnvelopeStore,
212
- queryCache: new MemoryCache,
212
+ queryCache: new MemoryCache({ maxSize: Infinity }),
213
213
  inflight: new Map,
214
214
  queries: [],
215
215
  errors: []
@@ -230,9 +230,6 @@ function resolveAppFactory(module) {
230
230
  return createApp;
231
231
  }
232
232
  function collectCSS(themeCss, module) {
233
- const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
234
- const globalTags = module.styles ? module.styles.map((s) => `<style data-vertz-css>${s}</style>`).join(`
235
- `) : "";
236
233
  const alreadyIncluded = new Set;
237
234
  if (themeCss)
238
235
  alreadyIncluded.add(themeCss);
@@ -241,9 +238,12 @@ function collectCSS(themeCss, module) {
241
238
  alreadyIncluded.add(s);
242
239
  }
243
240
  const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
244
- const componentStyles = componentCss.map((s) => `<style data-vertz-css>${s}</style>`).join(`
245
- `);
246
- return [themeTag, globalTags, componentStyles].filter(Boolean).join(`
241
+ const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
242
+ const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
243
+ `)}</style>` : "";
244
+ const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
245
+ `)}</style>` : "";
246
+ return [themeTag, globalTag, componentTag].filter(Boolean).join(`
247
247
  `);
248
248
  }
249
249
  async function ssrRenderToString(module, url, options) {
@@ -256,14 +256,40 @@ async function ssrRenderToString(module, url, options) {
256
256
  setGlobalSSRTimeout(ssrTimeout);
257
257
  const createApp = resolveAppFactory(module);
258
258
  let themeCss = "";
259
+ let themePreloadTags = "";
259
260
  if (module.theme) {
260
261
  try {
261
- themeCss = compileTheme(module.theme).css;
262
+ const compiled = compileTheme(module.theme, {
263
+ fallbackMetrics: options?.fallbackMetrics
264
+ });
265
+ themeCss = compiled.css;
266
+ themePreloadTags = compiled.preloadTags;
262
267
  } catch (e) {
263
268
  console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
264
269
  }
265
270
  }
266
271
  createApp();
272
+ const store = ssrStorage.getStore();
273
+ if (store) {
274
+ if (store.pendingRouteComponents?.size) {
275
+ const entries = Array.from(store.pendingRouteComponents.entries());
276
+ const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
277
+ promise.then((mod) => ({ route, factory: mod.default })),
278
+ new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
279
+ ])));
280
+ store.resolvedComponents = new Map;
281
+ for (const result of results) {
282
+ if (result.status === "fulfilled") {
283
+ const { route, factory } = result.value;
284
+ store.resolvedComponents.set(route, factory);
285
+ }
286
+ }
287
+ store.pendingRouteComponents = undefined;
288
+ }
289
+ if (!store.resolvedComponents) {
290
+ store.resolvedComponents = new Map;
291
+ }
292
+ }
267
293
  const queries = getSSRQueries();
268
294
  const resolvedQueries = [];
269
295
  if (queries.length > 0) {
@@ -275,7 +301,6 @@ async function ssrRenderToString(module, url, options) {
275
301
  }),
276
302
  new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
277
303
  ])));
278
- const store = ssrStorage.getStore();
279
304
  if (store)
280
305
  store.queries = [];
281
306
  }
@@ -288,7 +313,13 @@ async function ssrRenderToString(module, url, options) {
288
313
  key,
289
314
  data: JSON.parse(JSON.stringify(data))
290
315
  })) : [];
291
- return { html, css, ssrData };
316
+ return {
317
+ html,
318
+ css,
319
+ ssrData,
320
+ headTags: themePreloadTags,
321
+ discoveredRoutes: ctx.discoveredRoutes
322
+ };
292
323
  } finally {
293
324
  clearGlobalSSRTimeout();
294
325
  }
@@ -439,17 +470,75 @@ data: ${safeSerialize(entry)}
439
470
  });
440
471
  }
441
472
 
442
- // src/ssr-handler.ts
443
- function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
473
+ // src/ssr-access-set.ts
474
+ function getAccessSetForSSR(jwtPayload) {
475
+ if (!jwtPayload)
476
+ return null;
477
+ const acl = jwtPayload.acl;
478
+ if (!acl)
479
+ return null;
480
+ if (acl.overflow)
481
+ return null;
482
+ if (!acl.set)
483
+ return null;
484
+ return {
485
+ entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
486
+ name,
487
+ {
488
+ allowed: check.allowed,
489
+ reasons: check.reasons ?? [],
490
+ ...check.reason ? { reason: check.reason } : {},
491
+ ...check.meta ? { meta: check.meta } : {}
492
+ }
493
+ ])),
494
+ flags: acl.set.flags,
495
+ plan: acl.set.plan,
496
+ computedAt: acl.set.computedAt
497
+ };
498
+ }
499
+ function createAccessSetScript(accessSet, nonce) {
500
+ const json = JSON.stringify(accessSet);
501
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
502
+ const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
503
+ return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
504
+ }
505
+ function escapeAttr2(s) {
506
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
507
+ }
508
+
509
+ // src/ssr-session.ts
510
+ function createSessionScript(session, nonce) {
511
+ const json = JSON.stringify(session);
512
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
513
+ const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
514
+ return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
515
+ }
516
+ function escapeAttr3(s) {
517
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
518
+ }
519
+
520
+ // src/template-inject.ts
521
+ function injectIntoTemplate(options) {
522
+ const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
444
523
  let html;
445
524
  if (template.includes("<!--ssr-outlet-->")) {
446
525
  html = template.replace("<!--ssr-outlet-->", appHtml);
447
526
  } else {
448
527
  html = template.replace(/(<div[^>]*id="app"[^>]*>)([\s\S]*?)(<\/div>)/, `$1${appHtml}$3`);
449
528
  }
529
+ if (headTags) {
530
+ html = html.replace("</head>", `${headTags}
531
+ </head>`);
532
+ }
450
533
  if (appCss) {
534
+ html = html.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
535
+ <noscript>${match}</noscript>`);
451
536
  html = html.replace("</head>", `${appCss}
452
537
  </head>`);
538
+ }
539
+ if (sessionScript) {
540
+ html = html.replace("</body>", `${sessionScript}
541
+ </body>`);
453
542
  }
454
543
  if (ssrData.length > 0) {
455
544
  const nonceAttr = nonce != null ? ` nonce="${nonce}"` : "";
@@ -459,8 +548,44 @@ function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
459
548
  }
460
549
  return html;
461
550
  }
551
+
552
+ // src/ssr-handler.ts
553
+ import { compileTheme as compileTheme2 } from "@vertz/ui";
554
+ function sanitizeLinkHref(href) {
555
+ return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
556
+ }
557
+ function sanitizeLinkParam(value) {
558
+ return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
559
+ }
560
+ function buildLinkHeader(items) {
561
+ return items.map((item) => {
562
+ const parts = [
563
+ `<${sanitizeLinkHref(item.href)}>`,
564
+ "rel=preload",
565
+ `as=${sanitizeLinkParam(item.as)}`
566
+ ];
567
+ if (item.type)
568
+ parts.push(`type=${sanitizeLinkParam(item.type)}`);
569
+ if (item.crossorigin)
570
+ parts.push("crossorigin");
571
+ return parts.join("; ");
572
+ }).join(", ");
573
+ }
574
+ function buildModulepreloadTags(paths) {
575
+ return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
576
+ `);
577
+ }
462
578
  function createSSRHandler(options) {
463
- const { module, ssrTimeout, inlineCSS, nonce } = options;
579
+ const {
580
+ module,
581
+ ssrTimeout,
582
+ inlineCSS,
583
+ nonce,
584
+ fallbackMetrics,
585
+ modulepreload,
586
+ cacheControl,
587
+ sessionResolver
588
+ } = options;
464
589
  let template = options.template;
465
590
  if (inlineCSS) {
466
591
  for (const [href, css] of Object.entries(inlineCSS)) {
@@ -470,13 +595,38 @@ function createSSRHandler(options) {
470
595
  template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
471
596
  }
472
597
  }
598
+ let linkHeader;
599
+ if (module.theme) {
600
+ const compiled = compileTheme2(module.theme, { fallbackMetrics });
601
+ if (compiled.preloadItems.length > 0) {
602
+ linkHeader = buildLinkHeader(compiled.preloadItems);
603
+ }
604
+ }
605
+ const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
473
606
  return async (request) => {
474
607
  const url = new URL(request.url);
475
608
  const pathname = url.pathname;
476
609
  if (request.headers.get("x-vertz-nav") === "1") {
477
610
  return handleNavRequest(module, pathname, ssrTimeout);
478
611
  }
479
- return handleHTMLRequest(module, template, pathname, ssrTimeout, nonce);
612
+ let sessionScript = "";
613
+ if (sessionResolver) {
614
+ try {
615
+ const sessionResult = await sessionResolver(request);
616
+ if (sessionResult) {
617
+ const scripts = [];
618
+ scripts.push(createSessionScript(sessionResult.session, nonce));
619
+ if (sessionResult.accessSet != null) {
620
+ scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
621
+ }
622
+ sessionScript = scripts.join(`
623
+ `);
624
+ }
625
+ } catch (resolverErr) {
626
+ console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
627
+ }
628
+ }
629
+ return handleHTMLRequest(module, template, pathname, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript);
480
630
  };
481
631
  }
482
632
  async function handleNavRequest(module, url, ssrTimeout) {
@@ -502,15 +652,28 @@ data: {}
502
652
  });
503
653
  }
504
654
  }
505
- async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
655
+ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript) {
506
656
  try {
507
- const result = await ssrRenderToString(module, url, { ssrTimeout });
508
- const html = injectIntoTemplate(template, result.html, result.css, result.ssrData, nonce);
509
- return new Response(html, {
510
- status: 200,
511
- headers: { "Content-Type": "text/html; charset=utf-8" }
657
+ const result = await ssrRenderToString(module, url, { ssrTimeout, fallbackMetrics });
658
+ const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
659
+ `);
660
+ const html = injectIntoTemplate({
661
+ template,
662
+ appHtml: result.html,
663
+ appCss: result.css,
664
+ ssrData: result.ssrData,
665
+ nonce,
666
+ headTags: allHeadTags || undefined,
667
+ sessionScript
512
668
  });
513
- } catch {
669
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
670
+ if (linkHeader)
671
+ headers.Link = linkHeader;
672
+ if (cacheControl)
673
+ headers["Cache-Control"] = cacheControl;
674
+ return new Response(html, { status: 200, headers });
675
+ } catch (err) {
676
+ console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
514
677
  return new Response("Internal Server Error", {
515
678
  status: 500,
516
679
  headers: { "Content-Type": "text/plain" }
@@ -518,4 +681,4 @@ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
518
681
  }
519
682
  }
520
683
 
521
- export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, createSSRHandler };
684
+ export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, getAccessSetForSSR, createAccessSetScript, createSessionScript, injectIntoTemplate, createSSRHandler };
@@ -413,14 +413,14 @@ function installDomShim() {
413
413
  querySelector: () => null,
414
414
  querySelectorAll: () => [],
415
415
  getElementById: () => null,
416
+ addEventListener: () => {},
417
+ removeEventListener: () => {},
416
418
  cookie: ""
417
419
  };
418
420
  globalThis.document = fakeDocument;
419
421
  if (typeof window === "undefined") {
420
422
  globalThis.window = {
421
423
  location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
422
- addEventListener: () => {},
423
- removeEventListener: () => {},
424
424
  history: {
425
425
  pushState: () => {},
426
426
  replaceState: () => {}
@@ -0,0 +1,57 @@
1
+ // @bun
2
+ // src/bun-plugin/image-paths.ts
3
+ import { createHash } from "crypto";
4
+ import { readFileSync } from "fs";
5
+ import { basename, dirname, extname, resolve } from "path";
6
+ var EXT_MAP = {
7
+ jpeg: ".jpg",
8
+ jpg: ".jpg",
9
+ png: ".png",
10
+ webp: ".webp",
11
+ gif: ".gif"
12
+ };
13
+ var MIME_MAP = {
14
+ jpeg: "image/jpeg",
15
+ jpg: "image/jpeg",
16
+ png: "image/png",
17
+ webp: "image/webp",
18
+ gif: "image/gif"
19
+ };
20
+ var IMG_CONTENT_TYPES = {
21
+ webp: "image/webp",
22
+ png: "image/png",
23
+ jpg: "image/jpeg",
24
+ jpeg: "image/jpeg",
25
+ gif: "image/gif",
26
+ avif: "image/avif"
27
+ };
28
+ function imageContentType(ext) {
29
+ return ext && IMG_CONTENT_TYPES[ext] || "application/octet-stream";
30
+ }
31
+ function isValidImageName(imgName) {
32
+ return !imgName.includes("..") && !imgName.includes("\x00");
33
+ }
34
+ function resolveImageSrc(src, sourceFile, projectRoot) {
35
+ if (src.startsWith("/"))
36
+ return resolve(projectRoot, src.slice(1));
37
+ return resolve(dirname(sourceFile), src);
38
+ }
39
+ function computeImageOutputPaths(sourcePath, width, height, quality, fit) {
40
+ let sourceBuffer;
41
+ try {
42
+ sourceBuffer = readFileSync(sourcePath);
43
+ } catch {
44
+ return null;
45
+ }
46
+ const hash = createHash("sha256").update(sourceBuffer).update(`${width}x${height}q${quality}f${fit}`).digest("hex").slice(0, 12);
47
+ const name = basename(sourcePath, extname(sourcePath));
48
+ const ext = extname(sourcePath).slice(1);
49
+ return {
50
+ webp1x: `/__vertz_img/${name}-${hash}-${width}w.webp`,
51
+ webp2x: `/__vertz_img/${name}-${hash}-${width * 2}w.webp`,
52
+ fallback: `/__vertz_img/${name}-${hash}-${width * 2}w${EXT_MAP[ext] ?? ".jpg"}`,
53
+ fallbackType: MIME_MAP[ext] ?? "image/jpeg"
54
+ };
55
+ }
56
+
57
+ export { imageContentType, isValidImageName, resolveImageSrc, computeImageOutputPaths };
@@ -1,4 +1,5 @@
1
- import { Theme } from "@vertz/ui";
1
+ import { CompiledRoute } from "@vertz/ui";
2
+ import { FontFallbackMetrics, Theme } from "@vertz/ui";
2
3
  interface SSRModule {
3
4
  default?: () => unknown;
4
5
  App?: () => unknown;
@@ -21,6 +22,10 @@ interface SSRRenderResult {
21
22
  key: string;
22
23
  data: unknown;
23
24
  }>;
25
+ /** Font preload link tags for injection into <head>. */
26
+ headTags: string;
27
+ /** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
28
+ discoveredRoutes?: string[];
24
29
  }
25
30
  interface SSRDiscoverResult {
26
31
  resolved: Array<{
@@ -38,6 +43,8 @@ interface SSRDiscoverResult {
38
43
  */
39
44
  declare function ssrRenderToString(module: SSRModule, url: string, options?: {
40
45
  ssrTimeout?: number;
46
+ /** Pre-computed font fallback metrics (computed at server startup). */
47
+ fallbackMetrics?: Record<string, FontFallbackMetrics>;
41
48
  }): Promise<SSRRenderResult>;
42
49
  /**
43
50
  * Discover queries for a given URL without rendering.
@@ -47,6 +54,83 @@ declare function ssrRenderToString(module: SSRModule, url: string, options?: {
47
54
  declare function ssrDiscoverQueries(module: SSRModule, url: string, options?: {
48
55
  ssrTimeout?: number;
49
56
  }): Promise<SSRDiscoverResult>;
57
+ interface PrerenderResult {
58
+ /** The route path that was pre-rendered. */
59
+ path: string;
60
+ /** The complete HTML string. */
61
+ html: string;
62
+ }
63
+ interface PrerenderOptions {
64
+ /** Route paths to pre-render. */
65
+ routes: string[];
66
+ /** CSP nonce for inline scripts. */
67
+ nonce?: string;
68
+ }
69
+ /**
70
+ * Discover all route patterns from an SSR module.
71
+ *
72
+ * Renders `/` to trigger `createRouter()`, which registers route patterns
73
+ * with the SSR context. Returns the discovered patterns (including dynamic).
74
+ */
75
+ declare function discoverRoutes(module: SSRModule): Promise<string[]>;
76
+ /**
77
+ * Filter route patterns to only pre-renderable ones.
78
+ *
79
+ * Excludes:
80
+ * - Routes with `:param` segments
81
+ * - Routes with `*` wildcard
82
+ * - Routes with `prerender: false` (looked up in compiledRoutes)
83
+ */
84
+ declare function filterPrerenderableRoutes(patterns: string[], compiledRoutes?: CompiledRoute[]): string[];
85
+ /**
86
+ * Pre-render a list of routes into complete HTML strings.
87
+ *
88
+ * Routes are rendered sequentially (not in parallel) because the DOM shim
89
+ * uses process-global `document`/`window`. Concurrent renders would interleave.
90
+ *
91
+ * CSS from SSR (theme variables, font-face, global styles) is injected as
92
+ * inline `<style>` tags. The template's `<link>` tags only cover component
93
+ * CSS extracted by the bundler — theme and globals are computed at render time.
94
+ *
95
+ * @throws Error if any route fails to render (with hint about `prerender: false`)
96
+ */
97
+ declare function prerenderRoutes(module: SSRModule, template: string, options: PrerenderOptions): Promise<PrerenderResult[]>;
98
+ /**
99
+ * Strip `<script>` tags and `<link rel="modulepreload">` from pre-rendered HTML
100
+ * that has no interactive components (no `data-v-island` or `data-v-id` markers).
101
+ *
102
+ * Pages with islands or hydrated components need the client JS; purely static
103
+ * pages (like /manifesto) ship zero JavaScript.
104
+ */
105
+ declare function stripScriptsFromStaticHTML(html: string): string;
106
+ import { FontFallbackMetrics as FontFallbackMetrics2 } from "@vertz/ui";
107
+ import { AccessSet } from "@vertz/ui/auth";
108
+ interface SessionData {
109
+ user: {
110
+ id: string;
111
+ email: string;
112
+ role: string;
113
+ [key: string]: unknown;
114
+ };
115
+ /** Unix timestamp in milliseconds (JWT exp * 1000). */
116
+ expiresAt: number;
117
+ }
118
+ /** Resolved session data for SSR injection. */
119
+ interface SSRSessionInfo {
120
+ session: SessionData;
121
+ /**
122
+ * Access set from JWT acl claim.
123
+ * - Present (object): inline access set (no overflow)
124
+ * - null: access control is configured but the set overflowed the JWT
125
+ * - undefined: access control is not configured
126
+ */
127
+ accessSet?: AccessSet | null;
128
+ }
129
+ /**
130
+ * Callback that extracts session data from a request.
131
+ * Returns null when no valid session exists (expired, missing, or invalid cookie).
132
+ */
133
+ type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
50
134
  interface SSRHandlerOptions {
51
135
  /** The loaded SSR module (import('./dist/server/index.js')) */
52
136
  module: SSRModule;
@@ -72,15 +156,38 @@ interface SSRHandlerOptions {
72
156
  * so that strict Content-Security-Policy headers do not block it.
73
157
  */
74
158
  nonce?: string;
159
+ /** Pre-computed font fallback metrics (computed at server startup). */
160
+ fallbackMetrics?: Record<string, FontFallbackMetrics2>;
161
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
162
+ modulepreload?: string[];
163
+ /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
164
+ cacheControl?: string;
165
+ /**
166
+ * Resolves session data from request cookies for SSR injection.
167
+ * When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
168
+ * optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
169
+ */
170
+ sessionResolver?: SessionResolver;
171
+ }
172
+ declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
173
+ interface InjectIntoTemplateOptions {
174
+ template: string;
175
+ appHtml: string;
176
+ appCss: string;
177
+ ssrData: Array<{
178
+ key: string;
179
+ data: unknown;
180
+ }>;
181
+ nonce?: string;
182
+ headTags?: string;
183
+ /** Pre-built session + access set script tags for SSR injection. */
184
+ sessionScript?: string;
75
185
  }
76
186
  /**
77
- * Create a web-standard SSR request handler.
187
+ * Inject SSR output into the HTML template.
78
188
  *
79
- * Handles two types of requests:
80
- * - X-Vertz-Nav: 1 -> SSE Response with pre-fetched query data
81
- * - Normal HTML request -> SSR-rendered HTML Response
82
- *
83
- * Does NOT serve static files — that's the adapter/platform's job.
189
+ * Replaces <!--ssr-outlet--> or <div id="app"> content with rendered HTML,
190
+ * injects CSS before </head>, and ssrData before </body>.
84
191
  */
85
- declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
86
- export { ssrRenderToString, ssrDiscoverQueries, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult };
192
+ declare function injectIntoTemplate(options: InjectIntoTemplateOptions): string;
193
+ export { stripScriptsFromStaticHTML, ssrRenderToString, ssrDiscoverQueries, prerenderRoutes, injectIntoTemplate, filterPrerenderableRoutes, discoverRoutes, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult, PrerenderResult, PrerenderOptions };
package/dist/ssr/index.js CHANGED
@@ -1,11 +1,88 @@
1
1
  import {
2
2
  createSSRHandler,
3
+ injectIntoTemplate,
3
4
  ssrDiscoverQueries,
4
5
  ssrRenderToString
5
- } from "../shared/chunk-98972e43.js";
6
- import"../shared/chunk-n1arq9xq.js";
6
+ } from "../shared/chunk-969qgkdf.js";
7
+ import"../shared/chunk-9jjdzz8c.js";
8
+
9
+ // src/prerender.ts
10
+ async function discoverRoutes(module) {
11
+ const result = await ssrRenderToString(module, "/");
12
+ return result.discoveredRoutes ?? [];
13
+ }
14
+ function filterPrerenderableRoutes(patterns, compiledRoutes) {
15
+ return patterns.filter((pattern) => {
16
+ if (pattern.includes(":") || pattern.includes("*"))
17
+ return false;
18
+ if (compiledRoutes) {
19
+ const route = findCompiledRoute(compiledRoutes, pattern);
20
+ if (route?.prerender === false)
21
+ return false;
22
+ }
23
+ return true;
24
+ });
25
+ }
26
+ async function prerenderRoutes(module, template, options) {
27
+ const results = [];
28
+ for (const routePath of options.routes) {
29
+ let renderResult;
30
+ try {
31
+ renderResult = await ssrRenderToString(module, routePath);
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ throw new Error(`Pre-render failed for ${routePath}
35
+ ` + ` ${message}
36
+ ` + " Hint: If this route requires runtime data, add `prerender: false` to its route config.");
37
+ }
38
+ const html = injectIntoTemplate({
39
+ template,
40
+ appHtml: renderResult.html,
41
+ appCss: renderResult.css,
42
+ ssrData: renderResult.ssrData,
43
+ nonce: options.nonce,
44
+ headTags: renderResult.headTags || undefined
45
+ });
46
+ results.push({ path: routePath, html });
47
+ }
48
+ return results;
49
+ }
50
+ function stripScriptsFromStaticHTML(html) {
51
+ if (html.includes("data-v-island") || html.includes("data-v-id")) {
52
+ return html;
53
+ }
54
+ let result = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
55
+ result = result.replace(/<script\b[^>]*\/>/gi, "");
56
+ result = result.replace(/<link\b[^>]*\brel=["']modulepreload["'][^>]*\/?>/gi, "");
57
+ return result;
58
+ }
59
+ function findCompiledRoute(routes, pattern, prefix = "") {
60
+ for (const route of routes) {
61
+ const fullPattern = joinPatterns(prefix, route.pattern);
62
+ if (fullPattern === pattern)
63
+ return route;
64
+ if (route.children) {
65
+ const found = findCompiledRoute(route.children, pattern, fullPattern);
66
+ if (found)
67
+ return found;
68
+ }
69
+ }
70
+ return;
71
+ }
72
+ function joinPatterns(parent, child) {
73
+ if (!parent || parent === "/")
74
+ return child;
75
+ if (child === "/")
76
+ return parent;
77
+ return `${parent.replace(/\/$/, "")}/${child.replace(/^\//, "")}`;
78
+ }
7
79
  export {
80
+ stripScriptsFromStaticHTML,
8
81
  ssrRenderToString,
9
82
  ssrDiscoverQueries,
83
+ prerenderRoutes,
84
+ injectIntoTemplate,
85
+ filterPrerenderableRoutes,
86
+ discoverRoutes,
10
87
  createSSRHandler
11
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/ui-server",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz UI server-side rendering runtime",
@@ -56,18 +56,21 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@ampproject/remapping": "^2.3.0",
59
+ "@capsizecss/unpack": "^4.0.0",
59
60
  "@jridgewell/trace-mapping": "^0.3.31",
60
- "@vertz/core": "^0.2.14",
61
- "@vertz/ui": "^0.2.14",
62
- "@vertz/ui-compiler": "^0.2.14",
61
+ "@vertz/core": "^0.2.15",
62
+ "@vertz/ui": "^0.2.15",
63
+ "@vertz/ui-compiler": "^0.2.15",
63
64
  "magic-string": "^0.30.0",
65
+ "sharp": "^0.34.5",
64
66
  "ts-morph": "^27.0.2"
65
67
  },
66
68
  "devDependencies": {
69
+ "@vertz/codegen": "^0.2.15",
67
70
  "bun-types": "^1.3.10",
68
71
  "bunup": "^0.16.31",
69
72
  "@happy-dom/global-registrator": "^20.8.3",
70
- "happy-dom": "^18.0.1",
73
+ "happy-dom": "^20.8.3",
71
74
  "typescript": "^5.7.0"
72
75
  },
73
76
  "engines": {