@vertz/ui-server 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bun-dev-server.d.ts +36 -1
- package/dist/bun-dev-server.js +352 -23
- package/dist/bun-plugin/index.d.ts +73 -1
- package/dist/bun-plugin/index.js +964 -17
- package/dist/dom-shim/index.js +1 -1
- package/dist/index.d.ts +84 -12
- package/dist/index.js +127 -41
- package/dist/shared/{chunk-98972e43.js → chunk-969qgkdf.js} +186 -23
- package/dist/shared/{chunk-n1arq9xq.js → chunk-9jjdzz8c.js} +2 -2
- package/dist/shared/chunk-gggnhyqj.js +57 -0
- package/dist/ssr/index.d.ts +116 -9
- package/dist/ssr/index.js +79 -2
- package/package.json +8 -5
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
setGlobalSSRTimeout,
|
|
7
7
|
ssrStorage,
|
|
8
8
|
toVNode
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-9jjdzz8c.js";
|
|
10
10
|
|
|
11
11
|
// src/html-serializer.ts
|
|
12
12
|
var VOID_ELEMENTS = new Set([
|
|
@@ -209,7 +209,7 @@ function createRequestContext(url) {
|
|
|
209
209
|
contextScope: null,
|
|
210
210
|
entityStore: new EntityStore,
|
|
211
211
|
envelopeStore: new QueryEnvelopeStore,
|
|
212
|
-
queryCache: new MemoryCache,
|
|
212
|
+
queryCache: new MemoryCache({ maxSize: Infinity }),
|
|
213
213
|
inflight: new Map,
|
|
214
214
|
queries: [],
|
|
215
215
|
errors: []
|
|
@@ -230,9 +230,6 @@ function resolveAppFactory(module) {
|
|
|
230
230
|
return createApp;
|
|
231
231
|
}
|
|
232
232
|
function collectCSS(themeCss, module) {
|
|
233
|
-
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
234
|
-
const globalTags = module.styles ? module.styles.map((s) => `<style data-vertz-css>${s}</style>`).join(`
|
|
235
|
-
`) : "";
|
|
236
233
|
const alreadyIncluded = new Set;
|
|
237
234
|
if (themeCss)
|
|
238
235
|
alreadyIncluded.add(themeCss);
|
|
@@ -241,9 +238,12 @@ function collectCSS(themeCss, module) {
|
|
|
241
238
|
alreadyIncluded.add(s);
|
|
242
239
|
}
|
|
243
240
|
const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
|
|
244
|
-
const
|
|
245
|
-
`
|
|
246
|
-
|
|
241
|
+
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
242
|
+
const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
|
|
243
|
+
`)}</style>` : "";
|
|
244
|
+
const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
|
|
245
|
+
`)}</style>` : "";
|
|
246
|
+
return [themeTag, globalTag, componentTag].filter(Boolean).join(`
|
|
247
247
|
`);
|
|
248
248
|
}
|
|
249
249
|
async function ssrRenderToString(module, url, options) {
|
|
@@ -256,14 +256,40 @@ async function ssrRenderToString(module, url, options) {
|
|
|
256
256
|
setGlobalSSRTimeout(ssrTimeout);
|
|
257
257
|
const createApp = resolveAppFactory(module);
|
|
258
258
|
let themeCss = "";
|
|
259
|
+
let themePreloadTags = "";
|
|
259
260
|
if (module.theme) {
|
|
260
261
|
try {
|
|
261
|
-
|
|
262
|
+
const compiled = compileTheme(module.theme, {
|
|
263
|
+
fallbackMetrics: options?.fallbackMetrics
|
|
264
|
+
});
|
|
265
|
+
themeCss = compiled.css;
|
|
266
|
+
themePreloadTags = compiled.preloadTags;
|
|
262
267
|
} catch (e) {
|
|
263
268
|
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
264
269
|
}
|
|
265
270
|
}
|
|
266
271
|
createApp();
|
|
272
|
+
const store = ssrStorage.getStore();
|
|
273
|
+
if (store) {
|
|
274
|
+
if (store.pendingRouteComponents?.size) {
|
|
275
|
+
const entries = Array.from(store.pendingRouteComponents.entries());
|
|
276
|
+
const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
|
|
277
|
+
promise.then((mod) => ({ route, factory: mod.default })),
|
|
278
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
|
|
279
|
+
])));
|
|
280
|
+
store.resolvedComponents = new Map;
|
|
281
|
+
for (const result of results) {
|
|
282
|
+
if (result.status === "fulfilled") {
|
|
283
|
+
const { route, factory } = result.value;
|
|
284
|
+
store.resolvedComponents.set(route, factory);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
store.pendingRouteComponents = undefined;
|
|
288
|
+
}
|
|
289
|
+
if (!store.resolvedComponents) {
|
|
290
|
+
store.resolvedComponents = new Map;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
267
293
|
const queries = getSSRQueries();
|
|
268
294
|
const resolvedQueries = [];
|
|
269
295
|
if (queries.length > 0) {
|
|
@@ -275,7 +301,6 @@ async function ssrRenderToString(module, url, options) {
|
|
|
275
301
|
}),
|
|
276
302
|
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
|
|
277
303
|
])));
|
|
278
|
-
const store = ssrStorage.getStore();
|
|
279
304
|
if (store)
|
|
280
305
|
store.queries = [];
|
|
281
306
|
}
|
|
@@ -288,7 +313,13 @@ async function ssrRenderToString(module, url, options) {
|
|
|
288
313
|
key,
|
|
289
314
|
data: JSON.parse(JSON.stringify(data))
|
|
290
315
|
})) : [];
|
|
291
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
html,
|
|
318
|
+
css,
|
|
319
|
+
ssrData,
|
|
320
|
+
headTags: themePreloadTags,
|
|
321
|
+
discoveredRoutes: ctx.discoveredRoutes
|
|
322
|
+
};
|
|
292
323
|
} finally {
|
|
293
324
|
clearGlobalSSRTimeout();
|
|
294
325
|
}
|
|
@@ -439,17 +470,75 @@ data: ${safeSerialize(entry)}
|
|
|
439
470
|
});
|
|
440
471
|
}
|
|
441
472
|
|
|
442
|
-
// src/ssr-
|
|
443
|
-
function
|
|
473
|
+
// src/ssr-access-set.ts
|
|
474
|
+
function getAccessSetForSSR(jwtPayload) {
|
|
475
|
+
if (!jwtPayload)
|
|
476
|
+
return null;
|
|
477
|
+
const acl = jwtPayload.acl;
|
|
478
|
+
if (!acl)
|
|
479
|
+
return null;
|
|
480
|
+
if (acl.overflow)
|
|
481
|
+
return null;
|
|
482
|
+
if (!acl.set)
|
|
483
|
+
return null;
|
|
484
|
+
return {
|
|
485
|
+
entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
|
|
486
|
+
name,
|
|
487
|
+
{
|
|
488
|
+
allowed: check.allowed,
|
|
489
|
+
reasons: check.reasons ?? [],
|
|
490
|
+
...check.reason ? { reason: check.reason } : {},
|
|
491
|
+
...check.meta ? { meta: check.meta } : {}
|
|
492
|
+
}
|
|
493
|
+
])),
|
|
494
|
+
flags: acl.set.flags,
|
|
495
|
+
plan: acl.set.plan,
|
|
496
|
+
computedAt: acl.set.computedAt
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function createAccessSetScript(accessSet, nonce) {
|
|
500
|
+
const json = JSON.stringify(accessSet);
|
|
501
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
502
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
|
|
503
|
+
return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
|
|
504
|
+
}
|
|
505
|
+
function escapeAttr2(s) {
|
|
506
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/ssr-session.ts
|
|
510
|
+
function createSessionScript(session, nonce) {
|
|
511
|
+
const json = JSON.stringify(session);
|
|
512
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
513
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
|
|
514
|
+
return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
|
|
515
|
+
}
|
|
516
|
+
function escapeAttr3(s) {
|
|
517
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/template-inject.ts
|
|
521
|
+
function injectIntoTemplate(options) {
|
|
522
|
+
const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
|
|
444
523
|
let html;
|
|
445
524
|
if (template.includes("<!--ssr-outlet-->")) {
|
|
446
525
|
html = template.replace("<!--ssr-outlet-->", appHtml);
|
|
447
526
|
} else {
|
|
448
527
|
html = template.replace(/(<div[^>]*id="app"[^>]*>)([\s\S]*?)(<\/div>)/, `$1${appHtml}$3`);
|
|
449
528
|
}
|
|
529
|
+
if (headTags) {
|
|
530
|
+
html = html.replace("</head>", `${headTags}
|
|
531
|
+
</head>`);
|
|
532
|
+
}
|
|
450
533
|
if (appCss) {
|
|
534
|
+
html = html.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
|
|
535
|
+
<noscript>${match}</noscript>`);
|
|
451
536
|
html = html.replace("</head>", `${appCss}
|
|
452
537
|
</head>`);
|
|
538
|
+
}
|
|
539
|
+
if (sessionScript) {
|
|
540
|
+
html = html.replace("</body>", `${sessionScript}
|
|
541
|
+
</body>`);
|
|
453
542
|
}
|
|
454
543
|
if (ssrData.length > 0) {
|
|
455
544
|
const nonceAttr = nonce != null ? ` nonce="${nonce}"` : "";
|
|
@@ -459,8 +548,44 @@ function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
|
|
|
459
548
|
}
|
|
460
549
|
return html;
|
|
461
550
|
}
|
|
551
|
+
|
|
552
|
+
// src/ssr-handler.ts
|
|
553
|
+
import { compileTheme as compileTheme2 } from "@vertz/ui";
|
|
554
|
+
function sanitizeLinkHref(href) {
|
|
555
|
+
return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
556
|
+
}
|
|
557
|
+
function sanitizeLinkParam(value) {
|
|
558
|
+
return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
|
|
559
|
+
}
|
|
560
|
+
function buildLinkHeader(items) {
|
|
561
|
+
return items.map((item) => {
|
|
562
|
+
const parts = [
|
|
563
|
+
`<${sanitizeLinkHref(item.href)}>`,
|
|
564
|
+
"rel=preload",
|
|
565
|
+
`as=${sanitizeLinkParam(item.as)}`
|
|
566
|
+
];
|
|
567
|
+
if (item.type)
|
|
568
|
+
parts.push(`type=${sanitizeLinkParam(item.type)}`);
|
|
569
|
+
if (item.crossorigin)
|
|
570
|
+
parts.push("crossorigin");
|
|
571
|
+
return parts.join("; ");
|
|
572
|
+
}).join(", ");
|
|
573
|
+
}
|
|
574
|
+
function buildModulepreloadTags(paths) {
|
|
575
|
+
return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
|
|
576
|
+
`);
|
|
577
|
+
}
|
|
462
578
|
function createSSRHandler(options) {
|
|
463
|
-
const {
|
|
579
|
+
const {
|
|
580
|
+
module,
|
|
581
|
+
ssrTimeout,
|
|
582
|
+
inlineCSS,
|
|
583
|
+
nonce,
|
|
584
|
+
fallbackMetrics,
|
|
585
|
+
modulepreload,
|
|
586
|
+
cacheControl,
|
|
587
|
+
sessionResolver
|
|
588
|
+
} = options;
|
|
464
589
|
let template = options.template;
|
|
465
590
|
if (inlineCSS) {
|
|
466
591
|
for (const [href, css] of Object.entries(inlineCSS)) {
|
|
@@ -470,13 +595,38 @@ function createSSRHandler(options) {
|
|
|
470
595
|
template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
|
|
471
596
|
}
|
|
472
597
|
}
|
|
598
|
+
let linkHeader;
|
|
599
|
+
if (module.theme) {
|
|
600
|
+
const compiled = compileTheme2(module.theme, { fallbackMetrics });
|
|
601
|
+
if (compiled.preloadItems.length > 0) {
|
|
602
|
+
linkHeader = buildLinkHeader(compiled.preloadItems);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
|
|
473
606
|
return async (request) => {
|
|
474
607
|
const url = new URL(request.url);
|
|
475
608
|
const pathname = url.pathname;
|
|
476
609
|
if (request.headers.get("x-vertz-nav") === "1") {
|
|
477
610
|
return handleNavRequest(module, pathname, ssrTimeout);
|
|
478
611
|
}
|
|
479
|
-
|
|
612
|
+
let sessionScript = "";
|
|
613
|
+
if (sessionResolver) {
|
|
614
|
+
try {
|
|
615
|
+
const sessionResult = await sessionResolver(request);
|
|
616
|
+
if (sessionResult) {
|
|
617
|
+
const scripts = [];
|
|
618
|
+
scripts.push(createSessionScript(sessionResult.session, nonce));
|
|
619
|
+
if (sessionResult.accessSet != null) {
|
|
620
|
+
scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
|
|
621
|
+
}
|
|
622
|
+
sessionScript = scripts.join(`
|
|
623
|
+
`);
|
|
624
|
+
}
|
|
625
|
+
} catch (resolverErr) {
|
|
626
|
+
console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return handleHTMLRequest(module, template, pathname, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript);
|
|
480
630
|
};
|
|
481
631
|
}
|
|
482
632
|
async function handleNavRequest(module, url, ssrTimeout) {
|
|
@@ -502,15 +652,28 @@ data: {}
|
|
|
502
652
|
});
|
|
503
653
|
}
|
|
504
654
|
}
|
|
505
|
-
async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
|
|
655
|
+
async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript) {
|
|
506
656
|
try {
|
|
507
|
-
const result = await ssrRenderToString(module, url, { ssrTimeout });
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
657
|
+
const result = await ssrRenderToString(module, url, { ssrTimeout, fallbackMetrics });
|
|
658
|
+
const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
|
|
659
|
+
`);
|
|
660
|
+
const html = injectIntoTemplate({
|
|
661
|
+
template,
|
|
662
|
+
appHtml: result.html,
|
|
663
|
+
appCss: result.css,
|
|
664
|
+
ssrData: result.ssrData,
|
|
665
|
+
nonce,
|
|
666
|
+
headTags: allHeadTags || undefined,
|
|
667
|
+
sessionScript
|
|
512
668
|
});
|
|
513
|
-
|
|
669
|
+
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
670
|
+
if (linkHeader)
|
|
671
|
+
headers.Link = linkHeader;
|
|
672
|
+
if (cacheControl)
|
|
673
|
+
headers["Cache-Control"] = cacheControl;
|
|
674
|
+
return new Response(html, { status: 200, headers });
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
514
677
|
return new Response("Internal Server Error", {
|
|
515
678
|
status: 500,
|
|
516
679
|
headers: { "Content-Type": "text/plain" }
|
|
@@ -518,4 +681,4 @@ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
|
|
|
518
681
|
}
|
|
519
682
|
}
|
|
520
683
|
|
|
521
|
-
export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, createSSRHandler };
|
|
684
|
+
export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, getAccessSetForSSR, createAccessSetScript, createSessionScript, injectIntoTemplate, createSSRHandler };
|
|
@@ -413,14 +413,14 @@ function installDomShim() {
|
|
|
413
413
|
querySelector: () => null,
|
|
414
414
|
querySelectorAll: () => [],
|
|
415
415
|
getElementById: () => null,
|
|
416
|
+
addEventListener: () => {},
|
|
417
|
+
removeEventListener: () => {},
|
|
416
418
|
cookie: ""
|
|
417
419
|
};
|
|
418
420
|
globalThis.document = fakeDocument;
|
|
419
421
|
if (typeof window === "undefined") {
|
|
420
422
|
globalThis.window = {
|
|
421
423
|
location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
|
|
422
|
-
addEventListener: () => {},
|
|
423
|
-
removeEventListener: () => {},
|
|
424
424
|
history: {
|
|
425
425
|
pushState: () => {},
|
|
426
426
|
replaceState: () => {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/bun-plugin/image-paths.ts
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { basename, dirname, extname, resolve } from "path";
|
|
6
|
+
var EXT_MAP = {
|
|
7
|
+
jpeg: ".jpg",
|
|
8
|
+
jpg: ".jpg",
|
|
9
|
+
png: ".png",
|
|
10
|
+
webp: ".webp",
|
|
11
|
+
gif: ".gif"
|
|
12
|
+
};
|
|
13
|
+
var MIME_MAP = {
|
|
14
|
+
jpeg: "image/jpeg",
|
|
15
|
+
jpg: "image/jpeg",
|
|
16
|
+
png: "image/png",
|
|
17
|
+
webp: "image/webp",
|
|
18
|
+
gif: "image/gif"
|
|
19
|
+
};
|
|
20
|
+
var IMG_CONTENT_TYPES = {
|
|
21
|
+
webp: "image/webp",
|
|
22
|
+
png: "image/png",
|
|
23
|
+
jpg: "image/jpeg",
|
|
24
|
+
jpeg: "image/jpeg",
|
|
25
|
+
gif: "image/gif",
|
|
26
|
+
avif: "image/avif"
|
|
27
|
+
};
|
|
28
|
+
function imageContentType(ext) {
|
|
29
|
+
return ext && IMG_CONTENT_TYPES[ext] || "application/octet-stream";
|
|
30
|
+
}
|
|
31
|
+
function isValidImageName(imgName) {
|
|
32
|
+
return !imgName.includes("..") && !imgName.includes("\x00");
|
|
33
|
+
}
|
|
34
|
+
function resolveImageSrc(src, sourceFile, projectRoot) {
|
|
35
|
+
if (src.startsWith("/"))
|
|
36
|
+
return resolve(projectRoot, src.slice(1));
|
|
37
|
+
return resolve(dirname(sourceFile), src);
|
|
38
|
+
}
|
|
39
|
+
function computeImageOutputPaths(sourcePath, width, height, quality, fit) {
|
|
40
|
+
let sourceBuffer;
|
|
41
|
+
try {
|
|
42
|
+
sourceBuffer = readFileSync(sourcePath);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const hash = createHash("sha256").update(sourceBuffer).update(`${width}x${height}q${quality}f${fit}`).digest("hex").slice(0, 12);
|
|
47
|
+
const name = basename(sourcePath, extname(sourcePath));
|
|
48
|
+
const ext = extname(sourcePath).slice(1);
|
|
49
|
+
return {
|
|
50
|
+
webp1x: `/__vertz_img/${name}-${hash}-${width}w.webp`,
|
|
51
|
+
webp2x: `/__vertz_img/${name}-${hash}-${width * 2}w.webp`,
|
|
52
|
+
fallback: `/__vertz_img/${name}-${hash}-${width * 2}w${EXT_MAP[ext] ?? ".jpg"}`,
|
|
53
|
+
fallbackType: MIME_MAP[ext] ?? "image/jpeg"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { imageContentType, isValidImageName, resolveImageSrc, computeImageOutputPaths };
|
package/dist/ssr/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CompiledRoute } from "@vertz/ui";
|
|
2
|
+
import { FontFallbackMetrics, Theme } from "@vertz/ui";
|
|
2
3
|
interface SSRModule {
|
|
3
4
|
default?: () => unknown;
|
|
4
5
|
App?: () => unknown;
|
|
@@ -21,6 +22,10 @@ interface SSRRenderResult {
|
|
|
21
22
|
key: string;
|
|
22
23
|
data: unknown;
|
|
23
24
|
}>;
|
|
25
|
+
/** Font preload link tags for injection into <head>. */
|
|
26
|
+
headTags: string;
|
|
27
|
+
/** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
|
|
28
|
+
discoveredRoutes?: string[];
|
|
24
29
|
}
|
|
25
30
|
interface SSRDiscoverResult {
|
|
26
31
|
resolved: Array<{
|
|
@@ -38,6 +43,8 @@ interface SSRDiscoverResult {
|
|
|
38
43
|
*/
|
|
39
44
|
declare function ssrRenderToString(module: SSRModule, url: string, options?: {
|
|
40
45
|
ssrTimeout?: number;
|
|
46
|
+
/** Pre-computed font fallback metrics (computed at server startup). */
|
|
47
|
+
fallbackMetrics?: Record<string, FontFallbackMetrics>;
|
|
41
48
|
}): Promise<SSRRenderResult>;
|
|
42
49
|
/**
|
|
43
50
|
* Discover queries for a given URL without rendering.
|
|
@@ -47,6 +54,83 @@ declare function ssrRenderToString(module: SSRModule, url: string, options?: {
|
|
|
47
54
|
declare function ssrDiscoverQueries(module: SSRModule, url: string, options?: {
|
|
48
55
|
ssrTimeout?: number;
|
|
49
56
|
}): Promise<SSRDiscoverResult>;
|
|
57
|
+
interface PrerenderResult {
|
|
58
|
+
/** The route path that was pre-rendered. */
|
|
59
|
+
path: string;
|
|
60
|
+
/** The complete HTML string. */
|
|
61
|
+
html: string;
|
|
62
|
+
}
|
|
63
|
+
interface PrerenderOptions {
|
|
64
|
+
/** Route paths to pre-render. */
|
|
65
|
+
routes: string[];
|
|
66
|
+
/** CSP nonce for inline scripts. */
|
|
67
|
+
nonce?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Discover all route patterns from an SSR module.
|
|
71
|
+
*
|
|
72
|
+
* Renders `/` to trigger `createRouter()`, which registers route patterns
|
|
73
|
+
* with the SSR context. Returns the discovered patterns (including dynamic).
|
|
74
|
+
*/
|
|
75
|
+
declare function discoverRoutes(module: SSRModule): Promise<string[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Filter route patterns to only pre-renderable ones.
|
|
78
|
+
*
|
|
79
|
+
* Excludes:
|
|
80
|
+
* - Routes with `:param` segments
|
|
81
|
+
* - Routes with `*` wildcard
|
|
82
|
+
* - Routes with `prerender: false` (looked up in compiledRoutes)
|
|
83
|
+
*/
|
|
84
|
+
declare function filterPrerenderableRoutes(patterns: string[], compiledRoutes?: CompiledRoute[]): string[];
|
|
85
|
+
/**
|
|
86
|
+
* Pre-render a list of routes into complete HTML strings.
|
|
87
|
+
*
|
|
88
|
+
* Routes are rendered sequentially (not in parallel) because the DOM shim
|
|
89
|
+
* uses process-global `document`/`window`. Concurrent renders would interleave.
|
|
90
|
+
*
|
|
91
|
+
* CSS from SSR (theme variables, font-face, global styles) is injected as
|
|
92
|
+
* inline `<style>` tags. The template's `<link>` tags only cover component
|
|
93
|
+
* CSS extracted by the bundler — theme and globals are computed at render time.
|
|
94
|
+
*
|
|
95
|
+
* @throws Error if any route fails to render (with hint about `prerender: false`)
|
|
96
|
+
*/
|
|
97
|
+
declare function prerenderRoutes(module: SSRModule, template: string, options: PrerenderOptions): Promise<PrerenderResult[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Strip `<script>` tags and `<link rel="modulepreload">` from pre-rendered HTML
|
|
100
|
+
* that has no interactive components (no `data-v-island` or `data-v-id` markers).
|
|
101
|
+
*
|
|
102
|
+
* Pages with islands or hydrated components need the client JS; purely static
|
|
103
|
+
* pages (like /manifesto) ship zero JavaScript.
|
|
104
|
+
*/
|
|
105
|
+
declare function stripScriptsFromStaticHTML(html: string): string;
|
|
106
|
+
import { FontFallbackMetrics as FontFallbackMetrics2 } from "@vertz/ui";
|
|
107
|
+
import { AccessSet } from "@vertz/ui/auth";
|
|
108
|
+
interface SessionData {
|
|
109
|
+
user: {
|
|
110
|
+
id: string;
|
|
111
|
+
email: string;
|
|
112
|
+
role: string;
|
|
113
|
+
[key: string]: unknown;
|
|
114
|
+
};
|
|
115
|
+
/** Unix timestamp in milliseconds (JWT exp * 1000). */
|
|
116
|
+
expiresAt: number;
|
|
117
|
+
}
|
|
118
|
+
/** Resolved session data for SSR injection. */
|
|
119
|
+
interface SSRSessionInfo {
|
|
120
|
+
session: SessionData;
|
|
121
|
+
/**
|
|
122
|
+
* Access set from JWT acl claim.
|
|
123
|
+
* - Present (object): inline access set (no overflow)
|
|
124
|
+
* - null: access control is configured but the set overflowed the JWT
|
|
125
|
+
* - undefined: access control is not configured
|
|
126
|
+
*/
|
|
127
|
+
accessSet?: AccessSet | null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Callback that extracts session data from a request.
|
|
131
|
+
* Returns null when no valid session exists (expired, missing, or invalid cookie).
|
|
132
|
+
*/
|
|
133
|
+
type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
|
|
50
134
|
interface SSRHandlerOptions {
|
|
51
135
|
/** The loaded SSR module (import('./dist/server/index.js')) */
|
|
52
136
|
module: SSRModule;
|
|
@@ -72,15 +156,38 @@ interface SSRHandlerOptions {
|
|
|
72
156
|
* so that strict Content-Security-Policy headers do not block it.
|
|
73
157
|
*/
|
|
74
158
|
nonce?: string;
|
|
159
|
+
/** Pre-computed font fallback metrics (computed at server startup). */
|
|
160
|
+
fallbackMetrics?: Record<string, FontFallbackMetrics2>;
|
|
161
|
+
/** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
|
|
162
|
+
modulepreload?: string[];
|
|
163
|
+
/** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
|
|
164
|
+
cacheControl?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Resolves session data from request cookies for SSR injection.
|
|
167
|
+
* When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
|
|
168
|
+
* optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
|
|
169
|
+
*/
|
|
170
|
+
sessionResolver?: SessionResolver;
|
|
171
|
+
}
|
|
172
|
+
declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
|
|
173
|
+
interface InjectIntoTemplateOptions {
|
|
174
|
+
template: string;
|
|
175
|
+
appHtml: string;
|
|
176
|
+
appCss: string;
|
|
177
|
+
ssrData: Array<{
|
|
178
|
+
key: string;
|
|
179
|
+
data: unknown;
|
|
180
|
+
}>;
|
|
181
|
+
nonce?: string;
|
|
182
|
+
headTags?: string;
|
|
183
|
+
/** Pre-built session + access set script tags for SSR injection. */
|
|
184
|
+
sessionScript?: string;
|
|
75
185
|
}
|
|
76
186
|
/**
|
|
77
|
-
*
|
|
187
|
+
* Inject SSR output into the HTML template.
|
|
78
188
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* - Normal HTML request -> SSR-rendered HTML Response
|
|
82
|
-
*
|
|
83
|
-
* Does NOT serve static files — that's the adapter/platform's job.
|
|
189
|
+
* Replaces <!--ssr-outlet--> or <div id="app"> content with rendered HTML,
|
|
190
|
+
* injects CSS before </head>, and ssrData before </body>.
|
|
84
191
|
*/
|
|
85
|
-
declare function
|
|
86
|
-
export { ssrRenderToString, ssrDiscoverQueries, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult };
|
|
192
|
+
declare function injectIntoTemplate(options: InjectIntoTemplateOptions): string;
|
|
193
|
+
export { stripScriptsFromStaticHTML, ssrRenderToString, ssrDiscoverQueries, prerenderRoutes, injectIntoTemplate, filterPrerenderableRoutes, discoverRoutes, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult, PrerenderResult, PrerenderOptions };
|
package/dist/ssr/index.js
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSSRHandler,
|
|
3
|
+
injectIntoTemplate,
|
|
3
4
|
ssrDiscoverQueries,
|
|
4
5
|
ssrRenderToString
|
|
5
|
-
} from "../shared/chunk-
|
|
6
|
-
import"../shared/chunk-
|
|
6
|
+
} from "../shared/chunk-969qgkdf.js";
|
|
7
|
+
import"../shared/chunk-9jjdzz8c.js";
|
|
8
|
+
|
|
9
|
+
// src/prerender.ts
|
|
10
|
+
async function discoverRoutes(module) {
|
|
11
|
+
const result = await ssrRenderToString(module, "/");
|
|
12
|
+
return result.discoveredRoutes ?? [];
|
|
13
|
+
}
|
|
14
|
+
function filterPrerenderableRoutes(patterns, compiledRoutes) {
|
|
15
|
+
return patterns.filter((pattern) => {
|
|
16
|
+
if (pattern.includes(":") || pattern.includes("*"))
|
|
17
|
+
return false;
|
|
18
|
+
if (compiledRoutes) {
|
|
19
|
+
const route = findCompiledRoute(compiledRoutes, pattern);
|
|
20
|
+
if (route?.prerender === false)
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function prerenderRoutes(module, template, options) {
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const routePath of options.routes) {
|
|
29
|
+
let renderResult;
|
|
30
|
+
try {
|
|
31
|
+
renderResult = await ssrRenderToString(module, routePath);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(`Pre-render failed for ${routePath}
|
|
35
|
+
` + ` ${message}
|
|
36
|
+
` + " Hint: If this route requires runtime data, add `prerender: false` to its route config.");
|
|
37
|
+
}
|
|
38
|
+
const html = injectIntoTemplate({
|
|
39
|
+
template,
|
|
40
|
+
appHtml: renderResult.html,
|
|
41
|
+
appCss: renderResult.css,
|
|
42
|
+
ssrData: renderResult.ssrData,
|
|
43
|
+
nonce: options.nonce,
|
|
44
|
+
headTags: renderResult.headTags || undefined
|
|
45
|
+
});
|
|
46
|
+
results.push({ path: routePath, html });
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
function stripScriptsFromStaticHTML(html) {
|
|
51
|
+
if (html.includes("data-v-island") || html.includes("data-v-id")) {
|
|
52
|
+
return html;
|
|
53
|
+
}
|
|
54
|
+
let result = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
55
|
+
result = result.replace(/<script\b[^>]*\/>/gi, "");
|
|
56
|
+
result = result.replace(/<link\b[^>]*\brel=["']modulepreload["'][^>]*\/?>/gi, "");
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function findCompiledRoute(routes, pattern, prefix = "") {
|
|
60
|
+
for (const route of routes) {
|
|
61
|
+
const fullPattern = joinPatterns(prefix, route.pattern);
|
|
62
|
+
if (fullPattern === pattern)
|
|
63
|
+
return route;
|
|
64
|
+
if (route.children) {
|
|
65
|
+
const found = findCompiledRoute(route.children, pattern, fullPattern);
|
|
66
|
+
if (found)
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
function joinPatterns(parent, child) {
|
|
73
|
+
if (!parent || parent === "/")
|
|
74
|
+
return child;
|
|
75
|
+
if (child === "/")
|
|
76
|
+
return parent;
|
|
77
|
+
return `${parent.replace(/\/$/, "")}/${child.replace(/^\//, "")}`;
|
|
78
|
+
}
|
|
7
79
|
export {
|
|
80
|
+
stripScriptsFromStaticHTML,
|
|
8
81
|
ssrRenderToString,
|
|
9
82
|
ssrDiscoverQueries,
|
|
83
|
+
prerenderRoutes,
|
|
84
|
+
injectIntoTemplate,
|
|
85
|
+
filterPrerenderableRoutes,
|
|
86
|
+
discoverRoutes,
|
|
10
87
|
createSSRHandler
|
|
11
88
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/ui-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz UI server-side rendering runtime",
|
|
@@ -56,18 +56,21 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@ampproject/remapping": "^2.3.0",
|
|
59
|
+
"@capsizecss/unpack": "^4.0.0",
|
|
59
60
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
60
|
-
"@vertz/core": "^0.2.
|
|
61
|
-
"@vertz/ui": "^0.2.
|
|
62
|
-
"@vertz/ui-compiler": "^0.2.
|
|
61
|
+
"@vertz/core": "^0.2.15",
|
|
62
|
+
"@vertz/ui": "^0.2.15",
|
|
63
|
+
"@vertz/ui-compiler": "^0.2.15",
|
|
63
64
|
"magic-string": "^0.30.0",
|
|
65
|
+
"sharp": "^0.34.5",
|
|
64
66
|
"ts-morph": "^27.0.2"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
69
|
+
"@vertz/codegen": "^0.2.15",
|
|
67
70
|
"bun-types": "^1.3.10",
|
|
68
71
|
"bunup": "^0.16.31",
|
|
69
72
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
70
|
-
"happy-dom": "^
|
|
73
|
+
"happy-dom": "^20.8.3",
|
|
71
74
|
"typescript": "^5.7.0"
|
|
72
75
|
},
|
|
73
76
|
"engines": {
|