@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
@@ -1,17 +1,17 @@
1
1
  import meow from 'meow';
2
2
  import chalk from 'chalk';
3
3
  import path from 'path';
4
- import { createService, createFixPatch, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getSettings, uploadRemoteFile } from '@valbuild/server';
5
- import { DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST, Internal, FILE_REF_PROP, VAL_EXTENSION } from '@valbuild/core';
6
- import { filterRoutesByPatterns, validateRoutePatterns } from '@valbuild/shared/internal';
7
- import { glob } from 'fast-glob';
8
- import picocolors from 'picocolors';
4
+ import pc from 'picocolors';
9
5
  import fs from 'fs/promises';
6
+ import { glob } from 'fast-glob';
7
+ import { Internal, FILE_REF_PROP, DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST, VAL_EXTENSION } from '@valbuild/core';
8
+ import { createService, createFixPatch, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getSettings, uploadRemoteFile } from '@valbuild/server';
10
9
  import vm from 'node:vm';
11
10
  import ts from 'typescript';
12
11
  import z from 'zod';
13
12
  import { createRequire } from 'node:module';
14
- import fs$1 from 'fs';
13
+ import { filterRoutesByPatterns, validateRoutePatterns } from '@valbuild/shared/internal';
14
+ import nodeFs from 'fs';
15
15
 
