@valbuild/cli 0.93.0 → 0.94.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.
Files changed (26) hide show
  1. package/cli/dist/valbuild-cli-cli.cjs.dev.js +415 -160
  2. package/cli/dist/valbuild-cli-cli.cjs.prod.js +415 -160
  3. package/cli/dist/valbuild-cli-cli.esm.js +413 -158
  4. package/package.json +4 -4
  5. package/src/__fixtures__/basic/content/basic-errors.val.ts +7 -0
  6. package/src/__fixtures__/basic/content/basic-files.val.ts +14 -0
  7. package/src/__fixtures__/basic/content/basic-gallery-2.val.ts +17 -0
  8. package/src/__fixtures__/basic/content/basic-gallery-fail-on-non-unique-dir.val.ts +17 -0
  9. package/src/__fixtures__/basic/content/basic-gallery-missing-tracked.val.ts +17 -0
  10. package/src/__fixtures__/basic/content/basic-gallery-wrong-metadata.val.ts +17 -0
  11. package/src/__fixtures__/basic/content/basic-gallery.val.ts +18 -0
  12. package/src/__fixtures__/basic/content/basic-image-from-galleries.val.ts +15 -0
  13. package/src/__fixtures__/basic/content/basic-image-from-gallery.val.ts +12 -0
  14. package/src/__fixtures__/basic/content/basic-image.val.ts +7 -0
  15. package/src/__fixtures__/basic/content/basic-valid.val.ts +7 -0
  16. package/src/__fixtures__/basic/public/val/files/tracked.txt +1 -0
  17. package/src/__fixtures__/basic/public/val/files/untracked.txt +1 -0
  18. package/src/__fixtures__/basic/public/val/image.png +0 -0
  19. package/src/__fixtures__/basic/public/val/images/image.png +0 -0
  20. package/src/__fixtures__/basic/public/val/images2/image.png +0 -0
  21. package/src/__fixtures__/basic/public/val/images3/image.png +0 -0
  22. package/src/__fixtures__/basic/tsconfig.json +12 -0
  23. package/src/__fixtures__/basic/val.config.ts +5 -0
  24. package/src/runValidation.test.ts +386 -0
  25. package/src/runValidation.ts +1096 -0
  26. package/src/validate.ts +131 -887
@@ -3,17 +3,17 @@
3
3
  var meow = require('meow');
4
4
  var chalk = require('chalk');
5
5
  var path = require('path');
6
- var server = require('@valbuild/server');
7
- var core = require('@valbuild/core');
8
- var internal = require('@valbuild/shared/internal');
9
- var fastGlob = require('fast-glob');
10
- var picocolors = require('picocolors');
6
+ var pc = require('picocolors');
11
7
  var fs = require('fs/promises');
8
+ var fastGlob = require('fast-glob');
9
+ var core = require('@valbuild/core');
10
+ var server = require('@valbuild/server');
12
11
  var vm = require('node:vm');
13
12
  var ts = require('typescript');
14
13
  var z = require('zod');
15
14
  var node_module = require('node:module');
16
- var fs$1 = require('fs');
15
+ var internal = require('@valbuild/shared/internal');
16
+ var nodeFs = require('fs');
17
17
 
18
18
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
19
19
 
