@valbuild/cli 0.72.3 → 0.73.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/src/validate.ts CHANGED
@@ -1,10 +1,20 @@
1
1
  import path from "path";
2
- import { createFixPatch, createService } from "@valbuild/server";
3
2
  import {
3
+ createFixPatch,
4
+ createService,
5
+ getSettings,
6
+ getPersonalAccessTokenPath,
7
+ parsePersonalAccessTokenFile,
8
+ uploadRemoteFile,
9
+ } from "@valbuild/server";
10
+ import {
11
+ DEFAULT_VAL_REMOTE_HOST,
4
12
  FILE_REF_PROP,
5
13
  Internal,
6
14
  ModuleFilePath,
7
15
  ModulePath,
16
+ SerializedFileSchema,
17
+ SerializedImageSchema,
8
18
  SourcePath,
9
19
  ValidationFix,
10
20
  } from "@valbuild/core";
@@ -22,6 +32,7 @@ export async function validate({
22
32
  fix?: boolean;
23
33
  noEslint?: boolean;
24
34
  }) {
35
+ const valRemoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
25
36
  const projectRoot = root ? path.resolve(root) : process.cwd();
26
37
  const eslint = new ESLint({
27
38
  cwd: projectRoot,
@@ -121,6 +132,7 @@ export async function validate({
121
132
  }
122
133
  console.log("Validating...", valFiles.length, "files");
123
134
 
135
+ let publicProjectId: string | undefined;
124
136
  let didFix = false; // TODO: ugly
125
137
  async function validateFile(file: string): Promise<number> {
126
138
  const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
@@ -135,6 +147,12 @@ export async function validate({
135
147
  "utf-8",
136
148
  );
137
149
  const eslintResult = eslintResultsByFile?.[file];
150
+ const remoteFiles: Record<
151
+ SourcePath,
152
+ { ref: string; metadata?: Record<string, unknown> }
153
+ > = {};
154
+ let remoteFileBuckets: string[] | null = null;
155
+ let remoteFilesCounter = 0;
138
156
  eslintResult?.messages.forEach((m) => {
139
157
  // display surrounding code
140
158
  logEslintMessage(fileContent, moduleFilePath, m);
@@ -152,6 +170,7 @@ export async function validate({
152
170
  (prev, m) => (m.severity >= 2 ? prev + 1 : prev),
153
171
  0,
154
172
  ) || 0;
173
+ let fixedErrors = 0;
155
174
  if (valModule.errors) {
156
175
  if (valModule.errors.validation) {
157
176
  for (const [sourcePath, validationErrors] of Object.entries(
@@ -178,24 +197,31 @@ export async function validate({
178
197
  valModule.source,
179
198
  valModule.schema,
180
199
  );
181
- const filePath = path.join(
182
- projectRoot,
183
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
- (fileSource.source as any)?.[FILE_REF_PROP],
185
- );
200
+ let filePath: string | null = null;
186
201
  try {
202
+ filePath = path.join(
203
+ projectRoot,
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
+ (fileSource.source as any)?.[FILE_REF_PROP],
206
+ );
187
207
  await fs.access(filePath);
188
208
  } catch {
189
- console.log(
190
- picocolors.red("✘"),
191
- `File ${filePath} does not exist`,
192
- );
209
+ if (filePath) {
210
+ console.log(
211
+ picocolors.red("✘"),
212
+ `File ${filePath} does not exist`,
213
+ );
214
+ } else {
215
+ console.log(
216
+ picocolors.red("✘"),
217
+ `Expected file to be defined at: ${sourcePath} but no file was found`,
218
+ );
219
+ }
193
220
  errors += 1;
194
221
  continue;
195
222
  }
196
223
  }
197
224
  } else if (v.fixes.includes("keyof:check-keys")) {
198
- const prevErrors = errors;
199
225
  if (
200
226
  v.value &&
201
227
  typeof v.value === "object" &&
@@ -238,31 +264,302 @@ export async function validate({
238
264
  );
239
265
  errors += 1;
240
266
  }
241
- if (prevErrors < errors) {
267
+ } else if (
268
+ v.fixes.includes("image:upload-remote") ||
269
+ v.fixes.includes("file:upload-remote")
270
+ ) {
271
+ if (!fix) {
242
272
  console.log(
243
273
  picocolors.red("✘"),
244
- "Found error in",
245
- `${sourcePath}`,
274
+ `Remote file ${sourcePath} needs to be uploaded (use --fix to upload)`,
246
275
  );
276
+ errors += 1;
277
+ continue;
247
278
  }
279
+ const [, modulePath] =
280
+ Internal.splitModuleFilePathAndModulePath(
281
+ sourcePath as SourcePath,
282
+ );
283
+ if (valModule.source && valModule.schema) {
284
+ const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
285
+ modulePath,
286
+ valModule.source,
287
+ valModule.schema,
288
+ );
289
+ let filePath: string | null = null;
290
+ console.log(
291
+ sourcePath,
292
+ resolvedRemoteFileAtSourcePath.source,
293
+ );
294
+ try {
295
+ filePath = path.join(
296
+ projectRoot,
297
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
+ (resolvedRemoteFileAtSourcePath.source as any)?.[
299
+ FILE_REF_PROP
300
+ ],
301
+ );
302
+ await fs.access(filePath);
303
+ } catch {
304
+ if (filePath) {
305
+ console.log(
306
+ picocolors.red("✘"),
307
+ `File ${filePath} does not exist`,
308
+ );
309
+ } else {
310
+ console.log(
311
+ picocolors.red("✘"),
312
+ `Expected file to be defined at: ${sourcePath} but no file was found`,
313
+ );
314
+ }
315
+ errors += 1;
316
+ continue;
317
+ }
318
+ const patFile = getPersonalAccessTokenPath(projectRoot);
319
+ try {
320
+ await fs.access(patFile);
321
+ } catch {
322
+ // TODO: display this error only once:
323
+ console.log(
324
+ picocolors.red("✘"),
325
+ `File: ${path.join(projectRoot, 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`,
326
+ );
327
+ errors += 1;
328
+ continue;
329
+ }
330
+
331
+ const parsedPatFile = parsePersonalAccessTokenFile(
332
+ await fs.readFile(patFile, "utf-8"),
333
+ );
334
+ if (!parsedPatFile.success) {
335
+ console.log(
336
+ picocolors.red("✘"),
337
+ `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`,
338
+ );
339
+ errors += 1;
340
+ continue;
341
+ }
342
+ const { pat } = parsedPatFile.data;
343
+
344
+ if (remoteFiles[sourcePath as SourcePath]) {
345
+ console.log(
346
+ picocolors.yellow("⚠"),
347
+ `Remote file ${filePath} already uploaded`,
348
+ );
349
+ continue;
350
+ }
351
+ // TODO: parallelize this:
352
+ console.log(
353
+ picocolors.yellow("⚠"),
354
+ `Uploading remote file ${filePath}...`,
355
+ );
356
+
357
+ if (!resolvedRemoteFileAtSourcePath.schema) {
358
+ console.log(
359
+ picocolors.red("✘"),
360
+ `Cannot upload remote file: schema not found for ${sourcePath}`,
361
+ );
362
+ errors += 1;
363
+ continue;
364
+ }
365
+
366
+ const actualRemoteFileSource =
367
+ resolvedRemoteFileAtSourcePath.source;
368
+ const fileSourceMetadata = Internal.isFile(
369
+ actualRemoteFileSource,
370
+ )
371
+ ? actualRemoteFileSource.metadata
372
+ : undefined;
373
+ const resolveRemoteFileSchema =
374
+ resolvedRemoteFileAtSourcePath.schema;
375
+ if (!resolveRemoteFileSchema) {
376
+ console.log(
377
+ picocolors.red("✘"),
378
+ `Could not resolve schema for remote file: ${sourcePath}`,
379
+ );
380
+ errors += 1;
381
+ continue;
382
+ }
383
+ if (!publicProjectId || !remoteFileBuckets) {
384
+ let projectName = process.env.VAL_PROJECT;
385
+ if (!projectName) {
386
+ try {
387
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
388
+ projectName = require(`${root}/val.config`)?.config
389
+ ?.project;
390
+ } catch {
391
+ // ignore
392
+ }
393
+ }
394
+ if (!projectName) {
395
+ try {
396
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
397
+ projectName = require(`${root}/val.config.ts`)?.config
398
+ ?.project;
399
+ } catch {
400
+ // ignore
401
+ }
402
+ }
403
+ if (!projectName) {
404
+ try {
405
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
406
+ projectName = require(`${root}/val.config.js`)?.config
407
+ ?.project;
408
+ } catch {
409
+ // ignore
410
+ }
411
+ }
412
+ if (!projectName) {
413
+ console.log(
414
+ picocolors.red("✘"),
415
+ "Project name not found. Set VAL_PROJECT environment variable or add project name to val.config",
416
+ );
417
+ errors += 1;
418
+ continue;
419
+ }
420
+ const settingsRes = await getSettings(projectName, {
421
+ pat,
422
+ });
423
+ if (!settingsRes.success) {
424
+ console.log(
425
+ picocolors.red("✘"),
426
+ `Could not get public project id: ${settingsRes.message}.`,
427
+ );
428
+ errors += 1;
429
+ continue;
430
+ }
431
+ publicProjectId = settingsRes.data.publicProjectId;
432
+ remoteFileBuckets =
433
+ settingsRes.data.remoteFileBuckets.map((b) => b.bucket);
434
+ }
435
+ if (!publicProjectId) {
436
+ console.log(
437
+ picocolors.red("✘"),
438
+ "Could not get public project id",
439
+ );
440
+ errors += 1;
441
+ continue;
442
+ }
443
+ if (
444
+ resolveRemoteFileSchema.type !== "image" &&
445
+ resolveRemoteFileSchema.type !== "file"
446
+ ) {
447
+ console.log(
448
+ picocolors.red("✘"),
449
+ `The schema is the remote is neither image nor file: ${sourcePath}`,
450
+ );
451
+ }
452
+ remoteFilesCounter += 1;
453
+ const bucket =
454
+ remoteFileBuckets[
455
+ remoteFilesCounter % remoteFileBuckets.length
456
+ ];
457
+ if (!bucket) {
458
+ console.log(
459
+ picocolors.red("✘"),
460
+ `Internal error: could not allocate a bucket for the remote file located at ${sourcePath}`,
461
+ );
462
+ errors += 1;
463
+ continue;
464
+ }
465
+ let fileBuffer: Buffer;
466
+ try {
467
+ fileBuffer = await fs.readFile(filePath);
468
+ } catch (e) {
469
+ console.log(
470
+ picocolors.red("✘"),
471
+ `Error reading file: ${e}`,
472
+ );
473
+ errors += 1;
474
+ continue;
475
+ }
476
+ const relativeFilePath = path
477
+ .relative(projectRoot, filePath)
478
+ .split(path.sep)
479
+ .join("/") as `public/val/${string}`;
480
+ if (!relativeFilePath.startsWith("public/val/")) {
481
+ console.log(
482
+ picocolors.red("✘"),
483
+ `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`,
484
+ );
485
+ errors += 1;
486
+ continue;
487
+ }
488
+ const remoteFileUpload = await uploadRemoteFile(
489
+ valRemoteHost,
490
+ fileBuffer,
491
+ publicProjectId,
492
+ bucket,
493
+ relativeFilePath,
494
+ resolveRemoteFileSchema as
495
+ | SerializedFileSchema
496
+ | SerializedImageSchema,
497
+ fileSourceMetadata,
498
+ { pat },
499
+ );
500
+ if (!remoteFileUpload.success) {
501
+ console.log(
502
+ picocolors.red("✘"),
503
+ `Error uploading remote file: ${remoteFileUpload.error}`,
504
+ );
505
+ errors += 1;
506
+ continue;
507
+ }
508
+ console.log(
509
+ picocolors.yellow("⚠"),
510
+ `Uploaded remote file ${filePath}`,
511
+ );
512
+ remoteFiles[sourcePath as SourcePath] = {
513
+ ref: remoteFileUpload.ref,
514
+ metadata: fileSourceMetadata,
515
+ };
516
+ }
517
+ } else if (
518
+ v.fixes.includes("image:download-remote") ||
519
+ v.fixes.includes("file:download-remote")
520
+ ) {
521
+ if (fix) {
522
+ console.log(
523
+ picocolors.yellow("⚠"),
524
+ `Downloading remote file in ${sourcePath}...`,
525
+ );
526
+ } else {
527
+ console.log(
528
+ picocolors.red("✘"),
529
+ `Remote file ${sourcePath} needs to be downloaded (use --fix to download)`,
530
+ );
531
+ errors += 1;
532
+ continue;
533
+ }
534
+ } else if (
535
+ v.fixes.includes("image:check-remote") ||
536
+ v.fixes.includes("file:check-remote")
537
+ ) {
538
+ // skip
248
539
  } else {
249
540
  console.log(
250
541
  picocolors.red("✘"),
251
- "Found error in",
252
- `${sourcePath}:`,
253
- v.message,
542
+ "Unknown fix",
543
+ v.fixes,
544
+ "for",
545
+ sourcePath,
254
546
  );
255
547
  errors += 1;
548
+ continue;
256
549
  }
257
550
  const fixPatch = await createFixPatch(
258
- { projectRoot },
551
+ { projectRoot, remoteHost: valRemoteHost },
259
552
  !!fix,
260
553
  sourcePath as SourcePath,
261
554
  v,
555
+ remoteFiles,
556
+ valModule.source,
557
+ valModule.schema,
262
558
  );
263
559
  if (fix && fixPatch?.patch && fixPatch?.patch.length > 0) {
264
560
  await service.patch(moduleFilePath, fixPatch.patch);
265
561
  didFix = true;
562
+ fixedErrors += 1;
266
563
  console.log(
267
564
  picocolors.yellow("⚠"),
268
565
  "Applied fix for",
@@ -272,8 +569,10 @@ export async function validate({
272
569
  fixPatch?.remainingErrors?.forEach((e) => {
273
570
  errors += 1;
274
571
  console.log(
275
- v.fixes ? picocolors.yellow("⚠") : picocolors.red("✘"),
276
- `Found ${v.fixes ? "fixable " : ""}error in`,
572
+ e.fixes && e.fixes.length
573
+ ? picocolors.yellow("")
574
+ : picocolors.red("✘"),
575
+ `Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`,
277
576
  `${sourcePath}:`,
278
577
  e.message,
279
578
  );
@@ -282,7 +581,7 @@ export async function validate({
282
581
  errors += 1;
283
582
  console.log(
284
583
  picocolors.red("✘"),
285
- "Found error in",
584
+ "Got error in",
286
585
  `${sourcePath}:`,
287
586
  v.message,
288
587
  );
@@ -290,6 +589,16 @@ export async function validate({
290
589
  }
291
590
  }
292
591
  }
592
+ if (
593
+ fixedErrors === errors &&
594
+ (!valModule.errors.fatal || valModule.errors.fatal.length == 0)
595
+ ) {
596
+ console.log(
597
+ picocolors.green("✔"),
598
+ moduleFilePath,
599
+ "is valid (" + (Date.now() - start) + "ms)",
600
+ );
601
+ }
293
602
  for (const fatalError of valModule.errors.fatal || []) {
294
603
  errors += 1;
295
604
  console.log(
@@ -306,6 +615,13 @@ export async function validate({
306
615
  "is valid (" + (Date.now() - start) + "ms)",
307
616
  );
308
617
  }
618
+ if (errors > 0) {
619
+ console.log(
620
+ picocolors.red("✘"),
621
+ `${`/${file}`} contains ${errors} error${errors > 1 ? "s" : ""}`,
622
+ " (" + (Date.now() - start) + "ms)",
623
+ );
624
+ }
309
625
  return errors;
310
626
  }
311
627
  }
@@ -325,9 +641,9 @@ export async function validate({
325
641
  if (errors > 0) {
326
642
  console.log(
327
643
  picocolors.red("✘"),
328
- "Found",
644
+ "Got",
329
645
  errors,
330
- "validation error" + (errors > 1 ? "s" : ""),
646
+ "error" + (errors > 1 ? "s" : ""),
331
647
  );
332
648
  process.exit(1);
333
649
  } else {