@vertz/cli 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ import { Command } from "commander";
19
19
  import { createJiti } from "jiti";
20
20
 
21
21
  // src/commands/build.ts
22
+ import { readFileSync as readFileSync2 } from "node:fs";
23
+ import { resolve as resolve3 } from "node:path";
22
24
  import { err, ok } from "@vertz/errors";
23
25
 
24
26
  // src/dev-server/app-detector.ts
@@ -66,10 +68,7 @@ import * as esbuild from "esbuild";
66
68
 
67
69
  // src/pipeline/orchestrator.ts
68
70
  import { createCodegenPipeline, generate } from "@vertz/codegen";
69
- import {
70
- createCompiler,
71
- OpenAPIGenerator
72
- } from "@vertz/compiler";
71
+ import { createCompiler, OpenAPIGenerator } from "@vertz/compiler";
73
72
  var defaultPipelineConfig = {
74
73
  sourceDir: "src",
75
74
  outputDir: ".vertz/generated",
@@ -143,6 +142,13 @@ class PipelineOrchestrator {
143
142
  success = false;
144
143
  }
145
144
  }
145
+ if (success) {
146
+ const buildUIResult = await this.runBuildUI();
147
+ stages.push(buildUIResult);
148
+ if (!buildUIResult.success) {
149
+ success = false;
150
+ }
151
+ }
146
152
  } catch (error) {
147
153
  success = false;
148
154
  stages.push({
@@ -284,12 +290,36 @@ class PipelineOrchestrator {
284
290
  }
285
291
  async runBuildUI() {
286
292
  const startTime = performance.now();
287
- return {
288
- stage: "build-ui",
289
- success: true,
290
- durationMs: performance.now() - startTime,
291
- output: "UI build delegated to Vite"
292
- };
293
+ try {
294
+ if (this.config._uiCompilerValidator) {
295
+ const result = await this.config._uiCompilerValidator();
296
+ return {
297
+ stage: "build-ui",
298
+ success: true,
299
+ durationMs: performance.now() - startTime,
300
+ output: `UI compiler validated (${result.fileCount} source files)`
301
+ };
302
+ }
303
+ const { createVertzBunPlugin } = await import("@vertz/ui-server/bun-plugin");
304
+ const pluginOptions = {
305
+ hmr: false,
306
+ fastRefresh: false
307
+ };
308
+ createVertzBunPlugin(pluginOptions);
309
+ return {
310
+ stage: "build-ui",
311
+ success: true,
312
+ durationMs: performance.now() - startTime,
313
+ output: "UI compiler validated"
314
+ };
315
+ } catch (error) {
316
+ return {
317
+ stage: "build-ui",
318
+ success: false,
319
+ durationMs: performance.now() - startTime,
320
+ error: error instanceof Error ? error : new Error(String(error))
321
+ };
322
+ }
293
323
  }
294
324
  async runDbSync() {
295
325
  const startTime = performance.now();
@@ -815,11 +845,29 @@ class BuildOrchestrator {
815
845
  }
816
846
  }
817
847
  // src/production-build/ui-build-pipeline.ts
818
- import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
819
- import { resolve } from "node:path";
848
+ import {
849
+ cpSync,
850
+ existsSync as existsSync3,
851
+ mkdirSync as mkdirSync2,
852
+ readdirSync as readdirSync2,
853
+ readFileSync,
854
+ rmSync,
855
+ writeFileSync as writeFileSync2
856
+ } from "node:fs";
857
+ import { dirname, join as join3, resolve } from "node:path";
858
+ import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
820
859
  async function buildUI(config) {
821
860
  const startTime = performance.now();
822
- const { projectRoot, clientEntry, serverEntry, outputDir, minify, sourcemap, title = "Vertz App" } = config;
861
+ const {
862
+ projectRoot,
863
+ clientEntry,
864
+ serverEntry,
865
+ outputDir,
866
+ minify,
867
+ sourcemap,
868
+ title = "Vertz App",
869
+ description
870
+ } = config;
823
871
  const distDir = resolve(projectRoot, outputDir);
824
872
  const distClient = resolve(distDir, "client");
825
873
  const distServer = resolve(distDir, "server");
@@ -831,7 +879,8 @@ async function buildUI(config) {
831
879
  console.log("\uD83D\uDCE6 Building client...");
832
880
  const { plugin: clientPlugin, fileExtractions } = createVertzBunPlugin({
833
881
  hmr: false,
834
- fastRefresh: false
882
+ fastRefresh: false,
883
+ routeSplitting: true
835
884
  });
836
885
  const clientResult = await Bun.build({
837
886
  entrypoints: [clientEntry],
@@ -855,15 +904,21 @@ ${errors}`,
855
904
  }
856
905
  let clientJsPath = "";
857
906
  const clientCssPaths = [];
907
+ const chunkPaths = [];
858
908
  for (const output of clientResult.outputs) {
859
909
  const name = output.path.replace(distClient, "");
860
910
  if (output.kind === "entry-point") {
861
911
  clientJsPath = name;
862
912
  } else if (output.path.endsWith(".css")) {
863
913
  clientCssPaths.push(name);
914
+ } else if (output.kind === "chunk" && output.path.endsWith(".js")) {
915
+ chunkPaths.push(name);
864
916
  }
865
917
  }
866
918
  console.log(` JS entry: ${clientJsPath}`);
919
+ for (const chunk of chunkPaths) {
920
+ console.log(` JS chunk: ${chunk}`);
921
+ }
867
922
  for (const css of clientCssPaths) {
868
923
  console.log(` CSS: ${css}`);
869
924
  }
@@ -883,25 +938,44 @@ ${errors}`,
883
938
  console.log("\uD83D\uDCC4 Generating HTML...");
884
939
  const cssLinks = clientCssPaths.map((path) => ` <link rel="stylesheet" href="${path}">`).join(`
885
940
  `);
941
+ const modulepreloadLinks = chunkPaths.map((path) => ` <link rel="modulepreload" href="${path}">`).join(`
942
+ `);
943
+ const descriptionTag = description ? `
944
+ <meta name="description" content="${description.replace(/"/g, "&quot;")}" />` : "";
945
+ const publicDir = resolve(projectRoot, "public");
946
+ const hasFavicon = existsSync3(resolve(publicDir, "favicon.svg"));
947
+ const hasManifest = existsSync3(resolve(publicDir, "site.webmanifest"));
948
+ const faviconTag = hasFavicon ? `
949
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">` : "";
950
+ const manifestTag = hasManifest ? `
951
+ <link rel="manifest" href="/site.webmanifest">` : "";
952
+ const themeColorTag = `
953
+ <meta name="theme-color" content="#0a0a0b">`;
886
954
  const html = `<!doctype html>
887
955
  <html lang="en">
888
956
  <head>
889
957
  <meta charset="UTF-8" />
890
958
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
891
- <title>${title}</title>
959
+ <title>${title}</title>${descriptionTag}${themeColorTag}${faviconTag}${manifestTag}
892
960
  ${cssLinks}
961
+ ${modulepreloadLinks}
893
962
  </head>
894
963
  <body>
895
964
  <div id="app"></div>
896
965
  <script type="module" crossorigin src="${clientJsPath}"></script>
897
966
  </body>
898
967
  </html>`;
899
- writeFileSync2(resolve(distClient, "index.html"), html);
900
- const publicDir = resolve(projectRoot, "public");
968
+ writeFileSync2(resolve(distClient, "_shell.html"), html);
901
969
  if (existsSync3(publicDir)) {
902
970
  cpSync(publicDir, distClient, { recursive: true });
903
971
  console.log(" Copied public/ assets");
904
972
  }
973
+ const imagesDir = resolve(projectRoot, ".vertz", "images");
974
+ if (existsSync3(imagesDir)) {
975
+ const imgDest = resolve(distClient, "__vertz_img");
976
+ cpSync(imagesDir, imgDest, { recursive: true });
977
+ console.log(" Copied optimized images");
978
+ }
905
979
  console.log("\uD83D\uDCE6 Building server...");
906
980
  const jsxSwapPlugin = {
907
981
  name: "vertz-ssr-jsx-swap",
@@ -938,6 +1012,67 @@ ${errors}`,
938
1012
  };
939
1013
  }
940
1014
  console.log(" Server entry: dist/server/app.js");
1015
+ console.log("\uD83D\uDCC4 Pre-rendering routes...");
1016
+ const {
1017
+ discoverRoutes,
1018
+ filterPrerenderableRoutes,
1019
+ prerenderRoutes,
1020
+ stripScriptsFromStaticHTML
1021
+ } = await import("@vertz/ui-server/ssr");
1022
+ const ssrEntryPath = resolve(distServer, "app.js");
1023
+ let ssrModule;
1024
+ try {
1025
+ ssrModule = await import(ssrEntryPath);
1026
+ } catch (error) {
1027
+ console.log(" ⚠ Could not import SSR module for pre-rendering, skipping.");
1028
+ console.log(` ${error instanceof Error ? error.message : String(error)}`);
1029
+ const durationMs2 = performance.now() - startTime;
1030
+ console.log(`
1031
+ ✅ UI build complete (without pre-rendering)!`);
1032
+ console.log(` Client: ${distClient}/`);
1033
+ console.log(` Server: ${distServer}/`);
1034
+ return { success: true, durationMs: durationMs2 };
1035
+ }
1036
+ let allPatterns;
1037
+ try {
1038
+ allPatterns = await discoverRoutes(ssrModule);
1039
+ } catch (error) {
1040
+ console.log(" ⚠ Route discovery failed, skipping pre-rendering.");
1041
+ console.log(` ${error instanceof Error ? error.message : String(error)}`);
1042
+ const durationMs2 = performance.now() - startTime;
1043
+ console.log(`
1044
+ ✅ UI build complete (without pre-rendering)!`);
1045
+ console.log(` Client: ${distClient}/`);
1046
+ console.log(` Server: ${distServer}/`);
1047
+ return { success: true, durationMs: durationMs2 };
1048
+ }
1049
+ if (allPatterns.length === 0) {
1050
+ console.log(" No routes discovered (app may not use createRouter).");
1051
+ } else {
1052
+ console.log(` Discovered ${allPatterns.length} route(s): ${allPatterns.join(", ")}`);
1053
+ const prerenderableRoutes = filterPrerenderableRoutes(allPatterns);
1054
+ console.log(` Pre-rendering ${prerenderableRoutes.length} static route(s)...`);
1055
+ if (prerenderableRoutes.length > 0) {
1056
+ const results = await prerenderRoutes(ssrModule, html, {
1057
+ routes: prerenderableRoutes
1058
+ });
1059
+ const isIslandsMode = results.some((r) => r.html.includes("data-v-island"));
1060
+ for (const result of results) {
1061
+ const outPath = result.path === "/" ? resolve(distClient, "index.html") : resolve(distClient, `${result.path.replace(/^\//, "")}/index.html`);
1062
+ mkdirSync2(dirname(outPath), { recursive: true });
1063
+ const finalHtml = isIslandsMode ? stripScriptsFromStaticHTML(result.html) : result.html;
1064
+ const stripped = finalHtml !== result.html;
1065
+ writeFileSync2(outPath, finalHtml);
1066
+ const suffix = stripped ? " (static — JS stripped)" : "";
1067
+ console.log(` ✓ ${result.path} → ${outPath.replace(distClient, "dist/client")}${suffix}`);
1068
+ }
1069
+ }
1070
+ }
1071
+ console.log("\uD83D\uDDDC️ Pre-compressing assets with Brotli...");
1072
+ const compressedCount = brotliCompressDir(distClient);
1073
+ if (compressedCount > 0) {
1074
+ console.log(` Compressed ${compressedCount} file(s)`);
1075
+ }
941
1076
  const durationMs = performance.now() - startTime;
942
1077
  console.log(`
943
1078
  ✅ UI build complete!`);
@@ -952,19 +1087,52 @@ ${errors}`,
952
1087
  };
953
1088
  }
954
1089
  }
1090
+ var COMPRESSIBLE_EXTENSIONS = new Set([".html", ".js", ".css", ".svg", ".xml", ".txt", ".json"]);
1091
+ var MIN_COMPRESS_SIZE = 256;
1092
+ function brotliCompressDir(dir) {
1093
+ let count = 0;
1094
+ function walk(currentDir) {
1095
+ for (const entry of readdirSync2(currentDir, { withFileTypes: true })) {
1096
+ const fullPath = join3(currentDir, entry.name);
1097
+ if (entry.isDirectory()) {
1098
+ walk(fullPath);
1099
+ continue;
1100
+ }
1101
+ if (entry.name.endsWith(".br"))
1102
+ continue;
1103
+ const ext = entry.name.substring(entry.name.lastIndexOf("."));
1104
+ if (!COMPRESSIBLE_EXTENSIONS.has(ext))
1105
+ continue;
1106
+ const content = readFileSync(fullPath);
1107
+ if (content.length < MIN_COMPRESS_SIZE)
1108
+ continue;
1109
+ const compressed = brotliCompressSync(content, {
1110
+ params: {
1111
+ [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY
1112
+ }
1113
+ });
1114
+ if (compressed.length < content.length) {
1115
+ writeFileSync2(`${fullPath}.br`, compressed);
1116
+ count++;
1117
+ }
1118
+ }
1119
+ }
1120
+ walk(dir);
1121
+ return count;
1122
+ }
955
1123
  // src/utils/paths.ts
956
1124
  import { existsSync as existsSync4 } from "node:fs";
957
- import { dirname, join as join3, resolve as resolve2 } from "node:path";
1125
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
958
1126
  var ROOT_MARKERS = ["package.json", "vertz.config.ts", "vertz.config.js"];
959
1127
  function findProjectRoot(startDir) {
960
1128
  let current = resolve2(startDir ?? process.cwd());
961
1129
  while (true) {
962
1130
  for (const marker of ROOT_MARKERS) {
963
- if (existsSync4(join3(current, marker))) {
1131
+ if (existsSync4(join4(current, marker))) {
964
1132
  return current;
965
1133
  }
966
1134
  }
967
- const parent = dirname(current);
1135
+ const parent = dirname2(current);
968
1136
  if (parent === current) {
969
1137
  break;
970
1138
  }
@@ -1100,13 +1268,23 @@ async function buildUIOnly(detected, options) {
1100
1268
  console.log(` Sourcemap: ${sourcemap ? "enabled" : "disabled"}`);
1101
1269
  console.log("");
1102
1270
  }
1271
+ let title;
1272
+ let description;
1273
+ try {
1274
+ const pkgPath = resolve3(detected.projectRoot, "package.json");
1275
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1276
+ title = pkg.vertz?.title ?? pkg.title;
1277
+ description = pkg.vertz?.description ?? pkg.description;
1278
+ } catch {}
1103
1279
  const result = await buildUI({
1104
1280
  projectRoot: detected.projectRoot,
1105
1281
  clientEntry: detected.clientEntry,
1106
1282
  serverEntry,
1107
1283
  outputDir: "dist",
1108
1284
  minify: !noMinify,
1109
- sourcemap
1285
+ sourcemap,
1286
+ title,
1287
+ description
1110
1288
  });
1111
1289
  if (!result.success) {
1112
1290
  return err(new Error(result.error));
@@ -1302,13 +1480,13 @@ async function dbBaselineAction(options) {
1302
1480
  }
1303
1481
 
1304
1482
  // src/commands/dev.ts
1305
- import { join as join5 } from "node:path";
1483
+ import { join as join6 } from "node:path";
1306
1484
  import { err as err5, ok as ok5 } from "@vertz/errors";
1307
1485
 
1308
1486
  // src/dev-server/fullstack-server.ts
1309
1487
  import { spawn } from "node:child_process";
1310
1488
  import { existsSync as existsSync5 } from "node:fs";
1311
- import { join as join4, relative } from "node:path";
1489
+ import { join as join5, relative } from "node:path";
1312
1490
 
1313
1491
  // src/dev-server/process-manager.ts
1314
1492
  function createProcessManager(spawnFn) {
@@ -1345,14 +1523,14 @@ function createProcessManager(spawnFn) {
1345
1523
  return;
1346
1524
  const proc = child;
1347
1525
  child = undefined;
1348
- await new Promise((resolve3) => {
1526
+ await new Promise((resolve4) => {
1349
1527
  const timeout = setTimeout(() => {
1350
1528
  proc.kill("SIGKILL");
1351
- resolve3();
1529
+ resolve4();
1352
1530
  }, 2000);
1353
1531
  proc.on("exit", () => {
1354
1532
  clearTimeout(timeout);
1355
- resolve3();
1533
+ resolve4();
1356
1534
  });
1357
1535
  proc.kill("SIGTERM");
1358
1536
  });
@@ -1421,7 +1599,11 @@ Expected: export default createServer({ ... })`);
1421
1599
  throw new Error(`${serverEntry} default export must have a .handler function.
1422
1600
  Expected: export default createServer({ ... })`);
1423
1601
  }
1424
- return mod;
1602
+ let sessionResolver;
1603
+ if ("auth" in mod && mod.auth && typeof mod.auth.resolveSessionForSSR === "function") {
1604
+ sessionResolver = mod.auth.resolveSessionForSSR;
1605
+ }
1606
+ return { ...mod, sessionResolver };
1425
1607
  }
1426
1608
  function formatBanner(appType, port, host) {
1427
1609
  const url = `http://${host}:${port}`;
@@ -1442,19 +1624,22 @@ async function startDevServer(options) {
1442
1624
  }
1443
1625
  const { createBunDevServer } = await import("@vertz/ui-server/bun-dev-server");
1444
1626
  let apiHandler;
1627
+ let sessionResolver;
1445
1628
  if (mode.kind === "full-stack") {
1446
1629
  const serverMod = await importServerModule(mode.serverEntry);
1447
1630
  apiHandler = serverMod.handler;
1631
+ sessionResolver = serverMod.sessionResolver;
1448
1632
  }
1449
1633
  const uiEntry = `./${relative(detected.projectRoot, mode.uiEntry)}`;
1450
1634
  const clientEntry = mode.clientEntry ? `/${relative(detected.projectRoot, mode.clientEntry)}` : undefined;
1451
- const openapiPath = join4(detected.projectRoot, ".vertz/generated/openapi.json");
1635
+ const openapiPath = join5(detected.projectRoot, ".vertz/generated/openapi.json");
1452
1636
  const openapi = existsSync5(openapiPath) ? { specPath: openapiPath } : undefined;
1453
1637
  const devServer = createBunDevServer({
1454
1638
  entry: uiEntry,
1455
1639
  port,
1456
1640
  host,
1457
1641
  apiHandler,
1642
+ sessionResolver,
1458
1643
  openapi,
1459
1644
  ssrModule: mode.ssrModule,
1460
1645
  clientEntry,
@@ -1475,10 +1660,10 @@ function startApiOnlyServer(serverEntry, port) {
1475
1660
  stdio: "inherit"
1476
1661
  }));
1477
1662
  pm.start(serverEntry, { PORT: String(port) });
1478
- return new Promise((resolve3) => {
1663
+ return new Promise((resolve4) => {
1479
1664
  const shutdown = async () => {
1480
1665
  await pm.stop();
1481
- resolve3();
1666
+ resolve4();
1482
1667
  };
1483
1668
  process.on("SIGINT", shutdown);
1484
1669
  process.on("SIGTERM", shutdown);
@@ -1552,7 +1737,7 @@ async function devAction(options = {}) {
1552
1737
  }
1553
1738
  }
1554
1739
  const _watcher = createPipelineWatcher({
1555
- dir: join5(projectRoot, "src"),
1740
+ dir: join6(projectRoot, "src"),
1556
1741
  debounceMs: 100,
1557
1742
  onChange: async (changes) => {
1558
1743
  if (!isRunning)
@@ -1689,34 +1874,35 @@ function generateAction(options) {
1689
1874
  }
1690
1875
 
1691
1876
  // src/commands/start.ts
1692
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync } from "node:fs";
1693
- import { join as join6, resolve as resolve3 } from "node:path";
1877
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
1878
+ import { join as join7, resolve as resolve4 } from "node:path";
1694
1879
  import { err as err7, ok as ok7 } from "@vertz/errors";
1695
1880
  function discoverSSRModule(projectRoot) {
1696
- const serverDir = join6(projectRoot, "dist", "server");
1881
+ const serverDir = join7(projectRoot, "dist", "server");
1697
1882
  if (!existsSync6(serverDir))
1698
1883
  return;
1699
- const files = readdirSync2(serverDir).filter((f) => f.endsWith(".js"));
1884
+ const files = readdirSync3(serverDir).filter((f) => f.endsWith(".js"));
1700
1885
  if (files.length === 0)
1701
1886
  return;
1702
1887
  if (files.includes("app.js")) {
1703
- return join6(serverDir, "app.js");
1888
+ return join7(serverDir, "app.js");
1704
1889
  }
1705
1890
  const first = files[0];
1706
- return first ? join6(serverDir, first) : undefined;
1891
+ return first ? join7(serverDir, first) : undefined;
1707
1892
  }
1708
1893
  function validateBuildOutputs(projectRoot, appType) {
1709
1894
  const missing = [];
1710
1895
  if (appType === "api-only" || appType === "full-stack") {
1711
- const apiBuild = join6(projectRoot, ".vertz", "build", "index.js");
1896
+ const apiBuild = join7(projectRoot, ".vertz", "build", "index.js");
1712
1897
  if (!existsSync6(apiBuild)) {
1713
1898
  missing.push(".vertz/build/index.js");
1714
1899
  }
1715
1900
  }
1716
1901
  if (appType === "ui-only" || appType === "full-stack") {
1717
- const clientHtml = join6(projectRoot, "dist", "client", "index.html");
1718
- if (!existsSync6(clientHtml)) {
1719
- missing.push("dist/client/index.html");
1902
+ const shellHtml = join7(projectRoot, "dist", "client", "_shell.html");
1903
+ const clientHtml = join7(projectRoot, "dist", "client", "index.html");
1904
+ if (!existsSync6(shellHtml) && !existsSync6(clientHtml)) {
1905
+ missing.push("dist/client/_shell.html");
1720
1906
  }
1721
1907
  const ssrModule = discoverSSRModule(projectRoot);
1722
1908
  if (!ssrModule) {
@@ -1761,7 +1947,7 @@ async function startAction(options = {}) {
1761
1947
  }
1762
1948
  }
1763
1949
  async function startApiOnly(projectRoot, port, host, _verbose) {
1764
- const entryPath = resolve3(projectRoot, ".vertz", "build", "index.js");
1950
+ const entryPath = resolve4(projectRoot, ".vertz", "build", "index.js");
1765
1951
  let mod;
1766
1952
  try {
1767
1953
  mod = await import(entryPath);
@@ -1786,8 +1972,10 @@ async function startUIOnly(projectRoot, port, host, _verbose) {
1786
1972
  if (!ssrModulePath) {
1787
1973
  return err7(new Error('No SSR module found in dist/server/. Run "vertz build" first.'));
1788
1974
  }
1789
- const templatePath = resolve3(projectRoot, "dist", "client", "index.html");
1790
- const template = readFileSync(templatePath, "utf-8");
1975
+ const shellPath = resolve4(projectRoot, "dist", "client", "_shell.html");
1976
+ const legacyPath = resolve4(projectRoot, "dist", "client", "index.html");
1977
+ const templatePath = existsSync6(shellPath) ? shellPath : legacyPath;
1978
+ const template = readFileSync3(templatePath, "utf-8");
1791
1979
  let ssrModule;
1792
1980
  try {
1793
1981
  ssrModule = await import(ssrModulePath);
@@ -1801,16 +1989,22 @@ async function startUIOnly(projectRoot, port, host, _verbose) {
1801
1989
  template,
1802
1990
  inlineCSS
1803
1991
  });
1804
- const clientDir = resolve3(projectRoot, "dist", "client");
1992
+ const clientDir = resolve4(projectRoot, "dist", "client");
1805
1993
  const server = Bun.serve({
1806
1994
  port,
1807
1995
  hostname: host,
1808
1996
  async fetch(req) {
1809
1997
  const url = new URL(req.url);
1810
1998
  const pathname = url.pathname;
1999
+ if (req.headers.get("x-vertz-nav") === "1") {
2000
+ return ssrHandler(req);
2001
+ }
1811
2002
  const staticResponse = serveStaticFile(clientDir, pathname);
1812
2003
  if (staticResponse)
1813
2004
  return staticResponse;
2005
+ const prerenderResponse = servePrerenderHTML(clientDir, pathname);
2006
+ if (prerenderResponse)
2007
+ return prerenderResponse;
1814
2008
  return ssrHandler(req);
1815
2009
  }
1816
2010
  });
@@ -1819,7 +2013,7 @@ async function startUIOnly(projectRoot, port, host, _verbose) {
1819
2013
  return ok7(undefined);
1820
2014
  }
1821
2015
  async function startFullStack(projectRoot, port, host, _verbose) {
1822
- const apiEntryPath = resolve3(projectRoot, ".vertz", "build", "index.js");
2016
+ const apiEntryPath = resolve4(projectRoot, ".vertz", "build", "index.js");
1823
2017
  let apiMod;
1824
2018
  try {
1825
2019
  apiMod = await import(apiEntryPath);
@@ -1834,8 +2028,10 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1834
2028
  if (!ssrModulePath) {
1835
2029
  return err7(new Error('No SSR module found in dist/server/. Run "vertz build" first.'));
1836
2030
  }
1837
- const templatePath = resolve3(projectRoot, "dist", "client", "index.html");
1838
- const template = readFileSync(templatePath, "utf-8");
2031
+ const shellPath = resolve4(projectRoot, "dist", "client", "_shell.html");
2032
+ const legacyPath = resolve4(projectRoot, "dist", "client", "index.html");
2033
+ const templatePath = existsSync6(shellPath) ? shellPath : legacyPath;
2034
+ const template = readFileSync3(templatePath, "utf-8");
1839
2035
  let ssrModule;
1840
2036
  try {
1841
2037
  ssrModule = await import(ssrModulePath);
@@ -1849,7 +2045,7 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1849
2045
  template,
1850
2046
  inlineCSS
1851
2047
  });
1852
- const clientDir = resolve3(projectRoot, "dist", "client");
2048
+ const clientDir = resolve4(projectRoot, "dist", "client");
1853
2049
  const server = Bun.serve({
1854
2050
  port,
1855
2051
  hostname: host,
@@ -1859,9 +2055,15 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1859
2055
  if (pathname.startsWith("/api")) {
1860
2056
  return apiHandler(req);
1861
2057
  }
2058
+ if (req.headers.get("x-vertz-nav") === "1") {
2059
+ return ssrHandler(req);
2060
+ }
1862
2061
  const staticResponse = serveStaticFile(clientDir, pathname);
1863
2062
  if (staticResponse)
1864
2063
  return staticResponse;
2064
+ const prerenderResponse = servePrerenderHTML(clientDir, pathname);
2065
+ if (prerenderResponse)
2066
+ return prerenderResponse;
1865
2067
  return ssrHandler(req);
1866
2068
  }
1867
2069
  });
@@ -1870,25 +2072,43 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1870
2072
  return ok7(undefined);
1871
2073
  }
1872
2074
  function discoverInlineCSS(projectRoot) {
1873
- const cssDir = resolve3(projectRoot, "dist", "client", "assets");
2075
+ const cssDir = resolve4(projectRoot, "dist", "client", "assets");
1874
2076
  if (!existsSync6(cssDir))
1875
2077
  return;
1876
- const cssFiles = readdirSync2(cssDir).filter((f) => f.endsWith(".css"));
2078
+ const cssFiles = readdirSync3(cssDir).filter((f) => f.endsWith(".css"));
1877
2079
  if (cssFiles.length === 0)
1878
2080
  return;
1879
2081
  const result = {};
1880
2082
  for (const file of cssFiles) {
1881
- const content = readFileSync(join6(cssDir, file), "utf-8");
2083
+ const content = readFileSync3(join7(cssDir, file), "utf-8");
1882
2084
  result[`/assets/${file}`] = content;
1883
2085
  }
1884
2086
  return result;
1885
2087
  }
2088
+ function servePrerenderHTML(clientDir, pathname) {
2089
+ const htmlPath = pathname === "/" ? resolve4(clientDir, "index.html") : resolve4(clientDir, `${pathname.replace(/^\//, "")}/index.html`);
2090
+ if (!htmlPath.startsWith(clientDir))
2091
+ return null;
2092
+ if (htmlPath.endsWith("/_shell.html"))
2093
+ return null;
2094
+ const file = Bun.file(htmlPath);
2095
+ if (!file.size)
2096
+ return null;
2097
+ return new Response(file, {
2098
+ headers: {
2099
+ "Content-Type": "text/html; charset=utf-8",
2100
+ "Cache-Control": "public, max-age=0, must-revalidate"
2101
+ }
2102
+ });
2103
+ }
1886
2104
  function serveStaticFile(clientDir, pathname) {
1887
2105
  if (pathname === "/" || pathname === "/index.html")
1888
2106
  return null;
1889
- const filePath = resolve3(clientDir, `.${pathname}`);
2107
+ const filePath = resolve4(clientDir, `.${pathname}`);
1890
2108
  if (!filePath.startsWith(clientDir))
1891
2109
  return null;
2110
+ if (!existsSync6(filePath) || !statSync2(filePath).isFile())
2111
+ return null;
1892
2112
  const file = Bun.file(filePath);
1893
2113
  if (!file.size)
1894
2114
  return null;
@@ -1983,12 +2203,12 @@ function createCLI() {
1983
2203
  }
1984
2204
  });
1985
2205
  program.command("codegen").description("Generate SDK and CLI clients from the compiled API").option("--dry-run", "Preview generated files without writing").option("--output <dir>", "Output directory").action(async (opts) => {
1986
- const { resolve: resolve4, dirname: dirname2 } = await import("node:path");
2206
+ const { resolve: resolve5, dirname: dirname3 } = await import("node:path");
1987
2207
  const { mkdir, writeFile: fsWriteFile } = await import("node:fs/promises");
1988
2208
  let config;
1989
2209
  let compilerConfig;
1990
2210
  try {
1991
- const configPath = resolve4(process.cwd(), "vertz.config.ts");
2211
+ const configPath = resolve5(process.cwd(), "vertz.config.ts");
1992
2212
  const jiti = createJiti(import.meta.url);
1993
2213
  const configModule = await jiti.import(configPath);
1994
2214
  config = configModule.codegen ?? configModule.default?.codegen;
@@ -2013,7 +2233,7 @@ function createCLI() {
2013
2233
  const { createCodegenPipeline: createCodegenPipeline2 } = await import("@vertz/codegen");
2014
2234
  const pipeline = createCodegenPipeline2();
2015
2235
  const writeFile = async (path, content) => {
2016
- await mkdir(dirname2(path), { recursive: true });
2236
+ await mkdir(dirname3(path), { recursive: true });
2017
2237
  await fsWriteFile(path, content, "utf-8");
2018
2238
  };
2019
2239
  const result = await codegenAction({
@@ -2114,8 +2334,8 @@ Schema is in sync.`);
2114
2334
  if (!opts.force) {
2115
2335
  const readline = await import("node:readline");
2116
2336
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2117
- const answer = await new Promise((resolve4) => {
2118
- rl.question("This will drop all tables and re-apply migrations. Continue? (y/N) ", resolve4);
2337
+ const answer = await new Promise((resolve5) => {
2338
+ rl.question("This will drop all tables and re-apply migrations. Continue? (y/N) ", resolve5);
2119
2339
  });
2120
2340
  rl.close();
2121
2341
  if (answer.toLowerCase() !== "y") {
@@ -2429,12 +2649,12 @@ var defaultCLIConfig = {
2429
2649
  };
2430
2650
  // src/config/loader.ts
2431
2651
  import { existsSync as existsSync7 } from "node:fs";
2432
- import { join as join7, resolve as resolve4 } from "node:path";
2652
+ import { join as join8, resolve as resolve5 } from "node:path";
2433
2653
  var CONFIG_FILES = ["vertz.config.ts", "vertz.config.js", "vertz.config.mjs"];
2434
2654
  function findConfigFile(startDir) {
2435
- const dir = resolve4(startDir ?? process.cwd());
2655
+ const dir = resolve5(startDir ?? process.cwd());
2436
2656
  for (const filename of CONFIG_FILES) {
2437
- const filepath = join7(dir, filename);
2657
+ const filepath = join8(dir, filename);
2438
2658
  if (existsSync7(filepath)) {
2439
2659
  return filepath;
2440
2660
  }
@@ -2476,7 +2696,7 @@ async function loadConfig(configPath) {
2476
2696
  return deepMerge(defaultConfig, userConfig);
2477
2697
  }
2478
2698
  // src/deploy/detector.ts
2479
- import { join as join8 } from "node:path";
2699
+ import { join as join9 } from "node:path";
2480
2700
  var DETECTION_ORDER = [
2481
2701
  { file: "fly.toml", target: "fly" },
2482
2702
  { file: "railway.toml", target: "railway" },
@@ -2484,7 +2704,7 @@ var DETECTION_ORDER = [
2484
2704
  ];
2485
2705
  function detectTarget(projectRoot, existsFn) {
2486
2706
  for (const { file, target } of DETECTION_ORDER) {
2487
- if (existsFn(join8(projectRoot, file))) {
2707
+ if (existsFn(join9(projectRoot, file))) {
2488
2708
  return target;
2489
2709
  }
2490
2710
  }