@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.
@@ -7,7 +7,7 @@ import {
7
7
  installDomShim,
8
8
  removeDomShim,
9
9
  toVNode
10
- } from "../shared/chunk-n1arq9xq.js";
10
+ } from "../shared/chunk-9jjdzz8c.js";
11
11
  export {
12
12
  toVNode,
13
13
  removeDomShim,
package/dist/index.d.ts CHANGED
@@ -80,6 +80,27 @@ declare function renderAssetTags(assets: AssetDescriptor[]): string;
80
80
  * Returns an empty string if the CSS is empty.
81
81
  */
82
82
  declare function inlineCriticalCss(css: string): string;
83
+ import { FallbackFontName, FontDescriptor, FontFallbackMetrics } from "@vertz/ui";
84
+ /**
85
+ * Auto-detect which system font to use as fallback base.
86
+ *
87
+ * Scans the `fallback` array for generic CSS font family keywords:
88
+ * - 'sans-serif' or 'system-ui' → Arial
89
+ * - 'serif' → Times New Roman
90
+ * - 'monospace' → Courier New
91
+ *
92
+ * Skips non-generic entries (e.g., 'Georgia', 'Helvetica').
93
+ * If no generic keyword found, defaults to Arial.
94
+ */
95
+ declare function detectFallbackFont(fallback: readonly string[]): FallbackFontName;
96
+ /**
97
+ * Extract font metrics from .woff2 files and compute CSS fallback overrides.
98
+ *
99
+ * @param fonts - Font descriptors from theme definition.
100
+ * @param rootDir - Project root directory for resolving font file paths.
101
+ * @returns Map of font key → computed fallback metrics.
102
+ */
103
+ declare function extractFontMetrics(fonts: Record<string, FontDescriptor>, rootDir: string): Promise<Record<string, FontFallbackMetrics>>;
83
104
  /**
84
105
  * Collector for `<head>` metadata during SSR.
85
106
  *
@@ -190,7 +211,7 @@ interface PageOptions {
190
211
  * ```
191
212
  */
192
213
  declare function renderPage(vnode: VNode, options?: PageOptions): Response;
193
- import { Theme } from "@vertz/ui";
214
+ import { FontFallbackMetrics as FontFallbackMetrics2, Theme } from "@vertz/ui";
194
215
  interface RenderToHTMLOptions<AppFn extends () => VNode> {
195
216
  /** The app component function */
196
217
  app: AppFn;
@@ -215,6 +236,8 @@ interface RenderToHTMLOptions<AppFn extends () => VNode> {
215
236
  };
216
237
  /** Container selector (default '#app') */
217
238
  container?: string;
239
+ /** Pre-computed font fallback metrics (computed at server startup). */
240
+ fallbackMetrics?: Record<string, FontFallbackMetrics2>;
218
241
  }
219
242
  interface RenderToHTMLStreamOptions<AppFn extends () => VNode> extends RenderToHTMLOptions<AppFn> {
220
243
  /** CSP nonce for inline scripts */
@@ -350,7 +373,9 @@ declare function clearGlobalSSRTimeout(): void;
350
373
  * Returns undefined if not set or outside SSR context.
351
374
  */
352
375
  declare function getGlobalSSRTimeout(): number | undefined;
353
- import { Theme as Theme2 } from "@vertz/ui";
376
+ import { FontFallbackMetrics as FontFallbackMetrics4 } from "@vertz/ui";
377
+ import { FontFallbackMetrics as FontFallbackMetrics3, Theme as Theme2 } from "@vertz/ui";
378
+ import { SSRAuth as SSRAuth_jq1nwm } from "@vertz/ui/internals";
354
379
  interface SSRModule {
355
380
  default?: () => unknown;
356
381
  App?: () => unknown;
@@ -373,6 +398,14 @@ interface SSRRenderResult {
373
398
  key: string;
374
399
  data: unknown;
375
400
  }>;
401
+ /** Font preload link tags for injection into <head>. */
402
+ headTags: string;
403
+ /** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
404
+ discoveredRoutes?: string[];
405
+ /** Set when ProtectedRoute writes a redirect during SSR. Server should return 302. */
406
+ redirect?: {
407
+ to: string;
408
+ };
376
409
  }
377
410
  interface SSRDiscoverResult {
378
411
  resolved: Array<{
@@ -390,6 +423,10 @@ interface SSRDiscoverResult {
390
423
  */
391
424
  declare function ssrRenderToString(module: SSRModule, url: string, options?: {
392
425
  ssrTimeout?: number;
426
+ /** Pre-computed font fallback metrics (computed at server startup). */
427
+ fallbackMetrics?: Record<string, FontFallbackMetrics3>;
428
+ /** Auth state resolved from session cookie. Passed to SSRRenderContext for AuthProvider. */
429
+ ssrAuth?: SSRAuth_jq1nwm;
393
430
  }): Promise<SSRRenderResult>;
394
431
  /**
395
432
  * Discover queries for a given URL without rendering.
@@ -399,6 +436,41 @@ declare function ssrRenderToString(module: SSRModule, url: string, options?: {
399
436
  declare function ssrDiscoverQueries(module: SSRModule, url: string, options?: {
400
437
  ssrTimeout?: number;
401
438
  }): Promise<SSRDiscoverResult>;
439
+ import { AccessSet as AccessSet2 } from "@vertz/ui/auth";
440
+ interface SessionData {
441
+ user: {
442
+ id: string;
443
+ email: string;
444
+ role: string;
445
+ [key: string]: unknown;
446
+ };
447
+ /** Unix timestamp in milliseconds (JWT exp * 1000). */
448
+ expiresAt: number;
449
+ }
450
+ /** Resolved session data for SSR injection. */
451
+ interface SSRSessionInfo {
452
+ session: SessionData;
453
+ /**
454
+ * Access set from JWT acl claim.
455
+ * - Present (object): inline access set (no overflow)
456
+ * - null: access control is configured but the set overflowed the JWT
457
+ * - undefined: access control is not configured
458
+ */
459
+ accessSet?: AccessSet2 | null;
460
+ }
461
+ /**
462
+ * Callback that extracts session data from a request.
463
+ * Returns null when no valid session exists (expired, missing, or invalid cookie).
464
+ */
465
+ type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
466
+ /**
467
+ * Serialize a session into a `<script>` tag that sets
468
+ * `window.__VERTZ_SESSION__`.
469
+ *
470
+ * @param session - The session data to serialize
471
+ * @param nonce - Optional CSP nonce for the script tag
472
+ */
473
+ declare function createSessionScript(session: SessionData, nonce?: string): string;
402
474
  interface SSRHandlerOptions {
403
475
  /** The loaded SSR module (import('./dist/server/index.js')) */
404
476
  module: SSRModule;
@@ -424,16 +496,19 @@ interface SSRHandlerOptions {
424
496
  * so that strict Content-Security-Policy headers do not block it.
425
497
  */
426
498
  nonce?: string;
499
+ /** Pre-computed font fallback metrics (computed at server startup). */
500
+ fallbackMetrics?: Record<string, FontFallbackMetrics4>;
501
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
502
+ modulepreload?: string[];
503
+ /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
504
+ cacheControl?: string;
505
+ /**
506
+ * Resolves session data from request cookies for SSR injection.
507
+ * When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
508
+ * optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
509
+ */
510
+ sessionResolver?: SessionResolver;
427
511
  }
428
- /**
429
- * Create a web-standard SSR request handler.
430
- *
431
- * Handles two types of requests:
432
- * - X-Vertz-Nav: 1 -> SSE Response with pre-fetched query data
433
- * - Normal HTML request -> SSR-rendered HTML Response
434
- *
435
- * Does NOT serve static files — that's the adapter/platform's job.
436
- */
437
512
  declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
438
513
  interface GenerateSSRHtmlOptions {
439
514
  appHtml: string;
@@ -444,6 +519,10 @@ interface GenerateSSRHtmlOptions {
444
519
  }>;
445
520
  clientEntry: string;
446
521
  title?: string;
522
+ /** Extra HTML tags to inject into <head> before CSS (e.g., font preloads). */
523
+ headTags?: string;
524
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
525
+ modulepreload?: string[];
447
526
  }
448
527
  /**
449
528
  * Generate a complete HTML document from SSR render results.
@@ -499,4 +578,4 @@ declare function collectStreamChunks(stream: ReadableStream<Uint8Array>): Promis
499
578
  * @param nonce - Optional CSP nonce to add to the inline script tag.
500
579
  */
501
580
  declare function createTemplateChunk(slotId: number, resolvedHtml: string, nonce?: string): string;
502
- export { wrapWithHydrationMarkers, streamToString, ssrStorage, ssrRenderToString, ssrDiscoverQueries, setGlobalSSRTimeout, serializeToHtml, safeSerialize, resetSlotCounter, renderToStream, renderToHTMLStream, renderToHTML, renderPage, renderHeadToHtml, renderAssetTags, registerSSRQuery, rawHtml, isInSSR, inlineCriticalCss, getStreamingRuntimeScript, getSSRUrl, getSSRQueries, getGlobalSSRTimeout, getAccessSetForSSR, generateSSRHtml, encodeChunk, createTemplateChunk, createSlotPlaceholder, createSSRHandler, createSSRDataChunk, createSSRAdapter, createAccessSetScript, collectStreamChunks, clearGlobalSSRTimeout, VNode, SSRRenderResult, SSRQueryEntry2 as SSRQueryEntry, SSRModule, SSRHandlerOptions, SSRDiscoverResult, RenderToStreamOptions, RenderToHTMLStreamOptions, RenderToHTMLOptions, RawHtml, PageOptions, HydrationOptions, HeadEntry, HeadCollector, GenerateSSRHtmlOptions, AssetDescriptor };
581
+ export { wrapWithHydrationMarkers, streamToString, ssrStorage, ssrRenderToString, ssrDiscoverQueries, setGlobalSSRTimeout, serializeToHtml, safeSerialize, resetSlotCounter, renderToStream, renderToHTMLStream, renderToHTML, renderPage, renderHeadToHtml, renderAssetTags, registerSSRQuery, rawHtml, isInSSR, inlineCriticalCss, getStreamingRuntimeScript, getSSRUrl, getSSRQueries, getGlobalSSRTimeout, getAccessSetForSSR, generateSSRHtml, extractFontMetrics, encodeChunk, detectFallbackFont, createTemplateChunk, createSlotPlaceholder, createSessionScript, createSSRHandler, createSSRDataChunk, createSSRAdapter, createAccessSetScript, collectStreamChunks, clearGlobalSSRTimeout, VNode, SessionResolver, SessionData, SSRSessionInfo, SSRRenderResult, SSRQueryEntry2 as SSRQueryEntry, SSRModule, SSRHandlerOptions, SSRDiscoverResult, RenderToStreamOptions, RenderToHTMLStreamOptions, RenderToHTMLOptions, RawHtml, PageOptions, HydrationOptions, HeadEntry, HeadCollector, GenerateSSRHtmlOptions, AssetDescriptor };
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  collectStreamChunks,
3
+ createAccessSetScript,
3
4
  createRequestContext,
4
5
  createSSRDataChunk,
5
6
  createSSRHandler,
7
+ createSessionScript,
6
8
  createSlotPlaceholder,
7
9
  createTemplateChunk,
8
10
  encodeChunk,
9
11
  escapeAttr,
10
12
  escapeHtml,
13
+ getAccessSetForSSR,
11
14
  getStreamingRuntimeScript,
12
15
  renderToStream,
13
16
  resetSlotCounter,
@@ -16,7 +19,7 @@ import {
16
19
  ssrDiscoverQueries,
17
20
  ssrRenderToString,
18
21
  streamToString
19
- } from "./shared/chunk-98972e43.js";
22
+ } from "./shared/chunk-c5ee9yf1.js";
20
23
  import {
21
24
  clearGlobalSSRTimeout,
22
25
  createSSRAdapter,
@@ -28,7 +31,7 @@ import {
28
31
  registerSSRQuery,
29
32
  setGlobalSSRTimeout,
30
33
  ssrStorage
31
- } from "./shared/chunk-n1arq9xq.js";
34
+ } from "./shared/chunk-9jjdzz8c.js";
32
35
 
33
36
  // src/asset-pipeline.ts
34
37
  function renderAssetTags(assets) {
@@ -55,6 +58,108 @@ function inlineCriticalCss(css) {
55
58
  const safeCss = css.replace(/<\/style>/gi, "<\\/style>");
56
59
  return `<style>${safeCss}</style>`;
57
60
  }
61
+ // src/font-metrics.ts
62
+ import { readFile } from "node:fs/promises";
63
+ import { join } from "node:path";
64
+ import { fromBuffer } from "@capsizecss/unpack";
65
+ var SYSTEM_FONT_METRICS = {
66
+ Arial: {
67
+ ascent: 1854,
68
+ descent: -434,
69
+ lineGap: 67,
70
+ unitsPerEm: 2048,
71
+ xWidthAvg: 904
72
+ },
73
+ "Times New Roman": {
74
+ ascent: 1825,
75
+ descent: -443,
76
+ lineGap: 87,
77
+ unitsPerEm: 2048,
78
+ xWidthAvg: 819
79
+ },
80
+ "Courier New": {
81
+ ascent: 1705,
82
+ descent: -615,
83
+ lineGap: 0,
84
+ unitsPerEm: 2048,
85
+ xWidthAvg: 1229
86
+ }
87
+ };
88
+ function detectFallbackFont(fallback) {
89
+ for (const f of fallback) {
90
+ const lower = f.toLowerCase();
91
+ if (lower === "sans-serif" || lower === "system-ui")
92
+ return "Arial";
93
+ if (lower === "serif")
94
+ return "Times New Roman";
95
+ if (lower === "monospace")
96
+ return "Courier New";
97
+ }
98
+ return "Arial";
99
+ }
100
+ function formatPercent(value) {
101
+ return `${(value * 100).toFixed(2)}%`;
102
+ }
103
+ function computeFallbackMetrics(fontMetrics, fallbackFont) {
104
+ const systemMetrics = SYSTEM_FONT_METRICS[fallbackFont];
105
+ const fontNormalizedWidth = fontMetrics.xWidthAvg / fontMetrics.unitsPerEm;
106
+ const systemNormalizedWidth = systemMetrics.xWidthAvg / systemMetrics.unitsPerEm;
107
+ const sizeAdjust = fontNormalizedWidth / systemNormalizedWidth;
108
+ const ascentOverride = fontMetrics.ascent / (fontMetrics.unitsPerEm * sizeAdjust);
109
+ const descentOverride = Math.abs(fontMetrics.descent) / (fontMetrics.unitsPerEm * sizeAdjust);
110
+ const lineGapOverride = fontMetrics.lineGap / (fontMetrics.unitsPerEm * sizeAdjust);
111
+ return {
112
+ ascentOverride: formatPercent(ascentOverride),
113
+ descentOverride: formatPercent(descentOverride),
114
+ lineGapOverride: formatPercent(lineGapOverride),
115
+ sizeAdjust: formatPercent(sizeAdjust),
116
+ fallbackFont
117
+ };
118
+ }
119
+ function getPrimarySrcPath(descriptor) {
120
+ const { src } = descriptor;
121
+ if (!src)
122
+ return null;
123
+ if (typeof src === "string")
124
+ return src;
125
+ const first = src[0];
126
+ if (first)
127
+ return first.path;
128
+ return null;
129
+ }
130
+ function resolveFilePath(urlPath, rootDir) {
131
+ const cleaned = urlPath.startsWith("/") ? urlPath.slice(1) : urlPath;
132
+ return join(rootDir, cleaned);
133
+ }
134
+ async function extractFontMetrics(fonts, rootDir) {
135
+ const result = {};
136
+ for (const [key, descriptor] of Object.entries(fonts)) {
137
+ const adjustFontFallback = descriptor.adjustFontFallback ?? true;
138
+ if (adjustFontFallback === false)
139
+ continue;
140
+ const srcPath = getPrimarySrcPath(descriptor);
141
+ if (!srcPath)
142
+ continue;
143
+ if (!srcPath.toLowerCase().endsWith(".woff2"))
144
+ continue;
145
+ try {
146
+ const filePath = resolveFilePath(srcPath, rootDir);
147
+ const buffer = await readFile(filePath);
148
+ const metrics = await fromBuffer(buffer);
149
+ const fallbackFont = typeof adjustFontFallback === "string" ? adjustFontFallback : detectFallbackFont(descriptor.fallback);
150
+ result[key] = computeFallbackMetrics({
151
+ ascent: metrics.ascent,
152
+ descent: metrics.descent,
153
+ lineGap: metrics.lineGap,
154
+ unitsPerEm: metrics.unitsPerEm,
155
+ xWidthAvg: metrics.xWidthAvg
156
+ }, fallbackFont);
157
+ } catch (error) {
158
+ console.warn(`[vertz] Failed to extract font metrics for "${key}" from "${srcPath}":`, error instanceof Error ? error.message : error);
159
+ }
160
+ }
161
+ return result;
162
+ }
58
163
  // src/head.ts
59
164
  class HeadCollector {
60
165
  entries = [];
@@ -233,10 +338,10 @@ async function twoPassRender(options) {
233
338
  const pendingQueries = queries.filter((q) => !q.resolved);
234
339
  const vnode = options.app();
235
340
  const collectedCSS = getInjectedCSS();
236
- const themeCss = options.theme ? compileTheme(options.theme).css : "";
341
+ const themeCss = options.theme ? compileTheme(options.theme, { fallbackMetrics: options.fallbackMetrics }).css : "";
237
342
  const allStyles = [themeCss, ...options.styles ?? [], ...collectedCSS].filter(Boolean);
238
- const styleTags = allStyles.map((css) => `<style>${css}</style>`).join(`
239
- `);
343
+ const styleTags = allStyles.length > 0 ? `<style>${allStyles.join(`
344
+ `)}</style>` : "";
240
345
  const metaHtml = options.head?.meta?.map((m) => `<meta ${m.name ? `name="${m.name}"` : `property="${m.property}"`} content="${m.content}">`).join(`
241
346
  `) ?? "";
242
347
  const linkHtml = options.head?.links?.map((link) => `<link rel="${link.rel}" href="${link.href}">`).join(`
@@ -313,44 +418,21 @@ async function renderToHTML(appOrOptions, maybeOptions) {
313
418
  }
314
419
  });
315
420
  }
316
- // src/ssr-access-set.ts
317
- function getAccessSetForSSR(jwtPayload) {
318
- if (!jwtPayload)
319
- return null;
320
- const acl = jwtPayload.acl;
321
- if (!acl)
322
- return null;
323
- if (acl.overflow)
324
- return null;
325
- if (!acl.set)
326
- return null;
327
- return {
328
- entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
329
- name,
330
- {
331
- allowed: check.allowed,
332
- reasons: check.reasons ?? [],
333
- ...check.reason ? { reason: check.reason } : {},
334
- ...check.meta ? { meta: check.meta } : {}
335
- }
336
- ])),
337
- flags: acl.set.flags,
338
- plan: acl.set.plan,
339
- computedAt: acl.set.computedAt
340
- };
341
- }
342
- function createAccessSetScript(accessSet, nonce) {
343
- const json = JSON.stringify(accessSet);
344
- const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
345
- const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
346
- return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
347
- }
348
- function escapeAttr2(s) {
349
- return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
350
- }
351
421
  // src/ssr-html.ts
352
422
  function generateSSRHtml(options) {
353
- const { appHtml, css, ssrData, clientEntry, title = "Vertz App" } = options;
423
+ const {
424
+ appHtml,
425
+ css,
426
+ ssrData,
427
+ clientEntry,
428
+ title = "Vertz App",
429
+ headTags: rawHeadTags = "",
430
+ modulepreload
431
+ } = options;
432
+ const modulepreloadTags = modulepreload?.length ? modulepreload.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
433
+ `) : "";
434
+ const headTags = [rawHeadTags, modulepreloadTags].filter(Boolean).join(`
435
+ `);
354
436
  const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__ = ${JSON.stringify(ssrData)};</script>` : "";
355
437
  return `<!doctype html>
356
438
  <html lang="en">
@@ -358,6 +440,7 @@ function generateSSRHtml(options) {
358
440
  <meta charset="UTF-8" />
359
441
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
360
442
  <title>${escapeHtml(title)}</title>
443
+ ${headTags}
361
444
  ${css}
362
445
  </head>
363
446
  <body>
@@ -393,9 +476,12 @@ export {
393
476
  getGlobalSSRTimeout,
394
477
  getAccessSetForSSR,
395
478
  generateSSRHtml,
479
+ extractFontMetrics,
396
480
  encodeChunk,
481
+ detectFallbackFont,
397
482
  createTemplateChunk,
398
483
  createSlotPlaceholder,
484
+ createSessionScript,
399
485
  createSSRHandler,
400
486
  createSSRDataChunk,
401
487
  createSSRAdapter,
@@ -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: () => {}