@vertz/ui-server 0.2.30 → 0.2.32

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.
package/dist/index.js CHANGED
@@ -1,10 +1,19 @@
1
+ import {
2
+ createHoles,
3
+ createSSRHandler,
4
+ isAotDebugEnabled,
5
+ loadAotManifest,
6
+ ssrRenderAot
7
+ } from "./shared/chunk-gbcsa7h1.js";
8
+ import {
9
+ createNodeHandler
10
+ } from "./shared/chunk-es0406qq.js";
1
11
  import {
2
12
  collectStreamChunks,
3
13
  compileThemeCached,
4
14
  createAccessSetScript,
5
15
  createRequestContext,
6
16
  createSSRDataChunk,
7
- createSSRHandler,
8
17
  createSessionScript,
9
18
  createSlotPlaceholder,
10
19
  createTemplateChunk,
@@ -25,28 +34,30 @@ import {
25
34
  ssrRenderToString,
26
35
  streamToString,
27
36
  toPrefetchSession
28
- } from "./shared/chunk-bd0sgykf.js";
37
+ } from "./shared/chunk-34fexgex.js";
29
38
  import {
30
39
  clearGlobalSSRTimeout,
31
40
  createSSRAdapter,
32
41
  getGlobalSSRTimeout,
33
42
  getSSRQueries,
34
43
  getSSRUrl,
35
- installDomShim,
36
44
  isInSSR,
37
45
  rawHtml,
38
46
  registerSSRQuery,
39
47
  setGlobalSSRTimeout,
40
- ssrStorage,
41
- toVNode
42
- } from "./shared/chunk-gcwqkynf.js";
48
+ ssrStorage
49
+ } from "./shared/chunk-ybftdw1r.js";
50
+
51
+ // src/index.ts
52
+ import { extractRoutes } from "@vertz/ui-compiler";
43
53
 
44
54
  // src/aot-manifest-build.ts
45
55
  import { readdirSync, readFileSync } from "node:fs";
46
- import { join } from "node:path";
56
+ import { basename, join } from "node:path";
47
57
  import { compileForSSRAot } from "@vertz/ui-compiler";
