@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
package/dist/bun-dev-server.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
import { AccessSet } from "@vertz/ui/auth";
|
|
2
|
+
interface SessionData {
|
|
3
|
+
user: {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
role: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
/** Unix timestamp in milliseconds (JWT exp * 1000). */
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
/** Resolved session data for SSR injection. */
|
|
13
|
+
interface SSRSessionInfo {
|
|
14
|
+
session: SessionData;
|
|
15
|
+
/**
|
|
16
|
+
* Access set from JWT acl claim.
|
|
17
|
+
* - Present (object): inline access set (no overflow)
|
|
18
|
+
* - null: access control is configured but the set overflowed the JWT
|
|
19
|
+
* - undefined: access control is not configured
|
|
20
|
+
*/
|
|
21
|
+
accessSet?: AccessSet | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Callback that extracts session data from a request.
|
|
25
|
+
* Returns null when no valid session exists (expired, missing, or invalid cookie).
|
|
26
|
+
*/
|
|
27
|
+
type SessionResolver = (request: Request) => Promise<SSRSessionInfo | null>;
|
|
1
28
|
interface BunDevServerOptions {
|
|
2
29
|
/** SSR entry module (e.g., './src/app.tsx') */
|
|
3
30
|
entry: string;
|
|
@@ -31,6 +58,12 @@ interface BunDevServerOptions {
|
|
|
31
58
|
editor?: string;
|
|
32
59
|
/** Extra HTML tags to inject into the <head> (e.g., font preloads, meta tags). */
|
|
33
60
|
headTags?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Resolves session data from request cookies for SSR injection.
|
|
63
|
+
* When provided, SSR HTML includes `window.__VERTZ_SESSION__` and
|
|
64
|
+
* optionally `window.__VERTZ_ACCESS_SET__` for instant auth hydration.
|
|
65
|
+
*/
|
|
66
|
+
sessionResolver?: SessionResolver;
|
|
34
67
|
}
|
|
35
68
|
interface ErrorDetail {
|
|
36
69
|
message: string;
|
|
@@ -96,11 +129,13 @@ interface SSRPageHtmlOptions {
|
|
|
96
129
|
scriptTag: string;
|
|
97
130
|
editor?: string;
|
|
98
131
|
headTags?: string;
|
|
132
|
+
/** Pre-built session + access set script tags for SSR injection. */
|
|
133
|
+
sessionScript?: string;
|
|
99
134
|
}
|
|
100
135
|
/**
|
|
101
136
|
* Generate a full SSR HTML page with the given content, CSS, SSR data, and script tag.
|
|
102
137
|
*/
|
|
103
|
-
declare function generateSSRPageHtml({ title, css, bodyHtml, ssrData, scriptTag, editor, headTags }: SSRPageHtmlOptions): string;
|
|
138
|
+
declare function generateSSRPageHtml({ title, css, bodyHtml, ssrData, scriptTag, editor, headTags, sessionScript }: SSRPageHtmlOptions): string;
|
|
104
139
|
interface FetchInterceptorOptions {
|
|
105
140
|
apiHandler: (req: Request) => Promise<Response>;
|
|
106
141
|
origin: string;
|
package/dist/bun-dev-server.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
imageContentType,
|
|
4
|
+
isValidImageName
|
|
5
|
+
} from "./shared/chunk-gggnhyqj.js";
|
|
2
6
|
import {
|
|
3
7
|
__require
|
|
4
8
|
} from "./shared/chunk-eb80r8e8.js";
|
|
@@ -40,6 +44,53 @@ function createDebugLogger(logDir) {
|
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
// src/dev-image-proxy.ts
|
|
48
|
+
function jsonError(error, status) {
|
|
49
|
+
return new Response(JSON.stringify({ error, status }), {
|
|
50
|
+
status,
|
|
51
|
+
headers: { "Content-Type": "application/json" }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
55
|
+
async function handleDevImageProxy(request) {
|
|
56
|
+
const url = new URL(request.url);
|
|
57
|
+
const sourceUrl = url.searchParams.get("url");
|
|
58
|
+
if (!sourceUrl) {
|
|
59
|
+
return jsonError('Missing required "url" parameter', 400);
|
|
60
|
+
}
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(sourceUrl);
|
|
64
|
+
} catch {
|
|
65
|
+
return jsonError('Invalid "url" parameter', 400);
|
|
66
|
+
}
|
|
67
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
68
|
+
return jsonError("Only HTTP(S) URLs are allowed", 400);
|
|
69
|
+
}
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await fetch(sourceUrl, {
|
|
73
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
77
|
+
return jsonError("Image fetch timed out", 504);
|
|
78
|
+
}
|
|
79
|
+
return jsonError("Failed to fetch image", 502);
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
return jsonError(`Upstream returned status ${response.status}`, 502);
|
|
83
|
+
}
|
|
84
|
+
const contentType = response.headers.get("Content-Type") ?? "application/octet-stream";
|
|
85
|
+
return new Response(response.body, {
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": contentType,
|
|
89
|
+
"Cache-Control": "no-cache"
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
43
94
|
// src/diagnostics-collector.ts
|
|
44
95
|
class DiagnosticsCollector {
|
|
45
96
|
startTime = Date.now();
|
|
@@ -59,6 +110,10 @@ class DiagnosticsCollector {
|
|
|
59
110
|
manifestFileCount = 0;
|
|
60
111
|
manifestDurationMs = 0;
|
|
61
112
|
manifestWarnings = [];
|
|
113
|
+
manifestHmrUpdateCount = 0;
|
|
114
|
+
manifestLastHmrUpdate = null;
|
|
115
|
+
manifestLastHmrFile = null;
|
|
116
|
+
manifestLastHmrChanged = null;
|
|
62
117
|
errorCurrent = null;
|
|
63
118
|
errorLastCategory = null;
|
|
64
119
|
errorLastMessage = null;
|
|
@@ -67,6 +122,10 @@ class DiagnosticsCollector {
|
|
|
67
122
|
watcherLastChangeTime = null;
|
|
68
123
|
static MAX_RUNTIME_ERRORS = 10;
|
|
69
124
|
runtimeErrorsBuffer = [];
|
|
125
|
+
fieldSelectionManifestFileCount = 0;
|
|
126
|
+
fieldSelectionEntries = new Map;
|
|
127
|
+
static MAX_FIELD_MISSES = 50;
|
|
128
|
+
fieldMissesBuffer = [];
|
|
70
129
|
recordPluginConfig(filter, hmr, fastRefresh) {
|
|
71
130
|
this.pluginFilter = filter;
|
|
72
131
|
this.pluginHmr = hmr;
|
|
@@ -94,6 +153,12 @@ class DiagnosticsCollector {
|
|
|
94
153
|
this.manifestDurationMs = durationMs;
|
|
95
154
|
this.manifestWarnings = warnings;
|
|
96
155
|
}
|
|
156
|
+
recordManifestUpdate(file, changed, _durationMs) {
|
|
157
|
+
this.manifestHmrUpdateCount++;
|
|
158
|
+
this.manifestLastHmrUpdate = new Date().toISOString();
|
|
159
|
+
this.manifestLastHmrFile = file;
|
|
160
|
+
this.manifestLastHmrChanged = changed;
|
|
161
|
+
}
|
|
97
162
|
recordHMRAssets(bundledScriptUrl, bootstrapDiscovered) {
|
|
98
163
|
this.hmrBundledScriptUrl = bundledScriptUrl;
|
|
99
164
|
this.hmrBootstrapDiscovered = bootstrapDiscovered;
|
|
@@ -126,6 +191,24 @@ class DiagnosticsCollector {
|
|
|
126
191
|
clearRuntimeErrors() {
|
|
127
192
|
this.runtimeErrorsBuffer = [];
|
|
128
193
|
}
|
|
194
|
+
recordFieldSelectionManifest(fileCount) {
|
|
195
|
+
this.fieldSelectionManifestFileCount = fileCount;
|
|
196
|
+
}
|
|
197
|
+
recordFieldSelection(file, entry) {
|
|
198
|
+
this.fieldSelectionEntries.set(file, entry);
|
|
199
|
+
}
|
|
200
|
+
recordFieldMiss(type, id, field, querySource) {
|
|
201
|
+
this.fieldMissesBuffer.push({
|
|
202
|
+
type,
|
|
203
|
+
id,
|
|
204
|
+
field,
|
|
205
|
+
querySource,
|
|
206
|
+
timestamp: new Date().toISOString()
|
|
207
|
+
});
|
|
208
|
+
if (this.fieldMissesBuffer.length > DiagnosticsCollector.MAX_FIELD_MISSES) {
|
|
209
|
+
this.fieldMissesBuffer = this.fieldMissesBuffer.slice(this.fieldMissesBuffer.length - DiagnosticsCollector.MAX_FIELD_MISSES);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
129
212
|
getSnapshot() {
|
|
130
213
|
return {
|
|
131
214
|
status: "ok",
|
|
@@ -152,7 +235,11 @@ class DiagnosticsCollector {
|
|
|
152
235
|
manifest: {
|
|
153
236
|
fileCount: this.manifestFileCount,
|
|
154
237
|
durationMs: this.manifestDurationMs,
|
|
155
|
-
warnings: [...this.manifestWarnings]
|
|
238
|
+
warnings: [...this.manifestWarnings],
|
|
239
|
+
hmrUpdateCount: this.manifestHmrUpdateCount,
|
|
240
|
+
lastHmrUpdate: this.manifestLastHmrUpdate,
|
|
241
|
+
lastHmrFile: this.manifestLastHmrFile,
|
|
242
|
+
lastHmrChanged: this.manifestLastHmrChanged
|
|
156
243
|
},
|
|
157
244
|
errors: {
|
|
158
245
|
current: this.errorCurrent,
|
|
@@ -166,7 +253,12 @@ class DiagnosticsCollector {
|
|
|
166
253
|
lastChangedFile: this.watcherLastChangedFile,
|
|
167
254
|
lastChangeTime: this.watcherLastChangeTime
|
|
168
255
|
},
|
|
169
|
-
runtimeErrors: [...this.runtimeErrorsBuffer]
|
|
256
|
+
runtimeErrors: [...this.runtimeErrorsBuffer],
|
|
257
|
+
fieldSelection: {
|
|
258
|
+
manifestFileCount: this.fieldSelectionManifestFileCount,
|
|
259
|
+
entries: Object.fromEntries(this.fieldSelectionEntries),
|
|
260
|
+
misses: [...this.fieldMissesBuffer]
|
|
261
|
+
}
|
|
170
262
|
};
|
|
171
263
|
}
|
|
172
264
|
}
|
|
@@ -193,6 +285,109 @@ function runWithScopedFetch(interceptor, fn) {
|
|
|
193
285
|
return fetchScope.run(interceptor, fn);
|
|
194
286
|
}
|
|
195
287
|
|
|
288
|
+
// src/font-metrics.ts
|
|
289
|
+
import { readFile } from "fs/promises";
|
|
290
|
+
import { join as join2 } from "path";
|
|
291
|
+
import { fromBuffer } from "@capsizecss/unpack";
|
|
292
|
+
var SYSTEM_FONT_METRICS = {
|
|
293
|
+
Arial: {
|
|
294
|
+
ascent: 1854,
|
|
295
|
+
descent: -434,
|
|
296
|
+
lineGap: 67,
|
|
297
|
+
unitsPerEm: 2048,
|
|
298
|
+
xWidthAvg: 904
|
|
299
|
+
},
|
|
300
|
+
"Times New Roman": {
|
|
301
|
+
ascent: 1825,
|
|
302
|
+
descent: -443,
|
|
303
|
+
lineGap: 87,
|
|
304
|
+
unitsPerEm: 2048,
|
|
305
|
+
xWidthAvg: 819
|
|
306
|
+
},
|
|
307
|
+
"Courier New": {
|
|
308
|
+
ascent: 1705,
|
|
309
|
+
descent: -615,
|
|
310
|
+
lineGap: 0,
|
|
311
|
+
unitsPerEm: 2048,
|
|
312
|
+
xWidthAvg: 1229
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
function detectFallbackFont(fallback) {
|
|
316
|
+
for (const f of fallback) {
|
|
317
|
+
const lower = f.toLowerCase();
|
|
318
|
+
if (lower === "sans-serif" || lower === "system-ui")
|
|
319
|
+
return "Arial";
|
|
320
|
+
if (lower === "serif")
|
|
321
|
+
return "Times New Roman";
|
|
322
|
+
if (lower === "monospace")
|
|
323
|
+
return "Courier New";
|
|
324
|
+
}
|
|
325
|
+
return "Arial";
|
|
326
|
+
}
|
|
327
|
+
function formatPercent(value) {
|
|
328
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
329
|
+
}
|
|
330
|
+
function computeFallbackMetrics(fontMetrics, fallbackFont) {
|
|
331
|
+
const systemMetrics = SYSTEM_FONT_METRICS[fallbackFont];
|
|
332
|
+
const fontNormalizedWidth = fontMetrics.xWidthAvg / fontMetrics.unitsPerEm;
|
|
333
|
+
const systemNormalizedWidth = systemMetrics.xWidthAvg / systemMetrics.unitsPerEm;
|
|
334
|
+
const sizeAdjust = fontNormalizedWidth / systemNormalizedWidth;
|
|
335
|
+
const ascentOverride = fontMetrics.ascent / (fontMetrics.unitsPerEm * sizeAdjust);
|
|
336
|
+
const descentOverride = Math.abs(fontMetrics.descent) / (fontMetrics.unitsPerEm * sizeAdjust);
|
|
337
|
+
const lineGapOverride = fontMetrics.lineGap / (fontMetrics.unitsPerEm * sizeAdjust);
|
|
338
|
+
return {
|
|
339
|
+
ascentOverride: formatPercent(ascentOverride),
|
|
340
|
+
descentOverride: formatPercent(descentOverride),
|
|
341
|
+
lineGapOverride: formatPercent(lineGapOverride),
|
|
342
|
+
sizeAdjust: formatPercent(sizeAdjust),
|
|
343
|
+
fallbackFont
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function getPrimarySrcPath(descriptor) {
|
|
347
|
+
const { src } = descriptor;
|
|
348
|
+
if (!src)
|
|
349
|
+
return null;
|
|
350
|
+
if (typeof src === "string")
|
|
351
|
+
return src;
|
|
352
|
+
const first = src[0];
|
|
353
|
+
if (first)
|
|
354
|
+
return first.path;
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function resolveFilePath(urlPath, rootDir) {
|
|
358
|
+
const cleaned = urlPath.startsWith("/") ? urlPath.slice(1) : urlPath;
|
|
359
|
+
return join2(rootDir, cleaned);
|
|
360
|
+
}
|
|
361
|
+
async function extractFontMetrics(fonts, rootDir) {
|
|
362
|
+
const result = {};
|
|
363
|
+
for (const [key, descriptor] of Object.entries(fonts)) {
|
|
364
|
+
const adjustFontFallback = descriptor.adjustFontFallback ?? true;
|
|
365
|
+
if (adjustFontFallback === false)
|
|
366
|
+
continue;
|
|
367
|
+
const srcPath = getPrimarySrcPath(descriptor);
|
|
368
|
+
if (!srcPath)
|
|
369
|
+
continue;
|
|
370
|
+
if (!srcPath.toLowerCase().endsWith(".woff2"))
|
|
371
|
+
continue;
|
|
372
|
+
try {
|
|
373
|
+
const filePath = resolveFilePath(srcPath, rootDir);
|
|
374
|
+
const buffer = await readFile(filePath);
|
|
375
|
+
const metrics = await fromBuffer(buffer);
|
|
376
|
+
const fallbackFont = typeof adjustFontFallback === "string" ? adjustFontFallback : detectFallbackFont(descriptor.fallback);
|
|
377
|
+
result[key] = computeFallbackMetrics({
|
|
378
|
+
ascent: metrics.ascent,
|
|
379
|
+
descent: metrics.descent,
|
|
380
|
+
lineGap: metrics.lineGap,
|
|
381
|
+
unitsPerEm: metrics.unitsPerEm,
|
|
382
|
+
xWidthAvg: metrics.xWidthAvg
|
|
383
|
+
}, fallbackFont);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn(`[vertz] Failed to extract font metrics for "${key}" from "${srcPath}":`, error instanceof Error ? error.message : error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
196
391
|
// src/source-map-resolver.ts
|
|
197
392
|
import { readFileSync } from "fs";
|
|
198
393
|
import { resolve as resolvePath } from "path";
|
|
@@ -338,6 +533,17 @@ function createSourceMapResolver(projectRoot) {
|
|
|
338
533
|
};
|
|
339
534
|
}
|
|
340
535
|
|
|
536
|
+
// src/ssr-access-set.ts
|
|
537
|
+
function createAccessSetScript(accessSet, nonce) {
|
|
538
|
+
const json = JSON.stringify(accessSet);
|
|
539
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
540
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
541
|
+
return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
|
|
542
|
+
}
|
|
543
|
+
function escapeAttr(s) {
|
|
544
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
545
|
+
}
|
|
546
|
+
|
|
341
547
|
// src/ssr-render.ts
|
|
342
548
|
import { compileTheme } from "@vertz/ui";
|
|
343
549
|
import { EntityStore, MemoryCache, QueryEnvelopeStore } from "@vertz/ui/internals";
|
|
@@ -745,14 +951,14 @@ function installDomShim() {
|
|
|
745
951
|
querySelector: () => null,
|
|
746
952
|
querySelectorAll: () => [],
|
|
747
953
|
getElementById: () => null,
|
|
954
|
+
addEventListener: () => {},
|
|
955
|
+
removeEventListener: () => {},
|
|
748
956
|
cookie: ""
|
|
749
957
|
};
|
|
750
958
|
globalThis.document = fakeDocument;
|
|
751
959
|
if (typeof window === "undefined") {
|
|
752
960
|
globalThis.window = {
|
|
753
961
|
location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
|
|
754
|
-
addEventListener: () => {},
|
|
755
|
-
removeEventListener: () => {},
|
|
756
962
|
history: {
|
|
757
963
|
pushState: () => {},
|
|
758
964
|
replaceState: () => {}
|
|
@@ -865,14 +1071,14 @@ var RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
|
|
|
865
1071
|
function escapeHtml(text) {
|
|
866
1072
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
867
1073
|
}
|
|
868
|
-
function
|
|
1074
|
+
function escapeAttr2(value) {
|
|
869
1075
|
const str = typeof value === "string" ? value : String(value);
|
|
870
1076
|
return str.replace(/&/g, "&").replace(/"/g, """);
|
|
871
1077
|
}
|
|
872
1078
|
function serializeAttrs(attrs) {
|
|
873
1079
|
const parts = [];
|
|
874
1080
|
for (const [key, value] of Object.entries(attrs)) {
|
|
875
|
-
parts.push(` ${key}="${
|
|
1081
|
+
parts.push(` ${key}="${escapeAttr2(value)}"`);
|
|
876
1082
|
}
|
|
877
1083
|
return parts.join("");
|
|
878
1084
|
}
|
|
@@ -937,7 +1143,7 @@ async function streamToString(stream) {
|
|
|
937
1143
|
function createTemplateChunk(slotId, resolvedHtml, nonce) {
|
|
938
1144
|
const tmplId = `v-tmpl-${slotId}`;
|
|
939
1145
|
const slotRef = `v-slot-${slotId}`;
|
|
940
|
-
const nonceAttr = nonce != null ? ` nonce="${
|
|
1146
|
+
const nonceAttr = nonce != null ? ` nonce="${escapeAttr2(nonce)}"` : "";
|
|
941
1147
|
return `<template id="${tmplId}">${resolvedHtml}</template>` + `<script${nonceAttr}>` + `(function(){` + `var s=document.getElementById("${slotRef}");` + `var t=document.getElementById("${tmplId}");` + `if(s&&t){s.replaceWith(t.content.cloneNode(true));t.remove()}` + `})()` + "</script>";
|
|
942
1148
|
}
|
|
943
1149
|
|
|
@@ -964,7 +1170,7 @@ function renderToStream(tree, options) {
|
|
|
964
1170
|
}
|
|
965
1171
|
const { tag, attrs, children } = node;
|
|
966
1172
|
const isRawText = RAW_TEXT_ELEMENTS.has(tag);
|
|
967
|
-
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${
|
|
1173
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr2(v)}"`).join("");
|
|
968
1174
|
if (VOID_ELEMENTS.has(tag)) {
|
|
969
1175
|
return `<${tag}${attrStr}>`;
|
|
970
1176
|
}
|
|
@@ -1020,7 +1226,7 @@ function createRequestContext(url) {
|
|
|
1020
1226
|
contextScope: null,
|
|
1021
1227
|
entityStore: new EntityStore,
|
|
1022
1228
|
envelopeStore: new QueryEnvelopeStore,
|
|
1023
|
-
queryCache: new MemoryCache,
|
|
1229
|
+
queryCache: new MemoryCache({ maxSize: Infinity }),
|
|
1024
1230
|
inflight: new Map,
|
|
1025
1231
|
queries: [],
|
|
1026
1232
|
errors: []
|
|
@@ -1041,9 +1247,6 @@ function resolveAppFactory(module) {
|
|
|
1041
1247
|
return createApp;
|
|
1042
1248
|
}
|
|
1043
1249
|
function collectCSS(themeCss, module) {
|
|
1044
|
-
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
1045
|
-
const globalTags = module.styles ? module.styles.map((s) => `<style data-vertz-css>${s}</style>`).join(`
|
|
1046
|
-
`) : "";
|
|
1047
1250
|
const alreadyIncluded = new Set;
|
|
1048
1251
|
if (themeCss)
|
|
1049
1252
|
alreadyIncluded.add(themeCss);
|
|
@@ -1052,9 +1255,12 @@ function collectCSS(themeCss, module) {
|
|
|
1052
1255
|
alreadyIncluded.add(s);
|
|
1053
1256
|
}
|
|
1054
1257
|
const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
|
|
1055
|
-
const
|
|
1056
|
-
`
|
|
1057
|
-
|
|
1258
|
+
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
1259
|
+
const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
|
|
1260
|
+
`)}</style>` : "";
|
|
1261
|
+
const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
|
|
1262
|
+
`)}</style>` : "";
|
|
1263
|
+
return [themeTag, globalTag, componentTag].filter(Boolean).join(`
|
|
1058
1264
|
`);
|
|
1059
1265
|
}
|
|
1060
1266
|
async function ssrRenderToString(module, url, options) {
|
|
@@ -1062,19 +1268,57 @@ async function ssrRenderToString(module, url, options) {
|
|
|
1062
1268
|
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
1063
1269
|
ensureDomShim();
|
|
1064
1270
|
const ctx = createRequestContext(normalizedUrl);
|
|
1271
|
+
if (options?.ssrAuth) {
|
|
1272
|
+
ctx.ssrAuth = options.ssrAuth;
|
|
1273
|
+
}
|
|
1065
1274
|
return ssrStorage.run(ctx, async () => {
|
|
1066
1275
|
try {
|
|
1067
1276
|
setGlobalSSRTimeout(ssrTimeout);
|
|
1068
1277
|
const createApp = resolveAppFactory(module);
|
|
1069
1278
|
let themeCss = "";
|
|
1279
|
+
let themePreloadTags = "";
|
|
1070
1280
|
if (module.theme) {
|
|
1071
1281
|
try {
|
|
1072
|
-
|
|
1282
|
+
const compiled = compileTheme(module.theme, {
|
|
1283
|
+
fallbackMetrics: options?.fallbackMetrics
|
|
1284
|
+
});
|
|
1285
|
+
themeCss = compiled.css;
|
|
1286
|
+
themePreloadTags = compiled.preloadTags;
|
|
1073
1287
|
} catch (e) {
|
|
1074
1288
|
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
1075
1289
|
}
|
|
1076
1290
|
}
|
|
1077
1291
|
createApp();
|
|
1292
|
+
if (ctx.ssrRedirect) {
|
|
1293
|
+
return {
|
|
1294
|
+
html: "",
|
|
1295
|
+
css: "",
|
|
1296
|
+
ssrData: [],
|
|
1297
|
+
headTags: "",
|
|
1298
|
+
redirect: ctx.ssrRedirect
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const store = ssrStorage.getStore();
|
|
1302
|
+
if (store) {
|
|
1303
|
+
if (store.pendingRouteComponents?.size) {
|
|
1304
|
+
const entries = Array.from(store.pendingRouteComponents.entries());
|
|
1305
|
+
const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
|
|
1306
|
+
promise.then((mod) => ({ route, factory: mod.default })),
|
|
1307
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
|
|
1308
|
+
])));
|
|
1309
|
+
store.resolvedComponents = new Map;
|
|
1310
|
+
for (const result of results) {
|
|
1311
|
+
if (result.status === "fulfilled") {
|
|
1312
|
+
const { route, factory } = result.value;
|
|
1313
|
+
store.resolvedComponents.set(route, factory);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
store.pendingRouteComponents = undefined;
|
|
1317
|
+
}
|
|
1318
|
+
if (!store.resolvedComponents) {
|
|
1319
|
+
store.resolvedComponents = new Map;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1078
1322
|
const queries = getSSRQueries();
|
|
1079
1323
|
const resolvedQueries = [];
|
|
1080
1324
|
if (queries.length > 0) {
|
|
@@ -1086,7 +1330,6 @@ async function ssrRenderToString(module, url, options) {
|
|
|
1086
1330
|
}),
|
|
1087
1331
|
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
|
|
1088
1332
|
])));
|
|
1089
|
-
const store = ssrStorage.getStore();
|
|
1090
1333
|
if (store)
|
|
1091
1334
|
store.queries = [];
|
|
1092
1335
|
}
|
|
@@ -1099,7 +1342,13 @@ async function ssrRenderToString(module, url, options) {
|
|
|
1099
1342
|
key,
|
|
1100
1343
|
data: JSON.parse(JSON.stringify(data))
|
|
1101
1344
|
})) : [];
|
|
1102
|
-
return {
|
|
1345
|
+
return {
|
|
1346
|
+
html,
|
|
1347
|
+
css,
|
|
1348
|
+
ssrData,
|
|
1349
|
+
headTags: themePreloadTags,
|
|
1350
|
+
discoveredRoutes: ctx.discoveredRoutes
|
|
1351
|
+
};
|
|
1103
1352
|
} finally {
|
|
1104
1353
|
clearGlobalSSRTimeout();
|
|
1105
1354
|
}
|
|
@@ -1203,6 +1452,17 @@ data: ${safeSerialize(entry)}
|
|
|
1203
1452
|
});
|
|
1204
1453
|
}
|
|
1205
1454
|
|
|
1455
|
+
// src/ssr-session.ts
|
|
1456
|
+
function createSessionScript(session, nonce) {
|
|
1457
|
+
const json = JSON.stringify(session);
|
|
1458
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1459
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
|
|
1460
|
+
return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
|
|
1461
|
+
}
|
|
1462
|
+
function escapeAttr3(s) {
|
|
1463
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1206
1466
|
// src/bun-dev-server.ts
|
|
1207
1467
|
var MAX_TERMINAL_STACK_FRAMES = 5;
|
|
1208
1468
|
function formatTerminalRuntimeError(errors, parsedStack) {
|
|
@@ -1431,7 +1691,8 @@ function generateSSRPageHtml({
|
|
|
1431
1691
|
ssrData,
|
|
1432
1692
|
scriptTag,
|
|
1433
1693
|
editor = "vscode",
|
|
1434
|
-
headTags = ""
|
|
1694
|
+
headTags = "",
|
|
1695
|
+
sessionScript = ""
|
|
1435
1696
|
}) {
|
|
1436
1697
|
const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
|
|
1437
1698
|
return `<!doctype html>
|
|
@@ -1447,6 +1708,7 @@ function generateSSRPageHtml({
|
|
|
1447
1708
|
</head>
|
|
1448
1709
|
<body>
|
|
1449
1710
|
<div id="app">${bodyHtml}</div>
|
|
1711
|
+
${sessionScript}
|
|
1450
1712
|
${ssrDataScript}
|
|
1451
1713
|
${scriptTag}
|
|
1452
1714
|
</body>
|
|
@@ -1529,7 +1791,8 @@ function createBunDevServer(options) {
|
|
|
1529
1791
|
projectRoot = process.cwd(),
|
|
1530
1792
|
logRequests = true,
|
|
1531
1793
|
editor: editorOption,
|
|
1532
|
-
headTags = ""
|
|
1794
|
+
headTags = "",
|
|
1795
|
+
sessionResolver
|
|
1533
1796
|
} = options;
|
|
1534
1797
|
const editor = detectEditor(editorOption);
|
|
1535
1798
|
if (apiHandler) {
|
|
@@ -1783,7 +2046,7 @@ function createBunDevServer(options) {
|
|
|
1783
2046
|
}));
|
|
1784
2047
|
}
|
|
1785
2048
|
});
|
|
1786
|
-
const { plugin: serverPlugin } = createVertzBunPlugin({
|
|
2049
|
+
const { plugin: serverPlugin, updateManifest: updateServerManifest } = createVertzBunPlugin({
|
|
1787
2050
|
hmr: false,
|
|
1788
2051
|
fastRefresh: false,
|
|
1789
2052
|
logger,
|
|
@@ -1800,6 +2063,14 @@ function createBunDevServer(options) {
|
|
|
1800
2063
|
console.error("[Server] Failed to load SSR module:", e);
|
|
1801
2064
|
process.exit(1);
|
|
1802
2065
|
}
|
|
2066
|
+
let fontFallbackMetrics;
|
|
2067
|
+
if (ssrMod.theme?.fonts) {
|
|
2068
|
+
try {
|
|
2069
|
+
fontFallbackMetrics = await extractFontMetrics(ssrMod.theme.fonts, projectRoot);
|
|
2070
|
+
} catch (e) {
|
|
2071
|
+
console.warn("[Server] Failed to extract font metrics:", e);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
1803
2074
|
mkdirSync(devDir, { recursive: true });
|
|
1804
2075
|
const frInitPath = resolve(devDir, "fast-refresh-init.ts");
|
|
1805
2076
|
writeFileSync2(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
|
|
@@ -1884,6 +2155,31 @@ if (import.meta.hot) import.meta.hot.accept();
|
|
|
1884
2155
|
if (pathname === "/__vertz_diagnostics") {
|
|
1885
2156
|
return Response.json(diagnostics.getSnapshot());
|
|
1886
2157
|
}
|
|
2158
|
+
if (pathname === "/_vertz/image") {
|
|
2159
|
+
return handleDevImageProxy(request);
|
|
2160
|
+
}
|
|
2161
|
+
if (pathname.startsWith("/__vertz_img/")) {
|
|
2162
|
+
const imgName = pathname.slice("/__vertz_img/".length);
|
|
2163
|
+
if (!isValidImageName(imgName)) {
|
|
2164
|
+
return new Response("Forbidden", { status: 403 });
|
|
2165
|
+
}
|
|
2166
|
+
const imagesDir = resolve(projectRoot, ".vertz", "images");
|
|
2167
|
+
const imgPath = resolve(imagesDir, imgName);
|
|
2168
|
+
if (!imgPath.startsWith(imagesDir)) {
|
|
2169
|
+
return new Response("Forbidden", { status: 403 });
|
|
2170
|
+
}
|
|
2171
|
+
const file = Bun.file(imgPath);
|
|
2172
|
+
if (await file.exists()) {
|
|
2173
|
+
const ext = imgName.split(".").pop();
|
|
2174
|
+
return new Response(file, {
|
|
2175
|
+
headers: {
|
|
2176
|
+
"Content-Type": imageContentType(ext),
|
|
2177
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
return new Response("Not Found", { status: 404 });
|
|
2182
|
+
}
|
|
1887
2183
|
if (openapi && request.method === "GET" && pathname === "/api/openapi.json") {
|
|
1888
2184
|
return serveOpenAPISpec();
|
|
1889
2185
|
}
|
|
@@ -1944,16 +2240,53 @@ data: {}
|
|
|
1944
2240
|
skipSSRPaths,
|
|
1945
2241
|
originalFetch: globalThis.fetch
|
|
1946
2242
|
}) : null;
|
|
2243
|
+
let sessionScript = "";
|
|
2244
|
+
let ssrAuth;
|
|
2245
|
+
if (sessionResolver) {
|
|
2246
|
+
try {
|
|
2247
|
+
const sessionResult = await sessionResolver(request);
|
|
2248
|
+
if (sessionResult) {
|
|
2249
|
+
ssrAuth = {
|
|
2250
|
+
status: "authenticated",
|
|
2251
|
+
user: sessionResult.session.user,
|
|
2252
|
+
expiresAt: sessionResult.session.expiresAt
|
|
2253
|
+
};
|
|
2254
|
+
const scripts = [];
|
|
2255
|
+
scripts.push(createSessionScript(sessionResult.session));
|
|
2256
|
+
if (sessionResult.accessSet != null) {
|
|
2257
|
+
scripts.push(createAccessSetScript(sessionResult.accessSet));
|
|
2258
|
+
}
|
|
2259
|
+
sessionScript = scripts.join(`
|
|
2260
|
+
`);
|
|
2261
|
+
} else {
|
|
2262
|
+
ssrAuth = { status: "unauthenticated" };
|
|
2263
|
+
}
|
|
2264
|
+
} catch (resolverErr) {
|
|
2265
|
+
console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
1947
2268
|
const doRender = async () => {
|
|
1948
2269
|
logger.log("ssr", "render-start", { url: pathname });
|
|
1949
2270
|
const ssrStart = performance.now();
|
|
1950
|
-
const result = await ssrRenderToString(ssrMod, pathname, {
|
|
2271
|
+
const result = await ssrRenderToString(ssrMod, pathname + url.search, {
|
|
2272
|
+
ssrTimeout: 300,
|
|
2273
|
+
fallbackMetrics: fontFallbackMetrics,
|
|
2274
|
+
ssrAuth
|
|
2275
|
+
});
|
|
1951
2276
|
logger.log("ssr", "render-done", {
|
|
1952
2277
|
url: pathname,
|
|
1953
2278
|
durationMs: Math.round(performance.now() - ssrStart),
|
|
1954
2279
|
htmlBytes: result.html.length
|
|
1955
2280
|
});
|
|
2281
|
+
if (result.redirect) {
|
|
2282
|
+
return new Response(null, {
|
|
2283
|
+
status: 302,
|
|
2284
|
+
headers: { Location: result.redirect.to }
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
1956
2287
|
const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
|
|
2288
|
+
const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
|
|
2289
|
+
`);
|
|
1957
2290
|
const html = generateSSRPageHtml({
|
|
1958
2291
|
title,
|
|
1959
2292
|
css: result.css,
|
|
@@ -1961,7 +2294,8 @@ data: {}
|
|
|
1961
2294
|
ssrData: result.ssrData,
|
|
1962
2295
|
scriptTag,
|
|
1963
2296
|
editor,
|
|
1964
|
-
headTags
|
|
2297
|
+
headTags: combinedHeadTags,
|
|
2298
|
+
sessionScript
|
|
1965
2299
|
});
|
|
1966
2300
|
return new Response(html, {
|
|
1967
2301
|
status: 200,
|
|
@@ -2193,6 +2527,18 @@ data: {}
|
|
|
2193
2527
|
clearErrorForFileChange();
|
|
2194
2528
|
}
|
|
2195
2529
|
} catch {}
|
|
2530
|
+
if (stopped)
|
|
2531
|
+
return;
|
|
2532
|
+
if (filename.endsWith(".ts") || filename.endsWith(".tsx")) {
|
|
2533
|
+
const changedFilePath = resolve(srcDir, filename);
|
|
2534
|
+
try {
|
|
2535
|
+
const manifestStartMs = performance.now();
|
|
2536
|
+
const source = await Bun.file(changedFilePath).text();
|
|
2537
|
+
const { changed } = updateServerManifest(changedFilePath, source);
|
|
2538
|
+
const manifestDurationMs = Math.round(performance.now() - manifestStartMs);
|
|
2539
|
+
diagnostics.recordManifestUpdate(lastChangedFile, changed, manifestDurationMs);
|
|
2540
|
+
} catch {}
|
|
2541
|
+
}
|
|
2196
2542
|
if (stopped)
|
|
2197
2543
|
return;
|
|
2198
2544
|
const cacheCleared = clearSSRRequireCache();
|
|
@@ -2205,6 +2551,11 @@ data: {}
|
|
|
2205
2551
|
try {
|
|
2206
2552
|
const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
|
|
2207
2553
|
ssrMod = freshMod;
|
|
2554
|
+
if (freshMod.theme?.fonts) {
|
|
2555
|
+
try {
|
|
2556
|
+
fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
|
|
2557
|
+
} catch {}
|
|
2558
|
+
}
|
|
2208
2559
|
const durationMs = Math.round(performance.now() - ssrReloadStart);
|
|
2209
2560
|
diagnostics.recordSSRReload(true, durationMs);
|
|
2210
2561
|
logger.log("watcher", "ssr-reload", { status: "ok", durationMs });
|
|
@@ -2225,6 +2576,11 @@ data: {}
|
|
|
2225
2576
|
try {
|
|
2226
2577
|
const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
|
|
2227
2578
|
ssrMod = freshMod;
|
|
2579
|
+
if (freshMod.theme?.fonts) {
|
|
2580
|
+
try {
|
|
2581
|
+
fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
|
|
2582
|
+
} catch {}
|
|
2583
|
+
}
|
|
2228
2584
|
const durationMs = Math.round(performance.now() - ssrReloadStart);
|
|
2229
2585
|
diagnostics.recordSSRReload(true, durationMs);
|
|
2230
2586
|
logger.log("watcher", "ssr-reload", { status: "ok", durationMs, retry: true });
|