blumenjs 0.2.0 → 0.2.2

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.
@@ -68,7 +68,7 @@ async function select(question, options) {
68
68
  input: process.stdin,
69
69
  output: process.stdout
70
70
  });
71
- return new Promise((resolve3) => {
71
+ return new Promise((resolve6) => {
72
72
  console.log(`
73
73
  ${c.bold}${question}${c.reset}`);
74
74
  options.forEach((opt, i) => {
@@ -79,9 +79,9 @@ async function select(question, options) {
79
79
  rl.close();
80
80
  const idx = parseInt(answer, 10) - 1;
81
81
  if (idx >= 0 && idx < options.length) {
82
- resolve3(options[idx]);
82
+ resolve6(options[idx]);
83
83
  } else {
84
- resolve3(options[0]);
84
+ resolve6(options[0]);
85
85
  }
86
86
  });
87
87
  });
@@ -93,14 +93,14 @@ async function confirm(question, defaultYes = true) {
93
93
  output: process.stdout
94
94
  });
95
95
  const hint = defaultYes ? "Y/n" : "y/N";
96
- return new Promise((resolve3) => {
96
+ return new Promise((resolve6) => {
97
97
  rl.question(` ${c.bold}${question}${c.reset} ${c.dim}(${hint})${c.reset} `, (answer) => {
98
98
  rl.close();
99
99
  const a = answer.trim().toLowerCase();
100
100
  if (a === "")
101
- resolve3(defaultYes);
101
+ resolve6(defaultYes);
102
102
  else
103
- resolve3(a === "y" || a === "yes");
103
+ resolve6(a === "y" || a === "yes");
104
104
  });
105
105
  });
106
106
  }
@@ -243,7 +243,7 @@ async function dev() {
243
243
  label: " go",
244
244
  color: c.green,
245
245
  cmd: "go",
246
- args: ["run", "go-server/main.go", "go-server/image.go", "go-server/cache.go"],
246
+ args: ["run", "./go-server/"],
247
247
  readyPattern: /Go server starting/
248
248
  }
249
249
  ];
@@ -321,6 +321,39 @@ async function dev() {
321
321
 
322
322
  // cli/commands/build.ts
323
323
  import { execSync as execSync3 } from "child_process";
324
+ import * as fs2 from "fs";
325
+ import * as path2 from "path";
326
+ function generateChunkManifest() {
327
+ const jsDir = path2.resolve("static/js");
328
+ const chunksDir = path2.resolve("static/js/chunks");
329
+ const manifest = {};
330
+ if (fs2.existsSync(jsDir)) {
331
+ for (const file of fs2.readdirSync(jsDir)) {
332
+ if (!file.endsWith(".js") || file.endsWith(".map"))
333
+ continue;
334
+ const match = file.match(/^([^.]+)\.[a-f0-9]+\.js$/);
335
+ if (match) {
336
+ manifest[match[1]] = `/static/js/${file}`;
337
+ }
338
+ }
339
+ }
340
+ if (fs2.existsSync(chunksDir)) {
341
+ for (const file of fs2.readdirSync(chunksDir)) {
342
+ if (!file.endsWith(".js") || file.endsWith(".map"))
343
+ continue;
344
+ const match = file.match(/^([^.]+)\.[a-f0-9]+\.js$/);
345
+ if (match) {
346
+ manifest[match[1]] = `/static/js/chunks/${file}`;
347
+ }
348
+ }
349
+ }
350
+ fs2.mkdirSync("dist", { recursive: true });
351
+ fs2.writeFileSync("dist/chunk-manifest.json", JSON.stringify(manifest, null, 2));
352
+ const coreChunks = ["runtime", "vendor", "framework", "main"].filter((k) => manifest[k]);
353
+ const pageChunks = Object.keys(manifest).filter((k) => k.startsWith("page-"));
354
+ log.info(` Core chunks: ${coreChunks.join(", ")} (${coreChunks.length})`);
355
+ log.info(` Page chunks: ${pageChunks.length} page(s)`);
356
+ }
324
357
  async function build(args = []) {
325
358
  banner();
326
359
  const analyze = args.includes("--analyze");
@@ -335,13 +368,23 @@ async function build(args = []) {
335
368
  cmd: "npx tsx scripts/generate-routes.ts"
336
369
  },
337
370
  {
338
- label: "Building client bundle",
371
+ label: "Building client bundle (code splitting)",
339
372
  cmd: "npx webpack --mode production",
340
373
  env: analyze ? { BLUMEN_ANALYZE: "1" } : void 0
341
374
  },
375
+ {
376
+ label: "Generating chunk manifest",
377
+ fn: generateChunkManifest
378
+ },
342
379
  {
343
380
  label: "Building SSR server",
344
- cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external"
381
+ cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external --alias:@=./app"
382
+ },
383
+ {
384
+ label: "Pre-rendering static pages (SSG)",
385
+ cmd: "npx tsx scripts/ssg-prerender.ts",
386
+ optional: true
387
+ // Don't fail if no SSG pages exist
345
388
  }
346
389
  ];
347
390
  const startTime = Date.now();
@@ -349,15 +392,23 @@ async function build(args = []) {
349
392
  const step = steps[i];
350
393
  log.step(`[${i + 1}/${steps.length}] ${step.label}...`);
351
394
  try {
352
- execSync3(step.cmd, {
353
- stdio: "inherit",
354
- cwd: process.cwd(),
355
- env: { ...process.env, ...step.env || {} }
356
- });
395
+ if (step.fn) {
396
+ step.fn();
397
+ } else if (step.cmd) {
398
+ execSync3(step.cmd, {
399
+ stdio: "inherit",
400
+ cwd: process.cwd(),
401
+ env: { ...process.env, ...step.env || {} }
402
+ });
403
+ }
357
404
  log.success(step.label);
358
405
  } catch {
359
- log.error(`Failed: ${step.label}`);
360
- process.exit(1);
406
+ if (step.optional) {
407
+ log.success(step.label);
408
+ } else {
409
+ log.error(`Failed: ${step.label}`);
410
+ process.exit(1);
411
+ }
361
412
  }
362
413
  }
363
414
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
@@ -375,10 +426,10 @@ async function build(args = []) {
375
426
 
376
427
  // cli/commands/start.ts
377
428
  import { spawn as spawn2 } from "child_process";
378
- import * as fs2 from "fs";
429
+ import * as fs3 from "fs";
379
430
  async function start() {
380
431
  banner();
381
- if (!fs2.existsSync("dist/ssr-server.js")) {
432
+ if (!fs3.existsSync("dist/ssr-server.js")) {
382
433
  log.error("Production build not found.");
383
434
  log.info(
384
435
  `Run ${c.bold}blumen build${c.reset} first to create a production build.`
@@ -401,7 +452,7 @@ async function start() {
401
452
  label: " go",
402
453
  color: c.green,
403
454
  cmd: "go",
404
- args: ["run", "go-server/main.go", "go-server/image.go", "go-server/cache.go"],
455
+ args: ["run", "./go-server/"],
405
456
  readyPattern: /Go server starting/
406
457
  }
407
458
  ];
@@ -463,23 +514,23 @@ async function start() {
463
514
  }
464
515
 
465
516
  // cli/commands/create.ts
466
- import * as fs3 from "fs";
467
- import * as path2 from "path";
517
+ import * as fs4 from "fs";
518
+ import * as path3 from "path";
468
519
  import { execSync as execSync4 } from "child_process";
469
520
  function getFrameworkRoot() {
470
- const cliEntry = fs3.realpathSync(process.argv[1]);
471
- const cliDir = path2.dirname(cliEntry);
472
- return path2.resolve(cliDir, "..");
521
+ const cliEntry = fs4.realpathSync(process.argv[1]);
522
+ const cliDir = path3.dirname(cliEntry);
523
+ return path3.resolve(cliDir, "..");
473
524
  }
474
525
  function readProjectFile(relativePath) {
475
526
  const root = getFrameworkRoot();
476
- const bundledPath = path2.join(root, "templates", relativePath);
477
- if (fs3.existsSync(bundledPath)) {
478
- return fs3.readFileSync(bundledPath, "utf-8");
527
+ const bundledPath = path3.join(root, "templates", relativePath);
528
+ if (fs4.existsSync(bundledPath)) {
529
+ return fs4.readFileSync(bundledPath, "utf-8");
479
530
  }
480
- const sourcePath = path2.join(root, relativePath);
481
- if (fs3.existsSync(sourcePath)) {
482
- return fs3.readFileSync(sourcePath, "utf-8");
531
+ const sourcePath = path3.join(root, relativePath);
532
+ if (fs4.existsSync(sourcePath)) {
533
+ return fs4.readFileSync(sourcePath, "utf-8");
483
534
  }
484
535
  throw new Error(`Template file not found: ${relativePath}`);
485
536
  }
@@ -744,9 +795,9 @@ static/js/bundle.js
744
795
  .DS_Store
745
796
  `;
746
797
  function writeFile(base, relPath, content) {
747
- const fullPath = path2.join(base, relPath);
748
- fs3.mkdirSync(path2.dirname(fullPath), { recursive: true });
749
- fs3.writeFileSync(fullPath, content, "utf-8");
798
+ const fullPath = path3.join(base, relPath);
799
+ fs4.mkdirSync(path3.dirname(fullPath), { recursive: true });
800
+ fs4.writeFileSync(fullPath, content, "utf-8");
750
801
  }
751
802
  var TEMPLATE_MAP = {
752
803
  starter: {
@@ -815,8 +866,8 @@ async function create(projectName) {
815
866
  );
816
867
  process.exit(1);
817
868
  }
818
- const projectDir = path2.resolve(process.cwd(), projectName);
819
- if (fs3.existsSync(projectDir)) {
869
+ const projectDir = path3.resolve(process.cwd(), projectName);
870
+ if (fs4.existsSync(projectDir)) {
820
871
  log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
821
872
  process.exit(1);
822
873
  }
@@ -885,8 +936,8 @@ async function create(projectName) {
885
936
 
886
937
  // cli/commands/deploy.ts
887
938
  import { execSync as execSync5, spawnSync } from "child_process";
888
- import * as fs4 from "fs";
889
- import * as path3 from "path";
939
+ import * as fs5 from "fs";
940
+ import * as path4 from "path";
890
941
  var PLATFORMS = [
891
942
  { name: "Docker (any cloud)", ok: true, note: "Best option \u2014 multi-stage Dockerfile included" },
892
943
  { name: "Railway", ok: true, note: "Auto-detects Dockerfile, deploy with railway up" },
@@ -906,7 +957,7 @@ function hasCommand(cmd) {
906
957
  }
907
958
  }
908
959
  function ensureDockerfile() {
909
- if (!fs4.existsSync("Dockerfile")) {
960
+ if (!fs5.existsSync("Dockerfile")) {
910
961
  log.error("No Dockerfile found in the current directory.");
911
962
  log.info(
912
963
  `Run ${c.bold}blumen create${c.reset} to scaffold a project with Docker support.`
@@ -924,7 +975,7 @@ async function deployDocker() {
924
975
  );
925
976
  process.exit(1);
926
977
  }
927
- const projectName = path3.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
978
+ const projectName = path4.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
928
979
  const imageName = `blumen-${projectName}`;
929
980
  log.step(`Building image: ${c.bold}${imageName}${c.reset}...`);
930
981
  divider();
@@ -966,8 +1017,8 @@ async function deployDocker() {
966
1017
  async function deployFly() {
967
1018
  log.info("Fly.io Deployment\n");
968
1019
  ensureDockerfile();
969
- const projectName = path3.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
970
- if (!fs4.existsSync("fly.toml")) {
1020
+ const projectName = path4.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
1021
+ if (!fs5.existsSync("fly.toml")) {
971
1022
  log.step("Generating fly.toml...");
972
1023
  const flyConfig = `# Fly.io configuration for Blumen app
973
1024
  # Deploy with: fly deploy
@@ -997,7 +1048,7 @@ primary_region = "iad"
997
1048
  cpu_kind = "shared"
998
1049
  cpus = 1
999
1050
  `;
1000
- fs4.writeFileSync("fly.toml", flyConfig);
1051
+ fs5.writeFileSync("fly.toml", flyConfig);
1001
1052
  log.success("fly.toml created");
1002
1053
  } else {
1003
1054
  log.info("fly.toml already exists, skipping generation.");
@@ -1038,7 +1089,7 @@ primary_region = "iad"
1038
1089
  async function deployRailway() {
1039
1090
  log.info("Railway Deployment\n");
1040
1091
  ensureDockerfile();
1041
- if (!fs4.existsSync("railway.toml")) {
1092
+ if (!fs5.existsSync("railway.toml")) {
1042
1093
  log.step("Generating railway.toml...");
1043
1094
  const railwayConfig = `# Railway configuration for Blumen app
1044
1095
  # Deploy with: railway up
@@ -1054,7 +1105,7 @@ async function deployRailway() {
1054
1105
  restartPolicyType = "ON_FAILURE"
1055
1106
  restartPolicyMaxRetries = 5
1056
1107
  `;
1057
- fs4.writeFileSync("railway.toml", railwayConfig);
1108
+ fs5.writeFileSync("railway.toml", railwayConfig);
1058
1109
  log.success("railway.toml created");
1059
1110
  } else {
1060
1111
  log.info("railway.toml already exists, skipping generation.");
@@ -1149,12 +1200,12 @@ async function deploy(subcommand) {
1149
1200
  }
1150
1201
 
1151
1202
  // cli/commands/fonts.ts
1152
- import * as fs5 from "fs";
1153
- import * as path4 from "path";
1203
+ import * as fs6 from "fs";
1204
+ import * as path5 from "path";
1154
1205
  import * as https from "https";
1155
- var FONTS_DIR = path4.resolve("static/fonts");
1156
- var FONTS_CSS = path4.resolve("static/css/fonts.css");
1157
- var FONTS_CONFIG = path4.resolve("blumen.fonts.json");
1206
+ var FONTS_DIR = path5.resolve("static/fonts");
1207
+ var FONTS_CSS = path5.resolve("static/css/fonts.css");
1208
+ var FONTS_CONFIG = path5.resolve("blumen.fonts.json");
1158
1209
  var DEFAULT_MANIFEST = {
1159
1210
  fonts: []
1160
1211
  };
@@ -1172,13 +1223,13 @@ var FALLBACK_MAP = {
1172
1223
  "JetBrains Mono": { fallback: "'Courier New', Courier, monospace", sizeAdjust: "103%" }
1173
1224
  };
1174
1225
  function downloadFile(url, dest) {
1175
- return new Promise((resolve3, reject) => {
1176
- const file = fs5.createWriteStream(dest);
1226
+ return new Promise((resolve6, reject) => {
1227
+ const file = fs6.createWriteStream(dest);
1177
1228
  https.get(url, { headers: { "User-Agent": "Mozilla/5.0" } }, (response) => {
1178
1229
  if (response.statusCode === 301 || response.statusCode === 302) {
1179
1230
  const redirectUrl = response.headers.location;
1180
1231
  if (redirectUrl) {
1181
- downloadFile(redirectUrl, dest).then(resolve3).catch(reject);
1232
+ downloadFile(redirectUrl, dest).then(resolve6).catch(reject);
1182
1233
  return;
1183
1234
  }
1184
1235
  }
@@ -1189,17 +1240,17 @@ function downloadFile(url, dest) {
1189
1240
  response.pipe(file);
1190
1241
  file.on("finish", () => {
1191
1242
  file.close();
1192
- resolve3();
1243
+ resolve6();
1193
1244
  });
1194
1245
  }).on("error", (err) => {
1195
- fs5.unlink(dest, () => {
1246
+ fs6.unlink(dest, () => {
1196
1247
  });
1197
1248
  reject(err);
1198
1249
  });
1199
1250
  });
1200
1251
  }
1201
1252
  function fetchGoogleFontsCSS(family, weights, subsets) {
1202
- return new Promise((resolve3, reject) => {
1253
+ return new Promise((resolve6, reject) => {
1203
1254
  const weightStr = weights.join(";");
1204
1255
  const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weightStr}&display=swap&subset=${subsets.join(",")}`;
1205
1256
  https.get(url, {
@@ -1212,7 +1263,7 @@ function fetchGoogleFontsCSS(family, weights, subsets) {
1212
1263
  response.on("data", (chunk) => {
1213
1264
  data += chunk.toString();
1214
1265
  });
1215
- response.on("end", () => resolve3(data));
1266
+ response.on("end", () => resolve6(data));
1216
1267
  }).on("error", reject);
1217
1268
  });
1218
1269
  }
@@ -1282,11 +1333,11 @@ async function processFont(config) {
1282
1333
  const ext = font.format === "woff2" ? "woff2" : "woff";
1283
1334
  const safeName = config.family.replace(/\s+/g, "-").toLowerCase();
1284
1335
  const fileName = `${safeName}-${font.weight}-${font.style}.${ext}`;
1285
- const filePath = path4.join(FONTS_DIR, fileName);
1286
- if (!fs5.existsSync(filePath)) {
1336
+ const filePath = path5.join(FONTS_DIR, fileName);
1337
+ if (!fs6.existsSync(filePath)) {
1287
1338
  try {
1288
1339
  await downloadFile(font.url, filePath);
1289
- const size = fs5.statSync(filePath).size;
1340
+ const size = fs6.statSync(filePath).size;
1290
1341
  console.log(` Downloaded: ${fileName} (${(size / 1024).toFixed(1)} KB)`);
1291
1342
  } catch (err) {
1292
1343
  console.error(` Failed to download ${fileName}: ${err}`);
@@ -1307,13 +1358,13 @@ async function processFont(config) {
1307
1358
  return generatedCSS;
1308
1359
  }
1309
1360
  function loadManifest() {
1310
- if (fs5.existsSync(FONTS_CONFIG)) {
1311
- return JSON.parse(fs5.readFileSync(FONTS_CONFIG, "utf-8"));
1361
+ if (fs6.existsSync(FONTS_CONFIG)) {
1362
+ return JSON.parse(fs6.readFileSync(FONTS_CONFIG, "utf-8"));
1312
1363
  }
1313
1364
  return { ...DEFAULT_MANIFEST };
1314
1365
  }
1315
1366
  function saveManifest(manifest) {
1316
- fs5.writeFileSync(FONTS_CONFIG, JSON.stringify(manifest, null, 2), "utf-8");
1367
+ fs6.writeFileSync(FONTS_CONFIG, JSON.stringify(manifest, null, 2), "utf-8");
1317
1368
  }
1318
1369
  async function fontsCommand(args) {
1319
1370
  console.log("\n \u{1F338} Blumen Font Optimizer\n");
@@ -1355,8 +1406,8 @@ async function fontsCommand(args) {
1355
1406
  console.log(' blumen fonts --add "Roboto"');
1356
1407
  return;
1357
1408
  }
1358
- fs5.mkdirSync(FONTS_DIR, { recursive: true });
1359
- fs5.mkdirSync(path4.dirname(FONTS_CSS), { recursive: true });
1409
+ fs6.mkdirSync(FONTS_DIR, { recursive: true });
1410
+ fs6.mkdirSync(path5.dirname(FONTS_CSS), { recursive: true });
1360
1411
  let allCSS = `/* Auto-generated by Blumen Font Optimizer */
1361
1412
  /* Do not edit manually - run 'blumen fonts' to regenerate */
1362
1413
 
@@ -1365,10 +1416,10 @@ async function fontsCommand(args) {
1365
1416
  const css = await processFont(fontConfig);
1366
1417
  allCSS += css + "\n";
1367
1418
  }
1368
- fs5.writeFileSync(FONTS_CSS, allCSS, "utf-8");
1419
+ fs6.writeFileSync(FONTS_CSS, allCSS, "utf-8");
1369
1420
  console.log(`
1370
- \u2705 Font CSS written to: ${path4.relative(process.cwd(), FONTS_CSS)}`);
1371
- console.log(` \u2705 Font files saved to: ${path4.relative(process.cwd(), FONTS_DIR)}/`);
1421
+ \u2705 Font CSS written to: ${path5.relative(process.cwd(), FONTS_CSS)}`);
1422
+ console.log(` \u2705 Font files saved to: ${path5.relative(process.cwd(), FONTS_DIR)}/`);
1372
1423
  console.log(`
1373
1424
  To use these fonts, import the CSS in your _app.tsx or _document.tsx:`);
1374
1425
  console.log(` import '../../static/css/fonts.css';`);
@@ -1378,6 +1429,748 @@ async function fontsCommand(args) {
1378
1429
  console.log();
1379
1430
  }
1380
1431
 
1432
+ // cli/commands/audit.ts
1433
+ import { execSync as execSync6 } from "child_process";
1434
+ function severityColor(severity) {
1435
+ switch (severity) {
1436
+ case "critical":
1437
+ return c.red;
1438
+ case "high":
1439
+ return c.red;
1440
+ case "moderate":
1441
+ return c.yellow;
1442
+ case "low":
1443
+ return c.dim;
1444
+ default:
1445
+ return c.dim;
1446
+ }
1447
+ }
1448
+ function severityIcon(severity) {
1449
+ switch (severity) {
1450
+ case "critical":
1451
+ return "\u{1F534}";
1452
+ case "high":
1453
+ return "\u{1F7E0}";
1454
+ case "moderate":
1455
+ return "\u{1F7E1}";
1456
+ case "low":
1457
+ return "\u{1F535}";
1458
+ default:
1459
+ return "\u26AA";
1460
+ }
1461
+ }
1462
+ async function audit(args = []) {
1463
+ banner();
1464
+ const fix = args.includes("--fix");
1465
+ const ci = args.includes("--ci");
1466
+ const jsonOutput = args.includes("--json");
1467
+ if (fix) {
1468
+ log.info("Attempting to fix vulnerabilities...");
1469
+ log.blank();
1470
+ try {
1471
+ execSync6("npm audit fix", {
1472
+ stdio: "inherit",
1473
+ cwd: process.cwd()
1474
+ });
1475
+ log.blank();
1476
+ log.success("Audit fix completed.");
1477
+ } catch {
1478
+ log.warn("Some vulnerabilities could not be fixed automatically.");
1479
+ log.info(`Run ${c.bold}npm audit fix --force${c.reset} to force-fix (may include breaking changes).`);
1480
+ }
1481
+ return;
1482
+ }
1483
+ if (jsonOutput) {
1484
+ try {
1485
+ execSync6("npm audit --json", {
1486
+ stdio: "inherit",
1487
+ cwd: process.cwd()
1488
+ });
1489
+ } catch {
1490
+ process.exit(1);
1491
+ }
1492
+ return;
1493
+ }
1494
+ log.info("Scanning dependencies for known vulnerabilities...");
1495
+ log.blank();
1496
+ let auditOutput;
1497
+ let hasVulnerabilities = false;
1498
+ try {
1499
+ auditOutput = execSync6("npm audit --json 2>/dev/null", {
1500
+ cwd: process.cwd(),
1501
+ encoding: "utf-8"
1502
+ });
1503
+ } catch (err) {
1504
+ auditOutput = err.stdout || "{}";
1505
+ hasVulnerabilities = true;
1506
+ }
1507
+ try {
1508
+ const report = JSON.parse(auditOutput);
1509
+ const meta = report.metadata || {};
1510
+ const vulns = meta.vulnerabilities || {
1511
+ info: 0,
1512
+ low: 0,
1513
+ moderate: 0,
1514
+ high: 0,
1515
+ critical: 0,
1516
+ total: 0
1517
+ };
1518
+ const deps = meta.dependencies || {};
1519
+ const total = vulns.total || vulns.critical + vulns.high + vulns.moderate + vulns.low + (vulns.info || 0);
1520
+ divider();
1521
+ log.blank();
1522
+ if (total === 0) {
1523
+ log.success(`${c.bold}No vulnerabilities found!${c.reset} \u2728`);
1524
+ log.blank();
1525
+ if (deps.total) {
1526
+ log.info(`Scanned ${c.bold}${deps.total}${c.reset} dependencies.`);
1527
+ }
1528
+ } else {
1529
+ log.warn(`${c.bold}${total} vulnerabilit${total === 1 ? "y" : "ies"} found${c.reset}`);
1530
+ log.blank();
1531
+ const levels = [
1532
+ { name: "critical", count: vulns.critical },
1533
+ { name: "high", count: vulns.high },
1534
+ { name: "moderate", count: vulns.moderate },
1535
+ { name: "low", count: vulns.low }
1536
+ ];
1537
+ for (const level of levels) {
1538
+ if (level.count > 0) {
1539
+ const color = severityColor(level.name);
1540
+ const icon = severityIcon(level.name);
1541
+ console.log(` ${icon} ${color}${level.count} ${level.name}${c.reset}`);
1542
+ }
1543
+ }
1544
+ log.blank();
1545
+ if (deps.total) {
1546
+ log.info(`Scanned ${c.bold}${deps.total}${c.reset} dependencies.`);
1547
+ }
1548
+ log.info(`Run ${c.bold}blumen audit --fix${c.reset} to attempt automatic fixes.`);
1549
+ }
1550
+ log.blank();
1551
+ divider();
1552
+ log.blank();
1553
+ if (ci && (vulns.critical > 0 || vulns.high > 0)) {
1554
+ log.error("CI check failed: high or critical vulnerabilities detected.");
1555
+ process.exit(1);
1556
+ }
1557
+ } catch {
1558
+ log.info("Detailed report:");
1559
+ log.blank();
1560
+ try {
1561
+ execSync6("npm audit", {
1562
+ stdio: "inherit",
1563
+ cwd: process.cwd()
1564
+ });
1565
+ } catch {
1566
+ if (ci) {
1567
+ process.exit(1);
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ // cli/commands/test.ts
1574
+ import { execSync as execSync7 } from "child_process";
1575
+ async function test(args = []) {
1576
+ banner();
1577
+ const watch = args.includes("--watch");
1578
+ const coverage = args.includes("--coverage");
1579
+ const ui = args.includes("--ui");
1580
+ const pattern = args.filter((a) => !a.startsWith("--")).join(" ");
1581
+ let cmd = "npx vitest run";
1582
+ if (watch) {
1583
+ cmd = "npx vitest";
1584
+ log.info("Starting test watcher...");
1585
+ } else if (ui) {
1586
+ cmd = "npx vitest --ui";
1587
+ log.info("Opening Vitest UI...");
1588
+ } else if (coverage) {
1589
+ cmd = "npx vitest run --coverage";
1590
+ log.info("Running tests with coverage...");
1591
+ } else {
1592
+ log.info("Running tests...");
1593
+ }
1594
+ if (pattern) {
1595
+ cmd += ` ${pattern}`;
1596
+ }
1597
+ log.blank();
1598
+ divider();
1599
+ log.blank();
1600
+ try {
1601
+ execSync7(cmd, {
1602
+ stdio: "inherit",
1603
+ cwd: process.cwd(),
1604
+ env: {
1605
+ ...process.env,
1606
+ NODE_ENV: "test"
1607
+ }
1608
+ });
1609
+ log.blank();
1610
+ divider();
1611
+ log.blank();
1612
+ log.success("All tests passed! \u2728");
1613
+ log.blank();
1614
+ } catch (err) {
1615
+ log.blank();
1616
+ divider();
1617
+ log.blank();
1618
+ if (err.status) {
1619
+ log.error(`Tests failed with exit code ${err.status}`);
1620
+ process.exit(err.status);
1621
+ } else {
1622
+ log.error("Test runner encountered an error");
1623
+ process.exit(1);
1624
+ }
1625
+ }
1626
+ }
1627
+
1628
+ // cli/commands/bench.ts
1629
+ import * as http from "http";
1630
+ function percentile(sorted, p) {
1631
+ const idx = Math.ceil(p / 100 * sorted.length) - 1;
1632
+ return sorted[Math.max(0, idx)];
1633
+ }
1634
+ function makeRequest(url) {
1635
+ return new Promise((resolve6, reject) => {
1636
+ const start2 = performance.now();
1637
+ const req = http.get(url, (res) => {
1638
+ let body = "";
1639
+ res.on("data", (chunk) => {
1640
+ body += chunk;
1641
+ });
1642
+ res.on("end", () => {
1643
+ resolve6({
1644
+ latency: performance.now() - start2,
1645
+ status: res.statusCode || 0,
1646
+ cacheHeader: res.headers["x-blumen-cache"] || ""
1647
+ });
1648
+ });
1649
+ });
1650
+ req.on("error", reject);
1651
+ req.setTimeout(1e4, () => {
1652
+ req.destroy();
1653
+ reject(new Error("Timeout"));
1654
+ });
1655
+ });
1656
+ }
1657
+ async function runBenchmark(url, totalRequests, concurrency) {
1658
+ const latencies = [];
1659
+ let successful = 0;
1660
+ let failed = 0;
1661
+ let cacheHits = 0;
1662
+ let cacheMisses = 0;
1663
+ let streamed = 0;
1664
+ let completed = 0;
1665
+ const start2 = performance.now();
1666
+ for (let i = 0; i < totalRequests; i += concurrency) {
1667
+ const batchSize = Math.min(concurrency, totalRequests - i);
1668
+ const batch = Array.from(
1669
+ { length: batchSize },
1670
+ () => makeRequest(url).then((result) => {
1671
+ latencies.push(result.latency);
1672
+ if (result.status >= 200 && result.status < 400) {
1673
+ successful++;
1674
+ } else {
1675
+ failed++;
1676
+ }
1677
+ if (result.cacheHeader === "HIT" || result.cacheHeader === "STALE")
1678
+ cacheHits++;
1679
+ else if (result.cacheHeader === "MISS")
1680
+ cacheMisses++;
1681
+ else if (result.cacheHeader === "STREAM")
1682
+ streamed++;
1683
+ completed++;
1684
+ }).catch(() => {
1685
+ failed++;
1686
+ completed++;
1687
+ })
1688
+ );
1689
+ await Promise.all(batch);
1690
+ const pct = Math.round(completed / totalRequests * 100);
1691
+ process.stdout.write(`\r \u25CF Progress: ${pct}% (${completed}/${totalRequests})`);
1692
+ }
1693
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
1694
+ const totalTimeMs = performance.now() - start2;
1695
+ const sorted = latencies.sort((a, b) => a - b);
1696
+ return {
1697
+ url,
1698
+ totalRequests,
1699
+ successfulRequests: successful,
1700
+ failedRequests: failed,
1701
+ totalTimeMs,
1702
+ requestsPerSec: successful / totalTimeMs * 1e3,
1703
+ latencies: sorted,
1704
+ p50: sorted.length ? percentile(sorted, 50) : 0,
1705
+ p95: sorted.length ? percentile(sorted, 95) : 0,
1706
+ p99: sorted.length ? percentile(sorted, 99) : 0,
1707
+ min: sorted.length ? sorted[0] : 0,
1708
+ max: sorted.length ? sorted[sorted.length - 1] : 0,
1709
+ avg: sorted.length ? sorted.reduce((a, b) => a + b, 0) / sorted.length : 0,
1710
+ cacheHits,
1711
+ cacheMisses,
1712
+ streamed
1713
+ };
1714
+ }
1715
+ function formatMs(ms) {
1716
+ if (ms < 1)
1717
+ return `${(ms * 1e3).toFixed(0)}\xB5s`;
1718
+ if (ms < 1e3)
1719
+ return `${ms.toFixed(1)}ms`;
1720
+ return `${(ms / 1e3).toFixed(2)}s`;
1721
+ }
1722
+ function printResult(result) {
1723
+ console.log(` ${c.bold}URL${c.reset} ${result.url}`);
1724
+ console.log(` ${c.bold}Requests${c.reset} ${result.successfulRequests}/${result.totalRequests} successful${result.failedRequests > 0 ? ` (${result.failedRequests} failed)` : ""}`);
1725
+ console.log(` ${c.bold}Duration${c.reset} ${formatMs(result.totalTimeMs)}`);
1726
+ console.log(` ${c.bold}Throughput${c.reset} ${c.green}${result.requestsPerSec.toFixed(1)} req/sec${c.reset}`);
1727
+ console.log("");
1728
+ console.log(` ${c.bold}Latency${c.reset}`);
1729
+ console.log(` min ${formatMs(result.min)}`);
1730
+ console.log(` avg ${formatMs(result.avg)}`);
1731
+ console.log(` p50 ${formatMs(result.p50)}`);
1732
+ console.log(` p95 ${c.yellow}${formatMs(result.p95)}${c.reset}`);
1733
+ console.log(` p99 ${c.yellow}${formatMs(result.p99)}${c.reset}`);
1734
+ console.log(` max ${formatMs(result.max)}`);
1735
+ if (result.cacheHits > 0 || result.cacheMisses > 0 || result.streamed > 0) {
1736
+ console.log("");
1737
+ console.log(` ${c.bold}Cache${c.reset}`);
1738
+ if (result.cacheHits > 0)
1739
+ console.log(` HIT ${result.cacheHits} (${(result.cacheHits / result.totalRequests * 100).toFixed(0)}%)`);
1740
+ if (result.cacheMisses > 0)
1741
+ console.log(` MISS ${result.cacheMisses}`);
1742
+ if (result.streamed > 0)
1743
+ console.log(` STREAM ${result.streamed}`);
1744
+ }
1745
+ }
1746
+ async function bench(args = []) {
1747
+ banner();
1748
+ let baseUrl = "http://localhost:3000";
1749
+ let totalRequests = 200;
1750
+ let concurrency = 10;
1751
+ for (let i = 0; i < args.length; i++) {
1752
+ if (args[i] === "--url" && args[i + 1]) {
1753
+ baseUrl = args[++i];
1754
+ } else if (args[i] === "--requests" && args[i + 1]) {
1755
+ totalRequests = parseInt(args[++i], 10);
1756
+ } else if (args[i] === "--concurrency" && args[i + 1]) {
1757
+ concurrency = parseInt(args[++i], 10);
1758
+ }
1759
+ }
1760
+ log.info(`Benchmarking ${c.bold}${baseUrl}${c.reset}`);
1761
+ log.info(`${totalRequests} requests, ${concurrency} concurrent connections`);
1762
+ log.blank();
1763
+ divider();
1764
+ const endpoints = [
1765
+ { name: "Homepage (SSR)", path: "/" },
1766
+ { name: "About (SSR)", path: "/about" },
1767
+ { name: "Static Asset", path: "/static/js/runtime.js" }
1768
+ ];
1769
+ for (const endpoint of endpoints) {
1770
+ const url = `${baseUrl}${endpoint.path}`;
1771
+ log.blank();
1772
+ console.log(` ${c.bold}${c.cyan}\u25B8 ${endpoint.name}${c.reset}`);
1773
+ log.blank();
1774
+ try {
1775
+ const result = await runBenchmark(url, totalRequests, concurrency);
1776
+ printResult(result);
1777
+ } catch (err) {
1778
+ log.error(` Failed to benchmark ${url}: ${err.message}`);
1779
+ log.info(` Make sure the server is running (${c.bold}blumen dev${c.reset} or ${c.bold}blumen start${c.reset})`);
1780
+ }
1781
+ log.blank();
1782
+ divider();
1783
+ }
1784
+ log.blank();
1785
+ log.success("Benchmark complete! \u2728");
1786
+ log.blank();
1787
+ log.info(`For production load testing, use k6:`);
1788
+ log.info(` ${c.bold}k6 run benchmarks/k6-load-test.js${c.reset}`);
1789
+ log.blank();
1790
+ }
1791
+
1792
+ // cli/commands/export.ts
1793
+ import { execSync as execSync8, spawn as spawn3 } from "child_process";
1794
+ import * as fs7 from "fs";
1795
+ import * as path6 from "path";
1796
+ import * as http2 from "http";
1797
+ async function renderPage(route, ssrUrl) {
1798
+ const body = JSON.stringify({
1799
+ path: route,
1800
+ query: {},
1801
+ params: {}
1802
+ });
1803
+ return new Promise((resolve6, reject) => {
1804
+ const req = http2.request(ssrUrl, {
1805
+ method: "POST",
1806
+ headers: {
1807
+ "Content-Type": "application/json",
1808
+ "Content-Length": Buffer.byteLength(body)
1809
+ }
1810
+ }, (res) => {
1811
+ let data = "";
1812
+ res.on("data", (chunk) => data += chunk);
1813
+ res.on("end", () => {
1814
+ try {
1815
+ const json = JSON.parse(data);
1816
+ if (json.html) {
1817
+ resolve6(json.html);
1818
+ } else {
1819
+ reject(new Error("SSR response missing html field"));
1820
+ }
1821
+ } catch {
1822
+ reject(new Error(`Invalid SSR response: ${data.slice(0, 100)}`));
1823
+ }
1824
+ });
1825
+ });
1826
+ req.on("error", (err) => reject(new Error(`SSR server unreachable: ${err.message}`)));
1827
+ req.setTimeout(15e3, () => {
1828
+ req.destroy();
1829
+ reject(new Error("SSR timeout"));
1830
+ });
1831
+ req.write(body);
1832
+ req.end();
1833
+ });
1834
+ }
1835
+ function discoverRoutes() {
1836
+ const pagesDir = path6.resolve("app/pages");
1837
+ const routes = [];
1838
+ function scan(dir, prefix) {
1839
+ if (!fs7.existsSync(dir))
1840
+ return;
1841
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1842
+ for (const entry of entries) {
1843
+ if (entry.name.startsWith("_") || entry.name.startsWith("."))
1844
+ continue;
1845
+ if (entry.isDirectory()) {
1846
+ scan(path6.join(dir, entry.name), `${prefix}/${entry.name.toLowerCase()}`);
1847
+ } else if (entry.name.endsWith(".tsx") && !entry.name.startsWith("NotFound")) {
1848
+ const name = entry.name.replace(".tsx", "");
1849
+ if (name.toLowerCase() === "home") {
1850
+ routes.push("/");
1851
+ } else if (name === "index") {
1852
+ routes.push(prefix || "/");
1853
+ } else if (!name.includes("[")) {
1854
+ routes.push(`${prefix}/${name.toLowerCase()}`);
1855
+ }
1856
+ }
1857
+ }
1858
+ }
1859
+ scan(pagesDir, "");
1860
+ return routes;
1861
+ }
1862
+ async function waitForServer(url, maxWaitMs = 15e3) {
1863
+ const start2 = Date.now();
1864
+ while (Date.now() - start2 < maxWaitMs) {
1865
+ try {
1866
+ await new Promise((resolve6, reject) => {
1867
+ const req = http2.get(url, () => resolve6());
1868
+ req.on("error", reject);
1869
+ req.setTimeout(1e3, () => {
1870
+ req.destroy();
1871
+ reject(new Error("timeout"));
1872
+ });
1873
+ });
1874
+ return true;
1875
+ } catch {
1876
+ await new Promise((r) => setTimeout(r, 500));
1877
+ }
1878
+ }
1879
+ return false;
1880
+ }
1881
+ async function exportSite(args = []) {
1882
+ banner();
1883
+ let outDir = "dist/export";
1884
+ for (let i = 0; i < args.length; i++) {
1885
+ if (args[i] === "--out" && args[i + 1]) {
1886
+ outDir = args[++i];
1887
+ }
1888
+ }
1889
+ log.info("Exporting static site...");
1890
+ log.blank();
1891
+ log.info("Step 1/4: Building production bundle...");
1892
+ try {
1893
+ execSync8("npx tsx cli/blumen.ts build", { stdio: "inherit", cwd: process.cwd() });
1894
+ } catch {
1895
+ log.error("Build failed. Fix errors and try again.");
1896
+ process.exit(1);
1897
+ }
1898
+ log.blank();
1899
+ log.info("Step 2/4: Starting SSR server for pre-rendering...");
1900
+ const ssrProcess = spawn3("node", ["dist/ssr-server.js"], {
1901
+ cwd: process.cwd(),
1902
+ env: { ...process.env, NODE_ENV: "production", PORT: "4001" },
1903
+ stdio: "pipe"
1904
+ });
1905
+ const ssrUrl = "http://localhost:4001/render";
1906
+ const serverReady = await waitForServer("http://localhost:4001/health", 15e3);
1907
+ if (!serverReady) {
1908
+ log.warn("SSR server health check failed, attempting rendering anyway...");
1909
+ }
1910
+ log.blank();
1911
+ log.info("Step 3/4: Pre-rendering pages...");
1912
+ const routes = discoverRoutes();
1913
+ const outputDir = path6.resolve(outDir);
1914
+ fs7.mkdirSync(outputDir, { recursive: true });
1915
+ let success = 0;
1916
+ let failed = 0;
1917
+ for (const route of routes) {
1918
+ try {
1919
+ const html = await renderPage(route, ssrUrl);
1920
+ const filePath = route === "/" ? path6.join(outputDir, "index.html") : path6.join(outputDir, route.slice(1), "index.html");
1921
+ fs7.mkdirSync(path6.dirname(filePath), { recursive: true });
1922
+ fs7.writeFileSync(filePath, html, "utf-8");
1923
+ console.log(` ${c.green}\u2713${c.reset} ${route} \u2192 ${path6.relative(process.cwd(), filePath)}`);
1924
+ success++;
1925
+ } catch (err) {
1926
+ console.log(` ${c.red}\u2717${c.reset} ${route}: ${err.message}`);
1927
+ failed++;
1928
+ }
1929
+ }
1930
+ ssrProcess.kill("SIGTERM");
1931
+ log.blank();
1932
+ log.info("Step 4/4: Copying static assets...");
1933
+ const staticDir = path6.resolve("static");
1934
+ if (fs7.existsSync(staticDir)) {
1935
+ copyDir(staticDir, path6.join(outputDir, "static"));
1936
+ log.success("Static assets copied.");
1937
+ }
1938
+ const distJs = path6.resolve("dist/client");
1939
+ if (fs7.existsSync(distJs)) {
1940
+ copyDir(distJs, path6.join(outputDir, "static/js"));
1941
+ }
1942
+ log.blank();
1943
+ divider();
1944
+ log.blank();
1945
+ log.success(`Static export complete! \u2728`);
1946
+ log.info(`${success} page(s) exported${failed > 0 ? `, ${failed} failed` : ""}`);
1947
+ log.info(`Output: ${c.bold}${outDir}/${c.reset}`);
1948
+ log.blank();
1949
+ log.info(`Deploy anywhere:`);
1950
+ log.info(` ${c.dim}GitHub Pages:${c.reset} push ${outDir}/ to gh-pages branch`);
1951
+ log.info(` ${c.dim}Netlify:${c.reset} set publish directory to ${outDir}/`);
1952
+ log.info(` ${c.dim}S3:${c.reset} aws s3 sync ${outDir}/ s3://my-bucket`);
1953
+ log.info(` ${c.dim}Local:${c.reset} npx serve ${outDir}/`);
1954
+ log.blank();
1955
+ }
1956
+ function copyDir(src, dest) {
1957
+ fs7.mkdirSync(dest, { recursive: true });
1958
+ const entries = fs7.readdirSync(src, { withFileTypes: true });
1959
+ for (const entry of entries) {
1960
+ const srcPath = path6.join(src, entry.name);
1961
+ const destPath = path6.join(dest, entry.name);
1962
+ if (entry.isDirectory()) {
1963
+ copyDir(srcPath, destPath);
1964
+ } else {
1965
+ fs7.copyFileSync(srcPath, destPath);
1966
+ }
1967
+ }
1968
+ }
1969
+
1970
+ // cli/commands/migrate.ts
1971
+ import * as fs8 from "fs";
1972
+ import * as path7 from "path";
1973
+ function isNextJsProject(dir) {
1974
+ return fs8.existsSync(path7.join(dir, "next.config.js")) || fs8.existsSync(path7.join(dir, "next.config.mjs")) || fs8.existsSync(path7.join(dir, "next.config.ts")) || fs8.existsSync(path7.join(dir, "pages")) || fs8.existsSync(path7.join(dir, "src/pages"));
1975
+ }
1976
+ function findPagesDir(dir) {
1977
+ if (fs8.existsSync(path7.join(dir, "pages")))
1978
+ return path7.join(dir, "pages");
1979
+ if (fs8.existsSync(path7.join(dir, "src/pages")))
1980
+ return path7.join(dir, "src/pages");
1981
+ if (fs8.existsSync(path7.join(dir, "app")))
1982
+ return path7.join(dir, "app");
1983
+ return null;
1984
+ }
1985
+ function getAllTsxFiles(dir) {
1986
+ const results = [];
1987
+ if (!fs8.existsSync(dir))
1988
+ return results;
1989
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
1990
+ for (const entry of entries) {
1991
+ const fullPath = path7.join(dir, entry.name);
1992
+ if (entry.isDirectory()) {
1993
+ results.push(...getAllTsxFiles(fullPath));
1994
+ } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
1995
+ results.push(fullPath);
1996
+ }
1997
+ }
1998
+ return results;
1999
+ }
2000
+ function rewriteImports(content, depth) {
2001
+ const prefix = "../".repeat(depth) || "./";
2002
+ const sharedPrefix = `${prefix}../shared`;
2003
+ content = content.replace(
2004
+ /import\s+(?:Link|{[^}]*})\s+from\s+['"]next\/link['"]/g,
2005
+ `import { Link } from "${sharedPrefix}/Link"`
2006
+ );
2007
+ content = content.replace(
2008
+ /import\s+(?:Head|{[^}]*})\s+from\s+['"]next\/head['"]/g,
2009
+ `import { BlumenHead } from "${sharedPrefix}/BlumenHead"`
2010
+ );
2011
+ content = content.replace(
2012
+ /import\s+(?:Image|{[^}]*})\s+from\s+['"]next\/image['"]/g,
2013
+ `// TODO: Replace next/image with standard <img> or BlumenImage
2014
+ // import Image from "next/image"`
2015
+ );
2016
+ content = content.replace(
2017
+ /import\s+(?:{[^}]*})\s+from\s+['"]next\/router['"]/g,
2018
+ `import { useRouter } from "${sharedPrefix}/RouterContext"`
2019
+ );
2020
+ return content;
2021
+ }
2022
+ function rewriteDataFetching(content) {
2023
+ const changes = [];
2024
+ if (content.includes("getServerSideProps")) {
2025
+ content = content.replace(/getServerSideProps/g, "getServerProps");
2026
+ changes.push("getServerSideProps \u2192 getServerProps");
2027
+ }
2028
+ if (content.includes("getStaticProps") && !content.includes("force-static")) {
2029
+ content += `
2030
+
2031
+ // Mark this page for static generation
2032
+ export const dynamic = 'force-static';
2033
+ `;
2034
+ changes.push("Added export const dynamic = 'force-static'");
2035
+ }
2036
+ content = content.replace(/GetServerSidePropsContext/g, "BlumenContext");
2037
+ if (content.includes("BlumenContext") && !content.includes("import")) {
2038
+ changes.push("GetServerSidePropsContext \u2192 BlumenContext");
2039
+ }
2040
+ return { content, changes };
2041
+ }
2042
+ async function migrate(args = []) {
2043
+ banner();
2044
+ const dryRun = args.includes("--dry-run");
2045
+ const sourceDir = args.find((a) => !a.startsWith("--")) || ".";
2046
+ const absDir = path7.resolve(sourceDir);
2047
+ log.info(`Analyzing ${c.bold}${absDir}${c.reset} for Next.js project...`);
2048
+ log.blank();
2049
+ if (!isNextJsProject(absDir)) {
2050
+ log.error("No Next.js project detected.");
2051
+ log.info("Expected: next.config.js/mjs/ts or pages/ directory");
2052
+ process.exit(1);
2053
+ }
2054
+ log.success("Next.js project detected!");
2055
+ log.blank();
2056
+ const result = {
2057
+ moved: [],
2058
+ rewritten: [],
2059
+ warnings: [],
2060
+ errors: []
2061
+ };
2062
+ const pagesDir = findPagesDir(absDir);
2063
+ if (!pagesDir) {
2064
+ log.error("Could not find pages/ or src/pages/ directory");
2065
+ process.exit(1);
2066
+ }
2067
+ const isAppRouter = pagesDir.endsWith("/app");
2068
+ if (isAppRouter) {
2069
+ result.warnings.push("Next.js App Router detected. Migration from App Router requires manual review.");
2070
+ }
2071
+ log.info(`Pages directory: ${c.bold}${path7.relative(absDir, pagesDir)}/${c.reset}`);
2072
+ log.blank();
2073
+ divider();
2074
+ log.blank();
2075
+ console.log(` ${c.bold}${c.cyan}\u25B8 Step 1: Analyzing files${c.reset}`);
2076
+ log.blank();
2077
+ const files = getAllTsxFiles(pagesDir);
2078
+ console.log(` Found ${files.length} source file(s)`);
2079
+ log.blank();
2080
+ console.log(` ${c.bold}${c.cyan}\u25B8 Step 2: Rewriting imports${c.reset}`);
2081
+ log.blank();
2082
+ for (const file of files) {
2083
+ const relPath = path7.relative(pagesDir, file);
2084
+ const depth = relPath.split("/").length;
2085
+ let content = fs8.readFileSync(file, "utf-8");
2086
+ let modified = false;
2087
+ const fileChanges = [];
2088
+ const newContent = rewriteImports(content, depth);
2089
+ if (newContent !== content) {
2090
+ content = newContent;
2091
+ modified = true;
2092
+ fileChanges.push("Import rewrites");
2093
+ }
2094
+ const { content: dfContent, changes } = rewriteDataFetching(content);
2095
+ if (changes.length > 0) {
2096
+ content = dfContent;
2097
+ modified = true;
2098
+ fileChanges.push(...changes);
2099
+ }
2100
+ if (modified) {
2101
+ if (!dryRun) {
2102
+ fs8.writeFileSync(file, content, "utf-8");
2103
+ }
2104
+ console.log(` ${c.green}\u2713${c.reset} ${relPath}: ${fileChanges.join(", ")}`);
2105
+ result.rewritten.push(relPath);
2106
+ }
2107
+ }
2108
+ if (result.rewritten.length === 0) {
2109
+ console.log(` ${c.dim}No import rewrites needed${c.reset}`);
2110
+ }
2111
+ log.blank();
2112
+ console.log(` ${c.bold}${c.cyan}\u25B8 Step 3: File structure mapping${c.reset}`);
2113
+ log.blank();
2114
+ const mappings = [
2115
+ ["pages/_app.tsx", "\u2192", "app/shared/_app.tsx"],
2116
+ ["pages/_document.tsx", "\u2192", "app/shared/_document.tsx"],
2117
+ ["pages/index.tsx", "\u2192", "app/pages/Home.tsx"],
2118
+ ["pages/[slug].tsx", "\u2192", "app/pages/[slug].tsx"],
2119
+ ["pages/api/", "\u2192", "app/api/ (Blumen API routes)"],
2120
+ ["public/", "\u2192", "static/"],
2121
+ ["styles/", "\u2192", "app/shared/styles/"]
2122
+ ];
2123
+ for (const [from, arrow, to] of mappings) {
2124
+ console.log(` ${c.dim}${from}${c.reset} ${arrow} ${c.bold}${to}${c.reset}`);
2125
+ }
2126
+ log.blank();
2127
+ divider();
2128
+ log.blank();
2129
+ console.log(` ${c.bold}Migration Summary${c.reset}`);
2130
+ log.blank();
2131
+ const summaryTable = [
2132
+ ["Files analyzed", `${files.length}`],
2133
+ ["Imports rewritten", `${result.rewritten.length}`],
2134
+ ["Warnings", `${result.warnings.length}`],
2135
+ ["Mode", dryRun ? "Dry run (no changes)" : "Applied"]
2136
+ ];
2137
+ for (const [label, value] of summaryTable) {
2138
+ console.log(` ${c.dim}${label}:${c.reset} ${value}`);
2139
+ }
2140
+ log.blank();
2141
+ if (result.warnings.length > 0) {
2142
+ console.log(` ${c.yellow}${c.bold}Warnings:${c.reset}`);
2143
+ for (const w of result.warnings) {
2144
+ console.log(` ${c.yellow}\u26A0${c.reset} ${w}`);
2145
+ }
2146
+ log.blank();
2147
+ }
2148
+ console.log(` ${c.bold}Next Steps${c.reset}`);
2149
+ log.blank();
2150
+ console.log(` 1. Move your page files to ${c.bold}app/pages/${c.reset}`);
2151
+ console.log(` 2. Move ${c.bold}_app.tsx${c.reset} to ${c.bold}app/shared/_app.tsx${c.reset}`);
2152
+ console.log(` 3. Move ${c.bold}public/${c.reset} to ${c.bold}static/${c.reset}`);
2153
+ console.log(` 4. Replace ${c.bold}next/image${c.reset} usage with standard ${c.bold}<img>${c.reset} or ${c.bold}<BlumenImage>${c.reset}`);
2154
+ console.log(` 5. Run ${c.bold}blumen dev${c.reset} to test`);
2155
+ log.blank();
2156
+ console.log(` ${c.bold}API Comparison${c.reset}`);
2157
+ log.blank();
2158
+ const comparison = [
2159
+ ["next/link", "Link from shared/Link", "\u2713 Auto-converted"],
2160
+ ["next/head", "BlumenHead", "\u2713 Auto-converted"],
2161
+ ["next/router", "useRouter from RouterContext", "\u2713 Auto-converted"],
2162
+ ["getServerSideProps", "getServerProps", "\u2713 Auto-converted"],
2163
+ ["getStaticProps", "getStaticProps", "\u2713 Same API + force-static"],
2164
+ ["next/image", "<img> or <BlumenImage>", "\u26A0 Manual review"],
2165
+ ["API Routes (pages/api)", "app/api/ handlers", "\u26A0 Manual migration"],
2166
+ ["Middleware", "Go middleware", "\u26A0 Manual migration"]
2167
+ ];
2168
+ for (const [next, blumen, status] of comparison) {
2169
+ console.log(` ${c.dim}${next.padEnd(25)}${c.reset}\u2192 ${blumen.padEnd(30)} ${status}`);
2170
+ }
2171
+ log.blank();
2172
+ }
2173
+
1381
2174
  // cli/blumen.ts
1382
2175
  async function main() {
1383
2176
  const command = process.argv[2];
@@ -1402,6 +2195,21 @@ async function main() {
1402
2195
  console.log(
1403
2196
  ` fonts Download and optimize fonts`
1404
2197
  );
2198
+ console.log(
2199
+ ` audit Scan dependencies for vulnerabilities`
2200
+ );
2201
+ console.log(
2202
+ ` test Run unit and component tests`
2203
+ );
2204
+ console.log(
2205
+ ` bench Run performance benchmarks`
2206
+ );
2207
+ console.log(
2208
+ ` export Export as a fully static site`
2209
+ );
2210
+ console.log(
2211
+ ` migrate Migrate a Next.js project to Blumen`
2212
+ );
1405
2213
  console.log("");
1406
2214
  console.log(` ${c.bold}Options${c.reset}`);
1407
2215
  console.log(` --help Show this help message`);
@@ -1432,6 +2240,21 @@ async function main() {
1432
2240
  case "fonts":
1433
2241
  await fontsCommand(process.argv.slice(3));
1434
2242
  break;
2243
+ case "audit":
2244
+ await audit(process.argv.slice(3));
2245
+ break;
2246
+ case "test":
2247
+ await test(process.argv.slice(3));
2248
+ break;
2249
+ case "bench":
2250
+ await bench(process.argv.slice(3));
2251
+ break;
2252
+ case "export":
2253
+ await exportSite(process.argv.slice(3));
2254
+ break;
2255
+ case "migrate":
2256
+ await migrate(process.argv.slice(3));
2257
+ break;
1435
2258
  default:
1436
2259
  log.error(
1437
2260
  `Unknown command: ${c.bold}${command}${c.reset}`