@valbuild/cli 0.150.0 → 0.150.1

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.
@@ -311,6 +311,16 @@ async function* runValidation({
311
311
  sourcePath
312
312
  };
313
313
  }
314
+ if (result.warning !== undefined) {
315
+ yield {
316
+ type: "validation-warning",
317
+ sourcePath,
318
+ ...result.warning,
319
+ ...(v.keyError ? {
320
+ keyError: true
321
+ } : {})
322
+ };
323
+ }
314
324
  if (result.fixableErrorMessage !== undefined) {
315
325
  fileErrors += 1;
316
326
  yield {
@@ -648,10 +658,13 @@ function getCachedFile(moduleFilePath, projectRoot, cache) {
648
658
  return entry;
649
659
  }
650
660
 
661
+ /** How many of a warning group's files are listed before "… N more". */
662
+ const WARNING_FILES_SHOWN = 5;
651
663
  async function validate({
652
664
  root,
653
665
  fix,
654
- watch
666
+ watch,
667
+ strict
655
668
  }) {
656
669
  const projectRoot = root ? path.resolve(root) : process.cwd();
657
670
  let prettier;
@@ -663,7 +676,10 @@ async function validate({
663
676
  const runOnce = () => validateOnce({
664
677
  projectRoot,
665
678
  fix: !!fix,
666
- prettier
679
+ prettier,
680
+ ...(strict ? {
681
+ strict: true
682
+ } : {})
667
683
  });
668
684
  if (!watch) {
669
685
  const totalErrors = await runOnce();
@@ -677,7 +693,7 @@ async function validate({
677
693
 
678
694
  /**
679
695
  * One validation pass over the project, printed as `val validate` prints it,
680
- * returning the number of errors found.
696
+ * returning the number of errors found -- and of warnings, under `strict`.
681
697
  *
682
698
  * Re-reads config and val files on each call so it always reflects the latest
683
699
  * state on disk: `val validate` calls it once or on every change (`--watch`),
@@ -687,7 +703,8 @@ async function validateOnce({
687
703
  projectRoot,
688
704
  fix,
689
705
  prettier,
690
- blockingOnly = false
706
+ blockingOnly = false,
707
+ strict = false
691
708
  }) {
692
709
  const valConfigFile = await findAndEvalValConfigFile(projectRoot);
693
710
  const resolvedValConfigFile = valConfigFile ? {
@@ -704,6 +721,7 @@ async function validateOnce({
704
721
  console.log(pc.greenBright(`Found ${valFiles.length} files...`));
705
722
  const fixedFiles = new Set();
706
723
  let totalErrors = 0;
724
+ let totalWarnings = 0;
707
725
  let repairedOnSave = 0;
708
726
 
709
727
  // Caches each val file's parsed source so files are read/parsed at most once
@@ -750,10 +768,15 @@ async function validateOnce({
750
768
  })) {
751
769
  switch (event.type) {
752
770
  case "file-valid":
753
- valid.push({
754
- file: relFile(event.file),
755
- durationMs: event.durationMs
756
- });
771
+ // No errors, but it may have warnings: then it is shown with them.
772
+ if (reports.has(relFile(event.file))) {
773
+ reportFor(event.file).durationMs = event.durationMs;
774
+ } else {
775
+ valid.push({
776
+ file: relFile(event.file),
777
+ durationMs: event.durationMs
778
+ });
779
+ }
757
780
  break;
758
781
  case "file-error-count":
759
782
  reportFor(event.file).durationMs = event.durationMs;
@@ -781,6 +804,21 @@ async function validateOnce({
781
804
  } : {})
782
805
  });
783
806
  break;
807
+ case "validation-warning":
808
+ totalWarnings += 1;
809
+ reportFor(moduleOf(event.sourcePath)).diagnostics.push({
810
+ severity: "warning",
811
+ sourcePath: event.sourcePath,
812
+ message: event.message,
813
+ files: event.files,
814
+ ...(event.hint !== undefined ? {
815
+ hint: event.hint
816
+ } : {}),
817
+ ...(event.keyError ? {
818
+ keyError: true
819
+ } : {})
820
+ });
821
+ break;
784
822
  case "unknown-fix":
785
823
  reportFor(moduleOf(event.sourcePath)).diagnostics.push({
786
824
  severity: "error",
@@ -823,28 +861,67 @@ async function validateOnce({
823
861
 
824
862
  // Renders a module's diagnostics under a single left "│" gutter bar, with the
825
863
  // file name on top and blank gutter lines for air. Output flows least- to
826
- // most-actionable top-to-bottom, so fixable diagnostics are shown last (at the
827
- // bottom, nearest the prompt).
864
+ // most-actionable top-to-bottom: warnings first, then errors, and fixable
865
+ // errors last (at the bottom, nearest the prompt).
828
866
  const renderModule = report => {
829
867
  const bar = pc.dim("│");
830
- const diagnostics = [...report.diagnostics].sort((a, b) => a.severity === b.severity ? 0 : a.severity === "fixable" ? 1 : -1);
868
+ const rank = {
869
+ warning: 0,
870
+ error: 1,
871
+ fixable: 2
872
+ };
873
+ const diagnostics = [...report.diagnostics].sort((a, b) => rank[a.severity] - rank[b.severity]);
874
+ const warningCount = diagnostics.filter(d => d.severity === "warning").length;
831
875
  const fixableCount = diagnostics.filter(d => d.severity === "fixable").length;
832
- const total = diagnostics.length;
833
- const hasError = fixableCount < total;
834
- const symbol = hasError ? pc.red("✘") : pc.yellow("⚠");
835
- let label;
836
- if (fixableCount === total) {
837
- label = `${fixableCount} fixable`;
838
- } else if (fixableCount > 0) {
839
- label = `${total} error${total > 1 ? "s" : ""} (${fixableCount} fixable)`;
840
- } else {
841
- label = `${total} error${total > 1 ? "s" : ""}`;
876
+ const errorCount = diagnostics.length - warningCount;
877
+ const symbol = errorCount > 0 ? pc.red("✘") : pc.yellow("⚠");
878
+ const labels = [];
879
+ if (errorCount > 0) {
880
+ labels.push(`${errorCount} error${errorCount > 1 ? "s" : ""}${fixableCount > 0 ? ` (${fixableCount} fixable)` : ""}`);
881
+ }
882
+ if (warningCount > 0) {
883
+ labels.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
842
884
  }
843
- console.log(`${pc.bold(report.file)} ${symbol} ${label} ${pc.dim(`(${report.durationMs}ms)`)}`);
885
+ console.log(`${pc.bold(report.file)} ${symbol} ${labels.join(" · ")} ${pc.dim(`(${report.durationMs}ms)`)}`);
886
+ const locationOf = d => sourcePathToLocationParts(d.sourcePath, projectRoot, sourceFileCache, d.keyError ? "key" : "value");
887
+
888
+ // Warnings say the same thing about many values (every local image in a
889
+ // gallery), so each message is printed once, over the places it is about.
890
+ const warningGroups = new Map();
844
891
  for (const d of diagnostics) {
845
- const target = d.keyError ? "key" : "value";
846
- const dsym = d.severity === "fixable" ? pc.yellow("⚠") : pc.red("✘");
847
- const parts = sourcePathToLocationParts(d.sourcePath, projectRoot, sourceFileCache, target);
892
+ if (d.severity !== "warning") continue;
893
+ const group = warningGroups.get(d.message);
894
+ if (group) {
895
+ group.push(d);
896
+ } else {
897
+ warningGroups.set(d.message, [d]);
898
+ }
899
+ }
900
+ for (const [message, group] of warningGroups) {
901
+ var _group$find;
902
+ console.log(bar);
903
+ console.log(`${bar} ${pc.yellow("⚠")} ${message}`);
904
+ console.log(bar);
905
+ for (const d of group.slice(0, WARNING_FILES_SHOWN)) {
906
+ const parts = locationOf(d);
907
+ const files = d.files.join(", ");
908
+ // `file:line:col` first, so VS Code's terminal links it.
909
+ const where = parts ? `${parts.relativeFile}:${parts.line}:${parts.character}` : d.sourcePath;
910
+ console.log(`${bar} ${where} ${pc.dim(files)}`);
911
+ }
912
+ if (group.length > WARNING_FILES_SHOWN) {
913
+ console.log(`${bar} ${pc.dim(`… ${group.length - WARNING_FILES_SHOWN} more`)}`);
914
+ }
915
+ const hint = (_group$find = group.find(d => d.hint !== undefined)) === null || _group$find === void 0 ? void 0 : _group$find.hint;
916
+ if (hint !== undefined) {
917
+ console.log(bar);
918
+ console.log(`${bar} ${pc.dim(`→ ${hint}`)}`);
919
+ }
920
+ }
921
+ for (const d of diagnostics) {
922
+ if (d.severity === "warning") continue;
923
+ const parts = locationOf(d);
924
+ const dsym = pc.red("✘");
848
925
  console.log(bar);
849
926
  if (parts) {
850
927
  // `file:line:col` (no key/value label) so VS Code's terminal links it.
@@ -853,7 +930,7 @@ async function validateOnce({
853
930
  } else {
854
931
  console.log(`${bar} ${dsym} ${d.message}`);
855
932
  }
856
- const frame = sourcePathToCodeFrame(d.sourcePath, projectRoot, sourceFileCache, target);
933
+ const frame = sourcePathToCodeFrame(d.sourcePath, projectRoot, sourceFileCache, d.keyError ? "key" : "value");
857
934
  if (frame !== undefined) {
858
935
  console.log(bar);
859
936
  for (const frameLine of frame.split("\n")) {
@@ -886,10 +963,13 @@ async function validateOnce({
886
963
 
887
964
  // Render the grouped report least- to most-actionable, top-to-bottom, so the
888
965
  // most important things end up at the bottom nearest the prompt: valid files
889
- // and skipped modules first, then error-only modules, then fixable modules.
966
+ // and skipped modules first, then warning-only modules, then error-only
967
+ // modules, then fixable modules.
890
968
  const allReports = [...reports.values()];
969
+ const hasErrors = r => r.diagnostics.some(d => d.severity !== "warning");
970
+ const warningModules = allReports.filter(r => !hasErrors(r));
891
971
  const fixableModules = allReports.filter(r => r.diagnostics.some(d => d.severity === "fixable"));
892
- const errorModules = allReports.filter(r => !r.diagnostics.some(d => d.severity === "fixable"));
972
+ const errorModules = allReports.filter(r => hasErrors(r) && !r.diagnostics.some(d => d.severity === "fixable"));
893
973
  for (const v of valid) {
894
974
  console.log(pc.green("✔"), pc.dim(`${v.file} valid (${v.durationMs}ms)`));
895
975
  }
@@ -899,34 +979,34 @@ async function validateOnce({
899
979
  if (allReports.length > 0) {
900
980
  console.log("");
901
981
  }
902
- for (const report of [...errorModules, ...fixableModules]) {
982
+ for (const report of [...warningModules, ...errorModules, ...fixableModules]) {
903
983
  renderModule(report);
904
984
  }
905
985
  const fixableTotal = allReports.reduce((n, r) => n + r.diagnostics.filter(d => d.severity === "fixable").length, 0);
986
+ const errorFiles = allReports.filter(hasErrors).length;
987
+ const warningFiles = allReports.filter(r => r.diagnostics.some(d => d.severity === "warning")).length;
988
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
989
+ const warnings = `${plural(totalWarnings, "warning")} in ${plural(warningFiles, "file")}`;
990
+ const warningPart = totalWarnings > 0 ? `${warnings}${strict ? ", which --strict counts as errors" : ""}` : undefined;
991
+ const tail = [...(valid.length > 0 ? [`${valid.length} valid`] : []), ...(skipped.length > 0 ? [`${skipped.length} skipped`] : [])];
906
992
  if (totalErrors > 0) {
907
- let summary = `${totalErrors} error${totalErrors > 1 ? "s" : ""}`;
993
+ let summary = plural(totalErrors, "error");
908
994
  if (fixableTotal > 0) {
909
995
  summary += ` (${fixableTotal} fixable)`;
910
996
  }
911
- summary += ` across ${allReports.length} file${allReports.length > 1 ? "s" : ""}`;
912
- if (valid.length > 0) {
913
- summary += ` · ${valid.length} valid`;
914
- }
915
- if (skipped.length > 0) {
916
- summary += ` · ${skipped.length} skipped`;
917
- }
918
- console.log(pc.red("✘"), summary);
997
+ summary += ` across ${plural(errorFiles, "file")}`;
998
+ console.log(pc.red("✘"), [summary, ...(warningPart ? [warningPart] : []), ...tail].join(" · "));
999
+ } else if (warningPart && strict) {
1000
+ // Not "No validation errors" beside a ✘: under --strict this run failed,
1001
+ // and the warnings are why.
1002
+ console.log(pc.red("✘"), [`${warnings}, and --strict fails on warnings`, ...tail].join(" · "));
1003
+ } else if (warningPart) {
1004
+ console.log(pc.green("✔"), ["No validation errors", warningPart, ...tail].join(" · "));
919
1005
  } else {
920
- let summary = "No validation errors found";
921
- if (valid.length > 0) {
922
- summary += ` · ${valid.length} valid`;
923
- }
924
- if (skipped.length > 0) {
925
- summary += ` · ${skipped.length} skipped`;
926
- }
927
- console.log(pc.green("✔"), summary);
1006
+ console.log(pc.green("✔"), ["No validation errors found", ...tail].join(" · "));
928
1007
  }
929
- return blockingOnly ? totalErrors - repairedOnSave : totalErrors;
1008
+ const errors = blockingOnly ? totalErrors - repairedOnSave : totalErrors;
1009
+ return strict ? errors + totalWarnings : errors;
930
1010
  }
931
1011
 
932
1012
  // Directory names anywhere in the path that should never be watched.
@@ -1173,9 +1253,10 @@ const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1173
1253
 
1174
1254
  /**
1175
1255
  * `val connect`: opens admin's `/new` on "your own hosting", with what it can
1176
- * tell from here filled in -- the project `val.config.ts` names and the
1177
- * GitHub repository `origin` points at -- so the person only confirms. The
1178
- * link is printed too, for when no browser opens (over SSH, in a container).
1256
+ * tell from here filled in -- the project `val.config.ts` names, the GitHub
1257
+ * repository `origin` points at, and which folder of it this app is in (a
1258
+ * monorepo has several) -- so the person only confirms. The link is printed
1259
+ * too, for when no browser opens (over SSH, in a container).
1179
1260
  */
1180
1261
  async function connect(options) {
1181
1262
  const {
@@ -1183,20 +1264,23 @@ async function connect(options) {
1183
1264
  } = options;
1184
1265
  const projectRoot = root ? path.resolve(root) : process.cwd();
1185
1266
  const project = await tryGetProject(projectRoot);
1186
- const gitRemote = await tryGetGitRemote(projectRoot);
1267
+ const git = findGitDir(projectRoot);
1268
+ const gitRemote = git ? await tryGetGitRemote(git.config) : null;
1187
1269
  const repo = gitRemote !== null ? getGitHubOwnerAndRepo(gitRemote) : null;
1270
+ const folder = git ? repoFolder(git.top, projectRoot) : null;
1188
1271
  console.log("");
1189
1272
  if (project) {
1190
1273
  console.log(`Found ${pc.bold(`${project.orgName}/${project.projectName}`)} in val.config.ts`);
1191
1274
  }
1192
1275
  if (repo) {
1193
- console.log(`Found ${pc.bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")`);
1276
+ console.log(`Found ${pc.bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")${folder ? `, this app in ${pc.bold(folder)}` : ""}`);
1194
1277
  } else {
1195
1278
  console.log(pc.dim("No GitHub repository found: push this project to GitHub, or pick or create one in the browser."));
1196
1279
  }
1197
1280
  const url = connectUrl(host, {
1198
1281
  project,
1199
- repo
1282
+ repo,
1283
+ folder
1200
1284
  });
1201
1285
  const opened = openInBrowser(url);
1202
1286
  console.log(`\n${opened ? "Finish in your browser. If it did not open, go to:" : "Finish in your browser:"}\n${pc.cyan(url)}\n`);
@@ -1213,6 +1297,7 @@ function connectUrl(host, found) {
1213
1297
  });
1214
1298
  if (found.repo) {
1215
1299
  params.set("repo", `${found.repo.owner}/${found.repo.repo}`);
1300
+ if (found.folder) params.set("folder", found.folder);
1216
1301
  }
1217
1302
  if (found.project) {
1218
1303
  params.set("org", found.project.orgName);
@@ -1277,12 +1362,19 @@ function getGitHubOwnerAndRepo(gitRemote) {
1277
1362
  repo: match[2]
1278
1363
  } : null;
1279
1364
  }
1280
- async function tryGetGitRemote(root) {
1365
+
1366
+ /**
1367
+ * Where the app is in its repository, with forward slashes: `apps/web`, or ""
1368
+ * when it is at the top. Null when it is not inside the repository at all.
1369
+ */
1370
+ function repoFolder(top, projectRoot) {
1371
+ const relative = path.relative(top, projectRoot);
1372
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return null;
1373
+ return relative.split(path.sep).join("/");
1374
+ }
1375
+ async function tryGetGitRemote(gitConfigPath) {
1281
1376
  try {
1282
- const gitConfig = tryGetGitConfig(root);
1283
- if (!gitConfig) {
1284
- return null;
1285
- }
1377
+ const gitConfig = fs$1.readFileSync(gitConfigPath, "utf-8");
1286
1378
  const remoteMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = (.+)/);
1287
1379
  if (remoteMatch && remoteMatch[1]) {
1288
1380
  return remoteMatch[1].trim();
@@ -1293,13 +1385,18 @@ async function tryGetGitRemote(root) {
1293
1385
  return null;
1294
1386
  }
1295
1387
  }
1296
- function tryGetGitConfig(root) {
1388
+
1389
+ /** The repository `root` is in: its top folder and its `.git/config`. */
1390
+ function findGitDir(root) {
1297
1391
  let currentDir = root;
1298
1392
  let lastDir = null;
1299
1393
  while (currentDir !== lastDir) {
1300
1394
  const gitConfigPath = path.join(currentDir, ".git", "config");
1301
1395
  if (fs$1.existsSync(gitConfigPath)) {
1302
- return fs$1.readFileSync(gitConfigPath, "utf-8");
1396
+ return {
1397
+ top: currentDir,
1398
+ config: gitConfigPath
1399
+ };
1303
1400
  }
1304
1401
  lastDir = currentDir;
1305
1402
  currentDir = path.dirname(currentDir);
@@ -2469,6 +2566,14 @@ function resolveArtifactsDir(options) {
2469
2566
  async function loadBuilder() {
2470
2567
  try {
2471
2568
  const [build, node] = await Promise.all([import('@valbuild/tanstack-build'), import('@valbuild/tanstack-build/node')]);
2569
+ /*
2570
+ * Native rolldown rather than its WebAssembly build, whose glue the site's
2571
+ * lockfile picks and can break (`rolldown.ts` in tanstack-build). Asked
2572
+ * for, because the project's own tanstack-build may predate the choice.
2573
+ */
2574
+ if ("useRolldown" in build && "nativeRolldown" in node) {
2575
+ build.useRolldown(node.nativeRolldown);
2576
+ }
2472
2577
  return {
2473
2578
  status: "ok",
2474
2579
  build,
@@ -2482,6 +2587,52 @@ async function loadBuilder() {
2482
2587
  }
2483
2588
  }
2484
2589
 
2590
+ /**
2591
+ * Whether `val publish` could build here: the builder loaded as a publish
2592
+ * loads it, and its bundler made to parse and bundle a module. Nothing is
2593
+ * read from the project and nothing is asked of content.
2594
+ *
2595
+ * What Val Build runs on an upgrade's install before a connected project
2596
+ * may publish it, since the project's CI is what builds a connected publish:
2597
+ * an upgrade that resolved a bundler which cannot load would otherwise land
2598
+ * on the branch and fail every build after it (architecture/quirks.md,
2599
+ * 2026-10-26).
2600
+ */
2601
+ async function checkBuildTools$1() {
2602
+ const loaded = await loadBuilder();
2603
+ if (loaded.status === "error") return loaded;
2604
+ try {
2605
+ // A builder older than `loadRolldown` loaded its bundler on import.
2606
+ if (!("loadRolldown" in loaded.build)) return {
2607
+ status: "ok"
2608
+ };
2609
+ const {
2610
+ build,
2611
+ parseAstAsync
2612
+ } = await loaded.build.loadRolldown();
2613
+ await parseAstAsync("export const checked = true;");
2614
+ const ENTRY = "\0val-check-build-tools";
2615
+ await build({
2616
+ input: ENTRY,
2617
+ cwd: "/",
2618
+ write: false,
2619
+ plugins: [{
2620
+ name: "val-check-build-tools",
2621
+ resolveId: id => id === ENTRY ? id : null,
2622
+ load: id => id === ENTRY ? "export default 1;" : null
2623
+ }]
2624
+ });
2625
+ return {
2626
+ status: "ok"
2627
+ };
2628
+ } catch (e) {
2629
+ return {
2630
+ status: "error",
2631
+ message: `rolldown loaded, and could not build: ${e instanceof Error ? e.message : String(e)}`
2632
+ };
2633
+ }
2634
+ }
2635
+
2485
2636
  /**
2486
2637
  * The `@valbuild/ui` this checkout runs: the one its `@valbuild/tanstack`
2487
2638
  * resolves, which is a transitive dependency pnpm does not hoist -- so it is
@@ -3974,13 +4125,17 @@ async function publish(options) {
3974
4125
  // the Studio leaves to the server to repair (a local image in a remote
3975
4126
  // project, metadata to fill in) is printed and published, as it would be
3976
4127
  // from the Studio. Every site made on /new starts with its template's
3977
- // images local, so counting those would refuse its first publish.
4128
+ // images local, so counting those would refuse its first publish: they are
4129
+ // warnings, and refuse a publish only under --strict.
3978
4130
  if (!options.skipValidation) {
3979
4131
  const projectRoot = options.root ? path.resolve(options.root) : process.cwd();
3980
4132
  const errorCount = await validateOnce({
3981
4133
  projectRoot,
3982
4134
  fix: false,
3983
- blockingOnly: true
4135
+ blockingOnly: true,
4136
+ ...(options.strict ? {
4137
+ strict: true
4138
+ } : {})
3984
4139
  });
3985
4140
  if (errorCount > 0) {
3986
4141
  error(`Not published: ${errorCount} validation error${errorCount === 1 ? "" : "s"}.`);
@@ -4199,6 +4354,20 @@ async function ciReport(options) {
4199
4354
  console.log(pc.dim(`Reported the build of ${read.report.commit.slice(0, 7)} as ${read.report.status}.`));
4200
4355
  }
4201
4356
 
4357
+ /**
4358
+ * `val check-build-tools` -- a thin shell around `checkBuildTools`: says
4359
+ * whether `val publish` could build here, and fails when it could not.
4360
+ */
4361
+ async function checkBuildTools() {
4362
+ const checked = await checkBuildTools$1();
4363
+ if (checked.status === "error") {
4364
+ console.error(pc.red(checked.message));
4365
+ process.exitCode = 1;
4366
+ return;
4367
+ }
4368
+ console.log("val publish can build here.");
4369
+ }
4370
+
4202
4371
  async function main() {
4203
4372
  const {
4204
4373
  input,
@@ -4228,6 +4397,7 @@ async function main() {
4228
4397
  --root [root], -r [root] Set project root directory (default process.cwd())
4229
4398
  --fix [fix] Attempt to fix validation errors
4230
4399
  --watch, -w Re-validate on changes to val.config, val.modules and *.val files
4400
+ --strict Fail on warnings too (local files in a project with remote files)
4231
4401
 
4232
4402
 
4233
4403
  Command: login
@@ -4285,6 +4455,13 @@ async function main() {
4285
4455
  --dry-run Verify, and stop before the site changes
4286
4456
  --skip-validation Publish without running "val validate" first. It runs by
4287
4457
  default, and an error stops the publish
4458
+ --strict Let a validation warning stop the publish too
4459
+
4460
+ Command: check-build-tools
4461
+ Description: load what "val publish" builds with, and build a one-line module with
4462
+ it: whether this install could publish, without asking content anything. Val Build
4463
+ runs it on an upgrade's install before a connected project can publish the upgrade.
4464
+ Fails when it could not.
4288
4465
 
4289
4466
  Command: list-unused-files
4290
4467
  Description: EXPERIMENTAL.
@@ -4379,6 +4556,9 @@ async function main() {
4379
4556
  skipValidation: {
4380
4557
  type: "boolean"
4381
4558
  },
4559
+ strict: {
4560
+ type: "boolean"
4561
+ },
4382
4562
  yes: {
4383
4563
  type: "boolean"
4384
4564
  },
@@ -4433,6 +4613,8 @@ async function main() {
4433
4613
  yes: flags.yes,
4434
4614
  verbose: flags.verbose
4435
4615
  });
4616
+ case "check-build-tools":
4617
+ return checkBuildTools();
4436
4618
  case "ci-report":
4437
4619
  return ciReport({
4438
4620
  root: flags.root,
@@ -4451,7 +4633,8 @@ async function main() {
4451
4633
  buildHash: flags.buildHash,
4452
4634
  linksOwnCss: flags.linksOwnCss,
4453
4635
  dryRun: flags.dryRun,
4454
- skipValidation: flags.skipValidation
4636
+ skipValidation: flags.skipValidation,
4637
+ strict: flags.strict
4455
4638
  });
4456
4639
  case "login":
4457
4640
  return login({
@@ -4475,7 +4658,8 @@ async function main() {
4475
4658
  return validate({
4476
4659
  root: flags.root,
4477
4660
  fix: flags.fix,
4478
- watch: flags.watch
4661
+ watch: flags.watch,
4662
+ strict: flags.strict
4479
4663
  });
4480
4664
  default:
4481
4665
  return error(`Unknown command "${input.join(" ")}"`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.150.0",
4
+ "version": "0.150.1",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -26,11 +26,11 @@
26
26
  "picocolors": "^1.1.1",
27
27
  "zod": "^4.4.3",
28
28
  "zod-validation-error": "^5.0.0",
29
- "@valbuild/language-server": "0.150.0",
30
- "@valbuild/server": "0.150.0",
31
- "@valbuild/shared": "0.149.7",
29
+ "@valbuild/core": "0.150.1",
32
30
  "@valbuild/eslint-plugin": "0.125.0",
33
- "@valbuild/core": "0.148.7"
31
+ "@valbuild/language-server": "0.150.1",
32
+ "@valbuild/server": "0.150.1",
33
+ "@valbuild/shared": "0.150.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "prettier": "*",
@@ -38,7 +38,7 @@
38
38
  "rolldown": ">=1.2.5",
39
39
  "@tanstack/router-generator": ">=1.167.0",
40
40
  "@tanstack/router-plugin": ">=1.168.0",
41
- "@valbuild/tanstack-build": "^0.149.2"
41
+ "@valbuild/tanstack-build": "^0.150.1"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@valbuild/tanstack-build": {
@@ -68,9 +68,9 @@
68
68
  "@tanstack/router-generator": "1.167.32",
69
69
  "@tanstack/router-plugin": "1.168.34",
70
70
  "prettier": "~3.8.5",
71
- "rolldown": "^1.2.5",
71
+ "rolldown": "1.2.9",
72
72
  "tsx": "^4.23.12",
73
- "@valbuild/tanstack-build": "0.149.2"
73
+ "@valbuild/tanstack-build": "0.150.1"
74
74
  },
75
75
  "scripts": {
76
76
  "start": "tsx src/cli.ts --",
@@ -0,0 +1,16 @@
1
+ import pc from "picocolors";
2
+ import { checkBuildTools as check } from "./publish/buildArtifacts";
3
+
4
+ /**
5
+ * `val check-build-tools` -- a thin shell around `checkBuildTools`: says
6
+ * whether `val publish` could build here, and fails when it could not.
7
+ */
8
+ export async function checkBuildTools(): Promise<void> {
9
+ const checked = await check();
10
+ if (checked.status === "error") {
11
+ console.error(pc.red(checked.message));
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ console.log("val publish can build here.");
16
+ }
package/src/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { debug } from "./debug";
11
11
  import { deleteUnappliablePatches } from "./deleteUnappliablePatches";
12
12
  import { publish } from "./publish";
13
13
  import { ciReport } from "./ciReport";
14
+ import { checkBuildTools } from "./checkBuildTools";
14
15
 
15
16
  async function main(): Promise<void> {
16
17
  const { input, flags, showHelp } = meow(
@@ -38,6 +39,7 @@ async function main(): Promise<void> {
38
39
  --root [root], -r [root] Set project root directory (default process.cwd())
39
40
  --fix [fix] Attempt to fix validation errors
40
41
  --watch, -w Re-validate on changes to val.config, val.modules and *.val files
42
+ --strict Fail on warnings too (local files in a project with remote files)
41
43
 
42
44
 
43
45
  Command: login
@@ -95,6 +97,13 @@ async function main(): Promise<void> {
95
97
  --dry-run Verify, and stop before the site changes
96
98
  --skip-validation Publish without running "val validate" first. It runs by
97
99
  default, and an error stops the publish
100
+ --strict Let a validation warning stop the publish too
101
+
102
+ Command: check-build-tools
103
+ Description: load what "val publish" builds with, and build a one-line module with
104
+ it: whether this install could publish, without asking content anything. Val Build
105
+ runs it on an upgrade's install before a connected project can publish the upgrade.
106
+ Fails when it could not.
98
107
 
99
108
  Command: list-unused-files
100
109
  Description: EXPERIMENTAL.
@@ -190,6 +199,9 @@ async function main(): Promise<void> {
190
199
  skipValidation: {
191
200
  type: "boolean",
192
201
  },
202
+ strict: {
203
+ type: "boolean",
204
+ },
193
205
  yes: {
194
206
  type: "boolean",
195
207
  },
@@ -250,6 +262,8 @@ async function main(): Promise<void> {
250
262
  yes: flags.yes,
251
263
  verbose: flags.verbose,
252
264
  });
265
+ case "check-build-tools":
266
+ return checkBuildTools();
253
267
  case "ci-report":
254
268
  return ciReport({
255
269
  root: flags.root,
@@ -269,6 +283,7 @@ async function main(): Promise<void> {
269
283
  linksOwnCss: flags.linksOwnCss,
270
284
  dryRun: flags.dryRun,
271
285
  skipValidation: flags.skipValidation,
286
+ strict: flags.strict,
272
287
  });
273
288
  case "login":
274
289
  return login({
@@ -297,6 +312,7 @@ async function main(): Promise<void> {
297
312
  root: flags.root,
298
313
  fix: flags.fix,
299
314
  watch: flags.watch,
315
+ strict: flags.strict,
300
316
  });
301
317
  default:
302
318
  return error(`Unknown command "${input.join(" ")}"`);