@vertz/ui-server 0.2.15 → 0.2.17

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) {
@@ -251,19 +251,57 @@ async function ssrRenderToString(module, url, options) {
251
251
  const ssrTimeout = options?.ssrTimeout ?? 300;
252
252
  ensureDomShim();
253
253
  const ctx = createRequestContext(normalizedUrl);
254
+ if (options?.ssrAuth) {
255
+ ctx.ssrAuth = options.ssrAuth;
256
+ }
254
257
  return ssrStorage.run(ctx, async () => {
255
258
  try {
256
259
  setGlobalSSRTimeout(ssrTimeout);
257
260
  const createApp = resolveAppFactory(module);
258
261
  let themeCss = "";
262
+ let themePreloadTags = "";
259
263
  if (module.theme) {
260
264
  try {
261
- themeCss = compileTheme(module.theme).css;
265
+ const compiled = compileTheme(module.theme, {
266
+ fallbackMetrics: options?.fallbackMetrics
267
+ });
268
+ themeCss = compiled.css;
269
+ themePreloadTags = compiled.preloadTags;
262
270
  } catch (e) {
263
271
  console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
264
272
  }
265
273
  }
266
274
  createApp();
275
+ if (ctx.ssrRedirect) {
276
+ return {
277
+ html: "",
278
+ css: "",
279
+ ssrData: [],
280
+ headTags: "",
281
+ redirect: ctx.ssrRedirect
282
+ };
283
+ }
284
+ const store = ssrStorage.getStore();
285
+ if (store) {
286
+ if (store.pendingRouteComponents?.size) {
287
+ const entries = Array.from(store.pendingRouteComponents.entries());
288
+ const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
289
+ promise.then((mod) => ({ route, factory: mod.default })),
290
+ new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
291
+ ])));
292
+ store.resolvedComponents = new Map;
293
+ for (const result of results) {
294
+ if (result.status === "fulfilled") {
295
+ const { route, factory } = result.value;
296
+ store.resolvedComponents.set(route, factory);
297
+ }
298
+ }
299
+ store.pendingRouteComponents = undefined;
300
+ }
301
+ if (!store.resolvedComponents) {
302
+ store.resolvedComponents = new Map;
303
+ }
304
+ }
267
305
  const queries = getSSRQueries();
268
306
  const resolvedQueries = [];
269
307
  if (queries.length > 0) {
@@ -275,7 +313,6 @@ async function ssrRenderToString(module, url, options) {
275
313
  }),
276
314
  new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
277
315
  ])));
278
- const store = ssrStorage.getStore();
279
316
  if (store)
280
317
  store.queries = [];
281
318
  }
@@ -288,7 +325,13 @@ async function ssrRenderToString(module, url, options) {
288
325
  key,
289
326
  data: JSON.parse(JSON.stringify(data))
290
327
  })) : [];
291
- return { html, css, ssrData };
328
+ return {
329
+ html,
330
+ css,
331
+ ssrData,
332
+ headTags: themePreloadTags,
333
+ discoveredRoutes: ctx.discoveredRoutes
334
+ };
292
335
  } finally {
293
336
  clearGlobalSSRTimeout();
294
337
  }
@@ -439,17 +482,75 @@ data: ${safeSerialize(entry)}
439
482
  });
440
483
  }
441
484
 