48
58
  function generateAotBuildManifest(srcDir) {
49
59
  const components = {};
60
+ const compiledFiles = {};
50
61
  const classificationLog = [];
51
62
  const tsxFiles = collectTsxFiles(srcDir);
52
63
  for (const filePath of tsxFiles) {
@@ -56,7 +67,14 @@ function generateAotBuildManifest(srcDir) {
56
67
  for (const comp of result.components) {
57
68
  components[comp.name] = {
58
69
  tier: comp.tier,
59
- holes: comp.holes
70
+ holes: comp.holes,
71
+ queryKeys: comp.queryKeys
72
+ };
73
+ }
74
+ if (result.components.length > 0) {
75
+ compiledFiles[filePath] = {
76
+ code: result.code,
77
+ components: result.components
60
78
  };
61
79
  }
62
80
  } catch (e) {
@@ -83,7 +101,61 @@ function generateAotBuildManifest(srcDir) {
83
101
  const pct = Math.round(aotCount / total * 100);
84
102
  classificationLog.push(`Coverage: ${aotCount}/${total} components (${pct}%)`);
85
103
  }
86
- return { components, classificationLog };
104
+ return { components, compiledFiles, classificationLog };
105
+ }
106
+ function buildAotRouteMap(components, routes) {
107
+ const routeMap = {};
108
+ for (const route of routes) {
109
+ const comp = components[route.componentName];
110
+ if (!comp || comp.tier === "runtime-fallback")
111
+ continue;
112
+ routeMap[route.pattern] = {
113
+ renderFn: `__ssr_${route.componentName}`,
114
+ holes: comp.holes,
115
+ queryKeys: comp.queryKeys
116
+ };
117
+ }
118
+ return routeMap;
119
+ }
120
+ function generateAotBarrel(compiledFiles, routeMap) {
121
+ const neededFns = new Set;
122
+ for (const entry of Object.values(routeMap)) {
123
+ neededFns.add(entry.renderFn);
124
+ }
125
+ const fnToFile = new Map;
126
+ for (const [filePath, compiled] of Object.entries(compiledFiles)) {
127
+ for (const comp of compiled.components) {
128
+ const fnName = `__ssr_${comp.name}`;
129
+ if (neededFns.has(fnName)) {
130
+ fnToFile.set(fnName, filePath);
131
+ }
132
+ }
133
+ }
134
+ const fileToFns = new Map;
135
+ for (const [fnName, filePath] of fnToFile) {
136
+ const existing = fileToFns.get(filePath) ?? [];
137
+ existing.push(fnName);
138
+ fileToFns.set(filePath, existing);
139
+ }
140
+ const lines = [];
141
+ const files = {};
142
+ let fileIndex = 0;
143
+ for (const [filePath, fns] of fileToFns) {
144
+ const moduleName = basename(filePath, ".tsx").replace(/[^a-zA-Z0-9_-]/g, "_");
145
+ const tempFileName = `__aot_${fileIndex}_${moduleName}`;
146
+ const moduleRef = `./${tempFileName}`;
147
+ lines.push(`export { ${fns.sort().join(", ")} } from '${moduleRef}';`);
148
+ const compiled = compiledFiles[filePath];
149
+ if (compiled) {
150
+ files[`${tempFileName}.tsx`] = compiled.code;
151
+ }
152
+ fileIndex++;
153
+ }
154
+ return {
155
+ barrelSource: lines.join(`
156
+ `),
157
+ files
158
+ };
87
159
  }
88
160
  function collectTsxFiles(dir) {
89
161
  const files = [];
@@ -665,147 +737,6 @@ function createAotManifestManager(options) {
665
737
  }
666
738
  };
667
739
  }
668
- // src/ssr-aot-pipeline.ts
669
- function createHoles(holeNames, module, url, queryCache, ssrAuth) {
670
- if (holeNames.length === 0)
671
- return {};
672
- const holes = {};
673
- for (const name of holeNames) {
674
- holes[name] = () => {
675
- const holeCtx = createRequestContext(url);
676
- for (const [key, data] of queryCache) {
677
- holeCtx.queryCache.set(key, data);
678
- }
679
- if (ssrAuth) {
680
- holeCtx.ssrAuth = ssrAuth;
681
- }
682
- holeCtx.resolvedComponents = new Map;
683
- return ssrStorage.run(holeCtx, () => {
684
- ensureDomShim();
685
- const factory = resolveHoleComponent(module, name);
686
- if (!factory) {
687
- return `<!-- AOT hole: ${name} not found -->`;
688
- }
689
- const node = factory();
690
- const vnode = toVNode(node);
691
- return serializeToHtml(vnode);
692
- });
693
- };
694
- }
695
- return holes;
696
- }
697
- function resolveHoleComponent(module, name) {
698
- const moduleRecord = module;
699
- const exported = moduleRecord[name];
700
- if (typeof exported === "function") {
701
- return exported;
702
- }
703
- return;
704
- }
705
- async function ssrRenderAot(module, url, options) {
706
- const { aotManifest, manifest } = options;
707
- const ssrTimeout = options.ssrTimeout ?? 300;
708
- const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
709
- const fallbackOptions = {
710
- ssrTimeout,
711
- fallbackMetrics: options.fallbackMetrics,
712
- ssrAuth: options.ssrAuth,
713
- manifest,
714
- prefetchSession: options.prefetchSession
715
- };
716
- const aotPatterns = Object.keys(aotManifest.routes);
717
- const matches = matchUrlToPatterns(normalizedUrl, aotPatterns);
718
- if (matches.length === 0) {
719
- return ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
720
- }
721
- const match = matches[matches.length - 1];
722
- if (!match) {
723
- return ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
724
- }
725
- const aotEntry = aotManifest.routes[match.pattern];
726
- if (!aotEntry) {
727
- return ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
728
- }
729
- const queryCache = new Map;
730
- try {
731
- setGlobalSSRTimeout(ssrTimeout);
732
- const holes = createHoles(aotEntry.holes, module, normalizedUrl, queryCache, options.ssrAuth);
733
- const ctx = {
734
- holes,
735
- getData: (key) => queryCache.get(key),
736
- session: options.prefetchSession,
737
- params: match.params
738
- };
739
- const data = {};
740
- for (const [key, value] of queryCache) {
741
- data[key] = value;
742
- }
743
- const html = aotEntry.render(data, ctx);
744
- if (options.diagnostics && isAotDebugEnabled()) {
745
- try {
746
- const domResult = await ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
747
- if (domResult.html !== html) {
748
- options.diagnostics.recordDivergence(match.pattern, html, domResult.html);
749
- }
750
- } catch {}
751
- }
752
- const css = collectCSSFromModule(module, options.fallbackMetrics);
753
- const ssrData = [];
754
- for (const [key, data2] of queryCache) {
755
- ssrData.push({ key, data: data2 });
756
- }
757
- return {
758
- html,
759
- css: css.cssString,
760
- ssrData,
761
- headTags: css.preloadTags
762
- };
763
- } finally {
764
- clearGlobalSSRTimeout();
765
- }
766
- }
767
- function isAotDebugEnabled() {
768
- const env = process.env.VERTZ_DEBUG;
769
- if (!env)
770
- return false;
771
- return env === "1" || env.split(",").includes("aot");
772
- }
773
- var domShimInstalled = false;
774
- function ensureDomShim() {
775
- if (domShimInstalled && typeof document !== "undefined")
776
- return;
777
- domShimInstalled = true;
778
- installDomShim();
779
- }
780
- function collectCSSFromModule(module, fallbackMetrics) {
781
- let themeCss = "";
782
- let preloadTags = "";
783
- if (module.theme) {
784
- try {
785
- const compiled = compileThemeCached(module.theme, fallbackMetrics);
786
- themeCss = compiled.css;
787
- preloadTags = compiled.preloadTags;
788
- } catch (e) {
789
- console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
790
- }
791
- }
792
- const alreadyIncluded = new Set;
793
- if (themeCss)
794
- alreadyIncluded.add(themeCss);
795
- if (module.styles) {
796
- for (const s of module.styles)
797
- alreadyIncluded.add(s);
798
- }
799
- const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
800
- const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
801
- const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
802
- `)}</style>` : "";
803
- const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
804
- `)}</style>` : "";
805
- const cssString = [themeTag, globalTag, componentTag].filter(Boolean).join(`
806
- `);
807
- return { cssString, preloadTags };
808
- }
809
740
  // src/ssr-aot-runtime.ts
810
741
  var UNITLESS_PROPERTIES = new Set([
811
742
  "animationIterationCount",
@@ -1089,6 +1020,7 @@ export {
1089
1020
  reconstructDescriptors,
1090
1021
  rawHtml,
1091
1022
  matchUrlToPatterns,
1023
+ loadAotManifest,
1092
1024
  isInSSR,
1093
1025
  isAotDebugEnabled,
1094
1026
  inlineCriticalCss,
@@ -1099,6 +1031,8 @@ export {
1099
1031
  getAccessSetForSSR,
1100
1032
  generateSSRHtml,
1101
1033
  generateAotBuildManifest,
1034
+ generateAotBarrel,
1035
+ extractRoutes,
1102
1036
  extractFontMetrics,
1103
1037
  evaluateAccessRule,
1104
1038
  encodeChunk,
@@ -1110,11 +1044,13 @@ export {
1110
1044
  createSSRDataChunk,
1111
1045
  createSSRAdapter,
1112
1046
  createPrefetchManifestManager,
1047
+ createNodeHandler,
1113
1048
  createHoles,
1114
1049
  createAotManifestManager,
1115
1050
  createAccessSetScript,
1116
1051
  collectStreamChunks,
1117
1052
  clearGlobalSSRTimeout,
1053
+ buildAotRouteMap,
1118
1054
  __ssr_style_object,
1119
1055
  __ssr_spread,
1120
1056
  __esc_attr,
@@ -0,0 +1,223 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { FontFallbackMetrics as FontFallbackMetrics4 } from "@vertz/ui";
3
+ /**
4
+ * SSR prefetch access rule evaluator.
5
+ *
6
+ * Evaluates serialized entity access rules against the current session
7
+ * to determine whether a query should be prefetched during SSR.
8
+ *
9
+ * The serialized rules come from the prefetch manifest (generated at build time).
10
+ * The session comes from the JWT decoded at request time.
11
+ */
12
+ /**
13
+ * Serialized access rule — the JSON-friendly format stored in the manifest.
14
+ * Mirrors SerializedRule from @vertz/server/auth/rules but defined here
15
+ * to avoid importing the server package into the SSR pipeline.
16
+ */
17
+ type SerializedAccessRule = {
18
+ type: "public";
19
+ } | {
20
+ type: "authenticated";
21
+ } | {
22
+ type: "role";
23
+ roles: string[];
24
+ } | {
25
+ type: "entitlement";
26
+ value: string;
27
+ } | {
28
+ type: "where";
29
+ conditions: Record<string, unknown>;
30
+ } | {
31
+ type: "all";
32
+ rules: SerializedAccessRule[];
33
+ } | {
34
+ type: "any";
35
+ rules: SerializedAccessRule[];
36
+ } | {
37
+ type: "fva";
38
+ maxAge: number;
39
+ } | {
40
+ type: "deny";
41
+ };
42
+ /**
43
+ * Minimal session shape needed for prefetch access evaluation.
44
+ * Extracted from the JWT at SSR request time.
45
+ */
46
+ type PrefetchSession = {
47
+ status: "authenticated";
48
+ roles?: string[];
49
+ entitlements?: Record<string, boolean>;
50
+ tenantId?: string;
51
+ } | {
52
+ status: "unauthenticated";
53
+ };
54
+ import { CompiledRoute, Theme } from "@vertz/ui";
55
+ interface SSRModule {
56
+ default?: () => unknown;
57
+ App?: () => unknown;
58
+ theme?: Theme;
59
+ /** Global CSS strings to include in every SSR response (e.g. resets, body styles). */
60
+ styles?: string[];
61
+ /**
62
+ * Return all CSS tracked by the bundled @vertz/ui instance.
63
+ * The Vite SSR build inlines @vertz/ui into the server bundle, creating
64
+ * a separate module instance from @vertz/ui-server's dependency. Without
65
+ * this, component CSS from module-level css() calls is invisible to the
66
+ * SSR renderer. Export `getInjectedCSS` from @vertz/ui in the app entry.
67
+ */
68
+ getInjectedCSS?: () => string[];
69
+ /** Compiled routes exported from the app for build-time SSG with generateParams. */
70
+ routes?: CompiledRoute[];
71
+ /** Code-generated API client for manifest-driven zero-discovery prefetching. */
72
+ api?: Record<string, Record<string, (...args: unknown[]) => unknown>>;
73
+ }
74
+ import { ExtractedQuery } from "@vertz/ui-compiler";
75
+ /** Serialized entity access rules from the prefetch manifest. */
76
+ type EntityAccessMap = Record<string, Partial<Record<string, SerializedAccessRule>>>;
77
+ interface SSRPrefetchManifest {
78
+ /** Route patterns present in the manifest. */
79
+ routePatterns: string[];
80
+ /** Entity access rules keyed by entity name → operation → serialized rule. */
81
+ entityAccess?: EntityAccessMap;
82
+ /** Route entries with query binding metadata for zero-discovery prefetch. */
83
+ routeEntries?: Record<string, {
84
+ queries: ExtractedQuery[];
85
+ }>;
86
+ }
87
+ /** Context passed to AOT render functions for accessing data and runtime holes. */
88
+ interface SSRAotContext {
89
+ /** Pre-generated closures for runtime-rendered components. */
90
+ holes: Record<string, () => string>;
91
+ /** Access query data by cache key. */
92
+ getData(key: string): unknown;
93
+ /** Auth session for conditional rendering. */
94
+ session: PrefetchSession | undefined;
95
+ /** Route params for the current request. */
96
+ params: Record<string, string>;
97
+ }
98
+ /** An AOT render function: takes props/data and context, returns HTML string. */
99
+ type AotRenderFn = (data: Record<string, unknown>, ctx: SSRAotContext) => string;
100
+ /** Per-route AOT entry in the manifest. */
101
+ interface AotRouteEntry {
102
+ /** The pre-compiled render function. */
103
+ render: AotRenderFn;
104
+ /** Component names that need runtime fallback (holes). */
105
+ holes: string[];
106
+ /** Query cache keys this route reads via ctx.getData(). */
107
+ queryKeys?: string[];
108
+ }
109
+ /**
110
+ * AOT manifest — maps route patterns to pre-compiled render functions.
111
+ *
112
+ * Generated at build time by the AOT compiler pipeline.
113
+ */
114
+ interface AotManifest {
115
+ /** Route pattern → AOT entry. */
116
+ routes: Record<string, AotRouteEntry>;
117
+ }
118
+ import { AccessSet } from "@vertz/ui/auth";
119
+ interface SessionData {
120
+ user: {
121
+ id: string;
122
+ email: string;
123
+ role: string;
124
+ [key: string]: unknown;
125
+ };
126
+ /** Unix timestamp in milliseconds (JWT exp * 1000). */
127
+ expiresAt: number;
128
+ }
129
+ /** Resolved session data for SSR injection. */
130
+ interface SSRSessionInfo {
131
+ session: SessionData;
132
+ /**
133
+ * Access set from JWT acl claim.
134
+ * - Present (object): inline access set (no overflow)
135
+ * - null: access control is configured but the set overflowed the JWT
136
+ * - undefined: access control is not configured
137
+ */
138
+ accessSet?: AccessSet | null;
139
+ }
140
+ /**
141
+ * Callback that extracts session data from a request.
142
+ * Returns null when no valid session exists (expired, missing, or invalid cookie).
143
+ */
144
+ type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
145
+ interface SSRHandlerOptions {
146
+ /** The loaded SSR module (import('./dist/server/index.js')) */
147
+ module: SSRModule;
148
+ /** HTML template string (contents of dist/client/index.html) */
149
+ template: string;
150
+ /** SSR timeout for queries (default: 300ms) */
151
+ ssrTimeout?: number;
152
+ /**
153
+ * Map of CSS asset URLs to their content for inlining.
154
+ * Replaces `<link rel="stylesheet" href="...">` tags with inline `<style>` tags.
155
+ * Eliminates extra network requests, preventing FOUC on slow connections.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * inlineCSS: { '/assets/vertz.css': await Bun.file('./dist/client/assets/vertz.css').text() }
160
+ * ```
161
+ */
162
+ inlineCSS?: Record<string, string>;
163
+ /**
164
+ * CSP nonce to inject on all inline `<script>` tags emitted during SSR.
165
+ *
166
+ * When set, the SSR data hydration script will include `nonce="<value>"`
167
+ * so that strict Content-Security-Policy headers do not block it.
168
+ */
169
+ nonce?: string;
170
+ /** Pre-computed font fallback metrics (computed at server startup). */
171
+ fallbackMetrics?: Record<string, FontFallbackMetrics4>;
172
+ /** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
173
+ modulepreload?: string[];
174
+ /**
175
+ * Route chunk manifest for per-route modulepreload injection.
176
+ * When provided, only chunks for the matched route are preloaded instead of all chunks.
177
+ */
178
+ routeChunkManifest?: {
179
+ routes: Record<string, string[]>;
180
+ };
181
+ /** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
182
+ cacheControl?: string;
183
+ /**
184
+ * Resolves session data from request cookies for SSR injection.
185
+ * When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
186
+ * optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
187
+ */
188
+ sessionResolver?: SessionResolver;
189
+ /**
190
+ * Prefetch manifest for single-pass SSR optimization.
191
+ *
192
+ * When provided with route entries and an API client export, enables
193
+ * zero-discovery rendering — queries are prefetched from the manifest
194
+ * without executing the component tree, then a single render pass
195
+ * produces the HTML. Without a manifest, SSR still uses the single-pass
196
+ * discovery-then-render approach (cheaper than two-pass).
197
+ */
198
+ manifest?: SSRPrefetchManifest;
199
+ /**
200
+ * Enable progressive HTML streaming. Default: false.
201
+ *
202
+ * When true, the Response body is a ReadableStream that sends `<head>`
203
+ * content (CSS, preloads, fonts) before `<body>` rendering is complete.
204
+ * This improves TTFB and FCP.
205
+ *
206
+ * Has no effect on zero-discovery routes (manifest with routeEntries),
207
+ * which always use buffered rendering.
208
+ */
209
+ progressiveHTML?: boolean;
210
+ /**
211
+ * AOT manifest with pre-compiled render functions.
212
+ *
213
+ * When provided, routes matching AOT entries are rendered via string-builder
214
+ * functions (no DOM shim), bypassing the reactive runtime for 4-6x speedup.
215
+ * Routes not in the manifest fall back to `ssrRenderSinglePass()`.
216
+ *
217
+ * Load via `loadAotManifest(serverDir)` at startup.
218
+ */
219
+ aotManifest?: AotManifest;
220
+ }
221
+ type NodeHandlerOptions = SSRHandlerOptions;
222
+ declare function createNodeHandler(options: NodeHandlerOptions): (req: IncomingMessage, res: ServerResponse) => void;
223
+ export { createNodeHandler, NodeHandlerOptions };
@@ -0,0 +1,8 @@
1
+ import {
2
+ createNodeHandler
3
+ } from "./shared/chunk-es0406qq.js";
4
+ import"./shared/chunk-34fexgex.js";
5
+ import"./shared/chunk-ybftdw1r.js";
6
+ export {
7
+ createNodeHandler
8
+ };