@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/bun-dev-server.js +47 -2
- package/dist/dom-shim/index.js +1 -1
- package/dist/index.d.ts +818 -747
- package/dist/index.js +86 -150
- package/dist/node-handler.d.ts +223 -0
- package/dist/node-handler.js +8 -0
- package/dist/shared/{chunk-bd0sgykf.js → chunk-34fexgex.js} +247 -431
- package/dist/shared/chunk-es0406qq.js +227 -0
- package/dist/shared/chunk-gbcsa7h1.js +471 -0
- package/dist/shared/{chunk-gcwqkynf.js → chunk-ybftdw1r.js} +1 -1
- package/dist/ssr/index.d.ts +119 -57
- package/dist/ssr/index.js +6 -3
- package/package.json +9 -5
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
setGlobalSSRTimeout,
|
|
7
7
|
ssrStorage,
|
|
8
8
|
toVNode
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ybftdw1r.js";
|
|
10
10
|
|
|
11
11
|
// src/html-serializer.ts
|
|
12
12
|
var VOID_ELEMENTS = new Set([
|
|
@@ -68,6 +68,43 @@ 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;
|
|
82
|
+
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
|
|
96
|
+
};
|
|
97
|
+
}
|
|
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>`;
|
|
103
|
+
}
|
|
104
|
+
function escapeAttr2(s) {
|
|
105
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
106
|
+
}
|
|
107
|
+
|
|
71
108
|
// src/slot-placeholder.ts
|
|
72
109
|
var slotCounter = 0;
|
|
73
110
|
function resetSlotCounter() {
|
|
@@ -493,6 +530,179 @@ data: ${safeSerialize(entry)}
|
|
|
493
530
|
});
|
|
494
531
|
}
|
|
495
532
|
|
|
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>`;
|
|
539
|
+
}
|
|
540
|
+
function escapeAttr3(s) {
|
|
541
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/template-split.ts
|
|
545
|
+
function splitTemplate(template, options) {
|
|
546
|
+
let processed = template;
|
|
547
|
+
if (options?.inlineCSS) {
|
|
548
|
+
processed = inlineCSSAssets(processed, options.inlineCSS);
|
|
549
|
+
}
|
|
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
|
+
};
|
|
557
|
+
}
|
|
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);
|
|
563
|
+
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
|
+
|
|
496
706
|
// src/ssr-access-evaluator.ts
|
|
497
707
|
function toPrefetchSession(ssrAuth, accessSet) {
|
|
498
708
|
if (!ssrAuth || ssrAuth.status !== "authenticated" || !ssrAuth.user) {
|
|
@@ -536,78 +746,6 @@ function evaluateAccessRule(rule, session) {
|
|
|
536
746
|
}
|
|
537
747
|
}
|
|
538
748
|
|
|
539
|
-
// src/ssr-access-set.ts
|
|
540
|
-
function getAccessSetForSSR(jwtPayload) {
|
|
541
|
-
if (!jwtPayload)
|
|
542
|
-
return null;
|
|
543
|
-
const acl = jwtPayload.acl;
|
|
544
|
-
if (!acl)
|
|
545
|
-
return null;
|
|
546
|
-
if (acl.overflow)
|
|
547
|
-
return null;
|
|
548
|
-
if (!acl.set)
|
|
549
|
-
return null;
|
|
550
|
-
return {
|
|
551
|
-
entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
|
|
552
|
-
name,
|
|
553
|
-
{
|
|
554
|
-
allowed: check.allowed,
|
|
555
|
-
reasons: check.reasons ?? [],
|
|
556
|
-
...check.reason ? { reason: check.reason } : {},
|
|
557
|
-
...check.meta ? { meta: check.meta } : {}
|
|
558
|
-
}
|
|
559
|
-
])),
|
|
560
|
-
flags: acl.set.flags,
|
|
561
|
-
plan: acl.set.plan,
|
|
562
|
-
plans: acl.set.plans ?? {},
|
|
563
|
-
computedAt: acl.set.computedAt
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
function createAccessSetScript(accessSet, nonce) {
|
|
567
|
-
const json = JSON.stringify(accessSet);
|
|
568
|
-
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
569
|
-
const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
|
|
570
|
-
return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
|
|
571
|
-
}
|
|
572
|
-
function escapeAttr2(s) {
|
|
573
|
-
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// src/ssr-route-matcher.ts
|
|
577
|
-
function matchUrlToPatterns(url, patterns) {
|
|
578
|
-
const path = (url.split("?")[0] ?? "").split("#")[0] ?? "";
|
|
579
|
-
const matches = [];
|
|
580
|
-
for (const pattern of patterns) {
|
|
581
|
-
const result = matchPattern(path, pattern);
|
|
582
|
-
if (result) {
|
|
583
|
-
matches.push(result);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
matches.sort((a, b) => {
|
|
587
|
-
const aSegments = a.pattern.split("/").length;
|
|
588
|
-
const bSegments = b.pattern.split("/").length;
|
|
589
|
-
return aSegments - bSegments;
|
|
590
|
-
});
|
|
591
|
-
return matches;
|
|
592
|
-
}
|
|
593
|
-
function matchPattern(path, pattern) {
|
|
594
|
-
const pathSegments = path.split("/").filter(Boolean);
|
|
595
|
-
const patternSegments = pattern.split("/").filter(Boolean);
|
|
596
|
-
if (patternSegments.length > pathSegments.length)
|
|
597
|
-
return;
|
|
598
|
-
const params = {};
|
|
599
|
-
for (let i = 0;i < patternSegments.length; i++) {
|
|
600
|
-
const seg = patternSegments[i];
|
|
601
|
-
const val = pathSegments[i];
|
|
602
|
-
if (seg.startsWith(":")) {
|
|
603
|
-
params[seg.slice(1)] = val;
|
|
604
|
-
} else if (seg !== val) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return { pattern, params };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
749
|
// src/ssr-manifest-prefetch.ts
|
|
612
750
|
function reconstructDescriptors(queries, routeParams, apiClient) {
|
|
613
751
|
if (!apiClient)
|
|
@@ -697,6 +835,41 @@ function resolveQueryBindings(bindings, routeParams) {
|
|
|
697
835
|
return resolved;
|
|
698
836
|
}
|
|
699
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
|
+
|
|
700
873
|
// src/ssr-single-pass.ts
|
|
701
874
|
async function ssrRenderSinglePass(module, url, options) {
|
|
702
875
|
if (options?.prefetch === false) {
|
|
@@ -1045,17 +1218,6 @@ function collectCSS2(themeCss, module) {
|
|
|
1045
1218
|
`);
|
|
1046
1219
|
}
|
|
1047
1220
|
|
|
1048
|
-
// src/ssr-session.ts
|
|
1049
|
-
function createSessionScript(session, nonce) {
|
|
1050
|
-
const json = JSON.stringify(session);
|
|
1051
|
-
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1052
|
-
const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
|
|
1053
|
-
return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
|
|
1054
|
-
}
|
|
1055
|
-
function escapeAttr3(s) {
|
|
1056
|
-
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
1221
|
// src/template-inject.ts
|
|
1060
1222
|
function injectIntoTemplate(options) {
|
|
1061
1223
|
const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
|
|
@@ -1119,350 +1281,4 @@ function replaceAppDivContent(template, appHtml) {
|
|
|
1119
1281
|
return template.slice(0, contentStart) + appHtml + template.slice(i);
|
|
1120
1282
|
}
|
|
1121
1283
|
|
|
1122
|
-
|
|
1123
|
-
function buildProgressiveResponse(options) {
|
|
1124
|
-
const { headChunk, renderStream, tailChunk, ssrData, nonce, headers } = options;
|
|
1125
|
-
const stream = new ReadableStream({
|
|
1126
|
-
async start(controller) {
|
|
1127
|
-
controller.enqueue(encodeChunk(headChunk));
|
|
1128
|
-
const reader = renderStream.getReader();
|
|
1129
|
-
let renderError;
|
|
1130
|
-
try {
|
|
1131
|
-
for (;; ) {
|
|
1132
|
-
const { done, value } = await reader.read();
|
|
1133
|
-
if (done)
|
|
1134
|
-
break;
|
|
1135
|
-
controller.enqueue(value);
|
|
1136
|
-
}
|
|
1137
|
-
} catch (err) {
|
|
1138
|
-
renderError = err instanceof Error ? err : new Error(String(err));
|
|
1139
|
-
}
|
|
1140
|
-
if (renderError) {
|
|
1141
|
-
console.error("[SSR] Render error after head sent:", renderError.message);
|
|
1142
|
-
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
1143
|
-
const errorScript = `<script${nonceAttr}>document.dispatchEvent(new CustomEvent('vertz:ssr-error',` + `{detail:{message:${safeSerialize(renderError.message)}}}))</script>`;
|
|
1144
|
-
controller.enqueue(encodeChunk(errorScript));
|
|
1145
|
-
}
|
|
1146
|
-
let tail = "";
|
|
1147
|
-
if (ssrData.length > 0) {
|
|
1148
|
-
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
1149
|
-
tail += `<script${nonceAttr}>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>`;
|
|
1150
|
-
}
|
|
1151
|
-
tail += tailChunk;
|
|
1152
|
-
controller.enqueue(encodeChunk(tail));
|
|
1153
|
-
controller.close();
|
|
1154
|
-
}
|
|
1155
|
-
});
|
|
1156
|
-
const responseHeaders = {
|
|
1157
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1158
|
-
...headers
|
|
1159
|
-
};
|
|
1160
|
-
return new Response(stream, { status: 200, headers: responseHeaders });
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// src/template-split.ts
|
|
1164
|
-
function splitTemplate(template, options) {
|
|
1165
|
-
let processed = template;
|
|
1166
|
-
if (options?.inlineCSS) {
|
|
1167
|
-
processed = inlineCSSAssets(processed, options.inlineCSS);
|
|
1168
|
-
}
|
|
1169
|
-
const outletMarker = "<!--ssr-outlet-->";
|
|
1170
|
-
const outletIndex = processed.indexOf(outletMarker);
|
|
1171
|
-
if (outletIndex !== -1) {
|
|
1172
|
-
return {
|
|
1173
|
-
headTemplate: processed.slice(0, outletIndex),
|
|
1174
|
-
tailTemplate: processed.slice(outletIndex + outletMarker.length)
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
const appDivMatch = processed.match(/<div[^>]*id="app"[^>]*>/);
|
|
1178
|
-
if (appDivMatch && appDivMatch.index != null) {
|
|
1179
|
-
const openTag = appDivMatch[0];
|
|
1180
|
-
const contentStart = appDivMatch.index + openTag.length;
|
|
1181
|
-
const closingIndex = findMatchingDivClose(processed, contentStart);
|
|
1182
|
-
return {
|
|
1183
|
-
headTemplate: processed.slice(0, contentStart),
|
|
1184
|
-
tailTemplate: processed.slice(closingIndex)
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
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.");
|
|
1188
|
-
}
|
|
1189
|
-
function findMatchingDivClose(html, startAfterOpen) {
|
|
1190
|
-
let depth = 1;
|
|
1191
|
-
let i = startAfterOpen;
|
|
1192
|
-
const len = html.length;
|
|
1193
|
-
while (i < len && depth > 0) {
|
|
1194
|
-
if (html[i] === "<") {
|
|
1195
|
-
if (html.startsWith("</div>", i)) {
|
|
1196
|
-
depth--;
|
|
1197
|
-
if (depth === 0)
|
|
1198
|
-
return i;
|
|
1199
|
-
i += 6;
|
|
1200
|
-
} else if (html.startsWith("<div", i) && /^<div[\s>]/.test(html.slice(i, i + 5))) {
|
|
1201
|
-
depth++;
|
|
1202
|
-
i += 4;
|
|
1203
|
-
} else {
|
|
1204
|
-
i++;
|
|
1205
|
-
}
|
|
1206
|
-
} else {
|
|
1207
|
-
i++;
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
return len;
|
|
1211
|
-
}
|
|
1212
|
-
function inlineCSSAssets(html, inlineCSS) {
|
|
1213
|
-
let result = html;
|
|
1214
|
-
for (const [href, css] of Object.entries(inlineCSS)) {
|
|
1215
|
-
const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1216
|
-
const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
|
|
1217
|
-
const safeCss = css.replace(/<\//g, "<\\/");
|
|
1218
|
-
result = result.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
|
|
1219
|
-
}
|
|
1220
|
-
result = result.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
|
|
1221
|
-
<noscript>${match}</noscript>`);
|
|
1222
|
-
return result;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// src/ssr-handler.ts
|
|
1226
|
-
function sanitizeLinkHref(href) {
|
|
1227
|
-
return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1228
|
-
}
|
|
1229
|
-
function sanitizeLinkParam(value) {
|
|
1230
|
-
return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
|
|
1231
|
-
}
|
|
1232
|
-
function buildLinkHeader(items) {
|
|
1233
|
-
return items.map((item) => {
|
|
1234
|
-
const parts = [
|
|
1235
|
-
`<${sanitizeLinkHref(item.href)}>`,
|
|
1236
|
-
"rel=preload",
|
|
1237
|
-
`as=${sanitizeLinkParam(item.as)}`
|
|
1238
|
-
];
|
|
1239
|
-
if (item.type)
|
|
1240
|
-
parts.push(`type=${sanitizeLinkParam(item.type)}`);
|
|
1241
|
-
if (item.crossorigin)
|
|
1242
|
-
parts.push("crossorigin");
|
|
1243
|
-
return parts.join("; ");
|
|
1244
|
-
}).join(", ");
|
|
1245
|
-
}
|
|
1246
|
-
function buildModulepreloadTags(paths) {
|
|
1247
|
-
return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
|
|
1248
|
-
`);
|
|
1249
|
-
}
|
|
1250
|
-
function resolveRouteModulepreload(routeChunkManifest, matchedPatterns, fallback) {
|
|
1251
|
-
if (routeChunkManifest && matchedPatterns?.length) {
|
|
1252
|
-
const chunkPaths = new Set;
|
|
1253
|
-
for (const pattern of matchedPatterns) {
|
|
1254
|
-
const chunks = routeChunkManifest.routes[pattern];
|
|
1255
|
-
if (chunks) {
|
|
1256
|
-
for (const chunk of chunks) {
|
|
1257
|
-
chunkPaths.add(chunk);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (chunkPaths.size > 0) {
|
|
1262
|
-
return buildModulepreloadTags([...chunkPaths]);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
return fallback;
|
|
1266
|
-
}
|
|
1267
|
-
function createSSRHandler(options) {
|
|
1268
|
-
const {
|
|
1269
|
-
module,
|
|
1270
|
-
ssrTimeout,
|
|
1271
|
-
inlineCSS,
|
|
1272
|
-
nonce,
|
|
1273
|
-
fallbackMetrics,
|
|
1274
|
-
modulepreload,
|
|
1275
|
-
routeChunkManifest,
|
|
1276
|
-
cacheControl,
|
|
1277
|
-
sessionResolver,
|
|
1278
|
-
manifest,
|
|
1279
|
-
progressiveHTML
|
|
1280
|
-
} = options;
|
|
1281
|
-
let template = options.template;
|
|
1282
|
-
if (inlineCSS) {
|
|
1283
|
-
for (const [href, css] of Object.entries(inlineCSS)) {
|
|
1284
|
-
const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1285
|
-
const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
|
|
1286
|
-
const safeCss = css.replace(/<\//g, "<\\/");
|
|
1287
|
-
template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
let linkHeader;
|
|
1291
|
-
if (module.theme) {
|
|
1292
|
-
const compiled = compileThemeCached(module.theme, fallbackMetrics);
|
|
1293
|
-
if (compiled.preloadItems.length > 0) {
|
|
1294
|
-
linkHeader = buildLinkHeader(compiled.preloadItems);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
|
|
1298
|
-
const splitResult = progressiveHTML ? splitTemplate(template) : undefined;
|
|
1299
|
-
if (splitResult && module.theme) {
|
|
1300
|
-
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'">
|
|
1301
|
-
<noscript>${match}</noscript>`);
|
|
1302
|
-
}
|
|
1303
|
-
return async (request) => {
|
|
1304
|
-
const url = new URL(request.url);
|
|
1305
|
-
const pathname = url.pathname;
|
|
1306
|
-
if (request.headers.get("x-vertz-nav") === "1") {
|
|
1307
|
-
return handleNavRequest(module, pathname, ssrTimeout);
|
|
1308
|
-
}
|
|
1309
|
-
let sessionScript = "";
|
|
1310
|
-
let ssrAuth;
|
|
1311
|
-
if (sessionResolver) {
|
|
1312
|
-
try {
|
|
1313
|
-
const sessionResult = await sessionResolver(request);
|
|
1314
|
-
if (sessionResult) {
|
|
1315
|
-
ssrAuth = {
|
|
1316
|
-
status: "authenticated",
|
|
1317
|
-
user: sessionResult.session.user,
|
|
1318
|
-
expiresAt: sessionResult.session.expiresAt
|
|
1319
|
-
};
|
|
1320
|
-
const scripts = [];
|
|
1321
|
-
scripts.push(createSessionScript(sessionResult.session, nonce));
|
|
1322
|
-
if (sessionResult.accessSet != null) {
|
|
1323
|
-
scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
|
|
1324
|
-
}
|
|
1325
|
-
sessionScript = scripts.join(`
|
|
1326
|
-
`);
|
|
1327
|
-
} else {
|
|
1328
|
-
ssrAuth = { status: "unauthenticated" };
|
|
1329
|
-
}
|
|
1330
|
-
} catch (resolverErr) {
|
|
1331
|
-
console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
const useProgressive = progressiveHTML && splitResult && !(manifest?.routeEntries && Object.keys(manifest.routeEntries).length > 0);
|
|
1335
|
-
if (useProgressive) {
|
|
1336
|
-
return handleProgressiveHTMLRequest(module, splitResult, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest);
|
|
1337
|
-
}
|
|
1338
|
-
return handleHTMLRequest(module, template, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest);
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
async function handleNavRequest(module, url, ssrTimeout) {
|
|
1342
|
-
try {
|
|
1343
|
-
const stream = await ssrStreamNavQueries(module, url, { ssrTimeout });
|
|
1344
|
-
return new Response(stream, {
|
|
1345
|
-
status: 200,
|
|
1346
|
-
headers: {
|
|
1347
|
-
"Content-Type": "text/event-stream",
|
|
1348
|
-
"Cache-Control": "no-cache"
|
|
1349
|
-
}
|
|
1350
|
-
});
|
|
1351
|
-
} catch {
|
|
1352
|
-
return new Response(`event: done
|
|
1353
|
-
data: {}
|
|
1354
|
-
|
|
1355
|
-
`, {
|
|
1356
|
-
status: 200,
|
|
1357
|
-
headers: {
|
|
1358
|
-
"Content-Type": "text/event-stream",
|
|
1359
|
-
"Cache-Control": "no-cache"
|
|
1360
|
-
}
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
async function handleProgressiveHTMLRequest(module, split, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, staticModulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest) {
|
|
1365
|
-
try {
|
|
1366
|
-
const result = await ssrRenderProgressive(module, url, {
|
|
1367
|
-
ssrTimeout,
|
|
1368
|
-
fallbackMetrics,
|
|
1369
|
-
ssrAuth,
|
|
1370
|
-
manifest
|
|
1371
|
-
});
|
|
1372
|
-
if (result.redirect) {
|
|
1373
|
-
return new Response(null, {
|
|
1374
|
-
status: 302,
|
|
1375
|
-
headers: { Location: result.redirect.to }
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
const modulepreloadTags = resolveRouteModulepreload(routeChunkManifest, result.matchedRoutePatterns, staticModulepreloadTags);
|
|
1379
|
-
let headChunk = split.headTemplate;
|
|
1380
|
-
const headCloseIdx = headChunk.lastIndexOf("</head>");
|
|
1381
|
-
if (headCloseIdx !== -1) {
|
|
1382
|
-
const injections = [];
|
|
1383
|
-
if (result.css)
|
|
1384
|
-
injections.push(result.css);
|
|
1385
|
-
if (result.headTags)
|
|
1386
|
-
injections.push(result.headTags);
|
|
1387
|
-
if (modulepreloadTags)
|
|
1388
|
-
injections.push(modulepreloadTags);
|
|
1389
|
-
if (sessionScript)
|
|
1390
|
-
injections.push(sessionScript);
|
|
1391
|
-
if (injections.length > 0) {
|
|
1392
|
-
headChunk = headChunk.slice(0, headCloseIdx) + injections.join(`
|
|
1393
|
-
`) + `
|
|
1394
|
-
` + headChunk.slice(headCloseIdx);
|
|
1395
|
-
}
|
|
1396
|
-
} else {
|
|
1397
|
-
if (result.css)
|
|
1398
|
-
headChunk += result.css;
|
|
1399
|
-
if (result.headTags)
|
|
1400
|
-
headChunk += result.headTags;
|
|
1401
|
-
if (modulepreloadTags)
|
|
1402
|
-
headChunk += modulepreloadTags;
|
|
1403
|
-
if (sessionScript)
|
|
1404
|
-
headChunk += sessionScript;
|
|
1405
|
-
}
|
|
1406
|
-
const headers = {};
|
|
1407
|
-
if (linkHeader)
|
|
1408
|
-
headers.Link = linkHeader;
|
|
1409
|
-
if (cacheControl)
|
|
1410
|
-
headers["Cache-Control"] = cacheControl;
|
|
1411
|
-
return buildProgressiveResponse({
|
|
1412
|
-
headChunk,
|
|
1413
|
-
renderStream: result.renderStream,
|
|
1414
|
-
tailChunk: split.tailTemplate,
|
|
1415
|
-
ssrData: result.ssrData,
|
|
1416
|
-
nonce,
|
|
1417
|
-
headers
|
|
1418
|
-
});
|
|
1419
|
-
} catch (err) {
|
|
1420
|
-
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
1421
|
-
return new Response("Internal Server Error", {
|
|
1422
|
-
status: 500,
|
|
1423
|
-
headers: { "Content-Type": "text/plain" }
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, staticModulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest) {
|
|
1428
|
-
try {
|
|
1429
|
-
const result = await ssrRenderSinglePass(module, url, {
|
|
1430
|
-
ssrTimeout,
|
|
1431
|
-
fallbackMetrics,
|
|
1432
|
-
ssrAuth,
|
|
1433
|
-
manifest
|
|
1434
|
-
});
|
|
1435
|
-
if (result.redirect) {
|
|
1436
|
-
return new Response(null, {
|
|
1437
|
-
status: 302,
|
|
1438
|
-
headers: { Location: result.redirect.to }
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
const modulepreloadTags = resolveRouteModulepreload(routeChunkManifest, result.matchedRoutePatterns, staticModulepreloadTags);
|
|
1442
|
-
const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
|
|
1443
|
-
`);
|
|
1444
|
-
const html = injectIntoTemplate({
|
|
1445
|
-
template,
|
|
1446
|
-
appHtml: result.html,
|
|
1447
|
-
appCss: result.css,
|
|
1448
|
-
ssrData: result.ssrData,
|
|
1449
|
-
nonce,
|
|
1450
|
-
headTags: allHeadTags || undefined,
|
|
1451
|
-
sessionScript
|
|
1452
|
-
});
|
|
1453
|
-
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
1454
|
-
if (linkHeader)
|
|
1455
|
-
headers.Link = linkHeader;
|
|
1456
|
-
if (cacheControl)
|
|
1457
|
-
headers["Cache-Control"] = cacheControl;
|
|
1458
|
-
return new Response(html, { status: 200, headers });
|
|
1459
|
-
} catch (err) {
|
|
1460
|
-
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
1461
|
-
return new Response("Internal Server Error", {
|
|
1462
|
-
status: 500,
|
|
1463
|
-
headers: { "Content-Type": "text/plain" }
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, compileThemeCached, createRequestContext, ssrRenderToString, ssrDiscoverQueries, toPrefetchSession, evaluateAccessRule, getAccessSetForSSR, createAccessSetScript, matchUrlToPatterns, reconstructDescriptors, ssrRenderSinglePass, createSessionScript, injectIntoTemplate, createSSRHandler };
|
|
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 };
|