@@ -38,12 +38,12 @@ function _interopNamespace(e) {
38
38
  var meow__default = /*#__PURE__*/_interopDefault(meow);
39
39
  var chalk__default = /*#__PURE__*/_interopDefault(chalk);
40
40
  var path__default = /*#__PURE__*/_interopDefault(path);
41
- var picocolors__default = /*#__PURE__*/_interopDefault(picocolors);
41
+ var pc__default = /*#__PURE__*/_interopDefault(pc);
42
42
  var fs__default = /*#__PURE__*/_interopDefault(fs);
43
43
  var vm__default = /*#__PURE__*/_interopDefault(vm);
44
44
  var ts__default = /*#__PURE__*/_interopDefault(ts);
45
45
  var z__default = /*#__PURE__*/_interopDefault(z);
46
- var fs__default$1 = /*#__PURE__*/_interopDefault(fs$1);
46
+ var nodeFs__default = /*#__PURE__*/_interopDefault(nodeFs);
47
47
 
48
48
  function error(message) {
49
49
  console.error(chalk__default["default"].red("❌Error: ") + message);
@@ -130,6 +130,7 @@ const textEncoder = new TextEncoder();
130
130
 
131
131
  // Handler functions
132
132
  async function handleFileMetadata(ctx) {
133
+ var _fileSource$source;
133
134
  const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
134
135
  if (!ctx.valModule.source || !ctx.valModule.schema) {
135
136
  return {
@@ -138,24 +139,21 @@ async function handleFileMetadata(ctx) {
138
139
  };
139
140
  }
140
141
  const fileSource = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
141
- let filePath = null;
142
- try {
143
- var _fileSource$source;
144
- filePath = path__default["default"].join(ctx.projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
- (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[core.FILE_REF_PROP]);
146
- await fs__default["default"].access(filePath);
147
- } catch {
148
- if (filePath) {
149
- return {
150
- success: false,
151
- errorMessage: `File ${filePath} does not exist`
152
- };
153
- } else {
154
- return {
155
- success: false,
156
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
157
- };
158
- }
142
+
143
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
+ const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[core.FILE_REF_PROP];
145
+ if (!fileRefProp) {
146
+ return {
147
+ success: false,
148
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
149
+ };
150
+ }
151
+ const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
152
+ if (!ctx.fs.fileExists(filePath)) {
153
+ return {
154
+ success: false,
155
+ errorMessage: `File ${filePath} does not exist`
156
+ };
159
157
  }
160
158
  return {
161
159
  success: true,
@@ -197,7 +195,7 @@ async function handleKeyOfCheck(ctx) {
197
195
  };
198
196
  }
199
197
  async function handleRemoteFileUpload(ctx) {
200
- var _ctx$valConfigFile2;
198
+ var _resolvedRemoteFileAt;
201
199
  if (!ctx.fix) {
202
200
  return {
203
201
  success: false,
@@ -212,35 +210,37 @@ async function handleRemoteFileUpload(ctx) {
212
210
  };
213
211
  }
214
212
  const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
215
- let filePath = null;
216
- try {
217
- var _resolvedRemoteFileAt;
218
- filePath = path__default["default"].join(ctx.projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
- (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP]);
220
- await fs__default["default"].access(filePath);
221
- } catch {
222
- if (filePath) {
223
- return {
224
- success: false,
225
- errorMessage: `File ${filePath} does not exist`
226
- };
227
- } else {
228
- return {
229
- success: false,
230
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
231
- };
232
- }
213
+
214
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
+ const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
216
+ if (!fileRefProp) {
217
+ return {
218
+ success: false,
219
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
220
+ };
221
+ }
222
+ const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
223
+ if (!ctx.fs.fileExists(filePath)) {
224
+ return {
225
+ success: false,
226
+ errorMessage: `File ${filePath} does not exist`
227
+ };
233
228
  }
234
229
  const patFile = server.getPersonalAccessTokenPath(ctx.projectRoot);
235
- try {
236
- await fs__default["default"].access(patFile);
237
- } catch {
230
+ if (!ctx.fs.fileExists(patFile)) {
238
231
  return {
239
232
  success: false,
240
233
  errorMessage: `File: ${path__default["default"].join(ctx.projectRoot, ctx.file)} has remote images that are not uploaded and you are not logged in.\n\nFix this error by logging in:\n\t"npx val login"\n`
241
234
  };
242
235
  }
243
- const parsedPatFile = server.parsePersonalAccessTokenFile(await fs__default["default"].readFile(patFile, "utf-8"));
236
+ const patFileContent = ctx.fs.readFile(patFile);
237
+ if (patFileContent === undefined) {
238
+ return {
239
+ success: false,
240
+ errorMessage: `Could not read personal access token file at ${patFile}`
241
+ };
242
+ }
243
+ const parsedPatFile = server.parsePersonalAccessTokenFile(patFileContent);
244
244
  if (!parsedPatFile.success) {
245
245
  return {
246
246
  success: false,
@@ -251,9 +251,12 @@ async function handleRemoteFileUpload(ctx) {
251
251
  pat
252
252
  } = parsedPatFile.data;
253
253
  if (ctx.remoteFiles[ctx.sourcePath]) {
254
- console.log(picocolors__default["default"].yellow("⚠"), `Remote file ${filePath} already uploaded`);
255
254
  return {
256
- success: true
255
+ success: true,
256
+ events: [{
257
+ type: "remote-already-uploaded",
258
+ filePath
259
+ }]
257
260
  };
258
261
  }
259
262
  if (!resolvedRemoteFileAtSourcePath.schema) {
@@ -271,22 +274,18 @@ async function handleRemoteFileUpload(ctx) {
271
274
  errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
272
275
  };
273
276
  }
277
+ const projectName = ctx.project;
274
278
  let publicProjectId = ctx.publicProjectId;
275
279
  let remoteFileBuckets = ctx.remoteFileBuckets;
276
280
  let remoteFilesCounter = ctx.remoteFilesCounter;
277
281
  if (!publicProjectId || !remoteFileBuckets) {
278
- let projectName = process.env.VAL_PROJECT;
279
- if (!projectName) {
280
- var _ctx$valConfigFile;
281
- projectName = (_ctx$valConfigFile = ctx.valConfigFile) === null || _ctx$valConfigFile === void 0 ? void 0 : _ctx$valConfigFile.project;
282
- }
283
282
  if (!projectName) {
284
283
  return {
285
284
  success: false,
286
- errorMessage: "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config"
285
+ errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
287
286
  };
288
287
  }
289
- const settingsRes = await server.getSettings(projectName, {
288
+ const settingsRes = await ctx.remote.getSettings(projectName, {
290
289
  pat
291
290
  });
292
291
  if (!settingsRes.success) {
@@ -304,7 +303,7 @@ async function handleRemoteFileUpload(ctx) {
304
303
  errorMessage: "Could not get public project id"
305
304
  };
306
305
  }
307
- if (!((_ctx$valConfigFile2 = ctx.valConfigFile) !== null && _ctx$valConfigFile2 !== void 0 && _ctx$valConfigFile2.project)) {
306
+ if (!projectName) {
308
307
  return {
309
308
  success: false,
310
309
  errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
@@ -324,13 +323,11 @@ async function handleRemoteFileUpload(ctx) {
324
323
  errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
325
324
  };
326
325
  }
327
- let fileBuffer;
328
- try {
329
- fileBuffer = await fs__default["default"].readFile(filePath);
330
- } catch (e) {
326
+ const fileBuffer = ctx.fs.readBuffer(filePath);
327
+ if (fileBuffer === undefined) {
331
328
  return {
332
329
  success: false,
333
- errorMessage: `Error reading file: ${e}`
330
+ errorMessage: `Error reading file: ${filePath}`
334
331
  };
335
332
  }
336
333
  const relativeFilePath = path__default["default"].relative(ctx.projectRoot, filePath).split(path__default["default"].sep).join("/");
@@ -345,7 +342,7 @@ async function handleRemoteFileUpload(ctx) {
345
342
  const fileExt = getFileExt(filePath);
346
343
  const schema = resolveRemoteFileSchema;
347
344
  const metadata = fileSourceMetadata;
348
- const ref = core.Internal.remote.createRemoteRef(ctx.valRemoteHost, {
345
+ const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
349
346
  publicProjectId,
350
347
  coreVersion,
351
348
  bucket,
@@ -353,8 +350,7 @@ async function handleRemoteFileUpload(ctx) {
353
350
  fileHash,
354
351
  filePath: relativeFilePath
355
352
  });
356
- console.log(picocolors__default["default"].yellow("⚠"), `Uploading remote file: '${ref}'...`);
357
- const remoteFileUpload = await server.uploadRemoteFile(ctx.contentHostUrl, ctx.valConfigFile.project, bucket, fileHash, fileExt, fileBuffer, {
353
+ const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
358
354
  pat
359
355
  });
360
356
  if (!remoteFileUpload.success) {
@@ -363,7 +359,6 @@ async function handleRemoteFileUpload(ctx) {
363
359
  errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
364
360
  };
365
361
  }
366
- console.log(picocolors__default["default"].green("✔"), `Completed upload of remote file: '${ref}'`);
367
362
  ctx.remoteFiles[ctx.sourcePath] = {
368
363
  ref,
369
364
  metadata: fileSourceMetadata
@@ -373,15 +368,25 @@ async function handleRemoteFileUpload(ctx) {
373
368
  shouldApplyPatch: true,
374
369
  publicProjectId,
375
370
  remoteFileBuckets,
376
- remoteFilesCounter
371
+ remoteFilesCounter,
372
+ events: [{
373
+ type: "remote-uploading",
374
+ ref
375
+ }, {
376
+ type: "remote-uploaded",
377
+ ref
378
+ }]
377
379
  };
378
380
  }
379
381
  async function handleRemoteFileDownload(ctx) {
380
382
  if (ctx.fix) {
381
- console.log(picocolors__default["default"].yellow("⚠"), `Downloading remote file in ${ctx.sourcePath}...`);
382
383
  return {
383
384
  success: true,
384
- shouldApplyPatch: true
385
+ shouldApplyPatch: true,
386
+ events: [{
387
+ type: "remote-downloading",
388
+ sourcePath: ctx.sourcePath
389
+ }]
385
390
  };
386
391
  } else {
387
392
  return {
@@ -546,6 +551,103 @@ async function handleRouteCheck(ctx) {
546
551
  success: true
547
552
  };
548
553
  }
554
+ async function handleUniqueFolderCheck(ctx) {
555
+ const value = ctx.validationError.value;
556
+ if (!value || typeof value.directory !== "string") {
557
+ return {
558
+ success: false,
559
+ errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
560
+ };
561
+ }
562
+ const {
563
+ directory
564
+ } = value;
565
+ const conflicts = [];
566
+ for (const file of ctx.valFiles) {
567
+ const otherModuleFilePath = `/${file}`;
568
+ if (otherModuleFilePath === ctx.moduleFilePath) continue;
569
+ const otherModule = await ctx.service.get(otherModuleFilePath, "", {
570
+ source: false,
571
+ schema: true,
572
+ validate: false
573
+ });
574
+ const schema = otherModule.schema;
575
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
576
+ conflicts.push(otherModuleFilePath);
577
+ }
578
+ }
579
+ if (conflicts.length > 0) {
580
+ return {
581
+ success: false,
582
+ errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
583
+ };
584
+ }
585
+ return {
586
+ success: true
587
+ };
588
+ }
589
+ async function handleCheckAllFiles(ctx) {
590
+ const value = ctx.validationError.value;
591
+ if (!value || typeof value.directory !== "string") {
592
+ return {
593
+ success: false,
594
+ errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
595
+ };
596
+ }
597
+ const {
598
+ directory
599
+ } = value;
600
+ const source = ctx.valModule.source;
601
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
602
+ return {
603
+ success: false,
604
+ errorMessage: `Could not get source for ${ctx.sourcePath}`
605
+ };
606
+ }
607
+ const trackedFiles = new Set(Object.keys(source));
608
+
609
+ // Check that all tracked files exist on disk
610
+ const missingTrackedFiles = [...trackedFiles].filter(f => {
611
+ return !ctx.fs.fileExists(path__default["default"].join(ctx.projectRoot, f));
612
+ });
613
+ if (missingTrackedFiles.length > 0) {
614
+ if (!ctx.fix) {
615
+ return {
616
+ success: false,
617
+ errorMessage: `Gallery in ${ctx.moduleFilePath} has tracked files that do not exist on disk: ${missingTrackedFiles.join(", ")}. Add the files or remove them from the gallery.`
618
+ };
619
+ }
620
+ // fix: true — let createFixPatch remove the missing entries
621
+ return {
622
+ success: true,
623
+ shouldApplyPatch: true
624
+ };
625
+ }
626
+ const dirPath = path__default["default"].join(ctx.projectRoot, directory);
627
+ const filesInDir = [];
628
+ try {
629
+ const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
630
+ for (const entry of entries) {
631
+ const relPath = "/" + path__default["default"].relative(ctx.projectRoot, entry).split(path__default["default"].sep).join("/");
632
+ filesInDir.push(relPath);
633
+ }
634
+ } catch {
635
+ // directory doesn't exist — no untracked files possible
636
+ }
637
+ const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
638
+ if (untrackedFiles.length > 0) {
639
+ return {
640
+ success: false,
641
+ errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
642
+ };
643
+ }
644
+
645
+ // All files accounted for — trigger metadata verification via createFixPatch
646
+ return {
647
+ success: true,
648
+ shouldApplyPatch: true
649
+ };
650
+ }
549
651
 
550
652
  // Fix handler registry
551
653
  const currentFixHandlers = {
@@ -560,7 +662,13 @@ const currentFixHandlers = {
560
662
  "image:download-remote": handleRemoteFileDownload,
561
663
  "file:download-remote": handleRemoteFileDownload,
562
664
  "image:check-remote": handleRemoteFileCheck,
563
- "file:check-remote": handleRemoteFileCheck
665
+ "images:check-remote": handleRemoteFileCheck,
666
+ "file:check-remote": handleRemoteFileCheck,
667
+ "files:check-remote": handleRemoteFileCheck,
668
+ "images:check-unique-folder": handleUniqueFolderCheck,
669
+ "files:check-unique-folder": handleUniqueFolderCheck,
670
+ "images:check-all-files": handleCheckAllFiles,
671
+ "files:check-all-files": handleCheckAllFiles
564
672
  };
565
673
  const deprecatedFixHandlers = {
566
674
  "image:replace-metadata": handleFileMetadata
@@ -569,30 +677,36 @@ const fixHandlers = {
569
677
  ...deprecatedFixHandlers,
570
678
  ...currentFixHandlers
571
679
  };
572
- async function validate({
680
+ function createDefaultValFSHost() {
681
+ return {
682
+ ...ts__default["default"].sys,
683
+ writeFile: (fileName, data, encoding) => {
684
+ nodeFs__default["default"].mkdirSync(path__default["default"].dirname(fileName), {
685
+ recursive: true
686
+ });
687
+ nodeFs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
688
+ },
689
+ rmFile: nodeFs__default["default"].rmSync,
690
+ readBuffer: fileName => {
691
+ try {
692
+ return nodeFs__default["default"].readFileSync(fileName);
693
+ } catch {
694
+ return undefined;
695
+ }
696
+ }
697
+ };
698
+ }
699
+ async function* runValidation({
573
700
  root,
574
- fix
701
+ fix,
702
+ valFiles,
703
+ project,
704
+ remote,
705
+ fs
575
706
  }) {
576
- const valRemoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST;
577
- const contentHostUrl = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
578
- const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
579
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
580
- console.log(picocolors__default["default"].greenBright(`Validating project${valConfigFile !== null && valConfigFile !== void 0 && valConfigFile.project ? ` '${picocolors__default["default"].inverse(valConfigFile === null || valConfigFile === void 0 ? void 0 : valConfigFile.project)}'` : ""}...`));
581
- const service = await server.createService(projectRoot, {});
582
- let prettier;
583
- try {
584
- prettier = (await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('prettier')); })).default;
585
- } catch {
586
- console.log("Prettier not found, skipping formatting");
587
- }
588
- const valFiles = await fastGlob.glob("**/*.val.{js,ts}", {
589
- ignore: ["node_modules/**"],
590
- cwd: projectRoot
591
- });
707
+ const projectRoot = path__default["default"].resolve(root);
708
+ const service = await server.createService(projectRoot, {}, fs);
592
709
  let errors = 0;
593
- console.log(picocolors__default["default"].greenBright(`Found ${valFiles.length} files...`));
594
- let publicProjectId;
595
- let didFix = false;
596
710
 
597
711
  // Create caches that persist across all file validations
598
712
  const keyOfCache = new Map();
@@ -600,7 +714,7 @@ async function validate({
600
714
  loaded: false,
601
715
  modules: {}
602
716
  };
603
- async function validateFile(file) {
717
+ async function* validateFile(file) {
604
718
  const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
605
719
  const start = Date.now();
606
720
  const valModule = await service.get(moduleFilePath, "", {
@@ -612,10 +726,14 @@ async function validate({
612
726
  let remoteFileBuckets = undefined;
613
727
  let remoteFilesCounter = 0;
614
728
  if (!valModule.errors) {
615
- console.log(picocolors__default["default"].green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
616
- return 0;
729
+ yield {
730
+ type: "file-valid",
731
+ file: moduleFilePath,
732
+ durationMs: Date.now() - start
733
+ };
734
+ return;
617
735
  } else {
618
- let errors = 0;
736
+ let fileErrors = 0;
619
737
  let fixedErrors = 0;
620
738
  if (valModule.errors) {
621
739
  if (valModule.errors.validation) {
@@ -623,8 +741,12 @@ async function validate({
623
741
  for (const v of validationErrors) {
624
742
  if (!v.fixes || v.fixes.length === 0) {
625
743
  // No fixes available - just report error
626
- errors += 1;
627
- console.log(picocolors__default["default"].red("✘"), "Got error in", `${sourcePath}:`, v.message);
744
+ fileErrors += 1;
745
+ yield {
746
+ type: "validation-error",
747
+ sourcePath,
748
+ message: v.message
749
+ };
628
750
  continue;
629
751
  }
630
752
 
@@ -632,8 +754,12 @@ async function validate({
632
754
  const fixType = v.fixes[0]; // Take first fix
633
755
  const handler = fixHandlers[fixType];
634
756
  if (!handler) {
635
- console.log(picocolors__default["default"].red("✘"), "Unknown fix", v.fixes, "for", sourcePath);
636
- errors += 1;
757
+ yield {
758
+ type: "unknown-fix",
759
+ sourcePath,
760
+ fixes: v.fixes
761
+ };
762
+ fileErrors += 1;
637
763
  continue;
638
764
  }
639
765
 
@@ -648,21 +774,25 @@ async function validate({
648
774
  valFiles,
649
775
  moduleFilePath,
650
776
  file,
777
+ fs,
651
778
  remoteFiles,
652
- publicProjectId,
779
+ publicProjectId: undefined,
653
780
  remoteFileBuckets,
654
781
  remoteFilesCounter,
655
- valRemoteHost,
656
- contentHostUrl,
657
- valConfigFile: valConfigFile ?? undefined,
782
+ remote,
783
+ project,
658
784
  keyOfCache,
659
785
  routerModulesCache
660
786
  });
661
787
 
662
- // Update shared state from handler result
663
- if (result.publicProjectId !== undefined) {
664
- publicProjectId = result.publicProjectId;
788
+ // Yield any events from handler
789
+ if (result.events) {
790
+ for (const event of result.events) {
791
+ yield event;
792
+ }
665
793
  }
794
+
795
+ // Update shared state from handler result
666
796
  if (result.remoteFileBuckets !== undefined) {
667
797
  remoteFileBuckets = result.remoteFileBuckets;
668
798
  }
@@ -670,69 +800,98 @@ async function validate({
670
800
  remoteFilesCounter = result.remoteFilesCounter;
671
801
  }
672
802
  if (!result.success) {
673
- console.log(picocolors__default["default"].red("✘"), result.errorMessage);
674
- errors += 1;
803
+ yield {
804
+ type: "validation-error",
805
+ sourcePath,
806
+ message: result.errorMessage ?? "Unknown error"
807
+ };
808
+ fileErrors += 1;
675
809
  continue;
676
810
  }
677
811
 
678
812
  // Apply patch if needed
679
813
  if (result.shouldApplyPatch) {
680
- var _fixPatch$remainingEr;
681
814
  const fixPatch = await server.createFixPatch({
682
815
  projectRoot,
683
- remoteHost: valRemoteHost
816
+ remoteHost: remote.remoteHost
684
817
  }, !!fix, sourcePath, v, remoteFiles, valModule.source, valModule.schema);
685
818
  if (fix && fixPatch !== null && fixPatch !== void 0 && fixPatch.patch && (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.patch.length) > 0) {
686
819
  await service.patch(moduleFilePath, fixPatch.patch);
687
- didFix = true;
688
820
  fixedErrors += 1;
689
- console.log(picocolors__default["default"].yellow("⚠"), "Applied fix for", sourcePath);
821
+ yield {
822
+ type: "fix-applied",
823
+ file,
824
+ sourcePath
825
+ };
826
+ } else if (!fix && fixPatch !== null && fixPatch !== void 0 && fixPatch.patch && (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.patch.length) > 0) {
827
+ fileErrors += 1;
828
+ yield {
829
+ type: "validation-fixable-error",
830
+ sourcePath,
831
+ message: v.message,
832
+ fixable: true
833
+ };
834
+ }
835
+ for (const e of (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.remainingErrors) ?? []) {
836
+ fileErrors += 1;
837
+ yield {
838
+ type: "validation-fixable-error",
839
+ sourcePath,
840
+ message: e.message,
841
+ fixable: !!(e.fixes && e.fixes.length)
842
+ };
690
843
  }
691
- fixPatch === null || fixPatch === void 0 || (_fixPatch$remainingEr = fixPatch.remainingErrors) === null || _fixPatch$remainingEr === void 0 || _fixPatch$remainingEr.forEach(e => {
692
- errors += 1;
693
- console.log(e.fixes && e.fixes.length ? picocolors__default["default"].yellow("⚠") : picocolors__default["default"].red("✘"), `Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`, `${sourcePath}:`, e.message);
694
- });
695
844
  }
696
845
  }
697
846
  }
698
847
  }
699
- if (fixedErrors === errors && (!valModule.errors.fatal || valModule.errors.fatal.length == 0)) {
700
- console.log(picocolors__default["default"].green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
848
+ if (fixedErrors === fileErrors && (!valModule.errors.fatal || valModule.errors.fatal.length == 0)) {
849
+ yield {
850
+ type: "file-valid",
851
+ file: moduleFilePath,
852
+ durationMs: Date.now() - start
853
+ };
701
854
  }
702
855
  for (const fatalError of valModule.errors.fatal || []) {
703
- errors += 1;
704
- console.log(picocolors__default["default"].red("✘"), moduleFilePath, "is invalid:", fatalError.message);
856
+ fileErrors += 1;
857
+ yield {
858
+ type: "fatal-error",
859
+ file: moduleFilePath,
860
+ message: fatalError.message
861
+ };
705
862
  }
706
863
  } else {
707
- console.log(picocolors__default["default"].green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
864
+ yield {
865
+ type: "file-valid",
866
+ file: moduleFilePath,
867
+ durationMs: Date.now() - start
868
+ };
708
869
  }
709
- if (errors > 0) {
710
- console.log(picocolors__default["default"].red("✘"), `${`/${file}`} contains ${errors} error${errors > 1 ? "s" : ""}`, " (" + (Date.now() - start) + "ms)");
870
+ if (fileErrors > 0) {
871
+ yield {
872
+ type: "file-error-count",
873
+ file: `/${file}`,
874
+ errorCount: fileErrors,
875
+ durationMs: Date.now() - start
876
+ };
711
877
  }
712
- return errors;
878
+ errors += fileErrors;
713
879
  }
714
880
  }
715
881
  for (const file of valFiles.sort()) {
716
- didFix = false;
717
- errors += await validateFile(file);
718
- if (prettier && didFix) {
719
- var _prettier;
720
- const filePath = path__default["default"].join(projectRoot, file);
721
- const fileContent = await fs__default["default"].readFile(filePath, "utf-8");
722
- const formattedContent = await ((_prettier = prettier) === null || _prettier === void 0 ? void 0 : _prettier.format(fileContent, {
723
- filepath: filePath
724
- }));
725
- await fs__default["default"].writeFile(filePath, formattedContent);
726
- }
882
+ yield* validateFile(file);
727
883
  }
884
+ service.dispose();
728
885
  if (errors > 0) {
729
- console.log(picocolors__default["default"].red("✘"), "Got", errors, "error" + (errors > 1 ? "s" : ""));
730
- process.exit(1);
886
+ yield {
887
+ type: "summary-errors",
888
+ count: errors
889
+ };
731
890
  } else {
732
- console.log(picocolors__default["default"].green("✔"), "No validation errors found");
891
+ yield {
892
+ type: "summary-success"
893
+ };
733
894
  }
734
- service.dispose();
735
- return;
736
895
  }
737
896
 
738
897
  // GPT generated levenshtein distance algorithm:
@@ -760,6 +919,102 @@ function findSimilar(key, targets) {
760
919
  })).sort((a, b) => a.distance - b.distance);
761
920
  }
762
921
 
922
+ async function validate({
923
+ root,
924
+ fix
925
+ }) {
926
+ const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
927
+ const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
928
+ const resolvedValConfigFile = valConfigFile ? {
929
+ ...valConfigFile,
930
+ project: process.env.VAL_PROJECT || valConfigFile.project
931
+ } : process.env.VAL_PROJECT ? {
932
+ project: process.env.VAL_PROJECT
933
+ } : undefined;
934
+ console.log(pc__default["default"].greenBright(`Validating project${resolvedValConfigFile !== null && resolvedValConfigFile !== void 0 && resolvedValConfigFile.project ? ` '${pc__default["default"].inverse(resolvedValConfigFile.project)}'` : ""}...`));
935
+ const valFiles = await fastGlob.glob("**/*.val.{js,ts}", {
936
+ ignore: ["node_modules/**"],
937
+ cwd: projectRoot
938
+ });
939
+ console.log(pc__default["default"].greenBright(`Found ${valFiles.length} files...`));
940
+ let prettier;
941
+ try {
942
+ prettier = (await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('prettier')); })).default;
943
+ } catch {
944
+ console.log("Prettier not found, skipping formatting");
945
+ }
946
+ const fixedFiles = new Set();
947
+ let totalErrors = 0;
948
+ for await (const event of runValidation({
949
+ root: projectRoot,
950
+ fix: !!fix,
951
+ valFiles,
952
+ project: resolvedValConfigFile === null || resolvedValConfigFile === void 0 ? void 0 : resolvedValConfigFile.project,
953
+ remote: {
954
+ remoteHost: process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST,
955
+ getSettings: (projectName, options) => server.getSettings(projectName, options),
956
+ uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) => server.uploadRemoteFile(process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST, project, bucket, fileHash, fileExt ?? "", fileBuffer, options)
957
+ },
958
+ fs: createDefaultValFSHost()
959
+ })) {
960
+ switch (event.type) {
961
+ case "file-valid":
962
+ console.log(pc__default["default"].green("✔"), event.file, "is valid (" + event.durationMs + "ms)");
963
+ break;
964
+ case "file-error-count":
965
+ console.log(pc__default["default"].red("✘"), `${event.file} contains ${event.errorCount} error${event.errorCount > 1 ? "s" : ""}`, " (" + event.durationMs + "ms)");
966
+ totalErrors += event.errorCount;
967
+ break;
968
+ case "validation-error":
969
+ console.log(pc__default["default"].red("✘"), "Got error in", `${event.sourcePath}:`, event.message);
970
+ break;
971
+ case "validation-fixable-error":
972
+ console.log(event.fixable ? pc__default["default"].yellow("⚠") : pc__default["default"].red("✘"), `Got ${event.fixable ? "fixable " : ""}error in`, `${event.sourcePath}:`, event.message);
973
+ break;
974
+ case "unknown-fix":
975
+ console.log(pc__default["default"].red("✘"), "Unknown fix", event.fixes, "for", event.sourcePath);
976
+ break;
977
+ case "fix-applied":
978
+ console.log(pc__default["default"].yellow("⚠"), "Applied fix for", event.sourcePath);
979
+ fixedFiles.add(event.file);
980
+ break;
981
+ case "fatal-error":
982
+ console.log(pc__default["default"].red("✘"), event.file, "is invalid:", event.message);
983
+ break;
984
+ case "remote-uploading":
985
+ console.log(pc__default["default"].yellow("⚠"), `Uploading remote file: '${event.ref}'...`);
986
+ break;
987
+ case "remote-uploaded":
988
+ console.log(pc__default["default"].green("✔"), `Completed upload of remote file: '${event.ref}'`);
989
+ break;
990
+ case "remote-already-uploaded":
991
+ console.log(pc__default["default"].yellow("⚠"), `Remote file ${event.filePath} already uploaded`);
992
+ break;
993
+ case "remote-downloading":
994
+ console.log(pc__default["default"].yellow("⚠"), `Downloading remote file in ${event.sourcePath}...`);
995
+ break;
996
+ }
997
+ }
998
+
999
+ // Run prettier on files that had fixes applied
1000
+ if (prettier) {
1001
+ for (const file of fixedFiles) {
1002
+ const filePath = path__default["default"].join(projectRoot, file);
1003
+ const fileContent = await fs__default["default"].readFile(filePath, "utf-8");
1004
+ const formattedContent = await prettier.format(fileContent, {
1005
+ filepath: filePath
1006
+ });
1007
+ await fs__default["default"].writeFile(filePath, formattedContent);
1008
+ }
1009
+ }
1010
+ if (totalErrors > 0) {
1011
+ console.log(pc__default["default"].red("✘"), "Got", totalErrors, "error" + (totalErrors > 1 ? "s" : ""));
1012
+ process.exit(1);
1013
+ } else {
1014
+ console.log(pc__default["default"].green("✔"), "No validation errors found");
1015
+ }
1016
+ }
1017
+
763
1018
  async function listUnusedFiles({
764
1019
  root
765
1020
  }) {
@@ -867,7 +1122,7 @@ async function connect(options) {
867
1122
  params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
868
1123
  }
869
1124
  const url = `${host$1}/connect?${params.toString()}`;
870
- console.log(picocolors__default["default"].dim(`\nFollow the instructions in your browser to complete the setup:\n${picocolors__default["default"].cyan(url)}\n`));
1125
+ console.log(pc__default["default"].dim(`\nFollow the instructions in your browser to complete the setup:\n${pc__default["default"].cyan(url)}\n`));
871
1126
  }
872
1127
  async function tryGetProject(projectRoot) {
873
1128
  const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
@@ -879,7 +1134,7 @@ async function tryGetProject(projectRoot) {
879
1134
  projectName: parts[1]
880
1135
  };
881
1136
  } else {
882
- console.error(picocolors__default["default"].red(`Invalid project format in val.config file: "${valConfigFile.project}". Expected format "orgName/projectName".`));
1137
+ console.error(pc__default["default"].red(`Invalid project format in val.config file: "${valConfigFile.project}". Expected format "orgName/projectName".`));
883
1138
  process.exit(1);
884
1139
  }
885
1140
  }
@@ -914,7 +1169,7 @@ async function tryGetGitRemote(root) {
914
1169
  }
915
1170
  return null;
916
1171
  } catch (error) {
917
- console.error(picocolors__default["default"].red("Failed to read .git/config file."), error);
1172
+ console.error(pc__default["default"].red("Failed to read .git/config file."), error);
918
1173
  return null;
919
1174
  }
920
1175
  }
@@ -923,8 +1178,8 @@ function tryGetGitConfig(root) {
923
1178
  let lastDir = null;
924
1179
  while (currentDir !== lastDir) {
925
1180
  const gitConfigPath = path__default["default"].join(currentDir, ".git", "config");
926
- if (fs__default$1["default"].existsSync(gitConfigPath)) {
927
- return fs__default$1["default"].readFileSync(gitConfigPath, "utf-8");
1181
+ if (nodeFs__default["default"].existsSync(gitConfigPath)) {
1182
+ return nodeFs__default["default"].readFileSync(gitConfigPath, "utf-8");
928
1183
  }
929
1184
  lastDir = currentDir;
930
1185
  currentDir = path__default["default"].dirname(currentDir);
@@ -939,7 +1194,7 @@ const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
939
1194
  async function login(options) {
940
1195
  try {
941
1196
  var _response$headers$get;
942
- console.log(picocolors__default["default"].cyan("\nStarting login process...\n"));
1197
+ console.log(pc__default["default"].cyan("\nStarting login process...\n"));
943
1198
 
944
1199
  // Step 1: Initiate login and get token and URL
945
1200
  const response = await fetch(`${host}/api/login`, {
@@ -952,7 +1207,7 @@ async function login(options) {
952
1207
  let url;
953
1208
  if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
954
1209
  const text = await response.text();
955
- console.error(picocolors__default["default"].red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
1210
+ console.error(pc__default["default"].red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
956
1211
  process.exit(1);
957
1212
  }
958
1213
  const json = await response.json();
@@ -961,12 +1216,12 @@ async function login(options) {
961
1216
  url = json.url;
962
1217
  }
963
1218
  if (!token || !url) {
964
- console.error(picocolors__default["default"].red("Unexpected response from the server."), json);
1219
+ console.error(pc__default["default"].red("Unexpected response from the server."), json);
965
1220
  process.exit(1);
966
1221
  }
967
- console.log(picocolors__default["default"].green("Open the following URL in your browser to log in:"));
968
- console.log(picocolors__default["default"].underline(picocolors__default["default"].blue(url)));
969
- console.log(picocolors__default["default"].dim("\nWaiting for login confirmation...\n"));
1222
+ console.log(pc__default["default"].green("Open the following URL in your browser to log in:"));
1223
+ console.log(pc__default["default"].underline(pc__default["default"].blue(url)));
1224
+ console.log(pc__default["default"].dim("\nWaiting for login confirmation...\n"));
970
1225
 
971
1226
  // Step 2: Poll for login confirmation
972
1227
  const result = await pollForConfirmation(token);
@@ -975,7 +1230,7 @@ async function login(options) {
975
1230
  const filePath = server.getPersonalAccessTokenPath(options.root || process.cwd());
976
1231
  saveToken(result, filePath);
977
1232
  } catch (error) {
978
- console.error(picocolors__default["default"].red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
1233
+ console.error(pc__default["default"].red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
979
1234
  process.exit(1);
980
1235
  }
981
1236
  }
@@ -988,7 +1243,7 @@ async function pollForConfirmation(token) {
988
1243
  method: "POST"
989
1244
  });
990
1245
  if (response.status === 500) {
991
- console.error(picocolors__default["default"].red("An error occurred on the server."));
1246
+ console.error(pc__default["default"].red("An error occurred on the server."));
992
1247
  process.exit(1);
993
1248
  }
994
1249
  if (response.status === 200) {
@@ -997,21 +1252,21 @@ async function pollForConfirmation(token) {
997
1252
  if (typeof json.profile.email === "string" && typeof json.pat === "string") {
998
1253
  return json;
999
1254
  } else {
1000
- console.error(picocolors__default["default"].red("Unexpected response from the server."));
1255
+ console.error(pc__default["default"].red("Unexpected response from the server."));
1001
1256
  process.exit(1);
1002
1257
  }
1003
1258
  }
1004
1259
  }
1005
1260
  }
1006
- console.error(picocolors__default["default"].red("Login confirmation timed out."));
1261
+ console.error(pc__default["default"].red("Login confirmation timed out."));
1007
1262
  process.exit(1);
1008
1263
  }
1009
1264
  function saveToken(result, filePath) {
1010
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(filePath), {
1265
+ nodeFs__default["default"].mkdirSync(path__default["default"].dirname(filePath), {
1011
1266
  recursive: true
1012
1267
  });
1013
- fs__default$1["default"].writeFileSync(filePath, JSON.stringify(result, null, 2));
1014
- console.log(picocolors__default["default"].green(`Token for ${picocolors__default["default"].cyan(result.profile.email)} saved to ${picocolors__default["default"].cyan(filePath)}`));
1268
+ nodeFs__default["default"].writeFileSync(filePath, JSON.stringify(result, null, 2));
1269
+ console.log(pc__default["default"].green(`Token for ${pc__default["default"].cyan(result.profile.email)} saved to ${pc__default["default"].cyan(filePath)}`));
1015
1270
  }
1016
1271
 
1017
1272
  async function main() {