442
- // src/ssr-handler.ts
443
- function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
485
+ // src/ssr-access-set.ts
486
+ function getAccessSetForSSR(jwtPayload) {
487
+ if (!jwtPayload)
488
+ return null;
489
+ const acl = jwtPayload.acl;
490
+ if (!acl)
491
+ return null;
492
+ if (acl.overflow)
493
+ return null;
494
+ if (!acl.set)
495
+ return null;
496
+ return {
497
+ entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
498
+ name,
499
+ {
500
+ allowed: check.allowed,
501
+ reasons: check.reasons ?? [],
502
+ ...check.reason ? { reason: check.reason } : {},
503
+ ...check.meta ? { meta: check.meta } : {}
504
+ }
505
+ ])),
506
+ flags: acl.set.flags,
507
+ plan: acl.set.plan,
508
+ computedAt: acl.set.computedAt
509
+ };
510
+ }
511
+ function createAccessSetScript(accessSet, nonce) {
512
+ const json = JSON.stringify(accessSet);
513
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
514
+ const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
515
+ return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
516
+ }
517
+ function escapeAttr2(s) {
518
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
519
+ }
520
+
521
+ // src/ssr-session.ts
522
+ function createSessionScript(session, nonce) {
523
+ const json = JSON.stringify(session);
524
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
525
+ const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
526
+ return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
527
+ }
528
+ function escapeAttr3(s) {
529
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
530
+ }
531
+
532
+ // src/template-inject.ts
533
+ function injectIntoTemplate(options) {
534
+ const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
444
535
  let html;
445
536
  if (template.includes("<!--ssr-outlet-->")) {
446
537
  html = template.replace("<!--ssr-outlet-->", appHtml);
447
538
  } else {
448
539
  html = template.replace(/(<div[^>]*id="app"[^>]*>)([\s\S]*?)(<\/div>)/, `$1${appHtml}$3`);
449
540
  }
541
+ if (headTags) {
542
+ html = html.replace("</head>", `${headTags}
543
+ </head>`);
544
+ }
450
545
  if (appCss) {
546
+ html = html.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
547
+ <noscript>${match}</noscript>`);
451
548
  html = html.replace("</head>", `${appCss}
452
549
  </head>`);
550
+ }
551
+ if (sessionScript) {
552
+ html = html.replace("</body>", `${sessionScript}
553
+ </body>`);
453
554
  }
454
555
  if (ssrData.length > 0) {
455
556
  const nonceAttr = nonce != null ? ` nonce="${nonce}"` : "";
@@ -459,8 +560,44 @@ function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
459
560
  }
460
561
  return html;
461
562
  }
563
+
564
+ // src/ssr-handler.ts
565
+ import { compileTheme as compileTheme2 } from "@vertz/ui";
566
+ function sanitizeLinkHref(href) {
567
+ return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
568
+ }
569
+ function sanitizeLinkParam(value) {
570
+ return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
571
+ }
572
+ function buildLinkHeader(items) {
573
+ return items.map((item) => {
574
+ const parts = [
575
+ `<${sanitizeLinkHref(item.href)}>`,
576
+ "rel=preload",
577
+ `as=${sanitizeLinkParam(item.as)}`
578
+ ];
579
+ if (item.type)
580
+ parts.push(`type=${sanitizeLinkParam(item.type)}`);
581
+ if (item.crossorigin)
582
+ parts.push("crossorigin");
583
+ return parts.join("; ");
584
+ }).join(", ");
585
+ }
586
+ function buildModulepreloadTags(paths) {
587
+ return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
588
+ `);
589
+ }
462
590
  function createSSRHandler(options) {
463
- const { module, ssrTimeout, inlineCSS, nonce } = options;
591
+ const {
592
+ module,
593
+ ssrTimeout,
594
+ inlineCSS,
595
+ nonce,
596
+ fallbackMetrics,
597
+ modulepreload,
598
+ cacheControl,
599
+ sessionResolver
600
+ } = options;
464
601
  let template = options.template;
465
602
  if (inlineCSS) {
466
603
  for (const [href, css] of Object.entries(inlineCSS)) {
@@ -470,13 +607,46 @@ function createSSRHandler(options) {
470
607
  template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
471
608
  }
472
609
  }
610
+ let linkHeader;
611
+ if (module.theme) {
612
+ const compiled = compileTheme2(module.theme, { fallbackMetrics });
613
+ if (compiled.preloadItems.length > 0) {
614
+ linkHeader = buildLinkHeader(compiled.preloadItems);
615
+ }
616
+ }
617
+ const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
473
618
  return async (request) => {
474
619
  const url = new URL(request.url);
475
620
  const pathname = url.pathname;
476
621
  if (request.headers.get("x-vertz-nav") === "1") {
477
622
  return handleNavRequest(module, pathname, ssrTimeout);
478
623
  }
479
- return handleHTMLRequest(module, template, pathname, ssrTimeout, nonce);
624
+ let sessionScript = "";
625
+ let ssrAuth;
626
+ if (sessionResolver) {
627
+ try {
628
+ const sessionResult = await sessionResolver(request);
629
+ if (sessionResult) {
630
+ ssrAuth = {
631
+ status: "authenticated",
632
+ user: sessionResult.session.user,
633
+ expiresAt: sessionResult.session.expiresAt
634
+ };
635
+ const scripts = [];
636
+ scripts.push(createSessionScript(sessionResult.session, nonce));
637
+ if (sessionResult.accessSet != null) {
638
+ scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
639
+ }
640
+ sessionScript = scripts.join(`
641
+ `);
642
+ } else {
643
+ ssrAuth = { status: "unauthenticated" };
644
+ }
645
+ } catch (resolverErr) {
646
+ console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
647
+ }
648
+ }
649
+ return handleHTMLRequest(module, template, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript, ssrAuth);
480
650
  };
481
651
  }
482
652
  async function handleNavRequest(module, url, ssrTimeout) {
@@ -502,15 +672,34 @@ data: {}
502
672
  });
