@vertz/ui-server 0.2.32 → 0.2.34

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.
@@ -68,41 +68,136 @@ function serializeToHtml(node) {
68
68
  return `<${tag}${attrStr}>${childrenHtml}</${tag}>`;
69
69
  }
70
70
 
71
- // src/ssr-access-set.ts
72
- function getAccessSetForSSR(jwtPayload) {
73
- if (!jwtPayload)
74
- return null;
75
- const acl = jwtPayload.acl;
76
- if (!acl)
77
- return null;
78
- if (acl.overflow)
79
- return null;
80
- if (!acl.set)
81
- return null;
71
+ // src/ssr-access-evaluator.ts
72
+ function toPrefetchSession(ssrAuth, accessSet) {
73
+ if (!ssrAuth || ssrAuth.status !== "authenticated" || !ssrAuth.user) {
74
+ return { status: "unauthenticated" };
75
+ }
76
+ const roles = ssrAuth.user.role ? [ssrAuth.user.role] : undefined;
77
+ const entitlements = accessSet != null ? Object.fromEntries(Object.entries(accessSet.entitlements).map(([name, check]) => [name, check.allowed])) : undefined;
82
78
  return {
83
- entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
84
- name,
85
- {
86
- allowed: check.allowed,
87
- reasons: check.reasons ?? [],
88
- ...check.reason ? { reason: check.reason } : {},
89
- ...check.meta ? { meta: check.meta } : {}
90
- }
91
- ])),
92
- flags: acl.set.flags,
93
- plan: acl.set.plan,
94
- plans: acl.set.plans ?? {},
95
- computedAt: acl.set.computedAt
79
+ status: "authenticated",
80
+ roles,
81
+ entitlements,
82
+ tenantId: ssrAuth.user.tenantId
96
83
  };
97
84
  }
