@vertz/ui-server 0.2.23 → 0.2.25
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 +19 -1
- package/dist/bun-dev-server.js +806 -25
- package/dist/bun-plugin/fast-refresh-dom-state.js +1 -1
- package/dist/bun-plugin/fast-refresh-runtime.js +6 -2
- package/dist/bun-plugin/index.d.ts +1 -1
- package/dist/bun-plugin/index.js +138 -47
- package/dist/dom-shim/index.d.ts +4 -0
- package/dist/dom-shim/index.js +1 -1
- package/dist/index.d.ts +186 -2
- package/dist/index.js +583 -3
- package/dist/shared/{chunk-2qsqp9xj.js → chunk-eenfpa59.js} +20 -10
- package/dist/shared/{chunk-bm16zy8d.js → chunk-gcwqkynf.js} +23 -3
- package/dist/shared/{chunk-5cny4vzm.js → chunk-yr65qdge.js} +7 -1
- package/dist/ssr/index.d.ts +2 -0
- package/dist/ssr/index.js +2 -2
- package/package.json +5 -5
package/dist/bun-dev-server.js
CHANGED
|
@@ -395,6 +395,58 @@ async function extractFontMetrics(fonts, rootDir) {
|
|
|
395
395
|
return result;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
// src/ready-gate.ts
|
|
399
|
+
function createReadyGate(options) {
|
|
400
|
+
let ready = false;
|
|
401
|
+
const pendingClients = new Set;
|
|
402
|
+
let timeoutHandle = null;
|
|
403
|
+
function doOpen(currentError) {
|
|
404
|
+
if (ready)
|
|
405
|
+
return;
|
|
406
|
+
ready = true;
|
|
407
|
+
if (timeoutHandle) {
|
|
408
|
+
clearTimeout(timeoutHandle);
|
|
409
|
+
timeoutHandle = null;
|
|
410
|
+
}
|
|
411
|
+
for (const ws of pendingClients) {
|
|
412
|
+
try {
|
|
413
|
+
ws.sendText(JSON.stringify({ type: "connected" }));
|
|
414
|
+
if (currentError) {
|
|
415
|
+
ws.sendText(JSON.stringify({
|
|
416
|
+
type: "error",
|
|
417
|
+
category: currentError.category,
|
|
418
|
+
errors: currentError.errors
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
pendingClients.clear();
|
|
424
|
+
}
|
|
425
|
+
if (options?.timeoutMs) {
|
|
426
|
+
timeoutHandle = setTimeout(() => {
|
|
427
|
+
if (!ready) {
|
|
428
|
+
options.onTimeoutWarning?.();
|
|
429
|
+
doOpen();
|
|
430
|
+
}
|
|
431
|
+
}, options.timeoutMs);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
get isReady() {
|
|
435
|
+
return ready;
|
|
436
|
+
},
|
|
437
|
+
onOpen(ws) {
|
|
438
|
+
if (ready)
|
|
439
|
+
return false;
|
|
440
|
+
pendingClients.add(ws);
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
onClose(ws) {
|
|
444
|
+
pendingClients.delete(ws);
|
|
445
|
+
},
|
|
446
|
+
open: doOpen
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
398
450
|
// src/source-map-resolver.ts
|
|
399
451
|
import { readFileSync } from "fs";
|
|
400
452
|
import { resolve as resolvePath } from "path";
|
|
@@ -540,6 +592,47 @@ function createSourceMapResolver(projectRoot) {
|
|
|
540
592
|
};
|
|
541
593
|
}
|
|
542
594
|
|
|
595
|
+
// src/ssr-access-evaluator.ts
|
|
596
|
+
function toPrefetchSession(ssrAuth) {
|
|
597
|
+
if (!ssrAuth || ssrAuth.status !== "authenticated" || !ssrAuth.user) {
|
|
598
|
+
return { status: "unauthenticated" };
|
|
599
|
+
}
|
|
600
|
+
const roles = ssrAuth.user.role ? [ssrAuth.user.role] : undefined;
|
|
601
|
+
return {
|
|
602
|
+
status: "authenticated",
|
|
603
|
+
roles,
|
|
604
|
+
tenantId: ssrAuth.user.tenantId
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function evaluateAccessRule(rule, session) {
|
|
608
|
+
switch (rule.type) {
|
|
609
|
+
case "public":
|
|
610
|
+
return true;
|
|
611
|
+
case "authenticated":
|
|
612
|
+
return session.status === "authenticated";
|
|
613
|
+
case "role":
|
|
614
|
+
if (session.status !== "authenticated")
|
|
615
|
+
return false;
|
|
616
|
+
return session.roles?.some((r) => rule.roles.includes(r)) === true;
|
|
617
|
+
case "entitlement":
|
|
618
|
+
if (session.status !== "authenticated")
|
|
619
|
+
return false;
|
|
620
|
+
return session.entitlements?.[rule.value] === true;
|
|
621
|
+
case "where":
|
|
622
|
+
return true;
|
|
623
|
+
case "fva":
|
|
624
|
+
return session.status === "authenticated";
|
|
625
|
+
case "deny":
|
|
626
|
+
return false;
|
|
627
|
+
case "all":
|
|
628
|
+
return rule.rules.every((r) => evaluateAccessRule(r, session));
|
|
629
|
+
case "any":
|
|
630
|
+
return rule.rules.some((r) => evaluateAccessRule(r, session));
|
|
631
|
+
default:
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
543
636
|
// src/ssr-access-set.ts
|
|
544
637
|
function createAccessSetScript(accessSet, nonce) {
|
|
545
638
|
const json = JSON.stringify(accessSet);
|
|
@@ -551,6 +644,124 @@ function escapeAttr(s) {
|
|
|
551
644
|
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
552
645
|
}
|
|
553
646
|
|
|
647
|
+
// src/ssr-prefetch-dev.ts
|
|
648
|
+
import {
|
|
649
|
+
analyzeComponentQueries,
|
|
650
|
+
generatePrefetchManifest
|
|
651
|
+
} from "@vertz/ui-compiler";
|
|
652
|
+
function createPrefetchManifestManager(options) {
|
|
653
|
+
const { routerPath, readFile: readFile2, resolveImport } = options;
|
|
654
|
+
let currentManifest = null;
|
|
655
|
+
let currentSSRManifest;
|
|
656
|
+
let rebuildCount = 0;
|
|
657
|
+
let lastRebuildMs = null;
|
|
658
|
+
let lastRebuildAt = null;
|
|
659
|
+
let fileToRouteIndices = new Map;
|
|
660
|
+
function buildFileIndex(routes) {
|
|
661
|
+
const index = new Map;
|
|
662
|
+
for (let i = 0;i < routes.length; i++) {
|
|
663
|
+
const file = routes[i]?.file;
|
|
664
|
+
if (file) {
|
|
665
|
+
const existing = index.get(file) ?? [];
|
|
666
|
+
existing.push(i);
|
|
667
|
+
index.set(file, existing);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return index;
|
|
671
|
+
}
|
|
672
|
+
function toSSRManifest(manifest) {
|
|
673
|
+
const routeEntries = {};
|
|
674
|
+
for (const route of manifest.routes) {
|
|
675
|
+
const existing = routeEntries[route.pattern];
|
|
676
|
+
if (existing) {
|
|
677
|
+
existing.queries.push(...route.queries);
|
|
678
|
+
} else {
|
|
679
|
+
routeEntries[route.pattern] = { queries: [...route.queries] };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
routePatterns: [...new Set(manifest.routes.map((r) => r.pattern))],
|
|
684
|
+
routeEntries
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function fullBuild(routerSourceOverride) {
|
|
688
|
+
const start = performance.now();
|
|
689
|
+
const routerSource = routerSourceOverride ?? readFile2(routerPath);
|
|
690
|
+
if (!routerSource) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const manifest = generatePrefetchManifest({
|
|
695
|
+
routerSource,
|
|
696
|
+
routerPath,
|
|
697
|
+
readFile: readFile2,
|
|
698
|
+
resolveImport
|
|
699
|
+
});
|
|
700
|
+
currentManifest = manifest;
|
|
701
|
+
currentSSRManifest = toSSRManifest(manifest);
|
|
702
|
+
fileToRouteIndices = buildFileIndex(manifest.routes);
|
|
703
|
+
rebuildCount++;
|
|
704
|
+
lastRebuildMs = Math.round(performance.now() - start);
|
|
705
|
+
lastRebuildAt = new Date().toISOString();
|
|
706
|
+
} catch {}
|
|
707
|
+
}
|
|
708
|
+
function incrementalUpdate(filePath, sourceText) {
|
|
709
|
+
if (!currentManifest)
|
|
710
|
+
return;
|
|
711
|
+
const indices = fileToRouteIndices.get(filePath);
|
|
712
|
+
if (!indices || indices.length === 0)
|
|
713
|
+
return;
|
|
714
|
+
const start = performance.now();
|
|
715
|
+
try {
|
|
716
|
+
const analysis = analyzeComponentQueries(sourceText, filePath);
|
|
717
|
+
const newRoutes = [...currentManifest.routes];
|
|
718
|
+
for (const idx of indices) {
|
|
719
|
+
const existing = newRoutes[idx];
|
|
720
|
+
if (existing) {
|
|
721
|
+
newRoutes[idx] = {
|
|
722
|
+
...existing,
|
|
723
|
+
queries: analysis.queries,
|
|
724
|
+
params: analysis.params
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const newManifest = {
|
|
729
|
+
...currentManifest,
|
|
730
|
+
routes: newRoutes,
|
|
731
|
+
generatedAt: new Date().toISOString()
|
|
732
|
+
};
|
|
733
|
+
currentManifest = newManifest;
|
|
734
|
+
currentSSRManifest = toSSRManifest(newManifest);
|
|
735
|
+
rebuildCount++;
|
|
736
|
+
lastRebuildMs = Math.round(performance.now() - start);
|
|
737
|
+
lastRebuildAt = new Date().toISOString();
|
|
738
|
+
} catch {}
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
build() {
|
|
742
|
+
fullBuild();
|
|
743
|
+
},
|
|
744
|
+
onFileChange(filePath, sourceText) {
|
|
745
|
+
if (filePath === routerPath) {
|
|
746
|
+
fullBuild(sourceText);
|
|
747
|
+
} else {
|
|
748
|
+
incrementalUpdate(filePath, sourceText);
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
getSSRManifest() {
|
|
752
|
+
return currentSSRManifest;
|
|
753
|
+
},
|
|
754
|
+
getSnapshot() {
|
|
755
|
+
return {
|
|
756
|
+
manifest: currentManifest,
|
|
757
|
+
rebuildCount,
|
|
758
|
+
lastRebuildMs,
|
|
759
|
+
lastRebuildAt
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
554
765
|
// src/ssr-render.ts
|
|
555
766
|
import { compileTheme } from "@vertz/ui";
|
|
556
767
|
import { EntityStore, MemoryCache, QueryEnvelopeStore } from "@vertz/ui/internals";
|
|
@@ -560,6 +771,7 @@ import { setAdapter } from "@vertz/ui/internals";
|
|
|
560
771
|
|
|
561
772
|
// src/dom-shim/ssr-node.ts
|
|
562
773
|
class SSRNode {
|
|
774
|
+
nodeType = 1;
|
|
563
775
|
childNodes = [];
|
|
564
776
|
parentNode = null;
|
|
565
777
|
get firstChild() {
|
|
@@ -605,6 +817,7 @@ class SSRNode {
|
|
|
605
817
|
|
|
606
818
|
// src/dom-shim/ssr-comment.ts
|
|
607
819
|
class SSRComment extends SSRNode {
|
|
820
|
+
nodeType = 8;
|
|
608
821
|
text;
|
|
609
822
|
constructor(text) {
|
|
610
823
|
super();
|
|
@@ -628,6 +841,7 @@ function rawHtml(html) {
|
|
|
628
841
|
|
|
629
842
|
// src/dom-shim/ssr-text-node.ts
|
|
630
843
|
class SSRTextNode extends SSRNode {
|
|
844
|
+
nodeType = 3;
|
|
631
845
|
text;
|
|
632
846
|
constructor(text) {
|
|
633
847
|
super();
|
|
@@ -643,6 +857,7 @@ class SSRTextNode extends SSRNode {
|
|
|
643
857
|
|
|
644
858
|
// src/dom-shim/ssr-fragment.ts
|
|
645
859
|
class SSRDocumentFragment extends SSRNode {
|
|
860
|
+
nodeType = 11;
|
|
646
861
|
children = [];
|
|
647
862
|
appendChild(child) {
|
|
648
863
|
if (child instanceof SSRTextNode) {
|
|
@@ -1113,19 +1328,35 @@ function installDomShim() {
|
|
|
1113
1328
|
cookie: ""
|
|
1114
1329
|
};
|
|
1115
1330
|
globalThis.document = fakeDocument;
|
|
1331
|
+
const windowStubs = {
|
|
1332
|
+
scrollTo: () => {},
|
|
1333
|
+
scroll: () => {},
|
|
1334
|
+
addEventListener: () => {},
|
|
1335
|
+
removeEventListener: () => {},
|
|
1336
|
+
dispatchEvent: () => true,
|
|
1337
|
+
getComputedStyle: () => ({}),
|
|
1338
|
+
matchMedia: () => ({ matches: false, addListener: () => {}, removeListener: () => {} })
|
|
1339
|
+
};
|
|
1116
1340
|
if (typeof window === "undefined") {
|
|
1117
1341
|
globalThis.window = {
|
|
1118
1342
|
location: { pathname: ssrStorage.getStore()?.url || "/", search: "", hash: "" },
|
|
1119
1343
|
history: {
|
|
1120
1344
|
pushState: () => {},
|
|
1121
1345
|
replaceState: () => {}
|
|
1122
|
-
}
|
|
1346
|
+
},
|
|
1347
|
+
...windowStubs
|
|
1123
1348
|
};
|
|
1124
1349
|
} else {
|
|
1125
|
-
globalThis.window
|
|
1126
|
-
|
|
1350
|
+
const win = globalThis.window;
|
|
1351
|
+
win.location = {
|
|
1352
|
+
...win.location || {},
|
|
1127
1353
|
pathname: ssrStorage.getStore()?.url || "/"
|
|
1128
1354
|
};
|
|
1355
|
+
for (const [key, val] of Object.entries(windowStubs)) {
|
|
1356
|
+
if (typeof win[key] !== "function") {
|
|
1357
|
+
win[key] = val;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1129
1360
|
}
|
|
1130
1361
|
globalThis.Node = SSRNode;
|
|
1131
1362
|
globalThis.HTMLElement = SSRElement;
|
|
@@ -1250,6 +1481,9 @@ function serializeToHtml(node) {
|
|
|
1250
1481
|
return node.html;
|
|
1251
1482
|
}
|
|
1252
1483
|
const { tag, attrs, children } = node;
|
|
1484
|
+
if (tag === "fragment") {
|
|
1485
|
+
return children.map((child) => serializeToHtml(child)).join("");
|
|
1486
|
+
}
|
|
1253
1487
|
const attrStr = serializeAttrs(attrs);
|
|
1254
1488
|
if (VOID_ELEMENTS.has(tag)) {
|
|
1255
1489
|
return `<${tag}${attrStr}>`;
|
|
@@ -1326,6 +1560,9 @@ function renderToStream(tree, options) {
|
|
|
1326
1560
|
return serializeToHtml(placeholder);
|
|
1327
1561
|
}
|
|
1328
1562
|
const { tag, attrs, children } = node;
|
|
1563
|
+
if (tag === "fragment") {
|
|
1564
|
+
return children.map((child) => walkAndSerialize(child)).join("");
|
|
1565
|
+
}
|
|
1329
1566
|
const isRawText = RAW_TEXT_ELEMENTS.has(tag);
|
|
1330
1567
|
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr2(v)}"`).join("");
|
|
1331
1568
|
if (VOID_ELEMENTS.has(tag)) {
|
|
@@ -1621,6 +1858,426 @@ function escapeAttr3(s) {
|
|
|
1621
1858
|
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1622
1859
|
}
|
|
1623
1860
|
|
|
1861
|
+
// src/ssr-single-pass.ts
|
|
1862
|
+
import { compileTheme as compileTheme2 } from "@vertz/ui";
|
|
1863
|
+
|
|
1864
|
+
// src/ssr-manifest-prefetch.ts
|
|
1865
|
+
function reconstructDescriptors(queries, routeParams, apiClient) {
|
|
1866
|
+
if (!apiClient)
|
|
1867
|
+
return [];
|
|
1868
|
+
const result = [];
|
|
1869
|
+
for (const query of queries) {
|
|
1870
|
+
const descriptor = reconstructSingle(query, routeParams, apiClient);
|
|
1871
|
+
if (descriptor) {
|
|
1872
|
+
result.push(descriptor);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return result;
|
|
1876
|
+
}
|
|
1877
|
+
function reconstructSingle(query, routeParams, apiClient) {
|
|
1878
|
+
const { entity, operation } = query;
|
|
1879
|
+
if (!entity || !operation)
|
|
1880
|
+
return;
|
|
1881
|
+
const entitySdk = apiClient[entity];
|
|
1882
|
+
if (!entitySdk)
|
|
1883
|
+
return;
|
|
1884
|
+
const method = entitySdk[operation];
|
|
1885
|
+
if (typeof method !== "function")
|
|
1886
|
+
return;
|
|
1887
|
+
const args = buildFactoryArgs(query, routeParams);
|
|
1888
|
+
if (args === undefined)
|
|
1889
|
+
return;
|
|
1890
|
+
try {
|
|
1891
|
+
const descriptor = method(...args);
|
|
1892
|
+
if (!descriptor || typeof descriptor._key !== "string" || typeof descriptor._fetch !== "function") {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
return { key: descriptor._key, fetch: descriptor._fetch };
|
|
1896
|
+
} catch {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function buildFactoryArgs(query, routeParams) {
|
|
1901
|
+
const { operation, idParam, queryBindings } = query;
|
|
1902
|
+
if (operation === "get") {
|
|
1903
|
+
if (idParam) {
|
|
1904
|
+
const id = routeParams[idParam];
|
|
1905
|
+
if (!id)
|
|
1906
|
+
return;
|
|
1907
|
+
const options = resolveQueryBindings(queryBindings, routeParams);
|
|
1908
|
+
if (options === undefined && queryBindings)
|
|
1909
|
+
return;
|
|
1910
|
+
return options ? [id, options] : [id];
|
|
1911
|
+
}
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (!queryBindings)
|
|
1915
|
+
return [];
|
|
1916
|
+
const resolved = resolveQueryBindings(queryBindings, routeParams);
|
|
1917
|
+
if (resolved === undefined)
|
|
1918
|
+
return;
|
|
1919
|
+
return [resolved];
|
|
1920
|
+
}
|
|
1921
|
+
function resolveQueryBindings(bindings, routeParams) {
|
|
1922
|
+
if (!bindings)
|
|
1923
|
+
return;
|
|
1924
|
+
const resolved = {};
|
|
1925
|
+
if (bindings.where) {
|
|
1926
|
+
const where = {};
|
|
1927
|
+
for (const [key, value] of Object.entries(bindings.where)) {
|
|
1928
|
+
if (value === null)
|
|
1929
|
+
return;
|
|
1930
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
1931
|
+
const paramName = value.slice(1);
|
|
1932
|
+
const paramValue = routeParams[paramName];
|
|
1933
|
+
if (!paramValue)
|
|
1934
|
+
return;
|
|
1935
|
+
where[key] = paramValue;
|
|
1936
|
+
} else {
|
|
1937
|
+
where[key] = value;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
resolved.where = where;
|
|
1941
|
+
}
|
|
1942
|
+
if (bindings.select)
|
|
1943
|
+
resolved.select = bindings.select;
|
|
1944
|
+
if (bindings.include)
|
|
1945
|
+
resolved.include = bindings.include;
|
|
1946
|
+
if (bindings.orderBy)
|
|
1947
|
+
resolved.orderBy = bindings.orderBy;
|
|
1948
|
+
if (bindings.limit !== undefined)
|
|
1949
|
+
resolved.limit = bindings.limit;
|
|
1950
|
+
return resolved;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/ssr-route-matcher.ts
|
|
1954
|
+
function matchUrlToPatterns(url, patterns) {
|
|
1955
|
+
const path = (url.split("?")[0] ?? "").split("#")[0] ?? "";
|
|
1956
|
+
const matches = [];
|
|
1957
|
+
for (const pattern of patterns) {
|
|
1958
|
+
const result = matchPattern(path, pattern);
|
|
1959
|
+
if (result) {
|
|
1960
|
+
matches.push(result);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
matches.sort((a, b) => {
|
|
1964
|
+
const aSegments = a.pattern.split("/").length;
|
|
1965
|
+
const bSegments = b.pattern.split("/").length;
|
|
1966
|
+
return aSegments - bSegments;
|
|
1967
|
+
});
|
|
1968
|
+
return matches;
|
|
1969
|
+
}
|
|
1970
|
+
function matchPattern(path, pattern) {
|
|
1971
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
1972
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
1973
|
+
if (patternSegments.length > pathSegments.length)
|
|
1974
|
+
return;
|
|
1975
|
+
const params = {};
|
|
1976
|
+
for (let i = 0;i < patternSegments.length; i++) {
|
|
1977
|
+
const seg = patternSegments[i];
|
|
1978
|
+
const val = pathSegments[i];
|
|
1979
|
+
if (seg.startsWith(":")) {
|
|
1980
|
+
params[seg.slice(1)] = val;
|
|
1981
|
+
} else if (seg !== val) {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return { pattern, params };
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// src/ssr-single-pass.ts
|
|
1989
|
+
async function ssrRenderSinglePass(module, url, options) {
|
|
1990
|
+
if (options?.prefetch === false) {
|
|
1991
|
+
return ssrRenderToString(module, url, options);
|
|
1992
|
+
}
|
|
1993
|
+
const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
|
|
1994
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
1995
|
+
ensureDomShim2();
|
|
1996
|
+
const zeroDiscoveryData = attemptZeroDiscovery(normalizedUrl, module, options, ssrTimeout);
|
|
1997
|
+
if (zeroDiscoveryData) {
|
|
1998
|
+
return renderWithPrefetchedData(module, normalizedUrl, zeroDiscoveryData, options);
|
|
1999
|
+
}
|
|
2000
|
+
const discoveryCtx = createRequestContext(normalizedUrl);
|
|
2001
|
+
if (options?.ssrAuth) {
|
|
2002
|
+
discoveryCtx.ssrAuth = options.ssrAuth;
|
|
2003
|
+
}
|
|
2004
|
+
const discoveredData = await ssrStorage.run(discoveryCtx, async () => {
|
|
2005
|
+
try {
|
|
2006
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
2007
|
+
const createApp = resolveAppFactory2(module);
|
|
2008
|
+
createApp();
|
|
2009
|
+
if (discoveryCtx.ssrRedirect) {
|
|
2010
|
+
return { redirect: discoveryCtx.ssrRedirect };
|
|
2011
|
+
}
|
|
2012
|
+
if (discoveryCtx.pendingRouteComponents?.size) {
|
|
2013
|
+
const entries = Array.from(discoveryCtx.pendingRouteComponents.entries());
|
|
2014
|
+
const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
|
|
2015
|
+
promise.then((mod) => ({ route, factory: mod.default })),
|
|
2016
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
|
|
2017
|
+
])));
|
|
2018
|
+
discoveryCtx.resolvedComponents = new Map;
|
|
2019
|
+
for (const result of results) {
|
|
2020
|
+
if (result.status === "fulfilled") {
|
|
2021
|
+
const { route, factory } = result.value;
|
|
2022
|
+
discoveryCtx.resolvedComponents.set(route, factory);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
discoveryCtx.pendingRouteComponents = undefined;
|
|
2026
|
+
}
|
|
2027
|
+
const queries = getSSRQueries();
|
|
2028
|
+
const eligibleQueries = filterByEntityAccess(queries, options?.manifest?.entityAccess, options?.prefetchSession);
|
|
2029
|
+
const resolvedQueries = [];
|
|
2030
|
+
if (eligibleQueries.length > 0) {
|
|
2031
|
+
await Promise.allSettled(eligibleQueries.map(({ promise, timeout, resolve, key }) => Promise.race([
|
|
2032
|
+
promise.then((data) => {
|
|
2033
|
+
resolve(data);
|
|
2034
|
+
resolvedQueries.push({ key, data });
|
|
2035
|
+
return "resolved";
|
|
2036
|
+
}),
|
|
2037
|
+
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
|
|
2038
|
+
])));
|
|
2039
|
+
}
|
|
2040
|
+
return {
|
|
2041
|
+
resolvedQueries,
|
|
2042
|
+
resolvedComponents: discoveryCtx.resolvedComponents
|
|
2043
|
+
};
|
|
2044
|
+
} finally {
|
|
2045
|
+
clearGlobalSSRTimeout();
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
if ("redirect" in discoveredData) {
|
|
2049
|
+
return {
|
|
2050
|
+
html: "",
|
|
2051
|
+
css: "",
|
|
2052
|
+
ssrData: [],
|
|
2053
|
+
headTags: "",
|
|
2054
|
+
redirect: discoveredData.redirect
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
const renderCtx = createRequestContext(normalizedUrl);
|
|
2058
|
+
if (options?.ssrAuth) {
|
|
2059
|
+
renderCtx.ssrAuth = options.ssrAuth;
|
|
2060
|
+
}
|
|
2061
|
+
for (const { key, data } of discoveredData.resolvedQueries) {
|
|
2062
|
+
renderCtx.queryCache.set(key, data);
|
|
2063
|
+
}
|
|
2064
|
+
renderCtx.resolvedComponents = discoveredData.resolvedComponents ?? new Map;
|
|
2065
|
+
return ssrStorage.run(renderCtx, async () => {
|
|
2066
|
+
try {
|
|
2067
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
2068
|
+
const createApp = resolveAppFactory2(module);
|
|
2069
|
+
let themeCss = "";
|
|
2070
|
+
let themePreloadTags = "";
|
|
2071
|
+
if (module.theme) {
|
|
2072
|
+
try {
|
|
2073
|
+
const compiled = compileTheme2(module.theme, {
|
|
2074
|
+
fallbackMetrics: options?.fallbackMetrics
|
|
2075
|
+
});
|
|
2076
|
+
themeCss = compiled.css;
|
|
2077
|
+
themePreloadTags = compiled.preloadTags;
|
|
2078
|
+
} catch (e) {
|
|
2079
|
+
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
const app = createApp();
|
|
2083
|
+
const vnode = toVNode(app);
|
|
2084
|
+
const stream = renderToStream(vnode);
|
|
2085
|
+
const html = await streamToString(stream);
|
|
2086
|
+
const css = collectCSS2(themeCss, module);
|
|
2087
|
+
const ssrData = discoveredData.resolvedQueries.map(({ key, data }) => ({
|
|
2088
|
+
key,
|
|
2089
|
+
data: JSON.parse(JSON.stringify(data))
|
|
2090
|
+
}));
|
|
2091
|
+
return {
|
|
2092
|
+
html,
|
|
2093
|
+
css,
|
|
2094
|
+
ssrData,
|
|
2095
|
+
headTags: themePreloadTags,
|
|
2096
|
+
discoveredRoutes: renderCtx.discoveredRoutes,
|
|
2097
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns
|
|
2098
|
+
};
|
|
2099
|
+
} finally {
|
|
2100
|
+
clearGlobalSSRTimeout();
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
function attemptZeroDiscovery(url, module, options, ssrTimeout) {
|
|
2105
|
+
const manifest = options?.manifest;
|
|
2106
|
+
if (!manifest?.routeEntries || !module.api)
|
|
2107
|
+
return null;
|
|
2108
|
+
const matches = matchUrlToPatterns(url, manifest.routePatterns);
|
|
2109
|
+
if (matches.length === 0)
|
|
2110
|
+
return null;
|
|
2111
|
+
const allQueries = [];
|
|
2112
|
+
let mergedParams = {};
|
|
2113
|
+
for (const match of matches) {
|
|
2114
|
+
const entry = manifest.routeEntries[match.pattern];
|
|
2115
|
+
if (entry) {
|
|
2116
|
+
allQueries.push(...entry.queries);
|
|
2117
|
+
}
|
|
2118
|
+
mergedParams = { ...mergedParams, ...match.params };
|
|
2119
|
+
}
|
|
2120
|
+
if (allQueries.length === 0)
|
|
2121
|
+
return null;
|
|
2122
|
+
const descriptors = reconstructDescriptors(allQueries, mergedParams, module.api);
|
|
2123
|
+
if (descriptors.length === 0)
|
|
2124
|
+
return null;
|
|
2125
|
+
return prefetchFromDescriptors(descriptors, ssrTimeout);
|
|
2126
|
+
}
|
|
2127
|
+
async function prefetchFromDescriptors(descriptors, ssrTimeout) {
|
|
2128
|
+
const resolvedQueries = [];
|
|
2129
|
+
await Promise.allSettled(descriptors.map(({ key, fetch: fetchFn }) => Promise.race([
|
|
2130
|
+
fetchFn().then((result) => {
|
|
2131
|
+
const data = unwrapResult(result);
|
|
2132
|
+
resolvedQueries.push({ key, data });
|
|
2133
|
+
return "resolved";
|
|
2134
|
+
}),
|
|
2135
|
+
new Promise((r) => setTimeout(r, ssrTimeout)).then(() => "timeout")
|
|
2136
|
+
])));
|
|
2137
|
+
return { resolvedQueries };
|
|
2138
|
+
}
|
|
2139
|
+
function unwrapResult(result) {
|
|
2140
|
+
if (result && typeof result === "object" && "ok" in result && "data" in result) {
|
|
2141
|
+
const r = result;
|
|
2142
|
+
if (r.ok)
|
|
2143
|
+
return r.data;
|
|
2144
|
+
}
|
|
2145
|
+
return result;
|
|
2146
|
+
}
|
|
2147
|
+
async function renderWithPrefetchedData(module, normalizedUrl, prefetchedData, options) {
|
|
2148
|
+
const data = await prefetchedData;
|
|
2149
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
2150
|
+
const renderCtx = createRequestContext(normalizedUrl);
|
|
2151
|
+
if (options?.ssrAuth) {
|
|
2152
|
+
renderCtx.ssrAuth = options.ssrAuth;
|
|
2153
|
+
}
|
|
2154
|
+
for (const { key, data: queryData } of data.resolvedQueries) {
|
|
2155
|
+
renderCtx.queryCache.set(key, queryData);
|
|
2156
|
+
}
|
|
2157
|
+
renderCtx.resolvedComponents = new Map;
|
|
2158
|
+
return ssrStorage.run(renderCtx, async () => {
|
|
2159
|
+
try {
|
|
2160
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
2161
|
+
const createApp = resolveAppFactory2(module);
|
|
2162
|
+
let themeCss = "";
|
|
2163
|
+
let themePreloadTags = "";
|
|
2164
|
+
if (module.theme) {
|
|
2165
|
+
try {
|
|
2166
|
+
const compiled = compileTheme2(module.theme, {
|
|
2167
|
+
fallbackMetrics: options?.fallbackMetrics
|
|
2168
|
+
});
|
|
2169
|
+
themeCss = compiled.css;
|
|
2170
|
+
themePreloadTags = compiled.preloadTags;
|
|
2171
|
+
} catch (e) {
|
|
2172
|
+
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const app = createApp();
|
|
2176
|
+
const vnode = toVNode(app);
|
|
2177
|
+
const stream = renderToStream(vnode);
|
|
2178
|
+
const html = await streamToString(stream);
|
|
2179
|
+
if (renderCtx.ssrRedirect) {
|
|
2180
|
+
return {
|
|
2181
|
+
html: "",
|
|
2182
|
+
css: "",
|
|
2183
|
+
ssrData: [],
|
|
2184
|
+
headTags: "",
|
|
2185
|
+
redirect: renderCtx.ssrRedirect,
|
|
2186
|
+
discoveredRoutes: renderCtx.discoveredRoutes,
|
|
2187
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
const css = collectCSS2(themeCss, module);
|
|
2191
|
+
const ssrData = data.resolvedQueries.map(({ key, data: d }) => ({
|
|
2192
|
+
key,
|
|
2193
|
+
data: JSON.parse(JSON.stringify(d))
|
|
2194
|
+
}));
|
|
2195
|
+
return {
|
|
2196
|
+
html,
|
|
2197
|
+
css,
|
|
2198
|
+
ssrData,
|
|
2199
|
+
headTags: themePreloadTags,
|
|
2200
|
+
discoveredRoutes: renderCtx.discoveredRoutes,
|
|
2201
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns
|
|
2202
|
+
};
|
|
2203
|
+
} finally {
|
|
2204
|
+
clearGlobalSSRTimeout();
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
var domShimInstalled2 = false;
|
|
2209
|
+
function ensureDomShim2() {
|
|
2210
|
+
if (domShimInstalled2 && typeof document !== "undefined")
|
|
2211
|
+
return;
|
|
2212
|
+
domShimInstalled2 = true;
|
|
2213
|
+
installDomShim();
|
|
2214
|
+
}
|
|
2215
|
+
function resolveAppFactory2(module) {
|
|
2216
|
+
const createApp = module.default || module.App;
|
|
2217
|
+
if (typeof createApp !== "function") {
|
|
2218
|
+
throw new Error("App entry must export a default function or named App function");
|
|
2219
|
+
}
|
|
2220
|
+
return createApp;
|
|
2221
|
+
}
|
|
2222
|
+
function filterByEntityAccess(queries, entityAccess, session) {
|
|
2223
|
+
if (!entityAccess || !session)
|
|
2224
|
+
return queries;
|
|
2225
|
+
return queries.filter(({ key }) => {
|
|
2226
|
+
const entity = extractEntityFromKey(key);
|
|
2227
|
+
const method = extractMethodFromKey(key);
|
|
2228
|
+
if (!entity)
|
|
2229
|
+
return true;
|
|
2230
|
+
const entityRules = entityAccess[entity];
|
|
2231
|
+
if (!entityRules)
|
|
2232
|
+
return true;
|
|
2233
|
+
const rule = entityRules[method];
|
|
2234
|
+
if (!rule)
|
|
2235
|
+
return true;
|
|
2236
|
+
return evaluateAccessRule(rule, session);
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
function extractEntityFromKey(key) {
|
|
2240
|
+
const pathStart = key.indexOf(":/");
|
|
2241
|
+
if (pathStart === -1)
|
|
2242
|
+
return;
|
|
2243
|
+
const path = key.slice(pathStart + 2);
|
|
2244
|
+
const firstSlash = path.indexOf("/");
|
|
2245
|
+
const questionMark = path.indexOf("?");
|
|
2246
|
+
if (firstSlash === -1 && questionMark === -1)
|
|
2247
|
+
return path;
|
|
2248
|
+
if (firstSlash === -1)
|
|
2249
|
+
return path.slice(0, questionMark);
|
|
2250
|
+
if (questionMark === -1)
|
|
2251
|
+
return path.slice(0, firstSlash);
|
|
2252
|
+
return path.slice(0, Math.min(firstSlash, questionMark));
|
|
2253
|
+
}
|
|
2254
|
+
function extractMethodFromKey(key) {
|
|
2255
|
+
const pathStart = key.indexOf(":/");
|
|
2256
|
+
if (pathStart === -1)
|
|
2257
|
+
return "list";
|
|
2258
|
+
const path = key.slice(pathStart + 2);
|
|
2259
|
+
const cleanPath = path.split("?")[0] ?? "";
|
|
2260
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
2261
|
+
return segments.length > 1 ? "get" : "list";
|
|
2262
|
+
}
|
|
2263
|
+
function collectCSS2(themeCss, module) {
|
|
2264
|
+
const alreadyIncluded = new Set;
|
|
2265
|
+
if (themeCss)
|
|
2266
|
+
alreadyIncluded.add(themeCss);
|
|
2267
|
+
if (module.styles) {
|
|
2268
|
+
for (const s of module.styles)
|
|
2269
|
+
alreadyIncluded.add(s);
|
|
2270
|
+
}
|
|
2271
|
+
const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
|
|
2272
|
+
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
2273
|
+
const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
|
|
2274
|
+
`)}</style>` : "";
|
|
2275
|
+
const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
|
|
2276
|
+
`)}</style>` : "";
|
|
2277
|
+
return [themeTag, globalTag, componentTag].filter(Boolean).join(`
|
|
2278
|
+
`);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1624
2281
|
// src/upstream-watcher.ts
|
|
1625
2282
|
import { existsSync, lstatSync, readdirSync, realpathSync, watch } from "fs";
|
|
1626
2283
|
import { join as join3 } from "path";
|
|
@@ -1981,11 +2638,13 @@ function generateSSRPageHtml({
|
|
|
1981
2638
|
scriptTag,
|
|
1982
2639
|
editor = "vscode",
|
|
1983
2640
|
headTags = "",
|
|
1984
|
-
sessionScript = ""
|
|
2641
|
+
sessionScript = "",
|
|
2642
|
+
htmlDataTheme
|
|
1985
2643
|
}) {
|
|
1986
2644
|
const ssrDataScript = ssrData.length > 0 ? `<script>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>` : "";
|
|
2645
|
+
const htmlAttrs = htmlDataTheme ? ` data-theme="${htmlDataTheme}"` : "";
|
|
1987
2646
|
return `<!doctype html>
|
|
1988
|
-
<html lang="en">
|
|
2647
|
+
<html lang="en"${htmlAttrs}>
|
|
1989
2648
|
<head>
|
|
1990
2649
|
<meta charset="UTF-8" />
|
|
1991
2650
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -2082,7 +2741,9 @@ function createBunDevServer(options) {
|
|
|
2082
2741
|
editor: editorOption,
|
|
2083
2742
|
headTags: headTagsOption = "",
|
|
2084
2743
|
sessionResolver,
|
|
2085
|
-
watchDeps
|
|
2744
|
+
watchDeps,
|
|
2745
|
+
themeFromRequest,
|
|
2746
|
+
onRestartNeeded
|
|
2086
2747
|
} = options;
|
|
2087
2748
|
const faviconTag = detectFaviconTag(projectRoot);
|
|
2088
2749
|
const headTags = [faviconTag, headTagsOption].filter(Boolean).join(`
|
|
@@ -2099,6 +2760,7 @@ function createBunDevServer(options) {
|
|
|
2099
2760
|
let srcWatcherRef = null;
|
|
2100
2761
|
let refreshTimeout = null;
|
|
2101
2762
|
let stopped = false;
|
|
2763
|
+
let ssrFallback = false;
|
|
2102
2764
|
const wsClients = new Set;
|
|
2103
2765
|
let currentError = null;
|
|
2104
2766
|
const sourceMapResolver = createSourceMapResolver(projectRoot);
|
|
@@ -2412,8 +3074,8 @@ function createBunDevServer(options) {
|
|
|
2412
3074
|
}
|
|
2413
3075
|
if (!pluginsRegistered) {
|
|
2414
3076
|
const { plugin: serverPlugin, updateManifest } = createVertzBunPlugin({
|
|
2415
|
-
hmr:
|
|
2416
|
-
fastRefresh:
|
|
3077
|
+
hmr: true,
|
|
3078
|
+
fastRefresh: true,
|
|
2417
3079
|
logger,
|
|
2418
3080
|
diagnostics
|
|
2419
3081
|
});
|
|
@@ -2424,16 +3086,34 @@ function createBunDevServer(options) {
|
|
|
2424
3086
|
const updateServerManifest = stableUpdateManifest;
|
|
2425
3087
|
let ssrMod;
|
|
2426
3088
|
try {
|
|
2427
|
-
|
|
3089
|
+
if (isRestarting) {
|
|
3090
|
+
mkdirSync(devDir, { recursive: true });
|
|
3091
|
+
const ssrBootPath = resolve(devDir, "ssr-reload-entry.ts");
|
|
3092
|
+
const ts = Date.now();
|
|
3093
|
+
writeFileSync2(ssrBootPath, `export * from '${entryPath}';
|
|
3094
|
+
`);
|
|
3095
|
+
ssrMod = await import(`${ssrBootPath}?t=${ts}`);
|
|
3096
|
+
} else {
|
|
3097
|
+
ssrMod = await import(entryPath);
|
|
3098
|
+
}
|
|
3099
|
+
ssrFallback = false;
|
|
2428
3100
|
if (logRequests) {
|
|
2429
3101
|
console.log("[Server] SSR module loaded");
|
|
2430
3102
|
}
|
|
2431
3103
|
} catch (e) {
|
|
2432
3104
|
console.error("[Server] Failed to load SSR module:", e);
|
|
2433
3105
|
if (isRestarting) {
|
|
2434
|
-
|
|
3106
|
+
ssrFallback = true;
|
|
3107
|
+
ssrMod = {};
|
|
3108
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
3109
|
+
const errStack = e instanceof Error ? e.stack : undefined;
|
|
3110
|
+
const { message: _, ...loc } = errStack ? parseSourceFromStack(errStack) : { message: "" };
|
|
3111
|
+
queueMicrotask(() => {
|
|
3112
|
+
broadcastError("ssr", [{ message: errMsg, ...loc, stack: errStack }]);
|
|
3113
|
+
});
|
|
3114
|
+
} else {
|
|
3115
|
+
process.exit(1);
|
|
2435
3116
|
}
|
|
2436
|
-
process.exit(1);
|
|
2437
3117
|
}
|
|
2438
3118
|
let fontFallbackMetrics;
|
|
2439
3119
|
if (ssrMod.theme?.fonts) {
|
|
@@ -2443,6 +3123,53 @@ function createBunDevServer(options) {
|
|
|
2443
3123
|
console.warn("[Server] Failed to extract font metrics:", e);
|
|
2444
3124
|
}
|
|
2445
3125
|
}
|
|
3126
|
+
let prefetchManager = null;
|
|
3127
|
+
const srcDir = resolve(projectRoot, "src");
|
|
3128
|
+
const routerCandidates = [resolve(srcDir, "router.tsx"), resolve(srcDir, "router.ts")];
|
|
3129
|
+
const routerPath = routerCandidates.find((p) => existsSync2(p));
|
|
3130
|
+
if (routerPath) {
|
|
3131
|
+
prefetchManager = createPrefetchManifestManager({
|
|
3132
|
+
routerPath,
|
|
3133
|
+
readFile: (path) => {
|
|
3134
|
+
try {
|
|
3135
|
+
return readFileSync2(path, "utf-8");
|
|
3136
|
+
} catch {
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
},
|
|
3140
|
+
resolveImport: (specifier, fromFile) => {
|
|
3141
|
+
if (!specifier.startsWith("."))
|
|
3142
|
+
return;
|
|
3143
|
+
const dir = dirname(fromFile);
|
|
3144
|
+
const base = resolve(dir, specifier);
|
|
3145
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
3146
|
+
const candidate = `${base}${ext}`;
|
|
3147
|
+
if (existsSync2(candidate))
|
|
3148
|
+
return candidate;
|
|
3149
|
+
}
|
|
3150
|
+
for (const ext of [".tsx", ".ts"]) {
|
|
3151
|
+
const candidate = resolve(base, `index${ext}`);
|
|
3152
|
+
if (existsSync2(candidate))
|
|
3153
|
+
return candidate;
|
|
3154
|
+
}
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
3158
|
+
try {
|
|
3159
|
+
const buildStart = performance.now();
|
|
3160
|
+
prefetchManager.build();
|
|
3161
|
+
const buildMs = Math.round(performance.now() - buildStart);
|
|
3162
|
+
logger.log("prefetch", "initial-build", { routerPath, durationMs: buildMs });
|
|
3163
|
+
if (logRequests) {
|
|
3164
|
+
const manifest = prefetchManager.getSSRManifest();
|
|
3165
|
+
const routeCount = manifest?.routePatterns.length ?? 0;
|
|
3166
|
+
console.log(`[Server] Prefetch manifest built (${routeCount} routes, ${buildMs}ms)`);
|
|
3167
|
+
}
|
|
3168
|
+
} catch (e) {
|
|
3169
|
+
console.warn("[Server] Failed to build prefetch manifest:", e instanceof Error ? e.message : e);
|
|
3170
|
+
prefetchManager = null;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
2446
3173
|
mkdirSync(devDir, { recursive: true });
|
|
2447
3174
|
const frInitPath = resolve(devDir, "fast-refresh-init.ts");
|
|
2448
3175
|
writeFileSync2(frInitPath, `import '@vertz/ui-server/fast-refresh-runtime';
|
|
@@ -2462,6 +3189,12 @@ if (import.meta.hot) import.meta.hot.accept();
|
|
|
2462
3189
|
setupOpenAPIWatcher();
|
|
2463
3190
|
let bundledScriptUrl = null;
|
|
2464
3191
|
let hmrBootstrapScript = null;
|
|
3192
|
+
const readyGate = createReadyGate({
|
|
3193
|
+
timeoutMs: 5000,
|
|
3194
|
+
onTimeoutWarning: () => {
|
|
3195
|
+
console.warn("[Server] HMR asset discovery timed out \u2014 unblocking clients");
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
2465
3198
|
const routes = {
|
|
2466
3199
|
"/__vertz_hmr": hmrShellModule
|
|
2467
3200
|
};
|
|
@@ -2527,6 +3260,12 @@ if (import.meta.hot) import.meta.hot.accept();
|
|
|
2527
3260
|
if (pathname === "/__vertz_diagnostics") {
|
|
2528
3261
|
return Response.json(diagnostics.getSnapshot());
|
|
2529
3262
|
}
|
|
3263
|
+
if (pathname === "/__vertz_prefetch_manifest") {
|
|
3264
|
+
if (!prefetchManager) {
|
|
3265
|
+
return Response.json({ error: "No prefetch manifest available (router file not found)" }, { status: 404 });
|
|
3266
|
+
}
|
|
3267
|
+
return Response.json(prefetchManager.getSnapshot());
|
|
3268
|
+
}
|
|
2530
3269
|
if (pathname === "/_vertz/image") {
|
|
2531
3270
|
return handleDevImageProxy(request);
|
|
2532
3271
|
}
|
|
@@ -2605,6 +3344,7 @@ data: {}
|
|
|
2605
3344
|
if (logRequests) {
|
|
2606
3345
|
console.log(`[Server] SSR: ${pathname}`);
|
|
2607
3346
|
}
|
|
3347
|
+
const ssrTheme = themeFromRequest?.(request) ?? undefined;
|
|
2608
3348
|
try {
|
|
2609
3349
|
const interceptor = apiHandler ? createFetchInterceptor({
|
|
2610
3350
|
apiHandler,
|
|
@@ -2652,10 +3392,12 @@ data: {}
|
|
|
2652
3392
|
const doRender = async () => {
|
|
2653
3393
|
logger.log("ssr", "render-start", { url: pathname });
|
|
2654
3394
|
const ssrStart = performance.now();
|
|
2655
|
-
const result = await
|
|
3395
|
+
const result = await ssrRenderSinglePass(ssrMod, pathname + url.search, {
|
|
2656
3396
|
ssrTimeout: 300,
|
|
2657
3397
|
fallbackMetrics: fontFallbackMetrics,
|
|
2658
|
-
ssrAuth
|
|
3398
|
+
ssrAuth,
|
|
3399
|
+
manifest: prefetchManager?.getSSRManifest(),
|
|
3400
|
+
prefetchSession: toPrefetchSession(ssrAuth)
|
|
2659
3401
|
});
|
|
2660
3402
|
logger.log("ssr", "render-done", {
|
|
2661
3403
|
url: pathname,
|
|
@@ -2668,19 +3410,22 @@ data: {}
|
|
|
2668
3410
|
headers: { Location: result.redirect.to }
|
|
2669
3411
|
});
|
|
2670
3412
|
}
|
|
3413
|
+
const bodyHtml = ssrTheme ? result.html.replace(/data-theme="[^"]*"/, `data-theme="${ssrTheme}"`) : result.html;
|
|
2671
3414
|
const scriptTag = buildScriptTag(bundledScriptUrl, hmrBootstrapScript, clientSrc);
|
|
2672
3415
|
const combinedHeadTags = [headTags, result.headTags].filter(Boolean).join(`
|
|
2673
3416
|
`);
|
|
2674
3417
|
const html = generateSSRPageHtml({
|
|
2675
3418
|
title,
|
|
2676
3419
|
css: result.css,
|
|
2677
|
-
bodyHtml
|
|
3420
|
+
bodyHtml,
|
|
2678
3421
|
ssrData: result.ssrData,
|
|
2679
3422
|
scriptTag,
|
|
2680
3423
|
editor,
|
|
2681
3424
|
headTags: combinedHeadTags,
|
|
2682
|
-
sessionScript
|
|
3425
|
+
sessionScript,
|
|
3426
|
+
htmlDataTheme: ssrTheme
|
|
2683
3427
|
});
|
|
3428
|
+
clearError();
|
|
2684
3429
|
return new Response(html, {
|
|
2685
3430
|
status: 200,
|
|
2686
3431
|
headers: {
|
|
@@ -2704,7 +3449,8 @@ data: {}
|
|
|
2704
3449
|
ssrData: [],
|
|
2705
3450
|
scriptTag,
|
|
2706
3451
|
editor,
|
|
2707
|
-
headTags
|
|
3452
|
+
headTags,
|
|
3453
|
+
htmlDataTheme: ssrTheme
|
|
2708
3454
|
});
|
|
2709
3455
|
return new Response(fallbackHtml, {
|
|
2710
3456
|
status: 200,
|
|
@@ -2720,13 +3466,15 @@ data: {}
|
|
|
2720
3466
|
wsClients.add(ws);
|
|
2721
3467
|
diagnostics.recordWebSocketChange(wsClients.size);
|
|
2722
3468
|
logger.log("ws", "client-connected", { total: wsClients.size });
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3469
|
+
if (!readyGate.onOpen(ws)) {
|
|
3470
|
+
ws.sendText(JSON.stringify({ type: "connected" }));
|
|
3471
|
+
if (currentError) {
|
|
3472
|
+
ws.sendText(JSON.stringify({
|
|
3473
|
+
type: "error",
|
|
3474
|
+
category: currentError.category,
|
|
3475
|
+
errors: currentError.errors
|
|
3476
|
+
}));
|
|
3477
|
+
}
|
|
2730
3478
|
}
|
|
2731
3479
|
},
|
|
2732
3480
|
message(ws, msg) {
|
|
@@ -2818,6 +3566,7 @@ data: {}
|
|
|
2818
3566
|
},
|
|
2819
3567
|
close(ws) {
|
|
2820
3568
|
wsClients.delete(ws);
|
|
3569
|
+
readyGate.onClose(ws);
|
|
2821
3570
|
diagnostics.recordWebSocketChange(wsClients.size);
|
|
2822
3571
|
}
|
|
2823
3572
|
},
|
|
@@ -2829,7 +3578,13 @@ data: {}
|
|
|
2829
3578
|
if (logRequests) {
|
|
2830
3579
|
console.log(`[Server] SSR+HMR dev server running at http://${host}:${server.port}`);
|
|
2831
3580
|
}
|
|
2832
|
-
|
|
3581
|
+
try {
|
|
3582
|
+
await discoverHMRAssets();
|
|
3583
|
+
} finally {
|
|
3584
|
+
if (!readyGate.isReady) {
|
|
3585
|
+
readyGate.open(currentError);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
2833
3588
|
async function discoverHMRAssets() {
|
|
2834
3589
|
try {
|
|
2835
3590
|
const res = await fetch(`http://${host}:${server?.port}/__vertz_hmr`);
|
|
@@ -2852,7 +3607,6 @@ data: {}
|
|
|
2852
3607
|
console.warn("[Server] Could not discover HMR bundled URL:", e);
|
|
2853
3608
|
}
|
|
2854
3609
|
}
|
|
2855
|
-
const srcDir = resolve(projectRoot, "src");
|
|
2856
3610
|
stopped = false;
|
|
2857
3611
|
if (existsSync2(srcDir)) {
|
|
2858
3612
|
srcWatcherRef = watch2(srcDir, { recursive: true }, (_event, filename) => {
|
|
@@ -2923,10 +3677,33 @@ data: {}
|
|
|
2923
3677
|
const { changed } = updateServerManifest(changedFilePath, source);
|
|
2924
3678
|
const manifestDurationMs = Math.round(performance.now() - manifestStartMs);
|
|
2925
3679
|
diagnostics.recordManifestUpdate(lastChangedFile, changed, manifestDurationMs);
|
|
3680
|
+
if (prefetchManager) {
|
|
3681
|
+
const prefetchStart = performance.now();
|
|
3682
|
+
prefetchManager.onFileChange(changedFilePath, source);
|
|
3683
|
+
const prefetchMs = Math.round(performance.now() - prefetchStart);
|
|
3684
|
+
logger.log("prefetch", "rebuild", {
|
|
3685
|
+
file: lastChangedFile,
|
|
3686
|
+
durationMs: prefetchMs,
|
|
3687
|
+
isRouter: changedFilePath === routerPath
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
2926
3690
|
} catch {}
|
|
2927
3691
|
}
|
|
2928
3692
|
if (stopped)
|
|
2929
3693
|
return;
|
|
3694
|
+
if (ssrFallback) {
|
|
3695
|
+
if (onRestartNeeded) {
|
|
3696
|
+
if (logRequests) {
|
|
3697
|
+
console.log("[Server] SSR in fallback mode \u2014 requesting process restart");
|
|
3698
|
+
}
|
|
3699
|
+
await devServer.stop();
|
|
3700
|
+
onRestartNeeded();
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
if (logRequests) {
|
|
3704
|
+
console.log("[Server] SSR in fallback mode \u2014 attempting re-import (best effort)");
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
2930
3707
|
const cacheCleared = clearSSRRequireCache();
|
|
2931
3708
|
logger.log("watcher", "cache-cleared", { entries: cacheCleared });
|
|
2932
3709
|
const ssrWrapperPath = resolve(devDir, "ssr-reload-entry.ts");
|
|
@@ -2937,6 +3714,7 @@ data: {}
|
|
|
2937
3714
|
try {
|
|
2938
3715
|
const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
|
|
2939
3716
|
ssrMod = freshMod;
|
|
3717
|
+
ssrFallback = false;
|
|
2940
3718
|
if (freshMod.theme?.fonts) {
|
|
2941
3719
|
try {
|
|
2942
3720
|
fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
|
|
@@ -2962,6 +3740,7 @@ data: {}
|
|
|
2962
3740
|
try {
|
|
2963
3741
|
const freshMod = await import(`${ssrWrapperPath}?t=${Date.now()}`);
|
|
2964
3742
|
ssrMod = freshMod;
|
|
3743
|
+
ssrFallback = false;
|
|
2965
3744
|
if (freshMod.theme?.fonts) {
|
|
2966
3745
|
try {
|
|
2967
3746
|
fontFallbackMetrics = await extractFontMetrics(freshMod.theme.fonts, projectRoot);
|
|
@@ -2982,6 +3761,7 @@ data: {}
|
|
|
2982
3761
|
logger.log("watcher", "ssr-reload", { status: "failed", error: errMsg });
|
|
2983
3762
|
const { message: _m, ...loc2 } = errStack ? parseSourceFromStack(errStack) : { message: "" };
|
|
2984
3763
|
broadcastError("ssr", [{ message: errMsg, ...loc2, stack: errStack }]);
|
|
3764
|
+
ssrFallback = true;
|
|
2985
3765
|
}
|
|
2986
3766
|
}
|
|
2987
3767
|
}, 100);
|
|
@@ -3048,6 +3828,7 @@ data: {}
|
|
|
3048
3828
|
lastBroadcastedError = "";
|
|
3049
3829
|
lastChangedFile = "";
|
|
3050
3830
|
clearGraceUntil = 0;
|
|
3831
|
+
ssrFallback = false;
|
|
3051
3832
|
terminalDedup.reset();
|
|
3052
3833
|
clearSSRRequireCache();
|
|
3053
3834
|
sourceMapResolver.invalidate();
|