@vertz/ui-server 0.2.15 → 0.2.17
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 +379 -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 +91 -12
- package/dist/index.js +127 -41
- package/dist/shared/{chunk-n1arq9xq.js → chunk-9jjdzz8c.js} +2 -2
- package/dist/shared/{chunk-98972e43.js → chunk-c5ee9yf1.js} +212 -23
- package/dist/shared/chunk-gggnhyqj.js +57 -0
- package/dist/ssr/index.d.ts +123 -9
- package/dist/ssr/index.js +79 -2
- 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-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) {
|
|
@@ -251,19 +251,57 @@ async function ssrRenderToString(module, url, options) {
|
|
|
251
251
|
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
252
252
|
ensureDomShim();
|
|
253
253
|
const ctx = createRequestContext(normalizedUrl);
|
|
254
|
+
if (options?.ssrAuth) {
|
|
255
|
+
ctx.ssrAuth = options.ssrAuth;
|
|
256
|
+
}
|
|
254
257
|
return ssrStorage.run(ctx, async () => {
|
|
255
258
|
try {
|
|
256
259
|
setGlobalSSRTimeout(ssrTimeout);
|
|
257
260
|
const createApp = resolveAppFactory(module);
|
|
258
261
|
let themeCss = "";
|
|
262
|
+
let themePreloadTags = "";
|
|
259
263
|
if (module.theme) {
|
|
260
264
|
try {
|
|
261
|
-
|
|
265
|
+
const compiled = compileTheme(module.theme, {
|
|
266
|
+
fallbackMetrics: options?.fallbackMetrics
|
|
267
|
+
});
|
|
268
|
+
themeCss = compiled.css;
|
|
269
|
+
themePreloadTags = compiled.preloadTags;
|
|
262
270
|
} catch (e) {
|
|
263
271
|
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
264
272
|
}
|
|
265
273
|
}
|
|
266
274
|
createApp();
|
|
275
|
+
if (ctx.ssrRedirect) {
|
|
276
|
+
return {
|
|
277
|
+
html: "",
|
|
278
|
+
css: "",
|
|
279
|
+
ssrData: [],
|
|
280
|
+
headTags: "",
|
|
281
|
+
redirect: ctx.ssrRedirect
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const store = ssrStorage.getStore();
|
|
285
|
+
if (store) {
|
|
286
|
+
if (store.pendingRouteComponents?.size) {
|
|
287
|
+
const entries = Array.from(store.pendingRouteComponents.entries());
|
|
288
|
+
const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
|
|
289
|
+
promise.then((mod) => ({ route, factory: mod.default })),
|
|
290
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
|
|
291
|
+
])));
|
|
292
|
+
store.resolvedComponents = new Map;
|
|
293
|
+
for (const result of results) {
|
|
294
|
+
if (result.status === "fulfilled") {
|
|
295
|
+
const { route, factory } = result.value;
|
|
296
|
+
store.resolvedComponents.set(route, factory);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
store.pendingRouteComponents = undefined;
|
|
300
|
+
}
|
|
301
|
+
if (!store.resolvedComponents) {
|
|
302
|
+
store.resolvedComponents = new Map;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
267
305
|
const queries = getSSRQueries();
|
|
268
306
|
const resolvedQueries = [];
|
|
269
307
|
if (queries.length > 0) {
|
|
@@ -275,7 +313,6 @@ async function ssrRenderToString(module, url, options) {
|
|
|
275
313
|
}),
|
|
276
314
|
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
|
|
277
315
|
])));
|
|
278
|
-
const store = ssrStorage.getStore();
|
|
279
316
|
if (store)
|
|
280
317
|
store.queries = [];
|
|
281
318
|
}
|
|
@@ -288,7 +325,13 @@ async function ssrRenderToString(module, url, options) {
|
|
|
288
325
|
key,
|
|
289
326
|
data: JSON.parse(JSON.stringify(data))
|
|
290
327
|
})) : [];
|
|
291
|
-
return {
|
|
328
|
+
return {
|
|
329
|
+
html,
|
|
330
|
+
css,
|
|
331
|
+
ssrData,
|
|
332
|
+
headTags: themePreloadTags,
|
|
333
|
+
discoveredRoutes: ctx.discoveredRoutes
|
|
334
|
+
};
|
|
292
335
|
} finally {
|
|
293
336
|
clearGlobalSSRTimeout();
|
|
294
337
|
}
|
|
@@ -439,17 +482,75 @@ data: ${safeSerialize(entry)}
|
|
|
439
482
|
});
|
|
440
483
|
}
|
|
441
484
|
|
|
442
|
-
// src/ssr-
|
|
443
|
-
function
|
|
485
|
+
// src/ssr-access-set.ts
|
|
486
|
+
function getAccessSetForSSR(jwtPayload) {
|
|
487
|
+
if (!jwtPayload)
|
|
488
|
+
return null;
|
|
489
|
+
const acl = jwtPayload.acl;
|
|
490
|
+
if (!acl)
|
|
491
|
+
return null;
|
|
492
|
+
if (acl.overflow)
|
|
493
|
+
return null;
|
|
494
|
+
if (!acl.set)
|
|
495
|
+
return null;
|
|
496
|
+
return {
|
|
497
|
+
entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
|
|
498
|
+
name,
|
|
499
|
+
{
|
|
500
|
+
allowed: check.allowed,
|
|
501
|
+
reasons: check.reasons ?? [],
|
|
502
|
+
...check.reason ? { reason: check.reason } : {},
|
|
503
|
+
...check.meta ? { meta: check.meta } : {}
|
|
504
|
+
}
|
|
505
|
+
])),
|
|
506
|
+
flags: acl.set.flags,
|
|
507
|
+
plan: acl.set.plan,
|
|
508
|
+
computedAt: acl.set.computedAt
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function createAccessSetScript(accessSet, nonce) {
|
|
512
|
+
const json = JSON.stringify(accessSet);
|
|
513
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
514
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
|
|
515
|
+
return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
|
|
516
|
+
}
|
|
517
|
+
function escapeAttr2(s) {
|
|
518
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/ssr-session.ts
|
|
522
|
+
function createSessionScript(session, nonce) {
|
|
523
|
+
const json = JSON.stringify(session);
|
|
524
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
525
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
|
|
526
|
+
return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
|
|
527
|
+
}
|
|
528
|
+
function escapeAttr3(s) {
|
|
529
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/template-inject.ts
|
|
533
|
+
function injectIntoTemplate(options) {
|
|
534
|
+
const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
|
|
444
535
|
let html;
|
|
445
536
|
if (template.includes("<!--ssr-outlet-->")) {
|
|
446
537
|
html = template.replace("<!--ssr-outlet-->", appHtml);
|
|
447
538
|
} else {
|
|
448
539
|
html = template.replace(/(<div[^>]*id="app"[^>]*>)([\s\S]*?)(<\/div>)/, `$1${appHtml}$3`);
|
|
449
540
|
}
|
|
541
|
+
if (headTags) {
|
|
542
|
+
html = html.replace("</head>", `${headTags}
|
|
543
|
+
</head>`);
|
|
544
|
+
}
|
|
450
545
|
if (appCss) {
|
|
546
|
+
html = html.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
|
|
547
|
+
<noscript>${match}</noscript>`);
|
|
451
548
|
html = html.replace("</head>", `${appCss}
|
|
452
549
|
</head>`);
|
|
550
|
+
}
|
|
551
|
+
if (sessionScript) {
|
|
552
|
+
html = html.replace("</body>", `${sessionScript}
|
|
553
|
+
</body>`);
|
|
453
554
|
}
|
|
454
555
|
if (ssrData.length > 0) {
|
|
455
556
|
const nonceAttr = nonce != null ? ` nonce="${nonce}"` : "";
|
|
@@ -459,8 +560,44 @@ function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
|
|
|
459
560
|
}
|
|
460
561
|
return html;
|
|
461
562
|
}
|
|
563
|
+
|
|
564
|
+
// src/ssr-handler.ts
|
|
565
|
+
import { compileTheme as compileTheme2 } from "@vertz/ui";
|
|
566
|
+
function sanitizeLinkHref(href) {
|
|
567
|
+
return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
568
|
+
}
|
|
569
|
+
function sanitizeLinkParam(value) {
|
|
570
|
+
return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
|
|
571
|
+
}
|
|
572
|
+
function buildLinkHeader(items) {
|
|
573
|
+
return items.map((item) => {
|
|
574
|
+
const parts = [
|
|
575
|
+
`<${sanitizeLinkHref(item.href)}>`,
|
|
576
|
+
"rel=preload",
|
|
577
|
+
`as=${sanitizeLinkParam(item.as)}`
|
|
578
|
+
];
|
|
579
|
+
if (item.type)
|
|
580
|
+
parts.push(`type=${sanitizeLinkParam(item.type)}`);
|
|
581
|
+
if (item.crossorigin)
|
|
582
|
+
parts.push("crossorigin");
|
|
583
|
+
return parts.join("; ");
|
|
584
|
+
}).join(", ");
|
|
585
|
+
}
|
|
586
|
+
function buildModulepreloadTags(paths) {
|
|
587
|
+
return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
|
|
588
|
+
`);
|
|
589
|
+
}
|
|
462
590
|
function createSSRHandler(options) {
|
|
463
|
-
const {
|
|
591
|
+
const {
|
|
592
|
+
module,
|
|
593
|
+
ssrTimeout,
|
|
594
|
+
inlineCSS,
|
|
595
|
+
nonce,
|
|
596
|
+
fallbackMetrics,
|
|
597
|
+
modulepreload,
|
|
598
|
+
cacheControl,
|
|
599
|
+
sessionResolver
|
|
600
|
+
} = options;
|
|
464
601
|
let template = options.template;
|
|
465
602
|
if (inlineCSS) {
|
|
466
603
|
for (const [href, css] of Object.entries(inlineCSS)) {
|
|
@@ -470,13 +607,46 @@ function createSSRHandler(options) {
|
|
|
470
607
|
template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
|
|
471
608
|
}
|
|
472
609
|
}
|
|
610
|
+
let linkHeader;
|
|
611
|
+
if (module.theme) {
|
|
612
|
+
const compiled = compileTheme2(module.theme, { fallbackMetrics });
|
|
613
|
+
if (compiled.preloadItems.length > 0) {
|
|
614
|
+
linkHeader = buildLinkHeader(compiled.preloadItems);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
|
|
473
618
|
return async (request) => {
|
|
474
619
|
const url = new URL(request.url);
|
|
475
620
|
const pathname = url.pathname;
|
|
476
621
|
if (request.headers.get("x-vertz-nav") === "1") {
|
|
477
622
|
return handleNavRequest(module, pathname, ssrTimeout);
|
|
478
623
|
}
|
|
479
|
-
|
|
624
|
+
let sessionScript = "";
|
|
625
|
+
let ssrAuth;
|
|
626
|
+
if (sessionResolver) {
|
|
627
|
+
try {
|
|
628
|
+
const sessionResult = await sessionResolver(request);
|
|
629
|
+
if (sessionResult) {
|
|
630
|
+
ssrAuth = {
|
|
631
|
+
status: "authenticated",
|
|
632
|
+
user: sessionResult.session.user,
|
|
633
|
+
expiresAt: sessionResult.session.expiresAt
|
|
634
|
+
};
|
|
635
|
+
const scripts = [];
|
|
636
|
+
scripts.push(createSessionScript(sessionResult.session, nonce));
|
|
637
|
+
if (sessionResult.accessSet != null) {
|
|
638
|
+
scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
|
|
639
|
+
}
|
|
640
|
+
sessionScript = scripts.join(`
|
|
641
|
+
`);
|
|
642
|
+
} else {
|
|
643
|
+
ssrAuth = { status: "unauthenticated" };
|
|
644
|
+
}
|
|
645
|
+
} catch (resolverErr) {
|
|
646
|
+
console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return handleHTMLRequest(module, template, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript, ssrAuth);
|
|
480
650
|
};
|
|
481
651
|
}
|
|
482
652
|
async function handleNavRequest(module, url, ssrTimeout) {
|
|
@@ -502,15 +672,34 @@ data: {}
|
|
|
502
672
|
});
|
|
503
673
|
}
|
|
504
674
|
}
|
|
505
|
-
async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
|
|
675
|
+
async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, cacheControl, sessionScript, ssrAuth) {
|
|
506
676
|
try {
|
|
507
|
-
const result = await ssrRenderToString(module, url, { ssrTimeout });
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
677
|
+
const result = await ssrRenderToString(module, url, { ssrTimeout, fallbackMetrics, ssrAuth });
|
|
678
|
+
if (result.redirect) {
|
|
679
|
+
return new Response(null, {
|
|
680
|
+
status: 302,
|
|
681
|
+
headers: { Location: result.redirect.to }
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
|
|
685
|
+
`);
|
|
686
|
+
const html = injectIntoTemplate({
|
|
687
|
+
template,
|
|
688
|
+
appHtml: result.html,
|
|
689
|
+
appCss: result.css,
|
|
690
|
+
ssrData: result.ssrData,
|
|
691
|
+
nonce,
|
|
692
|
+
headTags: allHeadTags || undefined,
|
|
693
|
+
sessionScript
|
|
512
694
|
});
|
|
513
|
-
|
|
695
|
+
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
696
|
+
if (linkHeader)
|
|
697
|
+
headers.Link = linkHeader;
|
|
698
|
+
if (cacheControl)
|
|
699
|
+
headers["Cache-Control"] = cacheControl;
|
|
700
|
+
return new Response(html, { status: 200, headers });
|
|
701
|
+
} catch (err) {
|
|
702
|
+
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
514
703
|
return new Response("Internal Server Error", {
|
|
515
704
|
status: 500,
|
|
516
705
|
headers: { "Content-Type": "text/plain" }
|
|
@@ -518,4 +707,4 @@ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
|
|
|
518
707
|
}
|
|
519
708
|
}
|
|
520
709
|
|
|
521
|
-
export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, createSSRHandler };
|
|
710
|
+
export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, createRequestContext, ssrRenderToString, ssrDiscoverQueries, getAccessSetForSSR, createAccessSetScript, createSessionScript, injectIntoTemplate, createSSRHandler };
|
|
@@ -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,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CompiledRoute } from "@vertz/ui";
|
|
2
|
+
import { FontFallbackMetrics, Theme } from "@vertz/ui";
|
|
3
|
+
import { SSRAuth as SSRAuth_jq1nwm } from "@vertz/ui/internals";
|
|
2
4
|
interface SSRModule {
|
|
3
5
|
default?: () => unknown;
|
|
4
6
|
App?: () => unknown;
|
|
@@ -21,6 +23,14 @@ interface SSRRenderResult {
|
|
|
21
23
|
key: string;
|
|
22
24
|
data: unknown;
|
|
23
25
|
}>;
|
|
26
|
+
/** Font preload link tags for injection into <head>. */
|
|
27
|
+
headTags: string;
|
|
28
|
+
/** Route patterns discovered by createRouter() during SSR (for build-time pre-rendering). */
|
|
29
|
+
discoveredRoutes?: string[];
|
|
30
|
+
/** Set when ProtectedRoute writes a redirect during SSR. Server should return 302. */
|
|
31
|
+
redirect?: {
|
|
32
|
+
to: string;
|
|
33
|
+
};
|
|
24
34
|
}
|
|
25
35
|
interface SSRDiscoverResult {
|
|
26
36
|
resolved: Array<{
|
|
@@ -38,6 +48,10 @@ interface SSRDiscoverResult {
|
|
|
38
48
|
*/
|
|
39
49
|
declare function ssrRenderToString(module: SSRModule, url: string, options?: {
|
|
40
50
|
ssrTimeout?: number;
|
|
51
|
+
/** Pre-computed font fallback metrics (computed at server startup). */
|
|
52
|
+
fallbackMetrics?: Record<string, FontFallbackMetrics>;
|
|
53
|
+
/** Auth state resolved from session cookie. Passed to SSRRenderContext for AuthProvider. */
|
|
54
|
+
ssrAuth?: SSRAuth_jq1nwm;
|
|
41
55
|
}): Promise<SSRRenderResult>;
|
|
42
56
|
/**
|
|
43
57
|
* Discover queries for a given URL without rendering.
|
|
@@ -47,6 +61,83 @@ declare function ssrRenderToString(module: SSRModule, url: string, options?: {
|
|
|
47
61
|
declare function ssrDiscoverQueries(module: SSRModule, url: string, options?: {
|
|
48
62
|
ssrTimeout?: number;
|
|
49
63
|
}): Promise<SSRDiscoverResult>;
|
|
64
|
+
interface PrerenderResult {
|
|
65
|
+
/** The route path that was pre-rendered. */
|
|
66
|
+
path: string;
|
|
67
|
+
/** The complete HTML string. */
|
|
68
|
+
html: string;
|
|
69
|
+
}
|
|
70
|
+
interface PrerenderOptions {
|
|
71
|
+
/** Route paths to pre-render. */
|
|
72
|
+
routes: string[];
|
|
73
|
+
/** CSP nonce for inline scripts. */
|
|
74
|
+
nonce?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Discover all route patterns from an SSR module.
|
|
78
|
+
*
|
|
79
|
+
* Renders `/` to trigger `createRouter()`, which registers route patterns
|
|
80
|
+
* with the SSR context. Returns the discovered patterns (including dynamic).
|
|
81
|
+
*/
|
|
82
|
+
declare function discoverRoutes(module: SSRModule): Promise<string[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Filter route patterns to only pre-renderable ones.
|
|
85
|
+
*
|
|
86
|
+
* Excludes:
|
|
87
|
+
* - Routes with `:param` segments
|
|
88
|
+
* - Routes with `*` wildcard
|
|
89
|
+
* - Routes with `prerender: false` (looked up in compiledRoutes)
|
|
90
|
+
*/
|
|
91
|
+
declare function filterPrerenderableRoutes(patterns: string[], compiledRoutes?: CompiledRoute[]): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Pre-render a list of routes into complete HTML strings.
|
|
94
|
+
*
|
|
95
|
+
* Routes are rendered sequentially (not in parallel) because the DOM shim
|
|
96
|
+
* uses process-global `document`/`window`. Concurrent renders would interleave.
|
|
97
|
+
*
|
|
98
|
+
* CSS from SSR (theme variables, font-face, global styles) is injected as
|
|
99
|
+
* inline `<style>` tags. The template's `<link>` tags only cover component
|
|
100
|
+
* CSS extracted by the bundler — theme and globals are computed at render time.
|
|
101
|
+
*
|
|
102
|
+
* @throws Error if any route fails to render (with hint about `prerender: false`)
|
|
103
|
+
*/
|
|
104
|
+
declare function prerenderRoutes(module: SSRModule, template: string, options: PrerenderOptions): Promise<PrerenderResult[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Strip `<script>` tags and `<link rel="modulepreload">` from pre-rendered HTML
|
|
107
|
+
* that has no interactive components (no `data-v-island` or `data-v-id` markers).
|
|
108
|
+
*
|
|
109
|
+
* Pages with islands or hydrated components need the client JS; purely static
|
|
110
|
+
* pages (like /manifesto) ship zero JavaScript.
|
|
111
|
+
*/
|
|
112
|
+
declare function stripScriptsFromStaticHTML(html: string): string;
|
|
113
|
+
import { FontFallbackMetrics as FontFallbackMetrics2 } from "@vertz/ui";
|
|
114
|
+
import { AccessSet } from "@vertz/ui/auth";
|
|
115
|
+
interface SessionData {
|
|
116
|
+
user: {
|
|
117
|
+
id: string;
|
|
118
|
+
email: string;
|
|
119
|
+
role: string;
|
|
120
|
+
[key: string]: unknown;
|
|
121
|
+
};
|
|
122
|
+
/** Unix timestamp in milliseconds (JWT exp * 1000). */
|
|
123
|
+
expiresAt: number;
|
|
124
|
+
}
|
|
125
|
+
/** Resolved session data for SSR injection. */
|
|
126
|
+
interface SSRSessionInfo {
|
|
127
|
+
session: SessionData;
|
|
128
|
+
/**
|
|
129
|
+
* Access set from JWT acl claim.
|
|
130
|
+
* - Present (object): inline access set (no overflow)
|
|
131
|
+
* - null: access control is configured but the set overflowed the JWT
|
|
132
|
+
* - undefined: access control is not configured
|
|
133
|
+
*/
|
|
134
|
+
accessSet?: AccessSet | null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Callback that extracts session data from a request.
|
|
138
|
+
* Returns null when no valid session exists (expired, missing, or invalid cookie).
|
|
139
|
+
*/
|
|
140
|
+
type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
|
|
50
141
|
interface SSRHandlerOptions {
|
|
51
142
|
/** The loaded SSR module (import('./dist/server/index.js')) */
|
|
52
143
|
module: SSRModule;
|
|
@@ -72,15 +163,38 @@ interface SSRHandlerOptions {
|
|
|
72
163
|
* so that strict Content-Security-Policy headers do not block it.
|
|
73
164
|
*/
|
|
74
165
|
nonce?: string;
|
|
166
|
+
/** Pre-computed font fallback metrics (computed at server startup). */
|
|
167
|
+
fallbackMetrics?: Record<string, FontFallbackMetrics2>;
|
|
168
|
+
/** Paths to inject as `<link rel="modulepreload">` in `<head>`. */
|
|
169
|
+
modulepreload?: string[];
|
|
170
|
+
/** Cache-Control header for HTML responses. Omit or undefined = no header (safe default). */
|
|
171
|
+
cacheControl?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Resolves session data from request cookies for SSR injection.
|
|
174
|
+
* When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
|
|
175
|
+
* optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
|
|
176
|
+
*/
|
|
177
|
+
sessionResolver?: SessionResolver;
|
|
178
|
+
}
|
|
179
|
+
declare function createSSRHandler(options: SSRHandlerOptions): (request: Request) => Promise<Response>;
|
|
180
|
+
interface InjectIntoTemplateOptions {
|
|
181
|
+
template: string;
|
|
182
|
+
appHtml: string;
|
|
183
|
+
appCss: string;
|
|
184
|
+
ssrData: Array<{
|
|
185
|
+
key: string;
|
|
186
|
+
data: unknown;
|
|
187
|
+
}>;
|
|
188
|
+
nonce?: string;
|
|
189
|
+
headTags?: string;
|
|
190
|
+
/** Pre-built session + access set script tags for SSR injection. */
|
|
191
|
+
sessionScript?: string;
|
|
75
192
|
}
|
|
76
193
|
/**
|
|
77
|
-
*
|
|
194
|
+
* Inject SSR output into the HTML template.
|
|
78
195
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* - Normal HTML request -> SSR-rendered HTML Response
|
|
82
|
-
*
|
|
83
|
-
* Does NOT serve static files — that's the adapter/platform's job.
|
|
196
|
+
* Replaces <!--ssr-outlet--> or <div id="app"> content with rendered HTML,
|
|
197
|
+
* injects CSS before </head>, and ssrData before </body>.
|
|
84
198
|
*/
|
|
85
|
-
declare function
|
|
86
|
-
export { ssrRenderToString, ssrDiscoverQueries, createSSRHandler, SSRRenderResult, SSRModule, SSRHandlerOptions, SSRDiscoverResult };
|
|
199
|
+
declare function injectIntoTemplate(options: InjectIntoTemplateOptions): string;
|
|
200
|
+
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-c5ee9yf1.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.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz UI server-side rendering runtime",
|
|
@@ -56,18 +56,22 @@
|
|
|
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.16",
|
|
62
|
+
"@vertz/ui": "^0.2.16",
|
|
63
|
+
"@vertz/ui-compiler": "^0.2.16",
|
|
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.16",
|
|
70
|
+
"@vertz/ui-auth": "^0.2.16",
|
|
67
71
|
"bun-types": "^1.3.10",
|
|
68
72
|
"bunup": "^0.16.31",
|
|
69
73
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
70
|
-
"happy-dom": "^
|
|
74
|
+
"happy-dom": "^20.8.3",
|
|
71
75
|
"typescript": "^5.7.0"
|
|
72
76
|
},
|
|
73
77
|
"engines": {
|