503
673
  }
504
674
  }
505
- async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
675
+ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript, ssrAuth) {
506
676
  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" }
677
+ const result = await ssrRenderToString(module, url, { ssrTimeout, fallbackMetrics, ssrAuth });
678
+ if (result.redirect) {
679
+ return new Response(null, {
680
+ status: 302,
681
+ headers: { Location: result.redirect.to }
682
+ });
683
+ }
684
+ const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
685
+ `);
686
+ const html = injectIntoTemplate({
687
+ template,
688
+ appHtml: result.html,
689
+ appCss: result.css,
690
+ ssrData: result.ssrData,
691
+ nonce,
692
+ headTags: allHeadTags || undefined,
693
+ sessionScript
512
694
  });
513
- } catch {
695
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
696
+ if (linkHeader)
697
+ headers.Link = linkHeader;
698
+ if (cacheControl)
699
+ headers["Cache-Control"] = cacheControl;
700
+ return new Response(html, { status: 200, headers });
701
+ } catch (err) {
702
+ console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
514
703
  return new Response("Internal Server Error", {
515
704
  status: 500,
516
705
  headers: { "Content-Type": "text/plain" }
@@ -518,4 +707,4 @@ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
518
707
  }
519
708
  }
520
709
 
521
- export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, createSSRHandler };
710
+ export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, getAccessSetForSSR, createAccessSetScript, createSessionScript, injectIntoTemplate, createSSRHandler };
@@ -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,6 @@
1
- import { Theme } from "@vertz/ui";
1
+ import { CompiledRoute } from "@vertz/ui";
2
+ import { FontFallbackMetrics, Theme } from "@vertz/ui";
3
+ import { SSRAuth as SSRAuth_jq1nwm } from "@vertz/ui/internals";
2
4
  interface SSRModule {
3
5
  default?: () => unknown;
4
6
  App?: () => unknown;
@@ -21,6 +23,14 @@ interface SSRRenderResult {
21
23
  key: string;
22
24
  data: unknown;
23
25
  }>;
26
+ /** Font preload link tags for injection into <head>. */
27
+ headTags: string;
28
+ /** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
29
+ discoveredRoutes?: string[];
30
+ /** Set when ProtectedRoute writes a redirect during SSR. Server should return 302. */
31
+ redirect?: {
32
+ to: string;
33
+ };
24
34
  }
25
35
  interface SSRDiscoverResult {
26
36
  resolved: Array<{
@@ -38,6 +48,10 @@ interface SSRDiscoverResult {
38
48
  */
39
49
  declare function ssrRenderToString(module: SSRModule, url: string, options?: {
40
50
  ssrTimeout?: number;
51
+ /** Pre-computed font fallback metrics (computed at server startup). */
52
+ fallbackMetrics?: Record<string, FontFallbackMetrics>;
53
+ /** Auth state resolved from session cookie. Passed to SSRRenderContext for AuthProvider. */
54
+ ssrAuth?: SSRAuth_jq1nwm;
41
55
  }): Promise<SSRRenderResult>;
42
56
  /**
43
57
  * Discover queries for a given URL without rendering.
@@ -47,6 +61,83 @@ declare function ssrRenderToString(module: SSRModule, url: string, options?: {
47
61
  declare function ssrDiscoverQueries(module: SSRModule, url: string, options?: {
48
62
  ssrTimeout?: number;
49
63
  }): Promise<SSRDiscoverResult>;
64
+ interface PrerenderResult {
65
+ /** The route path that was pre-rendered. */
66
+ path: string;
67
+ /** The complete HTML string. */
68
+ html: string;
69
+ }
70
+ interface PrerenderOptions {
71
+ /** Route paths to pre-render. */
72
+ routes: string[];
73
+ /** CSP nonce for inline scripts. */
74
+ nonce?: string;
75
+ }
76
+ /**
77
+ * Discover all route patterns from an SSR module.
78
+ *
79
+ * Renders `/` to trigger `createRouter()`, which registers route patterns
80
+ * with the SSR context. Returns the discovered patterns (including dynamic).
81
+ */
82
+ declare function discoverRoutes(module: SSRModule): Promise<string[]>;
83
+ /**
84
+ * Filter route patterns to only pre-renderable ones.
85
+ *
86
+ * Excludes:
87
+ * - Routes with `:param` segments
88
+ * - Routes with `*` wildcard
89
+ * - Routes with `prerender: false` (looked up in compiledRoutes)
90
+ */
91
+ declare function filterPrerenderableRoutes(patterns: string[], compiledRoutes?: CompiledRoute[]): string[];
92
+ /**
93
+ * Pre-render a list of routes into complete HTML strings.
94
+ *
95
+ * Routes are rendered sequentially (not in parallel) because the DOM shim
96
+ * uses process-global `document`/`window`. Concurrent renders would interleave.
97
+ *
98
+ * CSS from SSR (theme variables, font-face, global styles) is injected as
99
+ * inline `<style>` tags. The template's `<link>` tags only cover component
100
+ * CSS extracted by the bundler — theme and globals are computed at render time.
101
+ *
102
+ * @throws Error if any route fails to render (with hint about `prerender: false`)
103
+ */
104
+ declare function prerenderRoutes(module: SSRModule, template: string, options: PrerenderOptions): Promise<PrerenderResult[]>;
105
+ /**
106
+ * Strip `<script>` tags and `<link rel="modulepreload">` from pre-rendered HTML
107
+ * that has no interactive components (no `data-v-island` or `data-v-id` markers).
108
+ *
109
+ * Pages with islands or hydrated components need the client JS; purely static
110
+ * pages (like /manifesto) ship zero JavaScript.
111
+ */
112
+ declare function stripScriptsFromStaticHTML(html: string): string;
113
+ import { FontFallbackMetrics as FontFallbackMetrics2 } from "@vertz/ui";
114
+ import { AccessSet } from "@vertz/ui/auth";
115
+ interface SessionData {
116
+ user: {
117
+ id: string;
118
+ email: string;
119
+ role: string;
120
+ [key: string]: unknown;
121
+ };
122
+ /** Unix timestamp in milliseconds (JWT exp * 1000). */
123
+ expiresAt: number;
124
+ }
125
+ /** Resolved session data for SSR injection. */
126
+ interface SSRSessionInfo {
127
+ session: SessionData;
128
+ /**
129
+ * Access set from JWT acl claim.
130
+ * - Present (object): inline access set (no overflow)
131
+ * - null: access control is configured but the set overflowed the JWT
132
+ * - undefined: access control is not configured
133
+ */
134
+ accessSet?: AccessSet | null;
135
+ }
136
+ /**
137
+ * Callback that extracts session data from a request.
138
+ * Returns null when no valid session exists (expired, missing, or invalid cookie).
139
+ */
140
+ type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
50
141
  interface SSRHandlerOptions {
51
142
  /** The loaded SSR module (import('./dist/server/index.js')) */
52
143
  module: SSRModule;
@@ -72,15 +163,38 @@ interface SSRHandlerOptions {
72
163
  * so that strict Content-Security-Policy headers do not block it.
73
164
  */
74
165
  nonce?: string;
166
+ /** Pre-computed font fallback metrics (computed at server startup). */
167
+ fallbackMetrics?: Record<string, FontFallbackMetrics2>;
168
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
169
+ modulepreload?: string[];
170
+ /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
171
+ cacheControl?: string;
172
+ /**
173
+ * Resolves session data from request cookies for SSR injection.
174
+ * When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
175
+ * optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
176
+ */
177
+ sessionResolver?: SessionResolver;
178
+ }
179
+ declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
180
+ interface InjectIntoTemplateOptions {
181
+ template: string;
182
+ appHtml: string;
183
+ appCss: string;
184
+ ssrData: Array<{
185
+ key: string;
186
+ data: unknown;
187
+ }>;
188
+ nonce?: string;
189
+ headTags?: string;
190
+ /** Pre-built session + access set script tags for SSR injection. */
191
+ sessionScript?: string;
75
192
  }
76
193
  /**
77
- * Create a web-standard SSR request handler.
194
+ * Inject SSR output into the HTML template.
78
195
  *
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.
196
+ * Replaces <!--ssr-outlet--> or <div id="app"> content with rendered HTML,
197
+ * injects CSS before </head>, and ssrData before </body>.
84
198
  */
85
- declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
86
- export { ssrRenderToString, ssrDiscoverQueries, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult };
199
+ declare function injectIntoTemplate(options: InjectIntoTemplateOptions): string;
200
+ 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-c5ee9yf1.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.17",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz UI server-side rendering runtime",
@@ -56,18 +56,22 @@
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.16",
62
+ "@vertz/ui": "^0.2.16",
63
+ "@vertz/ui-compiler": "^0.2.16",
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.16",
70
+ "@vertz/ui-auth": "^0.2.16",
67
71
  "bun-types": "^1.3.10",
68
72
  "bunup": "^0.16.31",
69
73
  "@happy-dom/global-registrator": "^20.8.3",
70
- "happy-dom": "^18.0.1",
74
+ "happy-dom": "^20.8.3",
71
75
  "typescript": "^5.7.0"
72
76
  },
73
77
  "engines": {