98
- function createAccessSetScript(accessSet, nonce) {
99
- const json = JSON.stringify(accessSet);
100
- const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
101
- const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
102
- return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
85
+ function evaluateAccessRule(rule, session) {
86
+ switch (rule.type) {
87
+ case "public":
88
+ return true;
89
+ case "authenticated":
90
+ return session.status === "authenticated";
91
+ case "role":
92
+ if (session.status !== "authenticated")
93
+ return false;
94
+ return session.roles?.some((r) => rule.roles.includes(r)) === true;
95
+ case "entitlement":
96
+ if (session.status !== "authenticated")
97
+ return false;
98
+ return session.entitlements?.[rule.value] === true;
99
+ case "where":
100
+ return true;
101
+ case "fva":
102
+ return session.status === "authenticated";
103
+ case "deny":
104
+ return false;
105
+ case "all":
106
+ return rule.rules.every((r) => evaluateAccessRule(r, session));
107
+ case "any":
108
+ return rule.rules.some((r) => evaluateAccessRule(r, session));
109
+ default:
110
+ return false;
111
+ }
103
112
  }
104
- function escapeAttr2(s) {
105
- return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
113
+
114
+ // src/ssr-manifest-prefetch.ts
115
+ function reconstructDescriptors(queries, routeParams, apiClient) {
116
+ if (!apiClient)
117
+ return [];
118
+ const result = [];
119
+ for (const query of queries) {
120
+ const descriptor = reconstructSingle(query, routeParams, apiClient);
121
+ if (descriptor) {
122
+ result.push(descriptor);
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ function reconstructSingle(query, routeParams, apiClient) {
128
+ const { entity, operation } = query;
129
+ if (!entity || !operation)
130
+ return;
131
+ const entitySdk = apiClient[entity];
132
+ if (!entitySdk)
133
+ return;
134
+ const method = entitySdk[operation];
135
+ if (typeof method !== "function")
136
+ return;
137
+ const args = buildFactoryArgs(query, routeParams);
138
+ if (args === undefined)
139
+ return;
140
+ try {
141
+ const descriptor = method(...args);
142
+ if (!descriptor || typeof descriptor._key !== "string" || typeof descriptor._fetch !== "function") {
143
+ return;
144
+ }
145
+ return { key: descriptor._key, fetch: descriptor._fetch };
146
+ } catch {
147
+ return;
148
+ }
149
+ }
150
+ function buildFactoryArgs(query, routeParams) {
151
+ const { operation, idParam, queryBindings } = query;
152
+ if (operation === "get") {
153
+ if (idParam) {
154
+ const id = routeParams[idParam];
155
+ if (!id)
156
+ return;
157
+ const options = resolveQueryBindings(queryBindings, routeParams);
158
+ if (options === undefined && queryBindings)
159
+ return;
160
+ return options ? [id, options] : [id];
161
+ }
162
+ return;
163
+ }
164
+ if (!queryBindings)
165
+ return [];
166
+ const resolved = resolveQueryBindings(queryBindings, routeParams);
167
+ if (resolved === undefined)
168
+ return;
169
+ return [resolved];
170
+ }
171
+ function resolveQueryBindings(bindings, routeParams) {
172
+ if (!bindings)
173
+ return;
174
+ const resolved = {};
175
+ if (bindings.where) {
176
+ const where = {};
177
+ for (const [key, value] of Object.entries(bindings.where)) {
178
+ if (value === null)
179
+ return;
180
+ if (typeof value === "string" && value.startsWith("$")) {
181
+ const paramName = value.slice(1);
182
+ const paramValue = routeParams[paramName];
183
+ if (!paramValue)
184
+ return;
185
+ where[key] = paramValue;
186
+ } else {
187
+ where[key] = value;
188
+ }
189
+ }
190
+ resolved.where = where;
191
+ }
192
+ if (bindings.select)
193
+ resolved.select = bindings.select;
194
+ if (bindings.include)
195
+ resolved.include = bindings.include;
196
+ if (bindings.orderBy)
197
+ resolved.orderBy = bindings.orderBy;
198
+ if (bindings.limit !== undefined)
199
+ resolved.limit = bindings.limit;
200
+ return resolved;
106
201
  }
107
202
 
108
203
  // src/slot-placeholder.ts
@@ -530,366 +625,61 @@ data: ${safeSerialize(entry)}
530
625
  });
531
626
  }
532
627
 
533
- // src/ssr-session.ts
534
- function createSessionScript(session, nonce) {
535
- const json = JSON.stringify(session);
536
- const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
537
- const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
538
- return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
628
+ // src/ssr-route-matcher.ts
629
+ function matchUrlToPatterns(url, patterns) {
630
+ const path = (url.split("?")[0] ?? "").split("#")[0] ?? "";
631
+ const matches = [];
632
+ for (const pattern of patterns) {
633
+ const result = matchPattern(path, pattern);
634
+ if (result) {
635
+ matches.push(result);
636
+ }
637
+ }
638
+ matches.sort((a, b) => {
639
+ const aSegments = a.pattern.split("/").length;
640
+ const bSegments = b.pattern.split("/").length;
641
+ return aSegments - bSegments;
642
+ });
643
+ return matches;
539
644
  }
540
- function escapeAttr3(s) {
541
- return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
645
+ function matchPattern(path, pattern) {
646
+ const pathSegments = path.split("/").filter(Boolean);
647
+ const patternSegments = pattern.split("/").filter(Boolean);
648
+ if (patternSegments.length > pathSegments.length)
649
+ return;
650
+ const params = {};
651
+ for (let i = 0;i < patternSegments.length; i++) {
652
+ const seg = patternSegments[i];
653
+ const val = pathSegments[i];
654
+ if (seg.startsWith(":")) {
655
+ params[seg.slice(1)] = val;
656
+ } else if (seg !== val) {
657
+ return;
658
+ }
659
+ }
660
+ return { pattern, params };
542
661
  }
543
662
 
544
- // src/template-split.ts
545
- function splitTemplate(template, options) {
546
- let processed = template;
547
- if (options?.inlineCSS) {
548
- processed = inlineCSSAssets(processed, options.inlineCSS);
663
+ // src/ssr-single-pass.ts
664
+ async function ssrRenderSinglePass(module, url, options) {
665
+ if (options?.prefetch === false) {
666
+ return ssrRenderToString(module, url, options);
549
667
  }
550
- const outletMarker = "<!--ssr-outlet-->";
551
- const outletIndex = processed.indexOf(outletMarker);
552
- if (outletIndex !== -1) {
553
- return {
554
- headTemplate: processed.slice(0, outletIndex),
555
- tailTemplate: processed.slice(outletIndex + outletMarker.length)
556
- };
668
+ const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
669
+ const ssrTimeout = options?.ssrTimeout ?? 300;
670
+ ensureDomShim2();
671
+ const zeroDiscoveryData = attemptZeroDiscovery(normalizedUrl, module, options, ssrTimeout);
672
+ if (zeroDiscoveryData) {
673
+ return renderWithPrefetchedData(module, normalizedUrl, zeroDiscoveryData, options);
557
674
  }
558
- const appDivMatch = processed.match(/<div[^>]*id="app"[^>]*>/);
559
- if (appDivMatch && appDivMatch.index != null) {
560
- const openTag = appDivMatch[0];
561
- const contentStart = appDivMatch.index + openTag.length;
562
- const closingIndex = findMatchingDivClose(processed, contentStart);
675
+ const discoveredData = await runDiscoveryPhase(normalizedUrl, ssrTimeout, module, options);
676
+ if ("redirect" in discoveredData) {
563
677
  return {
564
- headTemplate: processed.slice(0, contentStart),
565
- tailTemplate: processed.slice(closingIndex)
566
- };
567
- }
568
- throw new Error('Could not find <!--ssr-outlet--> or <div id="app"> in the HTML template. ' + "The template must contain one of these markers for SSR content injection.");
569
- }
570
- function findMatchingDivClose(html, startAfterOpen) {
571
- let depth = 1;
572
- let i = startAfterOpen;
573
- const len = html.length;
574
- while (i < len && depth > 0) {
575
- if (html[i] === "<") {
576
- if (html.startsWith("</div>", i)) {
577
- depth--;
578
- if (depth === 0)
579
- return i;
580
- i += 6;
581
- } else if (html.startsWith("<div", i) && /^<div[\s>]/.test(html.slice(i, i + 5))) {
582
- depth++;
583
- i += 4;
584
- } else {
585
- i++;
586
- }
587
- } else {
588
- i++;
589
- }
590
- }
591
- return len;
592
- }
593
- function inlineCSSAssets(html, inlineCSS) {
594
- let result = html;
595
- for (const [href, css] of Object.entries(inlineCSS)) {
596
- const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
597
- const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
598
- const safeCss = css.replace(/<\//g, "<\\/");
599
- result = result.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
600
- }
601
- result = result.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
602
- <noscript>${match}</noscript>`);
603
- return result;
604
- }
605
-
606
- // src/ssr-handler-shared.ts
607
- function sanitizeLinkHref(href) {
608
- return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
609
- }
610
- function sanitizeLinkParam(value) {
611
- return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
612
- }
613
- function buildLinkHeader(items) {
614
- return items.map((item) => {
615
- const parts = [
616
- `<${sanitizeLinkHref(item.href)}>`,
617
- "rel=preload",
618
- `as=${sanitizeLinkParam(item.as)}`
619
- ];
620
- if (item.type)
621
- parts.push(`type=${sanitizeLinkParam(item.type)}`);
622
- if (item.crossorigin)
623
- parts.push("crossorigin");
624
- return parts.join("; ");
625
- }).join(", ");
626
- }
627
- function buildModulepreloadTags(paths) {
628
- return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
629
- `);
630
- }
631
- function resolveRouteModulepreload(routeChunkManifest, matchedPatterns, fallback) {
632
- if (routeChunkManifest && matchedPatterns?.length) {
633
- const chunkPaths = new Set;
634
- for (const pattern of matchedPatterns) {
635
- const chunks = routeChunkManifest.routes[pattern];
636
- if (chunks) {
637
- for (const chunk of chunks) {
638
- chunkPaths.add(chunk);
639
- }
640
- }
641
- }
642
- if (chunkPaths.size > 0) {
643
- return buildModulepreloadTags([...chunkPaths]);
644
- }
645
- }
646
- return fallback;
647
- }
648
- function preprocessInlineCSS(template, inlineCSS) {
649
- let result = template;
650
- for (const [href, css] of Object.entries(inlineCSS)) {
651
- const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
652
- const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
653
- const safeCss = css.replace(/<\//g, "<\\/");
654
- result = result.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
655
- }
656
- return result;
657
- }
658
- function precomputeHandlerState(options) {
659
- const { module, inlineCSS, fallbackMetrics, modulepreload, progressiveHTML } = options;
660
- let template = options.template;
661
- if (inlineCSS) {
662
- template = preprocessInlineCSS(template, inlineCSS);
663
- }
664
- let linkHeader;
665
- if (module.theme) {
666
- const compiled = compileThemeCached(module.theme, fallbackMetrics);
667
- if (compiled.preloadItems.length > 0) {
668
- linkHeader = buildLinkHeader(compiled.preloadItems);
669
- }
670
- }
671
- const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
672
- const splitResult = progressiveHTML ? splitTemplate(template) : undefined;
673
- if (splitResult && module.theme) {
674
- splitResult.headTemplate = splitResult.headTemplate.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
675
- <noscript>${match}</noscript>`);
676
- }
677
- return { template, linkHeader, modulepreloadTags, splitResult };
678
- }
679
- async function resolveSession(request, sessionResolver, nonce) {
680
- let sessionScript = "";
681
- let ssrAuth;
682
- try {
683
- const sessionResult = await sessionResolver(request);
684
- if (sessionResult) {
685
- ssrAuth = {
686
- status: "authenticated",
687
- user: sessionResult.session.user,
688
- expiresAt: sessionResult.session.expiresAt
689
- };
690
- const scripts = [];
691
- scripts.push(createSessionScript(sessionResult.session, nonce));
692
- if (sessionResult.accessSet != null) {
693
- scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
694
- }
695
- sessionScript = scripts.join(`
696
- `);
697
- } else {
698
- ssrAuth = { status: "unauthenticated" };
699
- }
700
- } catch (resolverErr) {
701
- console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
702
- }
703
- return { sessionScript, ssrAuth };
704
- }
705
-
706
- // src/ssr-access-evaluator.ts
707
- function toPrefetchSession(ssrAuth, accessSet) {
708
- if (!ssrAuth || ssrAuth.status !== "authenticated" || !ssrAuth.user) {
709
- return { status: "unauthenticated" };
710
- }
711
- const roles = ssrAuth.user.role ? [ssrAuth.user.role] : undefined;
712
- const entitlements = accessSet != null ? Object.fromEntries(Object.entries(accessSet.entitlements).map(([name, check]) => [name, check.allowed])) : undefined;
713
- return {
714
- status: "authenticated",
715
- roles,
716
- entitlements,
717
- tenantId: ssrAuth.user.tenantId
718
- };
719
- }
720
- function evaluateAccessRule(rule, session) {
721
- switch (rule.type) {
722
- case "public":
723
- return true;
724
- case "authenticated":
725
- return session.status === "authenticated";
726
- case "role":
727
- if (session.status !== "authenticated")
728
- return false;
729
- return session.roles?.some((r) => rule.roles.includes(r)) === true;
730
- case "entitlement":
731
- if (session.status !== "authenticated")
732
- return false;
733
- return session.entitlements?.[rule.value] === true;
734
- case "where":
735
- return true;
736
- case "fva":
737
- return session.status === "authenticated";
738
- case "deny":
739
- return false;
740
- case "all":
741
- return rule.rules.every((r) => evaluateAccessRule(r, session));
742
- case "any":
743
- return rule.rules.some((r) => evaluateAccessRule(r, session));
744
- default:
745
- return false;
746
- }
747
- }
748
-
749
- // src/ssr-manifest-prefetch.ts
750
- function reconstructDescriptors(queries, routeParams, apiClient) {
751
- if (!apiClient)
752
- return [];
753
- const result = [];
754
- for (const query of queries) {
755
- const descriptor = reconstructSingle(query, routeParams, apiClient);
756
- if (descriptor) {
757
- result.push(descriptor);
758
- }
759
- }
760
- return result;
761
- }
762
- function reconstructSingle(query, routeParams, apiClient) {
763
- const { entity, operation } = query;
764
- if (!entity || !operation)
765
- return;
766
- const entitySdk = apiClient[entity];
767
- if (!entitySdk)
768
- return;
769
- const method = entitySdk[operation];
770
- if (typeof method !== "function")
771
- return;
772
- const args = buildFactoryArgs(query, routeParams);
773
- if (args === undefined)
774
- return;
775
- try {
776
- const descriptor = method(...args);
777
- if (!descriptor || typeof descriptor._key !== "string" || typeof descriptor._fetch !== "function") {
778
- return;
779
- }
780
- return { key: descriptor._key, fetch: descriptor._fetch };
781
- } catch {
782
- return;
783
- }
784
- }
785
- function buildFactoryArgs(query, routeParams) {
786
- const { operation, idParam, queryBindings } = query;
787
- if (operation === "get") {
788
- if (idParam) {
789
- const id = routeParams[idParam];
790
- if (!id)
791
- return;
792
- const options = resolveQueryBindings(queryBindings, routeParams);
793
- if (options === undefined && queryBindings)
794
- return;
795
- return options ? [id, options] : [id];
796
- }
797
- return;
798
- }
799
- if (!queryBindings)
800
- return [];
801
- const resolved = resolveQueryBindings(queryBindings, routeParams);
802
- if (resolved === undefined)
803
- return;
804
- return [resolved];
805
- }
806
- function resolveQueryBindings(bindings, routeParams) {
807
- if (!bindings)
808
- return;
809
- const resolved = {};
810
- if (bindings.where) {
811
- const where = {};
812
- for (const [key, value] of Object.entries(bindings.where)) {
813
- if (value === null)
814
- return;
815
- if (typeof value === "string" && value.startsWith("$")) {
816
- const paramName = value.slice(1);
817
- const paramValue = routeParams[paramName];
818
- if (!paramValue)
819
- return;
820
- where[key] = paramValue;
821
- } else {
822
- where[key] = value;
823
- }
824
- }
825
- resolved.where = where;
826
- }
827
- if (bindings.select)
828
- resolved.select = bindings.select;
829
- if (bindings.include)
830
- resolved.include = bindings.include;
831
- if (bindings.orderBy)
832
- resolved.orderBy = bindings.orderBy;
833
- if (bindings.limit !== undefined)
834
- resolved.limit = bindings.limit;
835
- return resolved;
836
- }
837
-
838
- // src/ssr-route-matcher.ts
839
- function matchUrlToPatterns(url, patterns) {
840
- const path = (url.split("?")[0] ?? "").split("#")[0] ?? "";
841
- const matches = [];
842
- for (const pattern of patterns) {
843
- const result = matchPattern(path, pattern);
844
- if (result) {
845
- matches.push(result);
846
- }
847
- }
848
- matches.sort((a, b) => {
849
- const aSegments = a.pattern.split("/").length;
850
- const bSegments = b.pattern.split("/").length;
851
- return aSegments - bSegments;
852
- });
853
- return matches;
854
- }
855
- function matchPattern(path, pattern) {
856
- const pathSegments = path.split("/").filter(Boolean);
857
- const patternSegments = pattern.split("/").filter(Boolean);
858
- if (patternSegments.length > pathSegments.length)
859
- return;
860
- const params = {};
861
- for (let i = 0;i < patternSegments.length; i++) {
862
- const seg = patternSegments[i];
863
- const val = pathSegments[i];
864
- if (seg.startsWith(":")) {
865
- params[seg.slice(1)] = val;
866
- } else if (seg !== val) {
867
- return;
868
- }
869
- }
870
- return { pattern, params };
871
- }
872
-
873
- // src/ssr-single-pass.ts
874
- async function ssrRenderSinglePass(module, url, options) {
875
- if (options?.prefetch === false) {
876
- return ssrRenderToString(module, url, options);
877
- }
878
- const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
879
- const ssrTimeout = options?.ssrTimeout ?? 300;
880
- ensureDomShim2();
881
- const zeroDiscoveryData = attemptZeroDiscovery(normalizedUrl, module, options, ssrTimeout);
882
- if (zeroDiscoveryData) {
883
- return renderWithPrefetchedData(module, normalizedUrl, zeroDiscoveryData, options);
884
- }
885
- const discoveredData = await runDiscoveryPhase(normalizedUrl, ssrTimeout, module, options);
886
- if ("redirect" in discoveredData) {
887
- return {
888
- html: "",
889
- css: "",
890
- ssrData: [],
891
- headTags: "",
892
- redirect: discoveredData.redirect
678
+ html: "",
679
+ css: "",
680
+ ssrData: [],
681
+ headTags: "",
682
+ redirect: discoveredData.redirect
893
683
  };
894
684
  }
895
685
  const renderCtx = createRequestContext(normalizedUrl);
@@ -1176,31 +966,220 @@ function filterByEntityAccess(queries, entityAccess, session) {
1176
966
  return evaluateAccessRule(rule, session);
1177
967
  });
1178
968
  }
1179
- function extractEntityFromKey(key) {
1180
- const pathStart = key.indexOf(":/");
1181
- if (pathStart === -1)
969
+ function extractEntityFromKey(key) {
970
+ const pathStart = key.indexOf(":/");
971
+ if (pathStart === -1)
972
+ return;
973
+ const path = key.slice(pathStart + 2);
974
+ const firstSlash = path.indexOf("/");
975
+ const questionMark = path.indexOf("?");
976
+ if (firstSlash === -1 && questionMark === -1)
977
+ return path;
978
+ if (firstSlash === -1)
979
+ return path.slice(0, questionMark);
980
+ if (questionMark === -1)
981
+ return path.slice(0, firstSlash);
982
+ return path.slice(0, Math.min(firstSlash, questionMark));
983
+ }
984
+ function extractMethodFromKey(key) {
985
+ const pathStart = key.indexOf(":/");
986
+ if (pathStart === -1)
987
+ return "list";
988
+ const path = key.slice(pathStart + 2);
989
+ const cleanPath = path.split("?")[0] ?? "";
990
+ const segments = cleanPath.split("/").filter(Boolean);
991
+ return segments.length > 1 ? "get" : "list";
992
+ }
993
+ function collectCSS2(themeCss, module) {
994
+ const alreadyIncluded = new Set;
995
+ if (themeCss)
996
+ alreadyIncluded.add(themeCss);
997
+ if (module.styles) {
998
+ for (const s of module.styles)
999
+ alreadyIncluded.add(s);
1000
+ }
1001
+ const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
1002
+ const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
1003
+ const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
1004
+ `)}</style>` : "";
1005
+ const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
1006
+ `)}</style>` : "";
1007
+ return [themeTag, globalTag, componentTag].filter(Boolean).join(`
1008
+ `);
1009
+ }
1010
+
1011
+ // src/ssr-aot-pipeline.ts
1012
+ function createHoles(holeNames, module, url, queryCache, ssrAuth) {
1013
+ if (holeNames.length === 0)
1014
+ return {};
1015
+ const holes = {};
1016
+ for (const name of holeNames) {
1017
+ holes[name] = () => {
1018
+ const holeCtx = createRequestContext(url);
1019
+ for (const [key, data] of queryCache) {
1020
+ holeCtx.queryCache.set(key, data);
1021
+ }
1022
+ if (ssrAuth) {
1023
+ holeCtx.ssrAuth = ssrAuth;
1024
+ }
1025
+ holeCtx.resolvedComponents = new Map;
1026
+ return ssrStorage.run(holeCtx, () => {
1027
+ ensureDomShim3();
1028
+ const factory = resolveHoleComponent(module, name);
1029
+ if (!factory) {
1030
+ return `<!-- AOT hole: ${name} not found -->`;
1031
+ }
1032
+ const node = factory();
1033
+ const vnode = toVNode(node);
1034
+ return serializeToHtml(vnode);
1035
+ });
1036
+ };
1037
+ }
1038
+ return holes;
1039
+ }
1040
+ function resolveHoleComponent(module, name) {
1041
+ const moduleRecord = module;
1042
+ const exported = moduleRecord[name];
1043
+ if (typeof exported === "function") {
1044
+ return exported;
1045
+ }
1046
+ return;
1047
+ }
1048
+ async function ssrRenderAot(module, url, options) {
1049
+ const { aotManifest, manifest } = options;
1050
+ const ssrTimeout = options.ssrTimeout ?? 300;
1051
+ const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
1052
+ const fallbackOptions = {
1053
+ ssrTimeout,
1054
+ fallbackMetrics: options.fallbackMetrics,
1055
+ ssrAuth: options.ssrAuth,
1056
+ manifest,
1057
+ prefetchSession: options.prefetchSession
1058
+ };
1059
+ const aotPatterns = Object.keys(aotManifest.routes);
1060
+ const matches = matchUrlToPatterns(normalizedUrl, aotPatterns);
1061
+ if (matches.length === 0) {
1062
+ return ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
1063
+ }
1064
+ const match = matches[matches.length - 1];
1065
+ if (!match) {
1066
+ return ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
1067
+ }
1068
+ const aotEntry = aotManifest.routes[match.pattern];
1069
+ if (!aotEntry) {
1070
+ return ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
1071
+ }
1072
+ const queryCache = new Map;
1073
+ if (aotEntry.queryKeys && aotEntry.queryKeys.length > 0 && manifest?.routeEntries) {
1074
+ const apiClient = module.api;
1075
+ if (apiClient) {
1076
+ await prefetchForAot(aotEntry.queryKeys, manifest.routeEntries, match, apiClient, ssrTimeout, queryCache);
1077
+ }
1078
+ }
1079
+ try {
1080
+ setGlobalSSRTimeout(ssrTimeout);
1081
+ const holes = createHoles(aotEntry.holes, module, normalizedUrl, queryCache, options.ssrAuth);
1082
+ const ctx = {
1083
+ holes,
1084
+ getData: (key) => queryCache.get(key),
1085
+ session: options.prefetchSession,
1086
+ params: match.params
1087
+ };
1088
+ const data = {};
1089
+ for (const [key, value] of queryCache) {
1090
+ data[key] = value;
1091
+ }
1092
+ const html = aotEntry.render(data, ctx);
1093
+ if (options.diagnostics && isAotDebugEnabled()) {
1094
+ try {
1095
+ const domResult = await ssrRenderSinglePass(module, normalizedUrl, fallbackOptions);
1096
+ if (domResult.html !== html) {
1097
+ options.diagnostics.recordDivergence(match.pattern, html, domResult.html);
1098
+ }
1099
+ } catch {}
1100
+ }
1101
+ const css = collectCSSFromModule(module, options.fallbackMetrics);
1102
+ const ssrData = [];
1103
+ for (const [key, data2] of queryCache) {
1104
+ ssrData.push({ key, data: data2 });
1105
+ }
1106
+ return {
1107
+ html,
1108
+ css: css.cssString,
1109
+ ssrData,
1110
+ headTags: css.preloadTags,
1111
+ matchedRoutePatterns: [match.pattern]
1112
+ };
1113
+ } finally {
1114
+ clearGlobalSSRTimeout();
1115
+ }
1116
+ }
1117
+ async function prefetchForAot(queryKeys, routeEntries, match, apiClient, ssrTimeout, queryCache) {
1118
+ const entry = routeEntries[match.pattern];
1119
+ if (!entry)
1182
1120
  return;
1183
- const path = key.slice(pathStart + 2);
1184
- const firstSlash = path.indexOf("/");
1185
- const questionMark = path.indexOf("?");
1186
- if (firstSlash === -1 && questionMark === -1)
1187
- return path;
1188
- if (firstSlash === -1)
1189
- return path.slice(0, questionMark);
1190
- if (questionMark === -1)
1191
- return path.slice(0, firstSlash);
1192
- return path.slice(0, Math.min(firstSlash, questionMark));
1121
+ const queryKeySet = new Set(queryKeys);
1122
+ const fetchJobs = [];
1123
+ for (const query of entry.queries) {
1124
+ if (!query.entity || !query.operation)
1125
+ continue;
1126
+ const aotKey = `${query.entity}-${query.operation}`;
1127
+ if (!queryKeySet.has(aotKey))
1128
+ continue;
1129
+ const descriptors = reconstructDescriptors([query], match.params, apiClient);
1130
+ if (descriptors.length > 0 && descriptors[0]) {
1131
+ fetchJobs.push({ aotKey, fetchFn: descriptors[0].fetch });
1132
+ }
1133
+ }
1134
+ if (fetchJobs.length === 0)
1135
+ return;
1136
+ await Promise.allSettled(fetchJobs.map(({ aotKey, fetchFn }) => {
1137
+ let timer;
1138
+ return Promise.race([
1139
+ fetchFn().then((result) => {
1140
+ clearTimeout(timer);
1141
+ const data = unwrapResult2(result);
1142
+ queryCache.set(aotKey, data);
1143
+ }),
1144
+ new Promise((r) => {
1145
+ timer = setTimeout(r, ssrTimeout);
1146
+ })
1147
+ ]);
1148
+ }));
1193
1149
  }
1194
- function extractMethodFromKey(key) {
1195
- const pathStart = key.indexOf(":/");
1196
- if (pathStart === -1)
1197
- return "list";
1198
- const path = key.slice(pathStart + 2);
1199
- const cleanPath = path.split("?")[0] ?? "";
1200
- const segments = cleanPath.split("/").filter(Boolean);
1201
- return segments.length > 1 ? "get" : "list";
1150
+ function unwrapResult2(result) {
1151
+ if (result && typeof result === "object" && "ok" in result && "data" in result) {
1152
+ const r = result;
1153
+ if (r.ok)
1154
+ return r.data;
1155
+ }
1156
+ return result;
1202
1157
  }
1203
- function collectCSS2(themeCss, module) {
1158
+ function isAotDebugEnabled() {
1159
+ const env = process.env.VERTZ_DEBUG;
1160
+ if (!env)
1161
+ return false;
1162
+ return env === "1" || env.split(",").includes("aot");
1163
+ }
1164
+ var domShimInstalled3 = false;
1165
+ function ensureDomShim3() {
1166
+ if (domShimInstalled3 && typeof document !== "undefined")
1167
+ return;
1168
+ domShimInstalled3 = true;
1169
+ installDomShim();
1170
+ }
1171
+ function collectCSSFromModule(module, fallbackMetrics) {
1172
+ let themeCss = "";
1173
+ let preloadTags = "";
1174
+ if (module.theme) {
1175
+ try {
1176
+ const compiled = compileThemeCached(module.theme, fallbackMetrics);
1177
+ themeCss = compiled.css;
1178
+ preloadTags = compiled.preloadTags;
1179
+ } catch (e) {
1180
+ console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
1181
+ }
1182
+ }
1204
1183
  const alreadyIncluded = new Set;
1205
1184
  if (themeCss)
1206
1185
  alreadyIncluded.add(themeCss);
@@ -1214,8 +1193,219 @@ function collectCSS2(themeCss, module) {
1214
1193
  `)}</style>` : "";
1215
1194
  const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
1216
1195
  `)}</style>` : "";
1217
- return [themeTag, globalTag, componentTag].filter(Boolean).join(`
1196
+ const cssString = [themeTag, globalTag, componentTag].filter(Boolean).join(`
1197
+ `);
1198
+ return { cssString, preloadTags };
1199
+ }
1200
+
1201
+ // src/ssr-access-set.ts
1202
+ function getAccessSetForSSR(jwtPayload) {
1203
+ if (!jwtPayload)
1204
+ return null;
1205
+ const acl = jwtPayload.acl;
1206
+ if (!acl)
1207
+ return null;
1208
+ if (acl.overflow)
1209
+ return null;
1210
+ if (!acl.set)
1211
+ return null;
1212
+ return {
1213
+ entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
1214
+ name,
1215
+ {
1216
+ allowed: check.allowed,
1217
+ reasons: check.reasons ?? [],
1218
+ ...check.reason ? { reason: check.reason } : {},
1219
+ ...check.meta ? { meta: check.meta } : {}
1220
+ }
1221
+ ])),
1222
+ flags: acl.set.flags,
1223
+ plan: acl.set.plan,
1224
+ plans: acl.set.plans ?? {},
1225
+ computedAt: acl.set.computedAt
1226
+ };
1227
+ }
1228
+ function createAccessSetScript(accessSet, nonce) {
1229
+ const json = JSON.stringify(accessSet);
1230
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1231
+ const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
1232
+ return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
1233
+ }
1234
+ function escapeAttr2(s) {
1235
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
1236
+ }
1237
+
1238
+ // src/ssr-session.ts
1239
+ function createSessionScript(session, nonce) {
1240
+ const json = JSON.stringify(session);
1241
+ const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1242
+ const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
1243
+ return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
1244
+ }
1245
+ function escapeAttr3(s) {
1246
+ return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
1247
+ }
1248
+
1249
+ // src/template-split.ts
1250
+ function splitTemplate(template, options) {
1251
+ let processed = template;
1252
+ if (options?.inlineCSS) {
1253
+ processed = inlineCSSAssets(processed, options.inlineCSS);
1254
+ }
1255
+ const outletMarker = "<!--ssr-outlet-->";
1256
+ const outletIndex = processed.indexOf(outletMarker);
1257
+ if (outletIndex !== -1) {
1258
+ return {
1259
+ headTemplate: processed.slice(0, outletIndex),
1260
+ tailTemplate: processed.slice(outletIndex + outletMarker.length)
1261
+ };
1262
+ }
1263
+ const appDivMatch = processed.match(/<div[^>]*id="app"[^>]*>/);
1264
+ if (appDivMatch && appDivMatch.index != null) {
1265
+ const openTag = appDivMatch[0];
1266
+ const contentStart = appDivMatch.index + openTag.length;
1267
+ const closingIndex = findMatchingDivClose(processed, contentStart);
1268
+ return {
1269
+ headTemplate: processed.slice(0, contentStart),
1270
+ tailTemplate: processed.slice(closingIndex)
1271
+ };
1272
+ }
1273
+ throw new Error('Could not find <!--ssr-outlet--> or <div id="app"> in the HTML template. ' + "The template must contain one of these markers for SSR content injection.");
1274
+ }
1275
+ function findMatchingDivClose(html, startAfterOpen) {
1276
+ let depth = 1;
1277
+ let i = startAfterOpen;
1278
+ const len = html.length;
1279
+ while (i < len && depth > 0) {
1280
+ if (html[i] === "<") {
1281
+ if (html.startsWith("</div>", i)) {
1282
+ depth--;
1283
+ if (depth === 0)
1284
+ return i;
1285
+ i += 6;
1286
+ } else if (html.startsWith("<div", i) && /^<div[\s>]/.test(html.slice(i, i + 5))) {
1287
+ depth++;
1288
+ i += 4;
1289
+ } else {
1290
+ i++;
1291
+ }
1292
+ } else {
1293
+ i++;
1294
+ }
1295
+ }
1296
+ return len;
1297
+ }
1298
+ function inlineCSSAssets(html, inlineCSS) {
1299
+ let result = html;
1300
+ for (const [href, css] of Object.entries(inlineCSS)) {
1301
+ const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1302
+ const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
1303
+ const safeCss = css.replace(/<\//g, "<\\/");
1304
+ result = result.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
1305
+ }
1306
+ result = result.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
1307
+ <noscript>${match}</noscript>`);
1308
+ return result;
1309
+ }
1310
+
1311
+ // src/ssr-handler-shared.ts
1312
+ function sanitizeLinkHref(href) {
1313
+ return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
1314
+ }
1315
+ function sanitizeLinkParam(value) {
1316
+ return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
1317
+ }
1318
+ function buildLinkHeader(items) {
1319
+ return items.map((item) => {
1320
+ const parts = [
1321
+ `<${sanitizeLinkHref(item.href)}>`,
1322
+ "rel=preload",
1323
+ `as=${sanitizeLinkParam(item.as)}`
1324
+ ];
1325
+ if (item.type)
1326
+ parts.push(`type=${sanitizeLinkParam(item.type)}`);
1327
+ if (item.crossorigin)
1328
+ parts.push("crossorigin");
1329
+ return parts.join("; ");
1330
+ }).join(", ");
1331
+ }
1332
+ function buildModulepreloadTags(paths) {
1333
+ return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
1334
+ `);
1335
+ }
1336
+ function resolveRouteModulepreload(routeChunkManifest, matchedPatterns, fallback) {
1337
+ if (routeChunkManifest && matchedPatterns?.length) {
1338
+ const chunkPaths = new Set;
1339
+ for (const pattern of matchedPatterns) {
1340
+ const chunks = routeChunkManifest.routes[pattern];
1341
+ if (chunks) {
1342
+ for (const chunk of chunks) {
1343
+ chunkPaths.add(chunk);
1344
+ }
1345
+ }
1346
+ }
1347
+ if (chunkPaths.size > 0) {
1348
+ return buildModulepreloadTags([...chunkPaths]);
1349
+ }
1350
+ }
1351
+ return fallback;
1352
+ }
1353
+ function preprocessInlineCSS(template, inlineCSS) {
1354
+ let result = template;
1355
+ for (const [href, css] of Object.entries(inlineCSS)) {
1356
+ const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1357
+ const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
1358
+ const safeCss = css.replace(/<\//g, "<\\/");
1359
+ result = result.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
1360
+ }
1361
+ return result;
1362
+ }
1363
+ function precomputeHandlerState(options) {
1364
+ const { module, inlineCSS, fallbackMetrics, modulepreload, progressiveHTML } = options;
1365
+ let template = options.template;
1366
+ if (inlineCSS) {
1367
+ template = preprocessInlineCSS(template, inlineCSS);
1368
+ }
1369
+ let linkHeader;
1370
+ if (module.theme) {
1371
+ const compiled = compileThemeCached(module.theme, fallbackMetrics);
1372
+ if (compiled.preloadItems.length > 0) {
1373
+ linkHeader = buildLinkHeader(compiled.preloadItems);
1374
+ }
1375
+ }
1376
+ const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
1377
+ const splitResult = progressiveHTML ? splitTemplate(template) : undefined;
1378
+ if (splitResult && module.theme) {
1379
+ splitResult.headTemplate = splitResult.headTemplate.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
1380
+ <noscript>${match}</noscript>`);
1381
+ }
1382
+ return { template, linkHeader, modulepreloadTags, splitResult };
1383
+ }
1384
+ async function resolveSession(request, sessionResolver, nonce) {
1385
+ let sessionScript = "";
1386
+ let ssrAuth;
1387
+ try {
1388
+ const sessionResult = await sessionResolver(request);
1389
+ if (sessionResult) {
1390
+ ssrAuth = {
1391
+ status: "authenticated",
1392
+ user: sessionResult.session.user,
1393
+ expiresAt: sessionResult.session.expiresAt
1394
+ };
1395
+ const scripts = [];
1396
+ scripts.push(createSessionScript(sessionResult.session, nonce));
1397
+ if (sessionResult.accessSet != null) {
1398
+ scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
1399
+ }
1400
+ sessionScript = scripts.join(`
1218
1401
  `);
1402
+ } else {
1403
+ ssrAuth = { status: "unauthenticated" };
1404
+ }
1405
+ } catch (resolverErr) {
1406
+ console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
1407
+ }
1408
+ return { sessionScript, ssrAuth };
1219
1409
  }
1220
1410
 
1221
1411
  // src/template-inject.ts
@@ -1281,4 +1471,4 @@ function replaceAppDivContent(template, appHtml) {
1281
1471
  return template.slice(0, contentStart) + appHtml + template.slice(i);
1282
1472
  }
1283
1473
 
1284
- export { escapeHtml, escapeAttr, serializeToHtml, getAccessSetForSSR, createAccessSetScript, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, compileThemeCached, createRequestContext, ssrRenderToString, ssrDiscoverQueries, ssrStreamNavQueries, createSessionScript, resolveRouteModulepreload, precomputeHandlerState, resolveSession, toPrefetchSession, evaluateAccessRule, reconstructDescriptors, matchUrlToPatterns, ssrRenderSinglePass, ssrRenderProgressive, injectIntoTemplate };
1474
+ export { escapeHtml, escapeAttr, serializeToHtml, toPrefetchSession, evaluateAccessRule, reconstructDescriptors, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, compileThemeCached, createRequestContext, ssrRenderToString, ssrDiscoverQueries, ssrStreamNavQueries, matchUrlToPatterns, ssrRenderSinglePass, ssrRenderProgressive, createHoles, ssrRenderAot, isAotDebugEnabled, getAccessSetForSSR, createAccessSetScript, createSessionScript, resolveRouteModulepreload, precomputeHandlerState, resolveSession, injectIntoTemplate };