@valbuild/cli 0.72.4 → 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/cli/dist/valbuild-cli-cli.cjs.dev.js +282 -18
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +282 -18
- package/cli/dist/valbuild-cli-cli.esm.js +283 -20
- package/package.json +4 -4
- package/src/cli.ts +13 -1
- package/src/getVersions.ts +4 -4
- package/src/login.ts +112 -0
- package/src/utils/getFileExt.ts +4 -0
- package/src/utils/getValCoreVersion.ts +5 -0
- package/src/validate.ts +339 -23
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
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
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
|
-
|
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
|
-
"
|
252
|
-
|
253
|
-
|
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
|
-
|
276
|
-
|
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
|
-
"
|
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
|
-
"
|
644
|
+
"Got",
|
329
645
|
errors,
|
330
|
-
"
|
646
|
+
"error" + (errors > 1 ? "s" : ""),
|
331
647
|
);
|
332
648
|
process.exit(1);
|
333
649
|
} else {
|