@vertz/cli 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/vertz.js CHANGED
@@ -9,6 +9,8 @@ import { Command } from "commander";
9
9
  import { createJiti } from "jiti";
10
10
 
11
11
  // src/commands/build.ts
12
+ import { readFileSync as readFileSync2 } from "node:fs";
13
+ import { resolve as resolve3 } from "node:path";
12
14
  import { err, ok } from "@vertz/errors";
13
15
 
14
16
  // src/dev-server/app-detector.ts
@@ -56,10 +58,7 @@ import * as esbuild from "esbuild";
56
58
 
57
59
  // src/pipeline/orchestrator.ts
58
60
  import { createCodegenPipeline, generate } from "@vertz/codegen";
59
- import {
60
- createCompiler,
61
- OpenAPIGenerator
62
- } from "@vertz/compiler";
61
+ import { createCompiler, OpenAPIGenerator } from "@vertz/compiler";
63
62
  var defaultPipelineConfig = {
64
63
  sourceDir: "src",
65
64
  outputDir: ".vertz/generated",
@@ -133,6 +132,13 @@ class PipelineOrchestrator {
133
132
  success = false;
134
133
  }
135
134
  }
135
+ if (success) {
136
+ const buildUIResult = await this.runBuildUI();
137
+ stages.push(buildUIResult);
138
+ if (!buildUIResult.success) {
139
+ success = false;
140
+ }
141
+ }
136
142
  } catch (error) {
137
143
  success = false;
138
144
  stages.push({
@@ -274,12 +280,36 @@ class PipelineOrchestrator {
274
280
  }
275
281
  async runBuildUI() {
276
282
  const startTime = performance.now();
277
- return {
278
- stage: "build-ui",
279
- success: true,
280
- durationMs: performance.now() - startTime,
281
- output: "UI build delegated to Vite"
282
- };
283
+ try {
284
+ if (this.config._uiCompilerValidator) {
285
+ const result = await this.config._uiCompilerValidator();
286
+ return {
287
+ stage: "build-ui",
288
+ success: true,
289
+ durationMs: performance.now() - startTime,
290
+ output: `UI compiler validated (${result.fileCount} source files)`
291
+ };
292
+ }
293
+ const { createVertzBunPlugin } = await import("@vertz/ui-server/bun-plugin");
294
+ const pluginOptions = {
295
+ hmr: false,
296
+ fastRefresh: false
297
+ };
298
+ createVertzBunPlugin(pluginOptions);
299
+ return {
300
+ stage: "build-ui",
301
+ success: true,
302
+ durationMs: performance.now() - startTime,
303
+ output: "UI compiler validated"
304
+ };
305
+ } catch (error) {
306
+ return {
307
+ stage: "build-ui",
308
+ success: false,
309
+ durationMs: performance.now() - startTime,
310
+ error: error instanceof Error ? error : new Error(String(error))
311
+ };
312
+ }
283
313
  }
284
314
  async runDbSync() {
285
315
  const startTime = performance.now();
@@ -791,11 +821,29 @@ class BuildOrchestrator {
791
821
  }
792
822
  }
793
823
  // src/production-build/ui-build-pipeline.ts
794
- import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
795
- import { resolve } from "node:path";
824
+ import {
825
+ cpSync,
826
+ existsSync as existsSync3,
827
+ mkdirSync as mkdirSync2,
828
+ readdirSync as readdirSync2,
829
+ readFileSync,
830
+ rmSync,
831
+ writeFileSync as writeFileSync2
832
+ } from "node:fs";
833
+ import { dirname, join as join3, resolve } from "node:path";
834
+ import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
796
835
  async function buildUI(config) {
797
836
  const startTime = performance.now();
798
- const { projectRoot, clientEntry, serverEntry, outputDir, minify, sourcemap, title = "Vertz App" } = config;
837
+ const {
838
+ projectRoot,
839
+ clientEntry,
840
+ serverEntry,
841
+ outputDir,
842
+ minify,
843
+ sourcemap,
844
+ title = "Vertz App",
845
+ description
846
+ } = config;
799
847
  const distDir = resolve(projectRoot, outputDir);
800
848
  const distClient = resolve(distDir, "client");
801
849
  const distServer = resolve(distDir, "server");
@@ -807,7 +855,8 @@ async function buildUI(config) {
807
855
  console.log("\uD83D\uDCE6 Building client...");
808
856
  const { plugin: clientPlugin, fileExtractions } = createVertzBunPlugin({
809
857
  hmr: false,
810
- fastRefresh: false
858
+ fastRefresh: false,
859
+ routeSplitting: true
811
860
  });
812
861
  const clientResult = await Bun.build({
813
862
  entrypoints: [clientEntry],
@@ -831,15 +880,21 @@ ${errors}`,
831
880
  }
832
881
  let clientJsPath = "";
833
882
  const clientCssPaths = [];
883
+ const chunkPaths = [];
834
884
  for (const output of clientResult.outputs) {
835
885
  const name = output.path.replace(distClient, "");
836
886
  if (output.kind === "entry-point") {
837
887
  clientJsPath = name;
838
888
  } else if (output.path.endsWith(".css")) {
839
889
  clientCssPaths.push(name);
890
+ } else if (output.kind === "chunk" && output.path.endsWith(".js")) {
891
+ chunkPaths.push(name);
840
892
  }
841
893
  }
842
894
  console.log(` JS entry: ${clientJsPath}`);
895
+ for (const chunk of chunkPaths) {
896
+ console.log(` JS chunk: ${chunk}`);
897
+ }
843
898
  for (const css of clientCssPaths) {
844
899
  console.log(` CSS: ${css}`);
845
900
  }
@@ -859,25 +914,44 @@ ${errors}`,
859
914
  console.log("\uD83D\uDCC4 Generating HTML...");
860
915
  const cssLinks = clientCssPaths.map((path) => ` <link rel="stylesheet" href="${path}">`).join(`
861
916
  `);
917
+ const modulepreloadLinks = chunkPaths.map((path) => ` <link rel="modulepreload" href="${path}">`).join(`
918
+ `);
919
+ const descriptionTag = description ? `
920
+ <meta name="description" content="${description.replace(/"/g, "&quot;")}" />` : "";
921
+ const publicDir = resolve(projectRoot, "public");
922
+ const hasFavicon = existsSync3(resolve(publicDir, "favicon.svg"));
923
+ const hasManifest = existsSync3(resolve(publicDir, "site.webmanifest"));
924
+ const faviconTag = hasFavicon ? `
925
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">` : "";
926
+ const manifestTag = hasManifest ? `
927
+ <link rel="manifest" href="/site.webmanifest">` : "";
928
+ const themeColorTag = `
929
+ <meta name="theme-color" content="#0a0a0b">`;
862
930
  const html = `<!doctype html>
863
931
  <html lang="en">
864
932
  <head>
865
933
  <meta charset="UTF-8" />
866
934
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
867
- <title>${title}</title>
935
+ <title>${title}</title>${descriptionTag}${themeColorTag}${faviconTag}${manifestTag}
868
936
  ${cssLinks}
937
+ ${modulepreloadLinks}
869
938
  </head>
870
939
  <body>
871
940
  <div id="app"></div>
872
941
  <script type="module" crossorigin src="${clientJsPath}"></script>
873
942
  </body>
874
943
  </html>`;
875
- writeFileSync2(resolve(distClient, "index.html"), html);
876
- const publicDir = resolve(projectRoot, "public");
944
+ writeFileSync2(resolve(distClient, "_shell.html"), html);
877
945
  if (existsSync3(publicDir)) {
878
946
  cpSync(publicDir, distClient, { recursive: true });
879
947
  console.log(" Copied public/ assets");
880
948
  }
949
+ const imagesDir = resolve(projectRoot, ".vertz", "images");
950
+ if (existsSync3(imagesDir)) {
951
+ const imgDest = resolve(distClient, "__vertz_img");
952
+ cpSync(imagesDir, imgDest, { recursive: true });
953
+ console.log(" Copied optimized images");
954
+ }
881
955
  console.log("\uD83D\uDCE6 Building server...");
882
956
  const jsxSwapPlugin = {
883
957
  name: "vertz-ssr-jsx-swap",
@@ -914,6 +988,67 @@ ${errors}`,
914
988
  };
915
989
  }
916
990
  console.log(" Server entry: dist/server/app.js");
991
+ console.log("\uD83D\uDCC4 Pre-rendering routes...");
992
+ const {
993
+ discoverRoutes,
994
+ filterPrerenderableRoutes,
995
+ prerenderRoutes,
996
+ stripScriptsFromStaticHTML
997
+ } = await import("@vertz/ui-server/ssr");
998
+ const ssrEntryPath = resolve(distServer, "app.js");
999
+ let ssrModule;
1000
+ try {
1001
+ ssrModule = await import(ssrEntryPath);
1002
+ } catch (error) {
1003
+ console.log(" ⚠ Could not import SSR module for pre-rendering, skipping.");
1004
+ console.log(` ${error instanceof Error ? error.message : String(error)}`);
1005
+ const durationMs2 = performance.now() - startTime;
1006
+ console.log(`
1007
+ ✅ UI build complete (without pre-rendering)!`);
1008
+ console.log(` Client: ${distClient}/`);
1009
+ console.log(` Server: ${distServer}/`);
1010
+ return { success: true, durationMs: durationMs2 };
1011
+ }
1012
+ let allPatterns;
1013
+ try {
1014
+ allPatterns = await discoverRoutes(ssrModule);
1015
+ } catch (error) {
1016
+ console.log(" ⚠ Route discovery failed, skipping pre-rendering.");
1017
+ console.log(` ${error instanceof Error ? error.message : String(error)}`);
1018
+ const durationMs2 = performance.now() - startTime;
1019
+ console.log(`
1020
+ ✅ UI build complete (without pre-rendering)!`);
1021
+ console.log(` Client: ${distClient}/`);
1022
+ console.log(` Server: ${distServer}/`);
1023
+ return { success: true, durationMs: durationMs2 };
1024
+ }
1025
+ if (allPatterns.length === 0) {
1026
+ console.log(" No routes discovered (app may not use createRouter).");
1027
+ } else {
1028
+ console.log(` Discovered ${allPatterns.length} route(s): ${allPatterns.join(", ")}`);
1029
+ const prerenderableRoutes = filterPrerenderableRoutes(allPatterns);
1030
+ console.log(` Pre-rendering ${prerenderableRoutes.length} static route(s)...`);
1031
+ if (prerenderableRoutes.length > 0) {
1032
+ const results = await prerenderRoutes(ssrModule, html, {
1033
+ routes: prerenderableRoutes
1034
+ });
1035
+ const isIslandsMode = results.some((r) => r.html.includes("data-v-island"));
1036
+ for (const result of results) {
1037
+ const outPath = result.path === "/" ? resolve(distClient, "index.html") : resolve(distClient, `${result.path.replace(/^\//, "")}/index.html`);
1038
+ mkdirSync2(dirname(outPath), { recursive: true });
1039
+ const finalHtml = isIslandsMode ? stripScriptsFromStaticHTML(result.html) : result.html;
1040
+ const stripped = finalHtml !== result.html;
1041
+ writeFileSync2(outPath, finalHtml);
1042
+ const suffix = stripped ? " (static — JS stripped)" : "";
1043
+ console.log(` ✓ ${result.path} → ${outPath.replace(distClient, "dist/client")}${suffix}`);
1044
+ }
1045
+ }
1046
+ }
1047
+ console.log("\uD83D\uDDDC️ Pre-compressing assets with Brotli...");
1048
+ const compressedCount = brotliCompressDir(distClient);
1049
+ if (compressedCount > 0) {
1050
+ console.log(` Compressed ${compressedCount} file(s)`);
1051
+ }
917
1052
  const durationMs = performance.now() - startTime;
918
1053
  console.log(`
919
1054
  ✅ UI build complete!`);
@@ -928,19 +1063,52 @@ ${errors}`,
928
1063
  };
929
1064
  }
930
1065
  }
1066
+ var COMPRESSIBLE_EXTENSIONS = new Set([".html", ".js", ".css", ".svg", ".xml", ".txt", ".json"]);
1067
+ var MIN_COMPRESS_SIZE = 256;
1068
+ function brotliCompressDir(dir) {
1069
+ let count = 0;
1070
+ function walk(currentDir) {
1071
+ for (const entry of readdirSync2(currentDir, { withFileTypes: true })) {
1072
+ const fullPath = join3(currentDir, entry.name);
1073
+ if (entry.isDirectory()) {
1074
+ walk(fullPath);
1075
+ continue;
1076
+ }
1077
+ if (entry.name.endsWith(".br"))
1078
+ continue;
1079
+ const ext = entry.name.substring(entry.name.lastIndexOf("."));
1080
+ if (!COMPRESSIBLE_EXTENSIONS.has(ext))
1081
+ continue;
1082
+ const content = readFileSync(fullPath);
1083
+ if (content.length < MIN_COMPRESS_SIZE)
1084
+ continue;
1085
+ const compressed = brotliCompressSync(content, {
1086
+ params: {
1087
+ [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY
1088
+ }
1089
+ });
1090
+ if (compressed.length < content.length) {
1091
+ writeFileSync2(`${fullPath}.br`, compressed);
1092
+ count++;
1093
+ }
1094
+ }
1095
+ }
1096
+ walk(dir);
1097
+ return count;
1098
+ }
931
1099
  // src/utils/paths.ts
932
1100
  import { existsSync as existsSync4 } from "node:fs";
933
- import { dirname, join as join3, resolve as resolve2 } from "node:path";
1101
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
934
1102
  var ROOT_MARKERS = ["package.json", "vertz.config.ts", "vertz.config.js"];
935
1103
  function findProjectRoot(startDir) {
936
1104
  let current = resolve2(startDir ?? process.cwd());
937
1105
  while (true) {
938
1106
  for (const marker of ROOT_MARKERS) {
939
- if (existsSync4(join3(current, marker))) {
1107
+ if (existsSync4(join4(current, marker))) {
940
1108
  return current;
941
1109
  }
942
1110
  }
943
- const parent = dirname(current);
1111
+ const parent = dirname2(current);
944
1112
  if (parent === current) {
945
1113
  break;
946
1114
  }
@@ -1076,13 +1244,23 @@ async function buildUIOnly(detected, options) {
1076
1244
  console.log(` Sourcemap: ${sourcemap ? "enabled" : "disabled"}`);
1077
1245
  console.log("");
1078
1246
  }
1247
+ let title;
1248
+ let description;
1249
+ try {
1250
+ const pkgPath = resolve3(detected.projectRoot, "package.json");
1251
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1252
+ title = pkg.vertz?.title ?? pkg.title;
1253
+ description = pkg.vertz?.description ?? pkg.description;
1254
+ } catch {}
1079
1255
  const result = await buildUI({
1080
1256
  projectRoot: detected.projectRoot,
1081
1257
  clientEntry: detected.clientEntry,
1082
1258
  serverEntry,
1083
1259
  outputDir: "dist",
1084
1260
  minify: !noMinify,
1085
- sourcemap
1261
+ sourcemap,
1262
+ title,
1263
+ description
1086
1264
  });
1087
1265
  if (!result.success) {
1088
1266
  return err(new Error(result.error));
@@ -1278,13 +1456,13 @@ async function dbBaselineAction(options) {
1278
1456
  }
1279
1457
 
1280
1458
  // src/commands/dev.ts
1281
- import { join as join5 } from "node:path";
1459
+ import { join as join6 } from "node:path";
1282
1460
  import { err as err5, ok as ok5 } from "@vertz/errors";
1283
1461
 
1284
1462
  // src/dev-server/fullstack-server.ts
1285
1463
  import { spawn } from "node:child_process";
1286
1464
  import { existsSync as existsSync5 } from "node:fs";
1287
- import { join as join4, relative } from "node:path";
1465
+ import { join as join5, relative } from "node:path";
1288
1466
 
1289
1467
  // src/dev-server/process-manager.ts
1290
1468
  function createProcessManager(spawnFn) {
@@ -1321,14 +1499,14 @@ function createProcessManager(spawnFn) {
1321
1499
  return;
1322
1500
  const proc = child;
1323
1501
  child = undefined;
1324
- await new Promise((resolve3) => {
1502
+ await new Promise((resolve4) => {
1325
1503
  const timeout = setTimeout(() => {
1326
1504
  proc.kill("SIGKILL");
1327
- resolve3();
1505
+ resolve4();
1328
1506
  }, 2000);
1329
1507
  proc.on("exit", () => {
1330
1508
  clearTimeout(timeout);
1331
- resolve3();
1509
+ resolve4();
1332
1510
  });
1333
1511
  proc.kill("SIGTERM");
1334
1512
  });
@@ -1397,7 +1575,11 @@ Expected: export default createServer({ ... })`);
1397
1575
  throw new Error(`${serverEntry} default export must have a .handler function.
1398
1576
  Expected: export default createServer({ ... })`);
1399
1577
  }
1400
- return mod;
1578
+ let sessionResolver;
1579
+ if ("auth" in mod && mod.auth && typeof mod.auth.resolveSessionForSSR === "function") {
1580
+ sessionResolver = mod.auth.resolveSessionForSSR;
1581
+ }
1582
+ return { ...mod, sessionResolver };
1401
1583
  }
1402
1584
  function formatBanner(appType, port, host) {
1403
1585
  const url = `http://${host}:${port}`;
@@ -1418,19 +1600,22 @@ async function startDevServer(options) {
1418
1600
  }
1419
1601
  const { createBunDevServer } = await import("@vertz/ui-server/bun-dev-server");
1420
1602
  let apiHandler;
1603
+ let sessionResolver;
1421
1604
  if (mode.kind === "full-stack") {
1422
1605
  const serverMod = await importServerModule(mode.serverEntry);
1423
1606
  apiHandler = serverMod.handler;
1607
+ sessionResolver = serverMod.sessionResolver;
1424
1608
  }
1425
1609
  const uiEntry = `./${relative(detected.projectRoot, mode.uiEntry)}`;
1426
1610
  const clientEntry = mode.clientEntry ? `/${relative(detected.projectRoot, mode.clientEntry)}` : undefined;
1427
- const openapiPath = join4(detected.projectRoot, ".vertz/generated/openapi.json");
1611
+ const openapiPath = join5(detected.projectRoot, ".vertz/generated/openapi.json");
1428
1612
  const openapi = existsSync5(openapiPath) ? { specPath: openapiPath } : undefined;
1429
1613
  const devServer = createBunDevServer({
1430
1614
  entry: uiEntry,
1431
1615
  port,
1432
1616
  host,
1433
1617
  apiHandler,
1618
+ sessionResolver,
1434
1619
  openapi,
1435
1620
  ssrModule: mode.ssrModule,
1436
1621
  clientEntry,
@@ -1451,10 +1636,10 @@ function startApiOnlyServer(serverEntry, port) {
1451
1636
  stdio: "inherit"
1452
1637
  }));
1453
1638
  pm.start(serverEntry, { PORT: String(port) });
1454
- return new Promise((resolve3) => {
1639
+ return new Promise((resolve4) => {
1455
1640
  const shutdown = async () => {
1456
1641
  await pm.stop();
1457
- resolve3();
1642
+ resolve4();
1458
1643
  };
1459
1644
  process.on("SIGINT", shutdown);
1460
1645
  process.on("SIGTERM", shutdown);
@@ -1528,7 +1713,7 @@ async function devAction(options = {}) {
1528
1713
  }
1529
1714
  }
1530
1715
  const _watcher = createPipelineWatcher({
1531
- dir: join5(projectRoot, "src"),
1716
+ dir: join6(projectRoot, "src"),
1532
1717
  debounceMs: 100,
1533
1718
  onChange: async (changes) => {
1534
1719
  if (!isRunning)
@@ -1650,34 +1835,35 @@ function generateAction(options) {
1650
1835
  }
1651
1836
 
1652
1837
  // src/commands/start.ts
1653
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync } from "node:fs";
1654
- import { join as join6, resolve as resolve3 } from "node:path";
1838
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
1839
+ import { join as join7, resolve as resolve4 } from "node:path";
1655
1840
  import { err as err7, ok as ok7 } from "@vertz/errors";
1656
1841
  function discoverSSRModule(projectRoot) {
1657
- const serverDir = join6(projectRoot, "dist", "server");
1842
+ const serverDir = join7(projectRoot, "dist", "server");
1658
1843
  if (!existsSync6(serverDir))
1659
1844
  return;
1660
- const files = readdirSync2(serverDir).filter((f) => f.endsWith(".js"));
1845
+ const files = readdirSync3(serverDir).filter((f) => f.endsWith(".js"));
1661
1846
  if (files.length === 0)
1662
1847
  return;
1663
1848
  if (files.includes("app.js")) {
1664
- return join6(serverDir, "app.js");
1849
+ return join7(serverDir, "app.js");
1665
1850
  }
1666
1851
  const first = files[0];
1667
- return first ? join6(serverDir, first) : undefined;
1852
+ return first ? join7(serverDir, first) : undefined;
1668
1853
  }
1669
1854
  function validateBuildOutputs(projectRoot, appType) {
1670
1855
  const missing = [];
1671
1856
  if (appType === "api-only" || appType === "full-stack") {
1672
- const apiBuild = join6(projectRoot, ".vertz", "build", "index.js");
1857
+ const apiBuild = join7(projectRoot, ".vertz", "build", "index.js");
1673
1858
  if (!existsSync6(apiBuild)) {
1674
1859
  missing.push(".vertz/build/index.js");
1675
1860
  }
1676
1861
  }
1677
1862
  if (appType === "ui-only" || appType === "full-stack") {
1678
- const clientHtml = join6(projectRoot, "dist", "client", "index.html");
1679
- if (!existsSync6(clientHtml)) {
1680
- missing.push("dist/client/index.html");
1863
+ const shellHtml = join7(projectRoot, "dist", "client", "_shell.html");
1864
+ const clientHtml = join7(projectRoot, "dist", "client", "index.html");
1865
+ if (!existsSync6(shellHtml) && !existsSync6(clientHtml)) {
1866
+ missing.push("dist/client/_shell.html");
1681
1867
  }
1682
1868
  const ssrModule = discoverSSRModule(projectRoot);
1683
1869
  if (!ssrModule) {
@@ -1722,7 +1908,7 @@ async function startAction(options = {}) {
1722
1908
  }
1723
1909
  }
1724
1910
  async function startApiOnly(projectRoot, port, host, _verbose) {
1725
- const entryPath = resolve3(projectRoot, ".vertz", "build", "index.js");
1911
+ const entryPath = resolve4(projectRoot, ".vertz", "build", "index.js");
1726
1912
  let mod;
1727
1913
  try {
1728
1914
  mod = await import(entryPath);
@@ -1747,8 +1933,10 @@ async function startUIOnly(projectRoot, port, host, _verbose) {
1747
1933
  if (!ssrModulePath) {
1748
1934
  return err7(new Error('No SSR module found in dist/server/. Run "vertz build" first.'));
1749
1935
  }
1750
- const templatePath = resolve3(projectRoot, "dist", "client", "index.html");
1751
- const template = readFileSync(templatePath, "utf-8");
1936
+ const shellPath = resolve4(projectRoot, "dist", "client", "_shell.html");
1937
+ const legacyPath = resolve4(projectRoot, "dist", "client", "index.html");
1938
+ const templatePath = existsSync6(shellPath) ? shellPath : legacyPath;
1939
+ const template = readFileSync3(templatePath, "utf-8");
1752
1940
  let ssrModule;
1753
1941
  try {
1754
1942
  ssrModule = await import(ssrModulePath);
@@ -1762,16 +1950,22 @@ async function startUIOnly(projectRoot, port, host, _verbose) {
1762
1950
  template,
1763
1951
  inlineCSS
1764
1952
  });
1765
- const clientDir = resolve3(projectRoot, "dist", "client");
1953
+ const clientDir = resolve4(projectRoot, "dist", "client");
1766
1954
  const server = Bun.serve({
1767
1955
  port,
1768
1956
  hostname: host,
1769
1957
  async fetch(req) {
1770
1958
  const url = new URL(req.url);
1771
1959
  const pathname = url.pathname;
1960
+ if (req.headers.get("x-vertz-nav") === "1") {
1961
+ return ssrHandler(req);
1962
+ }
1772
1963
  const staticResponse = serveStaticFile(clientDir, pathname);
1773
1964
  if (staticResponse)
1774
1965
  return staticResponse;
1966
+ const prerenderResponse = servePrerenderHTML(clientDir, pathname);
1967
+ if (prerenderResponse)
1968
+ return prerenderResponse;
1775
1969
  return ssrHandler(req);
1776
1970
  }
1777
1971
  });
@@ -1780,7 +1974,7 @@ async function startUIOnly(projectRoot, port, host, _verbose) {
1780
1974
  return ok7(undefined);
1781
1975
  }
1782
1976
  async function startFullStack(projectRoot, port, host, _verbose) {
1783
- const apiEntryPath = resolve3(projectRoot, ".vertz", "build", "index.js");
1977
+ const apiEntryPath = resolve4(projectRoot, ".vertz", "build", "index.js");
1784
1978
  let apiMod;
1785
1979
  try {
1786
1980
  apiMod = await import(apiEntryPath);
@@ -1795,8 +1989,10 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1795
1989
  if (!ssrModulePath) {
1796
1990
  return err7(new Error('No SSR module found in dist/server/. Run "vertz build" first.'));
1797
1991
  }
1798
- const templatePath = resolve3(projectRoot, "dist", "client", "index.html");
1799
- const template = readFileSync(templatePath, "utf-8");
1992
+ const shellPath = resolve4(projectRoot, "dist", "client", "_shell.html");
1993
+ const legacyPath = resolve4(projectRoot, "dist", "client", "index.html");
1994
+ const templatePath = existsSync6(shellPath) ? shellPath : legacyPath;
1995
+ const template = readFileSync3(templatePath, "utf-8");
1800
1996
  let ssrModule;
1801
1997
  try {
1802
1998
  ssrModule = await import(ssrModulePath);
@@ -1810,7 +2006,7 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1810
2006
  template,
1811
2007
  inlineCSS
1812
2008
  });
1813
- const clientDir = resolve3(projectRoot, "dist", "client");
2009
+ const clientDir = resolve4(projectRoot, "dist", "client");
1814
2010
  const server = Bun.serve({
1815
2011
  port,
1816
2012
  hostname: host,
@@ -1820,9 +2016,15 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1820
2016
  if (pathname.startsWith("/api")) {
1821
2017
  return apiHandler(req);
1822
2018
  }
2019
+ if (req.headers.get("x-vertz-nav") === "1") {
2020
+ return ssrHandler(req);
2021
+ }
1823
2022
  const staticResponse = serveStaticFile(clientDir, pathname);
1824
2023
  if (staticResponse)
1825
2024
  return staticResponse;
2025
+ const prerenderResponse = servePrerenderHTML(clientDir, pathname);
2026
+ if (prerenderResponse)
2027
+ return prerenderResponse;
1826
2028
  return ssrHandler(req);
1827
2029
  }
1828
2030
  });
@@ -1831,25 +2033,43 @@ async function startFullStack(projectRoot, port, host, _verbose) {
1831
2033
  return ok7(undefined);
1832
2034
  }
1833
2035
  function discoverInlineCSS(projectRoot) {
1834
- const cssDir = resolve3(projectRoot, "dist", "client", "assets");
2036
+ const cssDir = resolve4(projectRoot, "dist", "client", "assets");
1835
2037
  if (!existsSync6(cssDir))
1836
2038
  return;
1837
- const cssFiles = readdirSync2(cssDir).filter((f) => f.endsWith(".css"));
2039
+ const cssFiles = readdirSync3(cssDir).filter((f) => f.endsWith(".css"));
1838
2040
  if (cssFiles.length === 0)
1839
2041
  return;
1840
2042
  const result = {};
1841
2043
  for (const file of cssFiles) {
1842
- const content = readFileSync(join6(cssDir, file), "utf-8");
2044
+ const content = readFileSync3(join7(cssDir, file), "utf-8");
1843
2045
  result[`/assets/${file}`] = content;
1844
2046
  }
1845
2047
  return result;
1846
2048
  }
2049
+ function servePrerenderHTML(clientDir, pathname) {
2050
+ const htmlPath = pathname === "/" ? resolve4(clientDir, "index.html") : resolve4(clientDir, `${pathname.replace(/^\//, "")}/index.html`);
2051
+ if (!htmlPath.startsWith(clientDir))
2052
+ return null;
2053
+ if (htmlPath.endsWith("/_shell.html"))
2054
+ return null;
2055
+ const file = Bun.file(htmlPath);
2056
+ if (!file.size)
2057
+ return null;
2058
+ return new Response(file, {
2059
+ headers: {
2060
+ "Content-Type": "text/html; charset=utf-8",
2061
+ "Cache-Control": "public, max-age=0, must-revalidate"
2062
+ }
2063
+ });
2064
+ }
1847
2065
  function serveStaticFile(clientDir, pathname) {
1848
2066
  if (pathname === "/" || pathname === "/index.html")
1849
2067
  return null;
1850
- const filePath = resolve3(clientDir, `.${pathname}`);
2068
+ const filePath = resolve4(clientDir, `.${pathname}`);
1851
2069
  if (!filePath.startsWith(clientDir))
1852
2070
  return null;
2071
+ if (!existsSync6(filePath) || !statSync2(filePath).isFile())
2072
+ return null;
1853
2073
  const file = Bun.file(filePath);
1854
2074
  if (!file.size)
1855
2075
  return null;
@@ -1944,12 +2164,12 @@ function createCLI() {
1944
2164
  }
1945
2165
  });
1946
2166
  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) => {
1947
- const { resolve: resolve4, dirname: dirname2 } = await import("node:path");
2167
+ const { resolve: resolve5, dirname: dirname3 } = await import("node:path");
1948
2168
  const { mkdir, writeFile: fsWriteFile } = await import("node:fs/promises");
1949
2169
  let config;
1950
2170
  let compilerConfig;
1951
2171
  try {
1952
- const configPath = resolve4(process.cwd(), "vertz.config.ts");
2172
+ const configPath = resolve5(process.cwd(), "vertz.config.ts");
1953
2173
  const jiti = createJiti(import.meta.url);
1954
2174
  const configModule = await jiti.import(configPath);
1955
2175
  config = configModule.codegen ?? configModule.default?.codegen;
@@ -1974,7 +2194,7 @@ function createCLI() {
1974
2194
  const { createCodegenPipeline: createCodegenPipeline2 } = await import("@vertz/codegen");
1975
2195
  const pipeline = createCodegenPipeline2();
1976
2196
  const writeFile = async (path, content) => {
1977
- await mkdir(dirname2(path), { recursive: true });
2197
+ await mkdir(dirname3(path), { recursive: true });
1978
2198
  await fsWriteFile(path, content, "utf-8");
1979
2199
  };
1980
2200
  const result = await codegenAction({
@@ -2075,8 +2295,8 @@ Schema is in sync.`);
2075
2295
  if (!opts.force) {
2076
2296
  const readline = await import("node:readline");
2077
2297
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2078
- const answer = await new Promise((resolve4) => {
2079
- rl.question("This will drop all tables and re-apply migrations. Continue? (y/N) ", resolve4);
2298
+ const answer = await new Promise((resolve5) => {
2299
+ rl.question("This will drop all tables and re-apply migrations. Continue? (y/N) ", resolve5);
2080
2300
  });
2081
2301
  rl.close();
2082
2302
  if (answer.toLowerCase() !== "y") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/cli",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz CLI — dev, build, create, and deploy commands",
@@ -34,13 +34,13 @@
34
34
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
35
35
  },
36
36
  "dependencies": {
37
- "@vertz/codegen": "^0.2.14",
38
- "@vertz/errors": "^0.2.14",
39
- "@vertz/tui": "^0.2.14",
40
- "@vertz/compiler": "^0.2.14",
41
- "@vertz/create-vertz-app": "^0.2.14",
42
- "@vertz/db": "^0.2.14",
43
- "@vertz/ui-server": "^0.2.14",
37
+ "@vertz/codegen": "^0.2.16",
38
+ "@vertz/errors": "^0.2.16",
39
+ "@vertz/tui": "^0.2.16",
40
+ "@vertz/compiler": "^0.2.16",
41
+ "@vertz/create-vertz-app": "^0.2.16",
42
+ "@vertz/db": "^0.2.16",
43
+ "@vertz/ui-server": "^0.2.16",
44
44
  "esbuild": "^0.25.0",
45
45
  "commander": "^14.0.0",
46
46
  "jiti": "^2.4.2"