@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @valbuild/cli
2
2
 
3
+ ## 0.150.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#938](https://github.com/valbuild/val/pull/938) [`b2903b6`](https://github.com/valbuild/val/commit/b2903b64bfbd85d3e293a506555a6dab39ab8aa8) Thanks [@freekh](https://github.com/freekh)! - `val connect` in a monorepo now tells Val which app you ran it in. The link it opens names the app's folder in the repository (`folder=apps/web`), so the new-project page picks that app's `val.config.ts` instead of asking you to choose.
8
+
9
+ - [#894](https://github.com/valbuild/val/pull/894) [`9171baa`](https://github.com/valbuild/val/commit/9171baad7beb1b2a355c70724d2339467b126e02) Thanks [@freekh](https://github.com/freekh)! - `val validate` no longer fails a project just because some of its files are
10
+ local while the project stores files remotely (`files: { remote: true }` or
11
+ `.remote()`). Every project made from a template starts like that, and the
12
+ site serves those files from `public/` until they are uploaded. They are now
13
+ reported as **warnings**, grouped into one block per file with the command
14
+ that uploads them:
15
+
16
+ ```
17
+ content/images.val.ts ⚠ 7 warnings (12ms)
18
+ │
19
+ │ ⚠ Local files in a project with remote files. Your site serves them from public/ for now; they move to remote storage when uploaded.
20
+ │
21
+ │ content/images.val.ts:7:5 /public/val/images/image-0_00000.png
22
+ │ content/images.val.ts:8:5 /public/val/images/image-1_00001.png
23
+ │ content/images.val.ts:9:5 /public/val/images/image-2_00002.png
24
+ │ content/images.val.ts:10:5 /public/val/images/image-3_00003.png
25
+ │ content/images.val.ts:11:5 /public/val/images/image-4_00004.png
26
+ │ … 2 more
27
+ │
28
+ │ → val validate --fix uploads them (needs val login)
29
+ │
30
+
31
+ ✔ No validation errors · 7 warnings in 1 file
32
+ ```
33
+
34
+ - `--strict` on `val validate` and `val publish` makes warnings fail too.
35
+ - `val validate --fix` still fails when it cannot upload them, for example
36
+ when you are not logged in.
37
+ - A local file that does not exist is still an error.
38
+ - The language server shows these as warnings.
39
+ - In the Studio, **Check everything** on the Validate page lists them with a
40
+ **Fix** button that uploads them, so a hosted project, which
41
+ has no command line, can move its template's images and files to remote
42
+ storage. That covers images, files and videos (a video's poster, captions
43
+ and HLS stream go up with it), images inside rich text included, and the
44
+ fields that picked a gallery image or a video from a set are updated with
45
+ it.
46
+ - On a hosted project, **Check everything** no longer reports the template's
47
+ images and videos as "The file is gone", and no longer offers a Fix that
48
+ would remove them from their gallery or video set.
49
+
50
+ Fixable errors are now printed with `✘`, like every other error, instead of
51
+ the `⚠` warnings use. They were always errors and still make `val validate`
52
+ fail.
53
+
54
+ - [#942](https://github.com/valbuild/val/pull/942) [`fd9ee2a`](https://github.com/valbuild/val/commit/fd9ee2ad76ef45bc6c4801aaccaae6e183960f11) Thanks [@freekh](https://github.com/freekh)! - `val publish` builds with native `rolldown` instead of rolldown's WebAssembly build. A site whose lockfile resolved `@napi-rs/wasm-runtime` 1.2.6 (released 2026-10-25) could no longer publish: `@rolldown/browser` 1.2.15 accepts that version but does not load with it, and `val publish` failed with "@valbuild/tanstack-build and rolldown … could not be loaded: Cannot set properties of undefined (setting 'initWorker')". Native `rolldown` does not use `@napi-rs/wasm-runtime` at all, so the site's version of it no longer matters. The Studio still builds in the browser with `@rolldown/browser`, from Val's own pinned versions.
55
+
56
+ New: `val check-build-tools` loads what `val publish` builds with and builds a one-line module with it, without asking content anything, and fails when it could not. Val Build runs it on an upgrade's install before a project whose CI publishes it can publish the upgrade.
57
+
58
+ - Updated dependencies [[`9171baa`](https://github.com/valbuild/val/commit/9171baad7beb1b2a355c70724d2339467b126e02), [`fd9ee2a`](https://github.com/valbuild/val/commit/fd9ee2ad76ef45bc6c4801aaccaae6e183960f11)]:
59
+ - @valbuild/core@0.150.1
60
+ - @valbuild/server@0.150.1
61
+ - @valbuild/shared@0.150.1
62
+ - @valbuild/language-server@0.150.1
63
+ - @valbuild/tanstack-build@0.150.1
64
+
3
65
  ## 0.150.0
4
66
 
5
67
  ### Patch Changes
@@ -346,6 +346,16 @@ async function* runValidation({
346
346
  sourcePath
347
347
  };
348
348
  }
349
+ if (result.warning !== undefined) {
350
+ yield {
351
+ type: "validation-warning",
352
+ sourcePath,
353
+ ...result.warning,
354
+ ...(v.keyError ? {
355
+ keyError: true
356
+ } : {})
357
+ };
358
+ }
349
359
  if (result.fixableErrorMessage !== undefined) {
350
360
  fileErrors += 1;
351
361
  yield {
@@ -683,10 +693,13 @@ function getCachedFile(moduleFilePath, projectRoot, cache) {
683
693
  return entry;
684
694
  }
685
695
 
696
+ /** How many of a warning group's files are listed before "… N more". */
697
+ const WARNING_FILES_SHOWN = 5;
686
698
  async function validate({
687
699
  root,
688
700
  fix,
689
- watch
701
+ watch,
702
+ strict
690
703
  }) {
691
704
  const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
692
705
  let prettier;
@@ -698,7 +711,10 @@ async function validate({
698
711
  const runOnce = () => validateOnce({
699
712
  projectRoot,
700
713
  fix: !!fix,
701
- prettier
714
+ prettier,
715
+ ...(strict ? {
716
+ strict: true
717
+ } : {})
702
718
  });
703
719
  if (!watch) {
704
720
  const totalErrors = await runOnce();
@@ -712,7 +728,7 @@ async function validate({
712
728
 
713
729
  /**
714
730
  * One validation pass over the project, printed as `val validate` prints it,
715
- * returning the number of errors found.
731
+ * returning the number of errors found -- and of warnings, under `strict`.
716
732
  *
717
733
  * Re-reads config and val files on each call so it always reflects the latest
718
734
  * state on disk: `val validate` calls it once or on every change (`--watch`),
@@ -722,7 +738,8 @@ async function validateOnce({
722
738
  projectRoot,
723
739
  fix,
724
740
  prettier,
725
- blockingOnly = false
741
+ blockingOnly = false,
742
+ strict = false
726
743
  }) {
727
744
  const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
728
745
  const resolvedValConfigFile = valConfigFile ? {
@@ -739,6 +756,7 @@ async function validateOnce({
739
756
  console.log(pc__default["default"].greenBright(`Found ${valFiles.length} files...`));
740
757
  const fixedFiles = new Set();
741
758
  let totalErrors = 0;
759
+ let totalWarnings = 0;
742
760
  let repairedOnSave = 0;
743
761
 
744
762
  // Caches each val file's parsed source so files are read/parsed at most once
@@ -785,10 +803,15 @@ async function validateOnce({
785
803
  })) {
786
804
  switch (event.type) {
787
805
  case "file-valid":
788
- valid.push({
789
- file: relFile(event.file),
790
- durationMs: event.durationMs
791
- });
806
+ // No errors, but it may have warnings: then it is shown with them.
807
+ if (reports.has(relFile(event.file))) {
808
+ reportFor(event.file).durationMs = event.durationMs;
809
+ } else {
810
+ valid.push({
811
+ file: relFile(event.file),
812
+ durationMs: event.durationMs
813
+ });
814
+ }
792
815
  break;
793
816
  case "file-error-count":
794
817
  reportFor(event.file).durationMs = event.durationMs;
@@ -816,6 +839,21 @@ async function validateOnce({
816
839
  } : {})
817
840
  });
818
841
  break;
842
+ case "validation-warning":
843
+ totalWarnings += 1;
844
+ reportFor(moduleOf(event.sourcePath)).diagnostics.push({
845
+ severity: "warning",
846
+ sourcePath: event.sourcePath,
847
+ message: event.message,
848
+ files: event.files,
849
+ ...(event.hint !== undefined ? {
850
+ hint: event.hint
851
+ } : {}),
852
+ ...(event.keyError ? {
853
+ keyError: true
854
+ } : {})
855
+ });
856
+ break;
819
857
  case "unknown-fix":
820
858
  reportFor(moduleOf(event.sourcePath)).diagnostics.push({
821
859
  severity: "error",
@@ -858,28 +896,67 @@ async function validateOnce({
858
896
 
859
897
  // Renders a module's diagnostics under a single left "│" gutter bar, with the
860
898
  // file name on top and blank gutter lines for air. Output flows least- to
861
- // most-actionable top-to-bottom, so fixable diagnostics are shown last (at the
862
- // bottom, nearest the prompt).
899
+ // most-actionable top-to-bottom: warnings first, then errors, and fixable
900
+ // errors last (at the bottom, nearest the prompt).
863
901
  const renderModule = report => {
864
902
  const bar = pc__default["default"].dim("│");
865
- const diagnostics = [...report.diagnostics].sort((a, b) => a.severity === b.severity ? 0 : a.severity === "fixable" ? 1 : -1);
903
+ const rank = {
904
+ warning: 0,
905
+ error: 1,
906
+ fixable: 2
907
+ };
908
+ const diagnostics = [...report.diagnostics].sort((a, b) => rank[a.severity] - rank[b.severity]);
909
+ const warningCount = diagnostics.filter(d => d.severity === "warning").length;
866
910
  const fixableCount = diagnostics.filter(d => d.severity === "fixable").length;
867
- const total = diagnostics.length;
868
- const hasError = fixableCount < total;
869
- const symbol = hasError ? pc__default["default"].red("✘") : pc__default["default"].yellow("⚠");
870
- let label;
871
- if (fixableCount === total) {
872
- label = `${fixableCount} fixable`;
873
- } else if (fixableCount > 0) {
874
- label = `${total} error${total > 1 ? "s" : ""} (${fixableCount} fixable)`;
875
- } else {
876
- label = `${total} error${total > 1 ? "s" : ""}`;
911
+ const errorCount = diagnostics.length - warningCount;
912
+ const symbol = errorCount > 0 ? pc__default["default"].red("✘") : pc__default["default"].yellow("⚠");
913
+ const labels = [];
914
+ if (errorCount > 0) {
915
+ labels.push(`${errorCount} error${errorCount > 1 ? "s" : ""}${fixableCount > 0 ? ` (${fixableCount} fixable)` : ""}`);
916
+ }
917
+ if (warningCount > 0) {
918
+ labels.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
877
919
  }
878
- console.log(`${pc__default["default"].bold(report.file)} ${symbol} ${label} ${pc__default["default"].dim(`(${report.durationMs}ms)`)}`);
920
+ console.log(`${pc__default["default"].bold(report.file)} ${symbol} ${labels.join(" · ")} ${pc__default["default"].dim(`(${report.durationMs}ms)`)}`);
921
+ const locationOf = d => sourcePathToLocationParts(d.sourcePath, projectRoot, sourceFileCache, d.keyError ? "key" : "value");
922
+
923
+ // Warnings say the same thing about many values (every local image in a
924
+ // gallery), so each message is printed once, over the places it is about.
925
+ const warningGroups = new Map();
879
926
  for (const d of diagnostics) {
880
- const target = d.keyError ? "key" : "value";
881
- const dsym = d.severity === "fixable" ? pc__default["default"].yellow("⚠") : pc__default["default"].red("✘");
882
- const parts = sourcePathToLocationParts(d.sourcePath, projectRoot, sourceFileCache, target);
927
+ if (d.severity !== "warning") continue;
928
+ const group = warningGroups.get(d.message);
929
+ if (group) {
930
+ group.push(d);
931
+ } else {
932
+ warningGroups.set(d.message, [d]);
933
+ }
934
+ }
935
+ for (const [message, group] of warningGroups) {
936
+ var _group$find;
937
+ console.log(bar);
938
+ console.log(`${bar} ${pc__default["default"].yellow("⚠")} ${message}`);
939
+ console.log(bar);
940
+ for (const d of group.slice(0, WARNING_FILES_SHOWN)) {
941
+ const parts = locationOf(d);
942
+ const files = d.files.join(", ");
943
+ // `file:line:col` first, so VS Code's terminal links it.
944
+ const where = parts ? `${parts.relativeFile}:${parts.line}:${parts.character}` : d.sourcePath;
945
+ console.log(`${bar} ${where} ${pc__default["default"].dim(files)}`);
946
+ }
947
+ if (group.length > WARNING_FILES_SHOWN) {
948
+ console.log(`${bar} ${pc__default["default"].dim(`… ${group.length - WARNING_FILES_SHOWN} more`)}`);
949
+ }
950
+ const hint = (_group$find = group.find(d => d.hint !== undefined)) === null || _group$find === void 0 ? void 0 : _group$find.hint;
951
+ if (hint !== undefined) {
952
+ console.log(bar);
953
+ console.log(`${bar} ${pc__default["default"].dim(`→ ${hint}`)}`);
954
+ }
955
+ }
956
+ for (const d of diagnostics) {
957
+ if (d.severity === "warning") continue;
958
+ const parts = locationOf(d);
959
+ const dsym = pc__default["default"].red("✘");
883
960
  console.log(bar);
884
961
  if (parts) {
885
962
  // `file:line:col` (no key/value label) so VS Code's terminal links it.
@@ -888,7 +965,7 @@ async function validateOnce({
888
965
  } else {
889
966
  console.log(`${bar} ${dsym} ${d.message}`);
890
967
  }
891
- const frame = sourcePathToCodeFrame(d.sourcePath, projectRoot, sourceFileCache, target);
968
+ const frame = sourcePathToCodeFrame(d.sourcePath, projectRoot, sourceFileCache, d.keyError ? "key" : "value");
892
969
  if (frame !== undefined) {
893
970
  console.log(bar);
894
971
  for (const frameLine of frame.split("\n")) {
@@ -921,10 +998,13 @@ async function validateOnce({
921
998
 
922
999
  // Render the grouped report least- to most-actionable, top-to-bottom, so the
923
1000
  // most important things end up at the bottom nearest the prompt: valid files
924
- // and skipped modules first, then error-only modules, then fixable modules.
1001
+ // and skipped modules first, then warning-only modules, then error-only
1002
+ // modules, then fixable modules.
925
1003
  const allReports = [...reports.values()];
1004
+ const hasErrors = r => r.diagnostics.some(d => d.severity !== "warning");
1005
+ const warningModules = allReports.filter(r => !hasErrors(r));
926
1006
  const fixableModules = allReports.filter(r => r.diagnostics.some(d => d.severity === "fixable"));
927
- const errorModules = allReports.filter(r => !r.diagnostics.some(d => d.severity === "fixable"));
1007
+ const errorModules = allReports.filter(r => hasErrors(r) && !r.diagnostics.some(d => d.severity === "fixable"));
928
1008
  for (const v of valid) {
929
1009
  console.log(pc__default["default"].green("✔"), pc__default["default"].dim(`${v.file} valid (${v.durationMs}ms)`));
930
1010
  }
@@ -934,34 +1014,34 @@ async function validateOnce({
934
1014
  if (allReports.length > 0) {
935
1015
  console.log("");
936
1016
  }
937
- for (const report of [...errorModules, ...fixableModules]) {
1017
+ for (const report of [...warningModules, ...errorModules, ...fixableModules]) {
938
1018
  renderModule(report);
939
1019
  }
940
1020
  const fixableTotal = allReports.reduce((n, r) => n + r.diagnostics.filter(d => d.severity === "fixable").length, 0);
1021
+ const errorFiles = allReports.filter(hasErrors).length;
1022
+ const warningFiles = allReports.filter(r => r.diagnostics.some(d => d.severity === "warning")).length;
1023
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1024
+ const warnings = `${plural(totalWarnings, "warning")} in ${plural(warningFiles, "file")}`;
1025
+ const warningPart = totalWarnings > 0 ? `${warnings}${strict ? ", which --strict counts as errors" : ""}` : undefined;
1026
+ const tail = [...(valid.length > 0 ? [`${valid.length} valid`] : []), ...(skipped.length > 0 ? [`${skipped.length} skipped`] : [])];
941
1027
  if (totalErrors > 0) {
942
- let summary = `${totalErrors} error${totalErrors > 1 ? "s" : ""}`;
1028
+ let summary = plural(totalErrors, "error");
943
1029
  if (fixableTotal > 0) {
944
1030
  summary += ` (${fixableTotal} fixable)`;
945
1031
  }
946
- summary += ` across ${allReports.length} file${allReports.length > 1 ? "s" : ""}`;
947
- if (valid.length > 0) {
948
- summary += ` · ${valid.length} valid`;
949
- }
950
- if (skipped.length > 0) {
951
- summary += ` · ${skipped.length} skipped`;
952
- }
953
- console.log(pc__default["default"].red("✘"), summary);
1032
+ summary += ` across ${plural(errorFiles, "file")}`;
1033
+ console.log(pc__default["default"].red("✘"), [summary, ...(warningPart ? [warningPart] : []), ...tail].join(" · "));
1034
+ } else if (warningPart && strict) {
1035
+ // Not "No validation errors" beside a ✘: under --strict this run failed,
1036
+ // and the warnings are why.
1037
+ console.log(pc__default["default"].red("✘"), [`${warnings}, and --strict fails on warnings`, ...tail].join(" · "));
1038
+ } else if (warningPart) {
1039
+ console.log(pc__default["default"].green("✔"), ["No validation errors", warningPart, ...tail].join(" · "));
954
1040
  } else {
955
- let summary = "No validation errors found";
956
- if (valid.length > 0) {
957
- summary += ` · ${valid.length} valid`;
958
- }
959
- if (skipped.length > 0) {
960
- summary += ` · ${skipped.length} skipped`;
961
- }
962
- console.log(pc__default["default"].green("✔"), summary);
1041
+ console.log(pc__default["default"].green("✔"), ["No validation errors found", ...tail].join(" · "));
963
1042
  }
964
- return blockingOnly ? totalErrors - repairedOnSave : totalErrors;
1043
+ const errors = blockingOnly ? totalErrors - repairedOnSave : totalErrors;
1044
+ return strict ? errors + totalWarnings : errors;
965
1045
  }
966
1046
 
967
1047
  // Directory names anywhere in the path that should never be watched.
@@ -1208,9 +1288,10 @@ const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1208
1288
 
1209
1289
  /**
1210
1290
  * `val connect`: opens admin's `/new` on "your own hosting", with what it can
1211
- * tell from here filled in -- the project `val.config.ts` names and the
1212
- * GitHub repository `origin` points at -- so the person only confirms. The
1213
- * link is printed too, for when no browser opens (over SSH, in a container).
1291
+ * tell from here filled in -- the project `val.config.ts` names, the GitHub
1292
+ * repository `origin` points at, and which folder of it this app is in (a
1293
+ * monorepo has several) -- so the person only confirms. The link is printed
1294
+ * too, for when no browser opens (over SSH, in a container).
1214
1295
  */
1215
1296
  async function connect(options) {
1216
1297
  const {
@@ -1218,20 +1299,23 @@ async function connect(options) {
1218
1299
  } = options;
1219
1300
  const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
1220
1301
  const project = await tryGetProject(projectRoot);
1221
- const gitRemote = await tryGetGitRemote(projectRoot);
1302
+ const git = findGitDir(projectRoot);
1303
+ const gitRemote = git ? await tryGetGitRemote(git.config) : null;
1222
1304
  const repo = gitRemote !== null ? getGitHubOwnerAndRepo(gitRemote) : null;
1305
+ const folder = git ? repoFolder(git.top, projectRoot) : null;
1223
1306
  console.log("");
1224
1307
  if (project) {
1225
1308
  console.log(`Found ${pc__default["default"].bold(`${project.orgName}/${project.projectName}`)} in val.config.ts`);
1226
1309
  }
1227
1310
  if (repo) {
1228
- console.log(`Found ${pc__default["default"].bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")`);
1311
+ console.log(`Found ${pc__default["default"].bold(`${repo.owner}/${repo.repo}`)} on GitHub (git remote "origin")${folder ? `, this app in ${pc__default["default"].bold(folder)}` : ""}`);
1229
1312
  } else {
1230
1313
  console.log(pc__default["default"].dim("No GitHub repository found: push this project to GitHub, or pick or create one in the browser."));
1231
1314
  }
1232
1315
  const url = connectUrl(host, {
1233
1316
  project,
1234
- repo
1317
+ repo,
1318
+ folder
1235
1319
  });
1236
1320
  const opened = openInBrowser(url);
1237
1321
  console.log(`\n${opened ? "Finish in your browser. If it did not open, go to:" : "Finish in your browser:"}\n${pc__default["default"].cyan(url)}\n`);
@@ -1248,6 +1332,7 @@ function connectUrl(host, found) {
1248
1332
  });
1249
1333
  if (found.repo) {
1250
1334
  params.set("repo", `${found.repo.owner}/${found.repo.repo}`);
1335
+ if (found.folder) params.set("folder", found.folder);
1251
1336
  }
1252
1337
  if (found.project) {
1253
1338
  params.set("org", found.project.orgName);
@@ -1312,12 +1397,19 @@ function getGitHubOwnerAndRepo(gitRemote) {
1312
1397
  repo: match[2]
1313
1398
  } : null;
1314
1399
  }
1315
- async function tryGetGitRemote(root) {
1400
+
1401
+ /**
1402
+ * Where the app is in its repository, with forward slashes: `apps/web`, or ""
1403
+ * when it is at the top. Null when it is not inside the repository at all.
1404
+ */
1405
+ function repoFolder(top, projectRoot) {
1406
+ const relative = path__default["default"].relative(top, projectRoot);
1407
+ if (relative.startsWith("..") || path__default["default"].isAbsolute(relative)) return null;
1408
+ return relative.split(path__default["default"].sep).join("/");
1409
+ }
1410
+ async function tryGetGitRemote(gitConfigPath) {
1316
1411
  try {
1317
- const gitConfig = tryGetGitConfig(root);
1318
- if (!gitConfig) {
1319
- return null;
1320
- }
1412
+ const gitConfig = fs__default$1["default"].readFileSync(gitConfigPath, "utf-8");
1321
1413
  const remoteMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = (.+)/);
1322
1414
  if (remoteMatch && remoteMatch[1]) {
1323
1415
  return remoteMatch[1].trim();
@@ -1328,13 +1420,18 @@ async function tryGetGitRemote(root) {
1328
1420
  return null;
1329
1421
  }
1330
1422
  }
1331
- function tryGetGitConfig(root) {
1423
+
1424
+ /** The repository `root` is in: its top folder and its `.git/config`. */
1425
+ function findGitDir(root) {
1332
1426
  let currentDir = root;
1333
1427
  let lastDir = null;
1334
1428
  while (currentDir !== lastDir) {
1335
1429
  const gitConfigPath = path__default["default"].join(currentDir, ".git", "config");
1336
1430
  if (fs__default$1["default"].existsSync(gitConfigPath)) {
1337
- return fs__default$1["default"].readFileSync(gitConfigPath, "utf-8");
1431
+ return {
1432
+ top: currentDir,
1433
+ config: gitConfigPath
1434
+ };
1338
1435
  }
1339
1436
  lastDir = currentDir;
1340
1437
  currentDir = path__default["default"].dirname(currentDir);
@@ -2504,6 +2601,14 @@ function resolveArtifactsDir(options) {
2504
2601
  async function loadBuilder() {
2505
2602
  try {
2506
2603
  const [build, node] = await Promise.all([Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('@valbuild/tanstack-build')); }), Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('@valbuild/tanstack-build/node')); })]);
2604
+ /*
2605
+ * Native rolldown rather than its WebAssembly build, whose glue the site's
2606
+ * lockfile picks and can break (`rolldown.ts` in tanstack-build). Asked
2607
+ * for, because the project's own tanstack-build may predate the choice.
2608
+ */
2609
+ if ("useRolldown" in build && "nativeRolldown" in node) {
2610
+ build.useRolldown(node.nativeRolldown);
2611
+ }
2507
2612
  return {
2508
2613
  status: "ok",
2509
2614
  build,
@@ -2517,6 +2622,52 @@ async function loadBuilder() {
2517
2622
  }
2518
2623
  }
2519
2624
 
2625
+ /**
2626
+ * Whether `val publish` could build here: the builder loaded as a publish
2627
+ * loads it, and its bundler made to parse and bundle a module. Nothing is
2628
+ * read from the project and nothing is asked of content.
2629
+ *
2630
+ * What Val Build runs on an upgrade's install before a connected project
2631
+ * may publish it, since the project's CI is what builds a connected publish:
2632
+ * an upgrade that resolved a bundler which cannot load would otherwise land
2633
+ * on the branch and fail every build after it (architecture/quirks.md,
2634
+ * 2026-10-26).
2635
+ */
2636
+ async function checkBuildTools$1() {
2637
+ const loaded = await loadBuilder();
2638
+ if (loaded.status === "error") return loaded;
2639
+ try {
2640
+ // A builder older than `loadRolldown` loaded its bundler on import.
2641
+ if (!("loadRolldown" in loaded.build)) return {
2642
+ status: "ok"
2643
+ };
2644
+ const {
2645
+ build,
2646
+ parseAstAsync
2647
+ } = await loaded.build.loadRolldown();
2648
+ await parseAstAsync("export const checked = true;");
2649
+ const ENTRY = "\0val-check-build-tools";
2650
+ await build({
2651
+ input: ENTRY,
2652
+ cwd: "/",
2653
+ write: false,
2654
+ plugins: [{
2655
+ name: "val-check-build-tools",
2656
+ resolveId: id => id === ENTRY ? id : null,
2657
+ load: id => id === ENTRY ? "export default 1;" : null
2658
+ }]
2659
+ });
2660
+ return {
2661
+ status: "ok"
2662
+ };
2663
+ } catch (e) {
2664
+ return {
2665
+ status: "error",
2666
+ message: `rolldown loaded, and could not build: ${e instanceof Error ? e.message : String(e)}`
2667
+ };
2668
+ }
2669
+ }
2670
+
2520
2671
  /**
2521
2672
  * The `@valbuild/ui` this checkout runs: the one its `@valbuild/tanstack`
2522
2673
  * resolves, which is a transitive dependency pnpm does not hoist -- so it is
@@ -4009,13 +4160,17 @@ async function publish(options) {
4009
4160
  // the Studio leaves to the server to repair (a local image in a remote
4010
4161
  // project, metadata to fill in) is printed and published, as it would be
4011
4162
  // from the Studio. Every site made on /new starts with its template's
4012
- // images local, so counting those would refuse its first publish.
4163
+ // images local, so counting those would refuse its first publish: they are
4164
+ // warnings, and refuse a publish only under --strict.
4013
4165
  if (!options.skipValidation) {
4014
4166
  const projectRoot = options.root ? path__default["default"].resolve(options.root) : process.cwd();
4015
4167
  const errorCount = await validateOnce({
4016
4168
  projectRoot,
4017
4169
  fix: false,
4018
- blockingOnly: true
4170
+ blockingOnly: true,
4171
+ ...(options.strict ? {
4172
+ strict: true
4173
+ } : {})
4019
4174
  });
4020
4175
  if (errorCount > 0) {
4021
4176
  error(`Not published: ${errorCount} validation error${errorCount === 1 ? "" : "s"}.`);
@@ -4234,6 +4389,20 @@ async function ciReport(options) {
4234
4389
  console.log(pc__default["default"].dim(`Reported the build of ${read.report.commit.slice(0, 7)} as ${read.report.status}.`));
4235
4390
  }
4236
4391
 
4392
+ /**
4393
+ * `val check-build-tools` -- a thin shell around `checkBuildTools`: says
4394
+ * whether `val publish` could build here, and fails when it could not.
4395
+ */
4396
+ async function checkBuildTools() {
4397
+ const checked = await checkBuildTools$1();
4398
+ if (checked.status === "error") {
4399
+ console.error(pc__default["default"].red(checked.message));
4400
+ process.exitCode = 1;
4401
+ return;
4402
+ }
4403
+ console.log("val publish can build here.");
4404
+ }
4405
+
4237
4406
  async function main() {
4238
4407
  const {
4239
4408
  input,
@@ -4263,6 +4432,7 @@ async function main() {
4263
4432
  --root [root], -r [root] Set project root directory (default process.cwd())
4264
4433
  --fix [fix] Attempt to fix validation errors
4265
4434
  --watch, -w Re-validate on changes to val.config, val.modules and *.val files
4435
+ --strict Fail on warnings too (local files in a project with remote files)
4266
4436
 
4267
4437
 
4268
4438
  Command: login
@@ -4320,6 +4490,13 @@ async function main() {
4320
4490
  --dry-run Verify, and stop before the site changes
4321
4491
  --skip-validation Publish without running "val validate" first. It runs by
4322
4492
  default, and an error stops the publish
4493
+ --strict Let a validation warning stop the publish too
4494
+
4495
+ Command: check-build-tools
4496
+ Description: load what "val publish" builds with, and build a one-line module with
4497
+ it: whether this install could publish, without asking content anything. Val Build
4498
+ runs it on an upgrade's install before a connected project can publish the upgrade.
4499
+ Fails when it could not.
4323
4500
 
4324
4501
  Command: list-unused-files
4325
4502
  Description: EXPERIMENTAL.
@@ -4414,6 +4591,9 @@ async function main() {
4414
4591
  skipValidation: {
4415
4592
  type: "boolean"
4416
4593
  },
4594
+ strict: {
4595
+ type: "boolean"
4596
+ },
4417
4597
  yes: {
4418
4598
  type: "boolean"
4419
4599
  },
@@ -4468,6 +4648,8 @@ async function main() {
4468
4648
  yes: flags.yes,
4469
4649
  verbose: flags.verbose
4470
4650
  });
4651
+ case "check-build-tools":
4652
+ return checkBuildTools();
4471
4653
  case "ci-report":
4472
4654
  return ciReport({
4473
4655
  root: flags.root,
@@ -4486,7 +4668,8 @@ async function main() {
4486
4668
  buildHash: flags.buildHash,
4487
4669
  linksOwnCss: flags.linksOwnCss,
4488
4670
  dryRun: flags.dryRun,
4489
- skipValidation: flags.skipValidation
4671
+ skipValidation: flags.skipValidation,
4672
+ strict: flags.strict
4490
4673
  });
4491
4674
  case "login":
4492
4675
  return login({
@@ -4510,7 +4693,8 @@ async function main() {
4510
4693
  return validate({
4511
4694
  root: flags.root,
4512
4695
  fix: flags.fix,
4513
- watch: flags.watch
4696
+ watch: flags.watch,
4697
+ strict: flags.strict
4514
4698
  });
4515
4699
  default:
4516
4700
  return error(`Unknown command "${input.join(" ")}"`);