@valbuild/cli 0.146.4 → 0.148.0

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,43 @@
1
1
  # @valbuild/cli
2
2
 
3
+ ## 0.148.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#818](https://github.com/valbuild/val/pull/818) [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1) Thanks [@freekh](https://github.com/freekh)! - `val publish` now runs `val validate` first, and does not publish content that
8
+ has validation errors the Studio's Publish would also refuse. What the Studio
9
+ repairs by itself on save -- a local image in a project with remote files, or
10
+ metadata still to be filled in -- is reported and published, as it is from the
11
+ Studio.
12
+
13
+ A connected project's CI publishes with `val publish`, so from this release a
14
+ push whose content does not validate fails its run instead of going live. The
15
+ run prints the same report `val validate` does. To fix it, run
16
+ `val validate --fix` in the project, commit what it changes, push, and fix by
17
+ hand whatever it reports that it cannot fix.
18
+
19
+ The check never fixes anything itself — CI does not rewrite what it was asked
20
+ to publish. `--skip-validation` publishes without it.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [[`df5544c`](https://github.com/valbuild/val/commit/df5544c6e92a4c3a52765630c9de191e854ee723), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`4176757`](https://github.com/valbuild/val/commit/41767579b01849473a94779122fd39c07dd99234), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1), [`562d032`](https://github.com/valbuild/val/commit/562d03280c3fcf97a80562e8b017dd2125ac4ad1)]:
25
+ - @valbuild/server@0.148.0
26
+ - @valbuild/tanstack-build@0.148.0
27
+ - @valbuild/shared@0.148.0
28
+ - @valbuild/core@0.148.0
29
+ - @valbuild/language-server@0.148.0
30
+
31
+ ## 0.147.0
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [[`2f09cd5`](https://github.com/valbuild/val/commit/2f09cd5e2f4fc37f06917899f24f9ad2ee8d3a8b), [`cb04e58`](https://github.com/valbuild/val/commit/cb04e586db1e3ffd20ec1fee744561b1c8f8cffd), [`cb04e58`](https://github.com/valbuild/val/commit/cb04e586db1e3ffd20ec1fee744561b1c8f8cffd)]:
36
+ - @valbuild/core@0.147.0
37
+ - @valbuild/shared@0.147.0
38
+ - @valbuild/server@0.147.0
39
+ - @valbuild/language-server@0.147.0
40
+
3
41
  ## 0.146.4
4
42
 
5
43
  ### Patch Changes
@@ -131,6 +131,10 @@ async function* runValidation({
131
131
  fs
132
132
  }) {
133
133
  const projectRoot = path__default["default"].resolve(root);
134
+ // One question per platform host for the whole run: each error is fixed by
135
+ // its own createFixPatch call, and a host that does not answer would
136
+ // otherwise cost a timeout per ref.
137
+ const askHostProject = server.askOncePerHost();
134
138
  const service = await server.createService(projectRoot, fs);
135
139
 
136
140
  // Modules registered in the project's val.modules. Files found on disk that
@@ -327,6 +331,9 @@ async function* runValidation({
327
331
  message: result.errorMessage ?? "Unknown error",
328
332
  ...(v.keyError ? {
329
333
  keyError: true
334
+ } : {}),
335
+ ...(internal.isRepairedOnSave(v) ? {
336
+ repairedOnSave: true
330
337
  } : {})
331
338
  };
332
339
  fileErrors += 1;
@@ -348,6 +355,9 @@ async function* runValidation({
348
355
  fixable: true,
349
356
  ...(v.keyError ? {
350
357
  keyError: true
358
+ } : {}),
359
+ ...(internal.isRepairedOnSave(v) ? {
360
+ repairedOnSave: true
351
361
  } : {})
352
362
  };
353
363
  }
@@ -356,7 +366,8 @@ async function* runValidation({
356
366
  if (result.shouldApplyPatch) {
357
367
  const fixPatch = await server.createFixPatch({
358
368
  projectRoot,
359
- remoteHost: remote.remoteHost
369
+ remoteHost: remote.remoteHost,
370
+ askHostProject
360
371
  }, !!fix, sourcePath, v, remoteFiles, valModule.source, valModule.schema);
361
372
  if (fix && fixPatch !== null && fixPatch !== void 0 && fixPatch.patch && (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.patch.length) > 0) {
362
373
  await service.patch(moduleFilePath, fixPatch.patch);
@@ -386,6 +397,9 @@ async function* runValidation({
386
397
  fixable: true,
387
398
  ...(v.keyError ? {
388
399
  keyError: true
400
+ } : {}),
401
+ ...(internal.isRepairedOnSave(v) ? {
402
+ repairedOnSave: true
389
403
  } : {})
390
404
  };
391
405
  }
@@ -400,6 +414,9 @@ async function* runValidation({
400
414
  fixable: !!(e.fixes && e.fixes.length),
401
415
  ...(e.keyError ? {
402
416
  keyError: true
417
+ } : {}),
418
+ ...(internal.isRepairedOnSave(e) ? {
419
+ repairedOnSave: true
403
420
  } : {})
404
421
  };
405
422
  }
@@ -678,255 +695,273 @@ async function validate({
678
695
  } catch {
679
696
  console.log("Prettier not found, skipping formatting");
680
697
  }
698
+ const runOnce = () => validateOnce({
699
+ projectRoot,
700
+ fix: !!fix,
701
+ prettier
702
+ });
703
+ if (!watch) {
704
+ const totalErrors = await runOnce();
705
+ if (totalErrors > 0) {
706
+ process.exit(1);
707
+ }
708
+ return;
709
+ }
710
+ await watchAndValidate(projectRoot, runOnce);
711
+ }
681
712
 
682
- // Runs a single validation pass over the project and returns the number of
683
- // errors found. Re-reads config and val files each call so it always reflects
684
- // the latest state on disk (used both for one-shot and watch mode).
685
- async function runOnce() {
686
- const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
687
- const resolvedValConfigFile = valConfigFile ? {
688
- ...valConfigFile,
689
- project: process.env.VAL_PROJECT || valConfigFile.project
690
- } : process.env.VAL_PROJECT ? {
691
- project: process.env.VAL_PROJECT
692
- } : undefined;
693
- console.log(pc__default["default"].greenBright(`Validating project${resolvedValConfigFile !== null && resolvedValConfigFile !== void 0 && resolvedValConfigFile.project ? ` '${pc__default["default"].inverse(resolvedValConfigFile.project)}'` : ""}...`));
694
- const valFiles = await fastGlob.glob("**/*.val.{js,ts}", {
695
- ignore: ["node_modules/**"],
696
- cwd: projectRoot
697
- });
698
- console.log(pc__default["default"].greenBright(`Found ${valFiles.length} files...`));
699
- const fixedFiles = new Set();
700
- let totalErrors = 0;
713
+ /**
714
+ * One validation pass over the project, printed as `val validate` prints it,
715
+ * returning the number of errors found.
716
+ *
717
+ * Re-reads config and val files on each call so it always reflects the latest
718
+ * state on disk: `val validate` calls it once or on every change (`--watch`),
719
+ * and `val publish` calls it before it builds anything.
720
+ */
721
+ async function validateOnce({
722
+ projectRoot,
723
+ fix,
724
+ prettier,
725
+ blockingOnly = false
726
+ }) {
727
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
728
+ const resolvedValConfigFile = valConfigFile ? {
729
+ ...valConfigFile,
730
+ project: process.env.VAL_PROJECT || valConfigFile.project
731
+ } : process.env.VAL_PROJECT ? {
732
+ project: process.env.VAL_PROJECT
733
+ } : undefined;
734
+ console.log(pc__default["default"].greenBright(`Validating project${resolvedValConfigFile !== null && resolvedValConfigFile !== void 0 && resolvedValConfigFile.project ? ` '${pc__default["default"].inverse(resolvedValConfigFile.project)}'` : ""}...`));
735
+ const valFiles = await fastGlob.glob("**/*.val.{js,ts}", {
736
+ ignore: ["node_modules/**"],
737
+ cwd: projectRoot
738
+ });
739
+ console.log(pc__default["default"].greenBright(`Found ${valFiles.length} files...`));
740
+ const fixedFiles = new Set();
741
+ let totalErrors = 0;
742
+ let repairedOnSave = 0;
701
743
 
702
- // Caches each val file's parsed source so files are read/parsed at most once
703
- // per pass when resolving sourcePaths to file locations and code frames.
704
- const sourceFileCache = new Map();
744
+ // Caches each val file's parsed source so files are read/parsed at most once
745
+ // per pass when resolving sourcePaths to file locations and code frames.
746
+ const sourceFileCache = new Map();
705
747
 
706
- // Diagnostics are buffered per module (keyed by relative file path) so we can
707
- // render them grouped and prioritised after the run, rather than streaming
708
- // them out interleaved. Transient progress (remote/fix-applied) still streams
709
- // live below.
710
- const reports = new Map();
711
- const valid = [];
712
- const skipped = [];
748
+ // Diagnostics are buffered per module (keyed by relative file path) so we can
749
+ // render them grouped and prioritised after the run, rather than streaming
750
+ // them out interleaved. Transient progress (remote/fix-applied) still streams
751
+ // live below.
752
+ const reports = new Map();
753
+ const valid = [];
754
+ const skipped = [];
713
755
 
714
- // Relative file path (no leading slash), matching the code frame's
715
- // relativeFile so headers, diagnostics and frames all agree.
716
- const relFile = file => file.replace(/^\//, "");
717
- // The module a sourcePath/file belongs to is the part before the `?p=...`.
718
- const moduleOf = sourcePathOrFile => relFile(sourcePathOrFile.split("?")[0]);
719
- const reportFor = file => {
720
- const key = relFile(file);
721
- let report = reports.get(key);
722
- if (!report) {
723
- report = {
724
- file: key,
725
- durationMs: 0,
726
- diagnostics: []
727
- };
728
- reports.set(key, report);
729
- }
730
- return report;
731
- };
732
- for await (const event of runValidation({
733
- root: projectRoot,
734
- fix: !!fix,
735
- valFiles,
736
- project: resolvedValConfigFile === null || resolvedValConfigFile === void 0 ? void 0 : resolvedValConfigFile.project,
737
- remote: {
738
- remoteHost: process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST,
739
- getSettings: (projectName, options) => server.getSettings(projectName, options),
740
- uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) => server.uploadRemoteFile(process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST, project, bucket, fileHash, fileExt ?? "", fileBuffer, options)
741
- },
742
- fs: server.createDefaultValFSHost()
743
- })) {
744
- switch (event.type) {
745
- case "file-valid":
746
- valid.push({
747
- file: relFile(event.file),
748
- durationMs: event.durationMs
749
- });
750
- break;
751
- case "file-error-count":
752
- reportFor(event.file).durationMs = event.durationMs;
753
- totalErrors += event.errorCount;
754
- break;
755
- case "validation-error":
756
- reportFor(moduleOf(event.sourcePath)).diagnostics.push({
757
- severity: "error",
758
- sourcePath: event.sourcePath,
759
- message: event.message,
760
- ...(event.keyError ? {
761
- keyError: true
762
- } : {})
763
- });
764
- break;
765
- case "validation-fixable-error":
766
- reportFor(moduleOf(event.sourcePath)).diagnostics.push({
767
- severity: event.fixable ? "fixable" : "error",
768
- sourcePath: event.sourcePath,
769
- message: event.message,
770
- ...(event.keyError ? {
771
- keyError: true
772
- } : {})
773
- });
774
- break;
775
- case "unknown-fix":
776
- reportFor(moduleOf(event.sourcePath)).diagnostics.push({
777
- severity: "error",
778
- sourcePath: event.sourcePath,
779
- message: `Unknown fix: ${event.fixes.join(", ")}`,
780
- ...(event.keyError ? {
781
- keyError: true
782
- } : {})
783
- });
784
- break;
785
- case "unregistered-module":
786
- skipped.push(event.file);
787
- break;
788
- case "fatal-error":
789
- // No sourcePath for fatal errors; group by file, render message only.
790
- reportFor(event.file).diagnostics.push({
791
- severity: "error",
792
- sourcePath: event.file,
793
- message: event.message
794
- });
795
- break;
796
- case "fix-applied":
797
- console.log(pc__default["default"].yellow("⚠"), "Applied fix for", event.sourcePath);
798
- fixedFiles.add(event.file);
799
- break;
800
- case "remote-uploading":
801
- console.log(pc__default["default"].yellow("⚠"), `Uploading remote file: '${event.ref}'...`);
802
- break;
803
- case "remote-uploaded":
804
- console.log(pc__default["default"].green("✔"), `Completed upload of remote file: '${event.ref}'`);
805
- break;
806
- case "remote-already-uploaded":
807
- console.log(pc__default["default"].yellow("⚠"), `Remote file ${event.filePath} already uploaded`);
808
- break;
809
- case "remote-downloading":
810
- console.log(pc__default["default"].yellow("⚠"), `Downloading remote file in ${event.sourcePath}...`);
811
- break;
812
- }
756
+ // Relative file path (no leading slash), matching the code frame's
757
+ // relativeFile so headers, diagnostics and frames all agree.
758
+ const relFile = file => file.replace(/^\//, "");
759
+ // The module a sourcePath/file belongs to is the part before the `?p=...`.
760
+ const moduleOf = sourcePathOrFile => relFile(sourcePathOrFile.split("?")[0]);
761
+ const reportFor = file => {
762
+ const key = relFile(file);
763
+ let report = reports.get(key);
764
+ if (!report) {
765
+ report = {
766
+ file: key,
767
+ durationMs: 0,
768
+ diagnostics: []
769
+ };
770
+ reports.set(key, report);
813
771
  }
772
+ return report;
773
+ };
774
+ for await (const event of runValidation({
775
+ root: projectRoot,
776
+ fix,
777
+ valFiles,
778
+ project: resolvedValConfigFile === null || resolvedValConfigFile === void 0 ? void 0 : resolvedValConfigFile.project,
779
+ remote: {
780
+ remoteHost: process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST,
781
+ getSettings: (projectName, options) => server.getSettings(projectName, options),
782
+ uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) => server.uploadRemoteFile(process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST, project, bucket, fileHash, fileExt ?? "", fileBuffer, options)
783
+ },
784
+ fs: server.createDefaultValFSHost()
785
+ })) {
786
+ switch (event.type) {
787
+ case "file-valid":
788
+ valid.push({
789
+ file: relFile(event.file),
790
+ durationMs: event.durationMs
791
+ });
792
+ break;
793
+ case "file-error-count":
794
+ reportFor(event.file).durationMs = event.durationMs;
795
+ totalErrors += event.errorCount;
796
+ break;
797
+ case "validation-error":
798
+ if (event.repairedOnSave) repairedOnSave += 1;
799
+ reportFor(moduleOf(event.sourcePath)).diagnostics.push({
800
+ severity: "error",
801
+ sourcePath: event.sourcePath,
802
+ message: event.message,
803
+ ...(event.keyError ? {
804
+ keyError: true
805
+ } : {})
806
+ });
807
+ break;
808
+ case "validation-fixable-error":
809
+ if (event.repairedOnSave) repairedOnSave += 1;
810
+ reportFor(moduleOf(event.sourcePath)).diagnostics.push({
811
+ severity: event.fixable ? "fixable" : "error",
812
+ sourcePath: event.sourcePath,
813
+ message: event.message,
814
+ ...(event.keyError ? {
815
+ keyError: true
816
+ } : {})
817
+ });
818
+ break;
819
+ case "unknown-fix":
820
+ reportFor(moduleOf(event.sourcePath)).diagnostics.push({
821
+ severity: "error",
822
+ sourcePath: event.sourcePath,
823
+ message: `Unknown fix: ${event.fixes.join(", ")}`,
824
+ ...(event.keyError ? {
825
+ keyError: true
826
+ } : {})
827
+ });
828
+ break;
829
+ case "unregistered-module":
830
+ skipped.push(event.file);
831
+ break;
832
+ case "fatal-error":
833
+ // No sourcePath for fatal errors; group by file, render message only.
834
+ reportFor(event.file).diagnostics.push({
835
+ severity: "error",
836
+ sourcePath: event.file,
837
+ message: event.message
838
+ });
839
+ break;
840
+ case "fix-applied":
841
+ console.log(pc__default["default"].yellow("⚠"), "Applied fix for", event.sourcePath);
842
+ fixedFiles.add(event.file);
843
+ break;
844
+ case "remote-uploading":
845
+ console.log(pc__default["default"].yellow("⚠"), `Uploading remote file: '${event.ref}'...`);
846
+ break;
847
+ case "remote-uploaded":
848
+ console.log(pc__default["default"].green("✔"), `Completed upload of remote file: '${event.ref}'`);
849
+ break;
850
+ case "remote-already-uploaded":
851
+ console.log(pc__default["default"].yellow("⚠"), `Remote file ${event.filePath} already uploaded`);
852
+ break;
853
+ case "remote-downloading":
854
+ console.log(pc__default["default"].yellow("⚠"), `Downloading remote file in ${event.sourcePath}...`);
855
+ break;
856
+ }
857
+ }
814
858
 
815
- // Renders a module's diagnostics under a single left "│" gutter bar, with the
816
- // file name on top and blank gutter lines for air. Output flows least- to
817
- // most-actionable top-to-bottom, so fixable diagnostics are shown last (at the
818
- // bottom, nearest the prompt).
819
- const renderModule = report => {
820
- const bar = pc__default["default"].dim("│");
821
- const diagnostics = [...report.diagnostics].sort((a, b) => a.severity === b.severity ? 0 : a.severity === "fixable" ? 1 : -1);
822
- const fixableCount = diagnostics.filter(d => d.severity === "fixable").length;
823
- const total = diagnostics.length;
824
- const hasError = fixableCount < total;
825
- const symbol = hasError ? pc__default["default"].red("✘") : pc__default["default"].yellow("⚠");
826
- let label;
827
- if (fixableCount === total) {
828
- label = `${fixableCount} fixable`;
829
- } else if (fixableCount > 0) {
830
- label = `${total} error${total > 1 ? "s" : ""} (${fixableCount} fixable)`;
859
+ // Renders a module's diagnostics under a single left "│" gutter bar, with the
860
+ // 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).
863
+ const renderModule = report => {
864
+ const bar = pc__default["default"].dim("│");
865
+ const diagnostics = [...report.diagnostics].sort((a, b) => a.severity === b.severity ? 0 : a.severity === "fixable" ? 1 : -1);
866
+ 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" : ""}`;
877
+ }
878
+ console.log(`${pc__default["default"].bold(report.file)} ${symbol} ${label} ${pc__default["default"].dim(`(${report.durationMs}ms)`)}`);
879
+ 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);
883
+ console.log(bar);
884
+ if (parts) {
885
+ // `file:line:col` (no key/value label) so VS Code's terminal links it.
886
+ console.log(`${bar} ${dsym} ${parts.relativeFile}:${parts.line}:${parts.character}`);
887
+ console.log(`${bar} ${d.message}`);
831
888
  } else {
832
- label = `${total} error${total > 1 ? "s" : ""}`;
889
+ console.log(`${bar} ${dsym} ${d.message}`);
833
890
  }
834
- console.log(`${pc__default["default"].bold(report.file)} ${symbol} ${label} ${pc__default["default"].dim(`(${report.durationMs}ms)`)}`);
835
- for (const d of diagnostics) {
836
- const target = d.keyError ? "key" : "value";
837
- const dsym = d.severity === "fixable" ? pc__default["default"].yellow("⚠") : pc__default["default"].red("✘");
838
- const parts = sourcePathToLocationParts(d.sourcePath, projectRoot, sourceFileCache, target);
891
+ const frame = sourcePathToCodeFrame(d.sourcePath, projectRoot, sourceFileCache, target);
892
+ if (frame !== undefined) {
839
893
  console.log(bar);
840
- if (parts) {
841
- // `file:line:col` (no key/value label) so VS Code's terminal links it.
842
- console.log(`${bar} ${dsym} ${parts.relativeFile}:${parts.line}:${parts.character}`);
843
- console.log(`${bar} ${d.message}`);
844
- } else {
845
- console.log(`${bar} ${dsym} ${d.message}`);
846
- }
847
- const frame = sourcePathToCodeFrame(d.sourcePath, projectRoot, sourceFileCache, target);
848
- if (frame !== undefined) {
849
- console.log(bar);
850
- for (const frameLine of frame.split("\n")) {
851
- console.log(`${bar} ${frameLine}`);
852
- }
853
- }
854
- if (d.severity === "fixable") {
855
- console.log(`${bar} ${pc__default["default"].dim("→ run with --fix to apply")}`);
894
+ for (const frameLine of frame.split("\n")) {
895
+ console.log(`${bar} ${frameLine}`);
856
896
  }
857
897
  }
858
- console.log(bar);
859
- console.log("");
860
- };
861
-
862
- // Format the files that had fixes applied, the way the project formats
863
- // everything else. See `formatFixedFiles` for why this cannot simply be
864
- // `prettier.format(code, { filepath })`.
865
- //
866
- // A file that could not be formatted is reported and skipped rather than
867
- // thrown: the fix itself landed, and aborting here would leave the fix
868
- // applied and the report below unwritten.
869
- if (prettier) {
870
- const {
871
- failures
872
- } = await formatFixedFiles(prettier, projectRoot, fixedFiles);
873
- for (const failure of failures) {
874
- console.log(pc__default["default"].yellow("⚠"), `Could not format ${relFile(failure.file)}: ${failure.message}`);
898
+ if (d.severity === "fixable") {
899
+ console.log(`${bar} ${pc__default["default"].dim("→ run with --fix to apply")}`);
875
900
  }
876
901
  }
902
+ console.log(bar);
903
+ console.log("");
904
+ };
877
905
 
878
- // Render the grouped report least- to most-actionable, top-to-bottom, so the
879
- // most important things end up at the bottom nearest the prompt: valid files
880
- // and skipped modules first, then error-only modules, then fixable modules.
881
- const allReports = [...reports.values()];
882
- const fixableModules = allReports.filter(r => r.diagnostics.some(d => d.severity === "fixable"));
883
- const errorModules = allReports.filter(r => !r.diagnostics.some(d => d.severity === "fixable"));
884
- for (const v of valid) {
885
- console.log(pc__default["default"].green("✔"), pc__default["default"].dim(`${v.file} valid (${v.durationMs}ms)`));
906
+ // Format the files that had fixes applied, the way the project formats
907
+ // everything else. See `formatFixedFiles` for why this cannot simply be
908
+ // `prettier.format(code, { filepath })`.
909
+ //
910
+ // A file that could not be formatted is reported and skipped rather than
911
+ // thrown: the fix itself landed, and aborting here would leave the fix
912
+ // applied and the report below unwritten.
913
+ if (prettier) {
914
+ const {
915
+ failures
916
+ } = await formatFixedFiles(prettier, projectRoot, fixedFiles);
917
+ for (const failure of failures) {
918
+ console.log(pc__default["default"].yellow("⚠"), `Could not format ${relFile(failure.file)}: ${failure.message}`);
886
919
  }
887
- for (const file of skipped) {
888
- console.log(pc__default["default"].yellow("⚠"), pc__default["default"].dim(`/${file} is not registered in val.modules - skipping`));
920
+ }
921
+
922
+ // Render the grouped report least- to most-actionable, top-to-bottom, so the
923
+ // 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.
925
+ const allReports = [...reports.values()];
926
+ 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"));
928
+ for (const v of valid) {
929
+ console.log(pc__default["default"].green("✔"), pc__default["default"].dim(`${v.file} valid (${v.durationMs}ms)`));
930
+ }
931
+ for (const file of skipped) {
932
+ console.log(pc__default["default"].yellow("⚠"), pc__default["default"].dim(`/${file} is not registered in val.modules - skipping`));
933
+ }
934
+ if (allReports.length > 0) {
935
+ console.log("");
936
+ }
937
+ for (const report of [...errorModules, ...fixableModules]) {
938
+ renderModule(report);
939
+ }
940
+ const fixableTotal = allReports.reduce((n, r) => n + r.diagnostics.filter(d => d.severity === "fixable").length, 0);
941
+ if (totalErrors > 0) {
942
+ let summary = `${totalErrors} error${totalErrors > 1 ? "s" : ""}`;
943
+ if (fixableTotal > 0) {
944
+ summary += ` (${fixableTotal} fixable)`;
889
945
  }
890
- if (allReports.length > 0) {
891
- console.log("");
946
+ summary += ` across ${allReports.length} file${allReports.length > 1 ? "s" : ""}`;
947
+ if (valid.length > 0) {
948
+ summary += ` · ${valid.length} valid`;
892
949
  }
893
- for (const report of [...errorModules, ...fixableModules]) {
894
- renderModule(report);
950
+ if (skipped.length > 0) {
951
+ summary += ` · ${skipped.length} skipped`;
895
952
  }
896
- const fixableTotal = allReports.reduce((n, r) => n + r.diagnostics.filter(d => d.severity === "fixable").length, 0);
897
- if (totalErrors > 0) {
898
- let summary = `${totalErrors} error${totalErrors > 1 ? "s" : ""}`;
899
- if (fixableTotal > 0) {
900
- summary += ` (${fixableTotal} fixable)`;
901
- }
902
- summary += ` across ${allReports.length} file${allReports.length > 1 ? "s" : ""}`;
903
- if (valid.length > 0) {
904
- summary += ` · ${valid.length} valid`;
905
- }
906
- if (skipped.length > 0) {
907
- summary += ` · ${skipped.length} skipped`;
908
- }
909
- console.log(pc__default["default"].red("✘"), summary);
910
- } else {
911
- let summary = "No validation errors found";
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__default["default"].green("✔"), summary);
953
+ console.log(pc__default["default"].red("✘"), summary);
954
+ } else {
955
+ let summary = "No validation errors found";
956
+ if (valid.length > 0) {
957
+ summary += ` · ${valid.length} valid`;
919
958
  }
920
- return totalErrors;
921
- }
922
- if (!watch) {
923
- const totalErrors = await runOnce();
924
- if (totalErrors > 0) {
925
- process.exit(1);
959
+ if (skipped.length > 0) {
960
+ summary += ` · ${skipped.length} skipped`;
926
961
  }
927
- return;
962
+ console.log(pc__default["default"].green("✔"), summary);
928
963
  }
929
- await watchAndValidate(projectRoot, runOnce);
964
+ return blockingOnly ? totalErrors - repairedOnSave : totalErrors;
930
965
  }
931
966
 
932
967
  // Directory names anywhere in the path that should never be watched.
@@ -3922,6 +3957,30 @@ function remoteFilesRefusal(read, {
3922
3957
  * person reads the lines.
3923
3958
  */
3924
3959
  async function publish(options) {
3960
+ // Content that does not validate is not published. This is the check that
3961
+ // lets the Studio trust a connected project's published content without
3962
+ // re-checking every file itself: whatever reached the site went through
3963
+ // here. Without `--fix`, so CI never rewrites what it was asked to publish.
3964
+ //
3965
+ // Refused on what the Studio's Publish refuses on, and no more: an error
3966
+ // the Studio leaves to the server to repair (a local image in a remote
3967
+ // project, metadata to fill in) is printed and published, as it would be
3968
+ // from the Studio. Every site made on /new starts with its template's
3969
+ // images local, so counting those would refuse its first publish.
3970
+ if (!options.skipValidation) {
3971
+ const projectRoot = options.root ? path__default["default"].resolve(options.root) : process.cwd();
3972
+ const errorCount = await validateOnce({
3973
+ projectRoot,
3974
+ fix: false,
3975
+ blockingOnly: true
3976
+ });
3977
+ if (errorCount > 0) {
3978
+ error(`Not published: ${errorCount} validation error${errorCount === 1 ? "" : "s"}.`);
3979
+ console.error(pc__default["default"].dim(` Run "val validate --fix" in the project, commit what it changes and push, ` + `and fix by hand what it cannot. --skip-validation publishes anyway.`));
3980
+ process.exitCode = 1;
3981
+ return;
3982
+ }
3983
+ }
3925
3984
  const result = await runPublish({
3926
3985
  ...(options.root ? {
3927
3986
  root: options.root
@@ -4216,6 +4275,8 @@ async function main() {
4216
4275
  --links-own-css The app links its own CSS (--no-links-own-css for the
4217
4276
  opposite; omit it when the build did not say)
4218
4277
  --dry-run Verify, and stop before the site changes
4278
+ --skip-validation Publish without running "val validate" first. It runs by
4279
+ default, and an error stops the publish
4219
4280
 
4220
4281
  Command: list-unused-files
4221
4282
  Description: EXPERIMENTAL.
@@ -4307,6 +4368,9 @@ async function main() {
4307
4368
  dryRun: {
4308
4369
  type: "boolean"
4309
4370
  },
4371
+ skipValidation: {
4372
+ type: "boolean"
4373
+ },
4310
4374
  yes: {
4311
4375
  type: "boolean"
4312
4376
  },
@@ -4378,7 +4442,8 @@ async function main() {
4378
4442
  layerRev: flags.layerRev,
4379
4443
  buildHash: flags.buildHash,
4380
4444
  linksOwnCss: flags.linksOwnCss,
4381
- dryRun: flags.dryRun
4445
+ dryRun: flags.dryRun,
4446
+ skipValidation: flags.skipValidation
4382
4447
  });
4383
4448
  case "login":
4384
4449
  return login({