@vertz/ui-server 0.2.14 → 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.
@@ -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,8 @@ 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";
354
378
  interface SSRModule {
355
379
  default?: () => unknown;
356
380
  App?: () => unknown;
@@ -373,6 +397,10 @@ interface SSRRenderResult {
373
397
  key: string;
374
398
  data: unknown;
375
399
  }>;
400
+ /** Font preload link tags for injection into <head>. */
401
+ headTags: string;
402
+ /** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
403
+ discoveredRoutes?: string[];
376
404
  }
377
405
  interface SSRDiscoverResult {
378
406
  resolved: Array<{
@@ -390,6 +418,8 @@ interface SSRDiscoverResult {
390
418
  */
391
419
  declare function ssrRenderToString(module: SSRModule, url: string, options?: {
392
420
  ssrTimeout?: number;
421
+ /** Pre-computed font fallback metrics (computed at server startup). */
422
+ fallbackMetrics?: Record<string, FontFallbackMetrics3>;
393
423
  }): Promise<SSRRenderResult>;
394
424
  /**
395
425
  * Discover queries for a given URL without rendering.
@@ -399,6 +429,41 @@ declare function ssrRenderToString(module: SSRModule, url: string, options?: {
399
429
  declare function ssrDiscoverQueries(module: SSRModule, url: string, options?: {
400
430
  ssrTimeout?: number;
401
431
  }): Promise<SSRDiscoverResult>;
432
+ import { AccessSet as AccessSet2 } from "@vertz/ui/auth";
433
+ interface SessionData {
434
+ user: {
435
+ id: string;
436
+ email: string;
437
+ role: string;
438
+ [key: string]: unknown;
439
+ };
440
+ /** Unix timestamp in milliseconds (JWT exp * 1000). */
441
+ expiresAt: number;
442
+ }
443
+ /** Resolved session data for SSR injection. */
444
+ interface SSRSessionInfo {
445
+ session: SessionData;
446
+ /**
447
+ * Access set from JWT acl claim.
448
+ * - Present (object): inline access set (no overflow)
449
+ * - null: access control is configured but the set overflowed the JWT
450
+ * - undefined: access control is not configured
451
+ */
452
+ accessSet?: AccessSet2 | null;
453
+ }
454
+ /**
455
+ * Callback that extracts session data from a request.
456
+ * Returns null when no valid session exists (expired, missing, or invalid cookie).
457
+ */
458
+ type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
459
+ /**
460
+ * Serialize a session into a `<script>` tag that sets
461
+ * `window.__VERTZ_SESSION__`.
462
+ *
463
+ * @param session - The session data to serialize
464
+ * @param nonce - Optional CSP nonce for the script tag
465
+ */
466
+ declare function createSessionScript(session: SessionData, nonce?: string): string;
402
467
  interface SSRHandlerOptions {
403
468
  /** The loaded SSR module (import('./dist/server/index.js')) */
404
469
  module: SSRModule;
@@ -424,16 +489,19 @@ interface SSRHandlerOptions {
424
489
  * so that strict Content-Security-Policy headers do not block it.
425
490
  */
426
491
  nonce?: string;
492
+ /** Pre-computed font fallback metrics (computed at server startup). */
493
+ fallbackMetrics?: Record<string, FontFallbackMetrics4>;
494
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
495
+ modulepreload?: string[];
496
+ /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
497
+ cacheControl?: string;
498
+ /**
499
+ * Resolves session data from request cookies for SSR injection.
500
+ * When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
501
+ * optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
502
+ */
503
+ sessionResolver?: SessionResolver;
427
504
  }
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
505
  declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
438
506
  interface GenerateSSRHtmlOptions {
439
507
  appHtml: string;
@@ -444,6 +512,10 @@ interface GenerateSSRHtmlOptions {
444
512
  }>;
445
513
  clientEntry: string;
446
514
  title?: string;
515
+ /** Extra HTML tags to inject into <head> before CSS (e.g., font preloads). */
516
+ headTags?: string;
517
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
518
+ modulepreload?: string[];
447
519
  }
448
520
  /**
449
521
  * Generate a complete HTML document from SSR render results.
@@ -499,4 +571,4 @@ declare function collectStreamChunks(stream: ReadableStream<Uint8Array>): Promis
499
571
  * @param nonce - Optional CSP nonce to add to the inline script tag.
500
572
  */
501
573
  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 };
574
+ 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-969qgkdf.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,