16
16
  function error(message) {
17
17
  console.error(chalk.red("❌Error: ") + message);
@@ -98,6 +98,7 @@ const textEncoder = new TextEncoder();
98
98
 
99
99
  // Handler functions
100
100
  async function handleFileMetadata(ctx) {
101
+ var _fileSource$source;
101
102
  const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
102
103
  if (!ctx.valModule.source || !ctx.valModule.schema) {
103
104
  return {
@@ -106,24 +107,21 @@ async function handleFileMetadata(ctx) {
106
107
  };
107
108
  }
108
109
  const fileSource = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
109
- let filePath = null;
110
- try {
111
- var _fileSource$source;
112
- filePath = path.join(ctx.projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
- (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[FILE_REF_PROP]);
114
- await fs.access(filePath);
115
- } catch {
116
- if (filePath) {
117
- return {
118
- success: false,
119
- errorMessage: `File ${filePath} does not exist`
120
- };
121
- } else {
122
- return {
123
- success: false,
124
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
125
- };
126
- }
110
+
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[FILE_REF_PROP];
113
+ if (!fileRefProp) {
114
+ return {
115
+ success: false,
116
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
117
+ };
118
+ }
119
+ const filePath = path.join(ctx.projectRoot, fileRefProp);
120
+ if (!ctx.fs.fileExists(filePath)) {
121
+ return {
122
+ success: false,
123
+ errorMessage: `File ${filePath} does not exist`
124
+ };
127
125
  }
128
126
  return {
129
127
  success: true,
@@ -165,7 +163,7 @@ async function handleKeyOfCheck(ctx) {
165
163
  };
166
164
  }
167
165
  async function handleRemoteFileUpload(ctx) {
168
- var _ctx$valConfigFile2;
166
+ var _resolvedRemoteFileAt;
169
167
  if (!ctx.fix) {
170
168
  return {
171
169
  success: false,
@@ -180,35 +178,37 @@ async function handleRemoteFileUpload(ctx) {
180
178
  };
181
179
  }
182
180
  const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
183
- let filePath = null;
184
- try {
185
- var _resolvedRemoteFileAt;
186
- filePath = path.join(ctx.projectRoot, // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
- (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[FILE_REF_PROP]);
188
- await fs.access(filePath);
189
- } catch {
190
- if (filePath) {
191
- return {
192
- success: false,
193
- errorMessage: `File ${filePath} does not exist`
194
- };
195
- } else {
196
- return {
197
- success: false,
198
- errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
199
- };
200
- }
181
+
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[FILE_REF_PROP];
184
+ if (!fileRefProp) {
185
+ return {
186
+ success: false,
187
+ errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
188
+ };
189
+ }
190
+ const filePath = path.join(ctx.projectRoot, fileRefProp);
191
+ if (!ctx.fs.fileExists(filePath)) {
192
+ return {
193
+ success: false,
194
+ errorMessage: `File ${filePath} does not exist`
195
+ };
201
196
  }
202
197
  const patFile = getPersonalAccessTokenPath(ctx.projectRoot);
203
- try {
204
- await fs.access(patFile);
205
- } catch {
198
+ if (!ctx.fs.fileExists(patFile)) {
206
199
  return {
207
200
  success: false,
208
201
  errorMessage: `File: ${path.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`
209
202
  };
210
203
  }
211
- const parsedPatFile = parsePersonalAccessTokenFile(await fs.readFile(patFile, "utf-8"));
204
+ const patFileContent = ctx.fs.readFile(patFile);
205
+ if (patFileContent === undefined) {
206
+ return {
207
+ success: false,
208
+ errorMessage: `Could not read personal access token file at ${patFile}`
209
+ };
210
+ }
211
+ const parsedPatFile = parsePersonalAccessTokenFile(patFileContent);
212
212
  if (!parsedPatFile.success) {
213
213
  return {
214
214
  success: false,
@@ -219,9 +219,12 @@ async function handleRemoteFileUpload(ctx) {
219
219
  pat
220
220
  } = parsedPatFile.data;
221
221
  if (ctx.remoteFiles[ctx.sourcePath]) {
222
- console.log(picocolors.yellow("⚠"), `Remote file ${filePath} already uploaded`);
223
222
  return {
224
- success: true
223
+ success: true,
224
+ events: [{
225
+ type: "remote-already-uploaded",
226
+ filePath
227
+ }]
225
228
  };
226
229
  }
227
230
  if (!resolvedRemoteFileAtSourcePath.schema) {
@@ -239,22 +242,18 @@ async function handleRemoteFileUpload(ctx) {
239
242
  errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
240
243
  };
241
244
  }
245
+ const projectName = ctx.project;
242
246
  let publicProjectId = ctx.publicProjectId;
243
247
  let remoteFileBuckets = ctx.remoteFileBuckets;
244
248
  let remoteFilesCounter = ctx.remoteFilesCounter;
245
249
  if (!publicProjectId || !remoteFileBuckets) {
246
- let projectName = process.env.VAL_PROJECT;
247
- if (!projectName) {
248
- var _ctx$valConfigFile;
249
- projectName = (_ctx$valConfigFile = ctx.valConfigFile) === null || _ctx$valConfigFile === void 0 ? void 0 : _ctx$valConfigFile.project;
250
- }
251
250
  if (!projectName) {
252
251
  return {
253
252
  success: false,
254
- errorMessage: "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config"
253
+ errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
255
254
  };
256
255
  }
257
- const settingsRes = await getSettings(projectName, {
256
+ const settingsRes = await ctx.remote.getSettings(projectName, {
258
257
  pat
259
258
  });
260
259
  if (!settingsRes.success) {
@@ -272,7 +271,7 @@ async function handleRemoteFileUpload(ctx) {
272
271
  errorMessage: "Could not get public project id"
273
272
  };
274
273
  }
275
- if (!((_ctx$valConfigFile2 = ctx.valConfigFile) !== null && _ctx$valConfigFile2 !== void 0 && _ctx$valConfigFile2.project)) {
274
+ if (!projectName) {
276
275
  return {
277
276
  success: false,
278
277
  errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
@@ -292,13 +291,11 @@ async function handleRemoteFileUpload(ctx) {
292
291
  errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
293
292
  };
294
293
  }
295
- let fileBuffer;
296
- try {
297
- fileBuffer = await fs.readFile(filePath);
298
- } catch (e) {
294
+ const fileBuffer = ctx.fs.readBuffer(filePath);
295
+ if (fileBuffer === undefined) {
299
296
  return {
300
297
  success: false,
301
- errorMessage: `Error reading file: ${e}`
298
+ errorMessage: `Error reading file: ${filePath}`
302
299
  };
303
300
  }
304
301
  const relativeFilePath = path.relative(ctx.projectRoot, filePath).split(path.sep).join("/");
@@ -313,7 +310,7 @@ async function handleRemoteFileUpload(ctx) {
313
310
  const fileExt = getFileExt(filePath);
314
311
  const schema = resolveRemoteFileSchema;
315
312
  const metadata = fileSourceMetadata;
316
- const ref = Internal.remote.createRemoteRef(ctx.valRemoteHost, {
313
+ const ref = Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
317
314
  publicProjectId,
318
315
  coreVersion,
319
316
  bucket,
@@ -321,8 +318,7 @@ async function handleRemoteFileUpload(ctx) {
321
318
  fileHash,
322
319
  filePath: relativeFilePath
323
320
  });
324
- console.log(picocolors.yellow("⚠"), `Uploading remote file: '${ref}'...`);
325
- const remoteFileUpload = await uploadRemoteFile(ctx.contentHostUrl, ctx.valConfigFile.project, bucket, fileHash, fileExt, fileBuffer, {
321
+ const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
326
322
  pat
327
323
  });
328
324
  if (!remoteFileUpload.success) {
@@ -331,7 +327,6 @@ async function handleRemoteFileUpload(ctx) {
331
327
  errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
332
328
  };
333
329
  }
334
- console.log(picocolors.green("✔"), `Completed upload of remote file: '${ref}'`);
335
330
  ctx.remoteFiles[ctx.sourcePath] = {
336
331
  ref,
337
332
  metadata: fileSourceMetadata
@@ -341,15 +336,25 @@ async function handleRemoteFileUpload(ctx) {
341
336
  shouldApplyPatch: true,
342
337
  publicProjectId,
343
338
  remoteFileBuckets,
344
- remoteFilesCounter
339
+ remoteFilesCounter,
340
+ events: [{
341
+ type: "remote-uploading",
342
+ ref
343
+ }, {
344
+ type: "remote-uploaded",
345
+ ref
346
+ }]
345
347
  };
346
348
  }
347
349
  async function handleRemoteFileDownload(ctx) {
348
350
  if (ctx.fix) {
349
- console.log(picocolors.yellow("⚠"), `Downloading remote file in ${ctx.sourcePath}...`);
350
351
  return {
351
352
  success: true,
352
- shouldApplyPatch: true
353
+ shouldApplyPatch: true,
354
+ events: [{
355
+ type: "remote-downloading",
356
+ sourcePath: ctx.sourcePath
357
+ }]
353
358
  };
354
359
  } else {
355
360
  return {
@@ -514,6 +519,103 @@ async function handleRouteCheck(ctx) {
514
519
  success: true
515
520
  };
516
521
  }
522
+ async function handleUniqueFolderCheck(ctx) {
523
+ const value = ctx.validationError.value;
524
+ if (!value || typeof value.directory !== "string") {
525
+ return {
526
+ success: false,
527
+ errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
528
+ };
529
+ }
530
+ const {
531
+ directory
532
+ } = value;
533
+ const conflicts = [];
534
+ for (const file of ctx.valFiles) {
535
+ const otherModuleFilePath = `/${file}`;
536
+ if (otherModuleFilePath === ctx.moduleFilePath) continue;
537
+ const otherModule = await ctx.service.get(otherModuleFilePath, "", {
538
+ source: false,
539
+ schema: true,
540
+ validate: false
541
+ });
542
+ const schema = otherModule.schema;
543
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
544
+ conflicts.push(otherModuleFilePath);
545
+ }
546
+ }
547
+ if (conflicts.length > 0) {
548
+ return {
549
+ success: false,
550
+ errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
551
+ };
552
+ }
553
+ return {
554
+ success: true
555
+ };
556
+ }
557
+ async function handleCheckAllFiles(ctx) {
558
+ const value = ctx.validationError.value;
559
+ if (!value || typeof value.directory !== "string") {
560
+ return {
561
+ success: false,
562
+ errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
563
+ };
564
+ }
565
+ const {
566
+ directory
567
+ } = value;
568
+ const source = ctx.valModule.source;
569
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
570
+ return {
571
+ success: false,
572
+ errorMessage: `Could not get source for ${ctx.sourcePath}`
573
+ };
574
+ }
575
+ const trackedFiles = new Set(Object.keys(source));
576
+
577
+ // Check that all tracked files exist on disk
578
+ const missingTrackedFiles = [...trackedFiles].filter(f => {
579
+ return !ctx.fs.fileExists(path.join(ctx.projectRoot, f));
580
+ });
581
+ if (missingTrackedFiles.length > 0) {
582
+ if (!ctx.fix) {
583
+ return {
584
+ success: false,
585
+ 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.`
586
+ };
587
+ }
588
+ // fix: true — let createFixPatch remove the missing entries
589
+ return {
590
+ success: true,
591
+ shouldApplyPatch: true
592
+ };
593
+ }
594
+ const dirPath = path.join(ctx.projectRoot, directory);
595
+ const filesInDir = [];
596
+ try {
597
+ const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
598
+ for (const entry of entries) {
599
+ const relPath = "/" + path.relative(ctx.projectRoot, entry).split(path.sep).join("/");
600
+ filesInDir.push(relPath);
601
+ }
602
+ } catch {
603
+ // directory doesn't exist — no untracked files possible
604
+ }
605
+ const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
606
+ if (untrackedFiles.length > 0) {
607
+ return {
608
+ success: false,
609
+ errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
610
+ };
611
+ }
612
+
613
+ // All files accounted for — trigger metadata verification via createFixPatch
614
+ return {
615
+ success: true,
616
+ shouldApplyPatch: true
617
+ };
618
+ }
517
619
 
518
620
  // Fix handler registry
519
621
  const currentFixHandlers = {
@@ -528,7 +630,13 @@ const currentFixHandlers = {
528
630
  "image:download-remote": handleRemoteFileDownload,
529
631
  "file:download-remote": handleRemoteFileDownload,
530
632
  "image:check-remote": handleRemoteFileCheck,
531
- "file:check-remote": handleRemoteFileCheck
633
+ "images:check-remote": handleRemoteFileCheck,
634
+ "file:check-remote": handleRemoteFileCheck,
635
+ "files:check-remote": handleRemoteFileCheck,
636
+ "images:check-unique-folder": handleUniqueFolderCheck,
637
+ "files:check-unique-folder": handleUniqueFolderCheck,
638
+ "images:check-all-files": handleCheckAllFiles,
639
+ "files:check-all-files": handleCheckAllFiles
532
640
  };
533
641
  const deprecatedFixHandlers = {
534
642
  "image:replace-metadata": handleFileMetadata
@@ -537,30 +645,36 @@ const fixHandlers = {
537
645
  ...deprecatedFixHandlers,
538
646
  ...currentFixHandlers
539
647
  };
540
- async function validate({
648
+ function createDefaultValFSHost() {
649
+ return {
650
+ ...ts.sys,
651
+ writeFile: (fileName, data, encoding) => {
652
+ nodeFs.mkdirSync(path.dirname(fileName), {
653
+ recursive: true
654
+ });
655
+ nodeFs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
656
+ },
657
+ rmFile: nodeFs.rmSync,
658
+ readBuffer: fileName => {
659
+ try {
660
+ return nodeFs.readFileSync(fileName);
661
+ } catch {
662
+ return undefined;
663
+ }
664
+ }
665
+ };
666
+ }
667
+ async function* runValidation({
541
668
  root,
542
- fix
669
+ fix,
670
+ valFiles,
671
+ project,
672
+ remote,
673
+ fs
543
674
  }) {
544
- const valRemoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
545
- const contentHostUrl = process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST;
546
- const projectRoot = root ? path.resolve(root) : process.cwd();
547
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
548
- console.log(picocolors.greenBright(`Validating project${valConfigFile !== null && valConfigFile !== void 0 && valConfigFile.project ? ` '${picocolors.inverse(valConfigFile === null || valConfigFile === void 0 ? void 0 : valConfigFile.project)}'` : ""}...`));
549
- const service = await createService(projectRoot, {});
550
- let prettier;
551
- try {
552
- prettier = (await import('prettier')).default;
553
- } catch {
554
- console.log("Prettier not found, skipping formatting");
555
- }
556
- const valFiles = await glob("**/*.val.{js,ts}", {
557
- ignore: ["node_modules/**"],
558
- cwd: projectRoot
559
- });
675
+ const projectRoot = path.resolve(root);
676
+ const service = await createService(projectRoot, {}, fs);
560
677
  let errors = 0;
561
- console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
562
- let publicProjectId;
563
- let didFix = false;
564
678
 
565
679
  // Create caches that persist across all file validations
566
680
  const keyOfCache = new Map();
@@ -568,7 +682,7 @@ async function validate({
568
682
  loaded: false,
569
683
  modules: {}
570
684
  };
571
- async function validateFile(file) {
685
+ async function* validateFile(file) {
572
686
  const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
573
687
  const start = Date.now();
574
688
  const valModule = await service.get(moduleFilePath, "", {
@@ -580,10 +694,14 @@ async function validate({
580
694
  let remoteFileBuckets = undefined;
581
695
  let remoteFilesCounter = 0;
582
696
  if (!valModule.errors) {
583
- console.log(picocolors.green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
584
- return 0;
697
+ yield {
698
+ type: "file-valid",
699
+ file: moduleFilePath,
700
+ durationMs: Date.now() - start
701
+ };
702
+ return;
585
703
  } else {
586
- let errors = 0;
704
+ let fileErrors = 0;
587
705
  let fixedErrors = 0;
588
706
  if (valModule.errors) {
589
707
  if (valModule.errors.validation) {
@@ -591,8 +709,12 @@ async function validate({
591
709
  for (const v of validationErrors) {
592
710
  if (!v.fixes || v.fixes.length === 0) {
593
711
  // No fixes available - just report error
594
- errors += 1;
595
- console.log(picocolors.red("✘"), "Got error in", `${sourcePath}:`, v.message);
712
+ fileErrors += 1;
713
+ yield {
714
+ type: "validation-error",
715
+ sourcePath,
716
+ message: v.message
717
+ };
596
718
  continue;
597
719
  }
598
720
 
@@ -600,8 +722,12 @@ async function validate({
600
722
  const fixType = v.fixes[0]; // Take first fix
601
723
  const handler = fixHandlers[fixType];
602
724
  if (!handler) {
603
- console.log(picocolors.red("✘"), "Unknown fix", v.fixes, "for", sourcePath);
604
- errors += 1;
725
+ yield {
726
+ type: "unknown-fix",
727
+ sourcePath,
728
+ fixes: v.fixes
729
+ };
730
+ fileErrors += 1;
605
731
  continue;
606
732
  }
607
733
 
@@ -616,21 +742,25 @@ async function validate({
616
742
  valFiles,
617
743
  moduleFilePath,
618
744
  file,
745
+ fs,
619
746
  remoteFiles,
620
- publicProjectId,
747
+ publicProjectId: undefined,
621
748
  remoteFileBuckets,
622
749
  remoteFilesCounter,
623
- valRemoteHost,
624
- contentHostUrl,
625
- valConfigFile: valConfigFile ?? undefined,
750
+ remote,
751
+ project,
626
752
  keyOfCache,
627
753
  routerModulesCache
628
754
  });
629
755
 
630
- // Update shared state from handler result
631
- if (result.publicProjectId !== undefined) {
632
- publicProjectId = result.publicProjectId;
756
+ // Yield any events from handler
757
+ if (result.events) {
758
+ for (const event of result.events) {
759
+ yield event;
760
+ }
633
761
  }
762
+
763
+ // Update shared state from handler result
634
764
  if (result.remoteFileBuckets !== undefined) {
635
765
  remoteFileBuckets = result.remoteFileBuckets;
636
766
  }
@@ -638,69 +768,98 @@ async function validate({
638
768
  remoteFilesCounter = result.remoteFilesCounter;
639
769
  }
640
770
  if (!result.success) {
641
- console.log(picocolors.red("✘"), result.errorMessage);
642
- errors += 1;
771
+ yield {
772
+ type: "validation-error",
773
+ sourcePath,
774
+ message: result.errorMessage ?? "Unknown error"
775
+ };
776
+ fileErrors += 1;
643
777
  continue;
644
778
  }
645
779
 
646
780
  // Apply patch if needed
647
781
  if (result.shouldApplyPatch) {
648
- var _fixPatch$remainingEr;
649
782
  const fixPatch = await createFixPatch({
650
783
  projectRoot,
651
- remoteHost: valRemoteHost
784
+ remoteHost: remote.remoteHost
652
785
  }, !!fix, sourcePath, v, remoteFiles, valModule.source, valModule.schema);
653
786
  if (fix && fixPatch !== null && fixPatch !== void 0 && fixPatch.patch && (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.patch.length) > 0) {
654
787
  await service.patch(moduleFilePath, fixPatch.patch);
655
- didFix = true;
656
788
  fixedErrors += 1;
657
- console.log(picocolors.yellow("⚠"), "Applied fix for", sourcePath);
789
+ yield {
790
+ type: "fix-applied",
791
+ file,
792
+ sourcePath
793
+ };
794
+ } else if (!fix && fixPatch !== null && fixPatch !== void 0 && fixPatch.patch && (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.patch.length) > 0) {
795
+ fileErrors += 1;
796
+ yield {
797
+ type: "validation-fixable-error",
798
+ sourcePath,
799
+ message: v.message,
800
+ fixable: true
801
+ };
802
+ }
803
+ for (const e of (fixPatch === null || fixPatch === void 0 ? void 0 : fixPatch.remainingErrors) ?? []) {
804
+ fileErrors += 1;
805
+ yield {
806
+ type: "validation-fixable-error",
807
+ sourcePath,
808
+ message: e.message,
809
+ fixable: !!(e.fixes && e.fixes.length)
810
+ };
658
811
  }
659
- fixPatch === null || fixPatch === void 0 || (_fixPatch$remainingEr = fixPatch.remainingErrors) === null || _fixPatch$remainingEr === void 0 || _fixPatch$remainingEr.forEach(e => {
660
- errors += 1;
661
- console.log(e.fixes && e.fixes.length ? picocolors.yellow("⚠") : picocolors.red("✘"), `Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`, `${sourcePath}:`, e.message);
662
- });
663
812
  }
664
813
  }
665
814
  }
666
815
  }
667
- if (fixedErrors === errors && (!valModule.errors.fatal || valModule.errors.fatal.length == 0)) {
668
- console.log(picocolors.green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
816
+ if (fixedErrors === fileErrors && (!valModule.errors.fatal || valModule.errors.fatal.length == 0)) {
817
+ yield {
818
+ type: "file-valid",
819
+ file: moduleFilePath,
820
+ durationMs: Date.now() - start
821
+ };
669
822
  }
670
823
  for (const fatalError of valModule.errors.fatal || []) {
671
- errors += 1;
672
- console.log(picocolors.red("✘"), moduleFilePath, "is invalid:", fatalError.message);
824
+ fileErrors += 1;
825
+ yield {
826
+ type: "fatal-error",
827
+ file: moduleFilePath,
828
+ message: fatalError.message
829
+ };
673
830
  }
674
831
  } else {
675
- console.log(picocolors.green("✔"), moduleFilePath, "is valid (" + (Date.now() - start) + "ms)");
832
+ yield {
833
+ type: "file-valid",
834
+ file: moduleFilePath,
835
+ durationMs: Date.now() - start
836
+ };
676
837
  }
677
- if (errors > 0) {
678
- console.log(picocolors.red("✘"), `${`/${file}`} contains ${errors} error${errors > 1 ? "s" : ""}`, " (" + (Date.now() - start) + "ms)");
838
+ if (fileErrors > 0) {
839
+ yield {
840
+ type: "file-error-count",
841
+ file: `/${file}`,
842
+ errorCount: fileErrors,
843
+ durationMs: Date.now() - start
844
+ };
679
845
  }
680
- return errors;
846
+ errors += fileErrors;
681
847
  }
682
848
  }
683
849
  for (const file of valFiles.sort()) {
684
- didFix = false;
685
- errors += await validateFile(file);
686
- if (prettier && didFix) {
687
- var _prettier;
688
- const filePath = path.join(projectRoot, file);
689
- const fileContent = await fs.readFile(filePath, "utf-8");
690
- const formattedContent = await ((_prettier = prettier) === null || _prettier === void 0 ? void 0 : _prettier.format(fileContent, {
691
- filepath: filePath
692
- }));
693
- await fs.writeFile(filePath, formattedContent);
694
- }
850
+ yield* validateFile(file);
695
851
  }
852
+ service.dispose();
696
853
  if (errors > 0) {
697
- console.log(picocolors.red("✘"), "Got", errors, "error" + (errors > 1 ? "s" : ""));
698
- process.exit(1);
854
+ yield {
855
+ type: "summary-errors",
856
+ count: errors
857
+ };
699
858
  } else {
700
- console.log(picocolors.green("✔"), "No validation errors found");
859
+ yield {
860
+ type: "summary-success"
861
+ };
701
862
  }
702
- service.dispose();
703
- return;
704
863
  }
705
864
 
706
865
  // GPT generated levenshtein distance algorithm:
@@ -728,6 +887,102 @@ function findSimilar(key, targets) {
728
887
  })).sort((a, b) => a.distance - b.distance);
729
888
  }
730
889
 
890
+ async function validate({
891
+ root,
892
+ fix
893
+ }) {
894
+ const projectRoot = root ? path.resolve(root) : process.cwd();
895
+ const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
896
+ const resolvedValConfigFile = valConfigFile ? {
897
+ ...valConfigFile,
898
+ project: process.env.VAL_PROJECT || valConfigFile.project
899
+ } : process.env.VAL_PROJECT ? {
900
+ project: process.env.VAL_PROJECT
901
+ } : undefined;
902
+ console.log(pc.greenBright(`Validating project${resolvedValConfigFile !== null && resolvedValConfigFile !== void 0 && resolvedValConfigFile.project ? ` '${pc.inverse(resolvedValConfigFile.project)}'` : ""}...`));
903
+ const valFiles = await glob("**/*.val.{js,ts}", {
904
+ ignore: ["node_modules/**"],
905
+ cwd: projectRoot
906
+ });
907
+ console.log(pc.greenBright(`Found ${valFiles.length} files...`));
908
+ let prettier;
909
+ try {
910
+ prettier = (await import('prettier')).default;
911
+ } catch {
912
+ console.log("Prettier not found, skipping formatting");
913
+ }
914
+ const fixedFiles = new Set();
915
+ let totalErrors = 0;
916
+ for await (const event of runValidation({
917
+ root: projectRoot,
918
+ fix: !!fix,
919
+ valFiles,
920
+ project: resolvedValConfigFile === null || resolvedValConfigFile === void 0 ? void 0 : resolvedValConfigFile.project,
921
+ remote: {
922
+ remoteHost: process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST,
923
+ getSettings: (projectName, options) => getSettings(projectName, options),
924
+ uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) => uploadRemoteFile(process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST, project, bucket, fileHash, fileExt ?? "", fileBuffer, options)
925
+ },
926
+ fs: createDefaultValFSHost()
927
+ })) {
928
+ switch (event.type) {
929
+ case "file-valid":
930
+ console.log(pc.green("✔"), event.file, "is valid (" + event.durationMs + "ms)");
931
+ break;
932
+ case "file-error-count":
933
+ console.log(pc.red("✘"), `${event.file} contains ${event.errorCount} error${event.errorCount > 1 ? "s" : ""}`, " (" + event.durationMs + "ms)");
934
+ totalErrors += event.errorCount;
935
+ break;
936
+ case "validation-error":
937
+ console.log(pc.red("✘"), "Got error in", `${event.sourcePath}:`, event.message);
938
+ break;
939
+ case "validation-fixable-error":
940
+ console.log(event.fixable ? pc.yellow("⚠") : pc.red("✘"), `Got ${event.fixable ? "fixable " : ""}error in`, `${event.sourcePath}:`, event.message);
941
+ break;
942
+ case "unknown-fix":
943
+ console.log(pc.red("✘"), "Unknown fix", event.fixes, "for", event.sourcePath);
944
+ break;
945
+ case "fix-applied":
946
+ console.log(pc.yellow("⚠"), "Applied fix for", event.sourcePath);
947
+ fixedFiles.add(event.file);
948
+ break;
949
+ case "fatal-error":
950
+ console.log(pc.red("✘"), event.file, "is invalid:", event.message);
951
+ break;
952
+ case "remote-uploading":
953
+ console.log(pc.yellow("⚠"), `Uploading remote file: '${event.ref}'...`);
954
+ break;
955
+ case "remote-uploaded":
956
+ console.log(pc.green("✔"), `Completed upload of remote file: '${event.ref}'`);
957
+ break;
958
+ case "remote-already-uploaded":
959
+ console.log(pc.yellow("⚠"), `Remote file ${event.filePath} already uploaded`);
960
+ break;
961
+ case "remote-downloading":
962
+ console.log(pc.yellow("⚠"), `Downloading remote file in ${event.sourcePath}...`);
963
+ break;
964
+ }
965
+ }
966
+
967
+ // Run prettier on files that had fixes applied
968
+ if (prettier) {
969
+ for (const file of fixedFiles) {
970
+ const filePath = path.join(projectRoot, file);
971
+ const fileContent = await fs.readFile(filePath, "utf-8");
972
+ const formattedContent = await prettier.format(fileContent, {
973
+ filepath: filePath
974
+ });
975
+ await fs.writeFile(filePath, formattedContent);
976
+ }
977
+ }
978
+ if (totalErrors > 0) {
979
+ console.log(pc.red("✘"), "Got", totalErrors, "error" + (totalErrors > 1 ? "s" : ""));
980
+ process.exit(1);
981
+ } else {
982
+ console.log(pc.green("✔"), "No validation errors found");
983
+ }
984
+ }
985
+
731
986
  async function listUnusedFiles({
732
987
  root
733
988
  }) {
@@ -835,7 +1090,7 @@ async function connect(options) {
835
1090
  params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
836
1091
  }
837
1092
  const url = `${host$1}/connect?${params.toString()}`;
838
- console.log(picocolors.dim(`\nFollow the instructions in your browser to complete the setup:\n${picocolors.cyan(url)}\n`));
1093
+ console.log(pc.dim(`\nFollow the instructions in your browser to complete the setup:\n${pc.cyan(url)}\n`));
839
1094
  }
840
1095
  async function tryGetProject(projectRoot) {
841
1096
  const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
@@ -847,7 +1102,7 @@ async function tryGetProject(projectRoot) {
847
1102
  projectName: parts[1]
848
1103
  };
849
1104
  } else {
850
- console.error(picocolors.red(`Invalid project format in val.config file: "${valConfigFile.project}". Expected format "orgName/projectName".`));
1105
+ console.error(pc.red(`Invalid project format in val.config file: "${valConfigFile.project}". Expected format "orgName/projectName".`));
851
1106
  process.exit(1);
852
1107
  }
853
1108
  }
@@ -882,7 +1137,7 @@ async function tryGetGitRemote(root) {
882
1137
  }
883
1138
  return null;
884
1139
  } catch (error) {
885
- console.error(picocolors.red("Failed to read .git/config file."), error);
1140
+ console.error(pc.red("Failed to read .git/config file."), error);
886
1141
  return null;
887
1142
  }
888
1143
  }
@@ -891,8 +1146,8 @@ function tryGetGitConfig(root) {
891
1146
  let lastDir = null;
892
1147
  while (currentDir !== lastDir) {
893
1148
  const gitConfigPath = path.join(currentDir, ".git", "config");
894
- if (fs$1.existsSync(gitConfigPath)) {
895
- return fs$1.readFileSync(gitConfigPath, "utf-8");
1149
+ if (nodeFs.existsSync(gitConfigPath)) {
1150
+ return nodeFs.readFileSync(gitConfigPath, "utf-8");
896
1151
  }
897
1152
  lastDir = currentDir;
898
1153
  currentDir = path.dirname(currentDir);
@@ -907,7 +1162,7 @@ const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
907
1162
  async function login(options) {
908
1163
  try {
909
1164
  var _response$headers$get;
910
- console.log(picocolors.cyan("\nStarting login process...\n"));
1165
+ console.log(pc.cyan("\nStarting login process...\n"));
911
1166
 
912
1167
  // Step 1: Initiate login and get token and URL
913
1168
  const response = await fetch(`${host}/api/login`, {
@@ -920,7 +1175,7 @@ async function login(options) {
920
1175
  let url;
921
1176
  if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
922
1177
  const text = await response.text();
923
- console.error(picocolors.red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
1178
+ console.error(pc.red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
924
1179
  process.exit(1);
925
1180
  }
926
1181
  const json = await response.json();
@@ -929,12 +1184,12 @@ async function login(options) {
929
1184
  url = json.url;
930
1185
  }
931
1186
  if (!token || !url) {
932
- console.error(picocolors.red("Unexpected response from the server."), json);
1187
+ console.error(pc.red("Unexpected response from the server."), json);
933
1188
  process.exit(1);
934
1189
  }
935
- console.log(picocolors.green("Open the following URL in your browser to log in:"));
936
- console.log(picocolors.underline(picocolors.blue(url)));
937
- console.log(picocolors.dim("\nWaiting for login confirmation...\n"));
1190
+ console.log(pc.green("Open the following URL in your browser to log in:"));
1191
+ console.log(pc.underline(pc.blue(url)));
1192
+ console.log(pc.dim("\nWaiting for login confirmation...\n"));
938
1193
 
939
1194
  // Step 2: Poll for login confirmation
940
1195
  const result = await pollForConfirmation(token);
@@ -943,7 +1198,7 @@ async function login(options) {
943
1198
  const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
944
1199
  saveToken(result, filePath);
945
1200
  } catch (error) {
946
- console.error(picocolors.red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
1201
+ console.error(pc.red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
947
1202
  process.exit(1);
948
1203
  }
949
1204
  }
@@ -956,7 +1211,7 @@ async function pollForConfirmation(token) {
956
1211
  method: "POST"
957
1212
  });
958
1213
  if (response.status === 500) {
959
- console.error(picocolors.red("An error occurred on the server."));
1214
+ console.error(pc.red("An error occurred on the server."));
960
1215
  process.exit(1);
961
1216
  }
962
1217
  if (response.status === 200) {
@@ -965,21 +1220,21 @@ async function pollForConfirmation(token) {
965
1220
  if (typeof json.profile.email === "string" && typeof json.pat === "string") {
966
1221
  return json;
967
1222
  } else {
968
- console.error(picocolors.red("Unexpected response from the server."));
1223
+ console.error(pc.red("Unexpected response from the server."));
969
1224
  process.exit(1);
970
1225
  }
971
1226
  }
972
1227
  }
973
1228
  }
974
- console.error(picocolors.red("Login confirmation timed out."));
1229
+ console.error(pc.red("Login confirmation timed out."));
975
1230
  process.exit(1);
976
1231
  }
977
1232
  function saveToken(result, filePath) {
978
- fs$1.mkdirSync(path.dirname(filePath), {
1233
+ nodeFs.mkdirSync(path.dirname(filePath), {
979
1234
  recursive: true
980
1235
  });
981
- fs$1.writeFileSync(filePath, JSON.stringify(result, null, 2));
982
- console.log(picocolors.green(`Token for ${picocolors.cyan(result.profile.email)} saved to ${picocolors.cyan(filePath)}`));
1236
+ nodeFs.writeFileSync(filePath, JSON.stringify(result, null, 2));
1237
+ console.log(pc.green(`Token for ${pc.cyan(result.profile.email)} saved to ${pc.cyan(filePath)}`));
983
1238
  }
984
1239
 
985
1240
  async function main() {