@valbuild/cli 0.150.0 → 0.150.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +248 -64
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +248 -64
- package/cli/dist/valbuild-cli-cli.esm.js +248 -64
- package/package.json +8 -8
- package/src/checkBuildTools.ts +16 -0
- package/src/cli.ts +16 -0
- package/src/connect.test.ts +32 -1
- package/src/connect.ts +27 -13
- package/src/publish/buildArtifacts.ts +51 -0
- package/src/publish.ts +5 -1
- package/src/runValidation.imageset.test.ts +23 -19
- package/src/runValidation.test.ts +64 -23
- package/src/runValidation.ts +9 -0
- package/src/validate.test.ts +4 -4
- package/src/validate.ts +186 -58
- package/src/validate.warnings.test.ts +170 -0
package/src/validate.ts
CHANGED
|
@@ -12,13 +12,29 @@ import {
|
|
|
12
12
|
type SourceFileCache,
|
|
13
13
|
} from "./utils/sourcePathToFileLocation";
|
|
14
14
|
|
|
15
|
-
type Diagnostic =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
type Diagnostic =
|
|
16
|
+
| {
|
|
17
|
+
// Both are errors (✘) and count towards the exit code; "fixable" adds
|
|
18
|
+
// "run --fix".
|
|
19
|
+
severity: "fixable" | "error";
|
|
20
|
+
sourcePath: string;
|
|
21
|
+
message: string;
|
|
22
|
+
keyError?: boolean;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
// ⚠: printed, grouped by message, and counted only under --strict.
|
|
26
|
+
severity: "warning";
|
|
27
|
+
sourcePath: string;
|
|
28
|
+
message: string;
|
|
29
|
+
files: string[];
|
|
30
|
+
hint?: string;
|
|
31
|
+
keyError?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type WarningDiagnostic = Extract<Diagnostic, { severity: "warning" }>;
|
|
35
|
+
|
|
36
|
+
/** How many of a warning group's files are listed before "… N more". */
|
|
37
|
+
const WARNING_FILES_SHOWN = 5;
|
|
22
38
|
|
|
23
39
|
type ModuleReport = {
|
|
24
40
|
// Relative file path (no leading slash).
|
|
@@ -31,10 +47,13 @@ export async function validate({
|
|
|
31
47
|
root,
|
|
32
48
|
fix,
|
|
33
49
|
watch,
|
|
50
|
+
strict,
|
|
34
51
|
}: {
|
|
35
52
|
root?: string;
|
|
36
53
|
fix?: boolean;
|
|
37
54
|
watch?: boolean;
|
|
55
|
+
/** Fail on warnings too. */
|
|
56
|
+
strict?: boolean;
|
|
38
57
|
}) {
|
|
39
58
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
40
59
|
|
|
@@ -45,7 +64,13 @@ export async function validate({
|
|
|
45
64
|
console.log("Prettier not found, skipping formatting");
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
const runOnce = () =>
|
|
67
|
+
const runOnce = () =>
|
|
68
|
+
validateOnce({
|
|
69
|
+
projectRoot,
|
|
70
|
+
fix: !!fix,
|
|
71
|
+
prettier,
|
|
72
|
+
...(strict ? { strict: true } : {}),
|
|
73
|
+
});
|
|
49
74
|
|
|
50
75
|
if (!watch) {
|
|
51
76
|
const totalErrors = await runOnce();
|
|
@@ -60,7 +85,7 @@ export async function validate({
|
|
|
60
85
|
|
|
61
86
|
/**
|
|
62
87
|
* One validation pass over the project, printed as `val validate` prints it,
|
|
63
|
-
* returning the number of errors found
|
|
88
|
+
* returning the number of errors found -- and of warnings, under `strict`.
|
|
64
89
|
*
|
|
65
90
|
* Re-reads config and val files on each call so it always reflects the latest
|
|
66
91
|
* state on disk: `val validate` calls it once or on every change (`--watch`),
|
|
@@ -71,6 +96,7 @@ export async function validateOnce({
|
|
|
71
96
|
fix,
|
|
72
97
|
prettier,
|
|
73
98
|
blockingOnly = false,
|
|
99
|
+
strict = false,
|
|
74
100
|
}: {
|
|
75
101
|
projectRoot: string;
|
|
76
102
|
fix: boolean;
|
|
@@ -82,6 +108,12 @@ export async function validateOnce({
|
|
|
82
108
|
* what `val publish` refuses on. Every error is still printed.
|
|
83
109
|
*/
|
|
84
110
|
blockingOnly?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Count warnings as errors (`--strict`). A warning is a local file in a
|
|
113
|
+
* project that stores files remotely: see `WARNING_FIXES` in
|
|
114
|
+
* `@valbuild/shared`.
|
|
115
|
+
*/
|
|
116
|
+
strict?: boolean;
|
|
85
117
|
}): Promise<number> {
|
|
86
118
|
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
87
119
|
|
|
@@ -109,6 +141,7 @@ export async function validateOnce({
|
|
|
109
141
|
|
|
110
142
|
const fixedFiles = new Set<string>();
|
|
111
143
|
let totalErrors = 0;
|
|
144
|
+
let totalWarnings = 0;
|
|
112
145
|
let repairedOnSave = 0;
|
|
113
146
|
|
|
114
147
|
// Caches each val file's parsed source so files are read/parsed at most once
|
|
@@ -163,10 +196,15 @@ export async function validateOnce({
|
|
|
163
196
|
})) {
|
|
164
197
|
switch (event.type) {
|
|
165
198
|
case "file-valid":
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
durationMs
|
|
169
|
-
}
|
|
199
|
+
// No errors, but it may have warnings: then it is shown with them.
|
|
200
|
+
if (reports.has(relFile(event.file))) {
|
|
201
|
+
reportFor(event.file).durationMs = event.durationMs;
|
|
202
|
+
} else {
|
|
203
|
+
valid.push({
|
|
204
|
+
file: relFile(event.file),
|
|
205
|
+
durationMs: event.durationMs,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
170
208
|
break;
|
|
171
209
|
case "file-error-count":
|
|
172
210
|
reportFor(event.file).durationMs = event.durationMs;
|
|
@@ -190,6 +228,17 @@ export async function validateOnce({
|
|
|
190
228
|
...(event.keyError ? { keyError: true } : {}),
|
|
191
229
|
});
|
|
192
230
|
break;
|
|
231
|
+
case "validation-warning":
|
|
232
|
+
totalWarnings += 1;
|
|
233
|
+
reportFor(moduleOf(event.sourcePath)).diagnostics.push({
|
|
234
|
+
severity: "warning",
|
|
235
|
+
sourcePath: event.sourcePath,
|
|
236
|
+
message: event.message,
|
|
237
|
+
files: event.files,
|
|
238
|
+
...(event.hint !== undefined ? { hint: event.hint } : {}),
|
|
239
|
+
...(event.keyError ? { keyError: true } : {}),
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
193
242
|
case "unknown-fix":
|
|
194
243
|
reportFor(moduleOf(event.sourcePath)).diagnostics.push({
|
|
195
244
|
severity: "error",
|
|
@@ -249,42 +298,94 @@ export async function validateOnce({
|
|
|
249
298
|
|
|
250
299
|
// Renders a module's diagnostics under a single left "│" gutter bar, with the
|
|
251
300
|
// file name on top and blank gutter lines for air. Output flows least- to
|
|
252
|
-
// most-actionable top-to-bottom
|
|
253
|
-
// bottom, nearest the prompt).
|
|
301
|
+
// most-actionable top-to-bottom: warnings first, then errors, and fixable
|
|
302
|
+
// errors last (at the bottom, nearest the prompt).
|
|
254
303
|
const renderModule = (report: ModuleReport) => {
|
|
255
304
|
const bar = picocolors.dim("│");
|
|
256
|
-
const
|
|
257
|
-
|
|
305
|
+
const rank: Record<Diagnostic["severity"], number> = {
|
|
306
|
+
warning: 0,
|
|
307
|
+
error: 1,
|
|
308
|
+
fixable: 2,
|
|
309
|
+
};
|
|
310
|
+
const diagnostics = [...report.diagnostics].sort(
|
|
311
|
+
(a, b) => rank[a.severity] - rank[b.severity],
|
|
258
312
|
);
|
|
313
|
+
const warningCount = diagnostics.filter(
|
|
314
|
+
(d) => d.severity === "warning",
|
|
315
|
+
).length;
|
|
259
316
|
const fixableCount = diagnostics.filter(
|
|
260
317
|
(d) => d.severity === "fixable",
|
|
261
318
|
).length;
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
319
|
+
const errorCount = diagnostics.length - warningCount;
|
|
320
|
+
const symbol =
|
|
321
|
+
errorCount > 0 ? picocolors.red("✘") : picocolors.yellow("⚠");
|
|
322
|
+
const labels: string[] = [];
|
|
323
|
+
if (errorCount > 0) {
|
|
324
|
+
labels.push(
|
|
325
|
+
`${errorCount} error${errorCount > 1 ? "s" : ""}${
|
|
326
|
+
fixableCount > 0 ? ` (${fixableCount} fixable)` : ""
|
|
327
|
+
}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (warningCount > 0) {
|
|
331
|
+
labels.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
272
332
|
}
|
|
273
333
|
console.log(
|
|
274
|
-
`${picocolors.bold(report.file)} ${symbol} ${
|
|
334
|
+
`${picocolors.bold(report.file)} ${symbol} ${labels.join(" · ")} ${picocolors.dim(
|
|
275
335
|
`(${report.durationMs}ms)`,
|
|
276
336
|
)}`,
|
|
277
337
|
);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const dsym =
|
|
281
|
-
d.severity === "fixable" ? picocolors.yellow("⚠") : picocolors.red("✘");
|
|
282
|
-
const parts = sourcePathToLocationParts(
|
|
338
|
+
const locationOf = (d: Diagnostic) =>
|
|
339
|
+
sourcePathToLocationParts(
|
|
283
340
|
d.sourcePath,
|
|
284
341
|
projectRoot,
|
|
285
342
|
sourceFileCache,
|
|
286
|
-
|
|
343
|
+
d.keyError ? "key" : "value",
|
|
287
344
|
);
|
|
345
|
+
|
|
346
|
+
// Warnings say the same thing about many values (every local image in a
|
|
347
|
+
// gallery), so each message is printed once, over the places it is about.
|
|
348
|
+
const warningGroups = new Map<string, WarningDiagnostic[]>();
|
|
349
|
+
for (const d of diagnostics) {
|
|
350
|
+
if (d.severity !== "warning") continue;
|
|
351
|
+
const group = warningGroups.get(d.message);
|
|
352
|
+
if (group) {
|
|
353
|
+
group.push(d);
|
|
354
|
+
} else {
|
|
355
|
+
warningGroups.set(d.message, [d]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const [message, group] of warningGroups) {
|
|
359
|
+
console.log(bar);
|
|
360
|
+
console.log(`${bar} ${picocolors.yellow("⚠")} ${message}`);
|
|
361
|
+
console.log(bar);
|
|
362
|
+
for (const d of group.slice(0, WARNING_FILES_SHOWN)) {
|
|
363
|
+
const parts = locationOf(d);
|
|
364
|
+
const files = d.files.join(", ");
|
|
365
|
+
// `file:line:col` first, so VS Code's terminal links it.
|
|
366
|
+
const where = parts
|
|
367
|
+
? `${parts.relativeFile}:${parts.line}:${parts.character}`
|
|
368
|
+
: d.sourcePath;
|
|
369
|
+
console.log(`${bar} ${where} ${picocolors.dim(files)}`);
|
|
370
|
+
}
|
|
371
|
+
if (group.length > WARNING_FILES_SHOWN) {
|
|
372
|
+
console.log(
|
|
373
|
+
`${bar} ${picocolors.dim(
|
|
374
|
+
`… ${group.length - WARNING_FILES_SHOWN} more`,
|
|
375
|
+
)}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const hint = group.find((d) => d.hint !== undefined)?.hint;
|
|
379
|
+
if (hint !== undefined) {
|
|
380
|
+
console.log(bar);
|
|
381
|
+
console.log(`${bar} ${picocolors.dim(`→ ${hint}`)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const d of diagnostics) {
|
|
386
|
+
if (d.severity === "warning") continue;
|
|
387
|
+
const parts = locationOf(d);
|
|
388
|
+
const dsym = picocolors.red("✘");
|
|
288
389
|
console.log(bar);
|
|
289
390
|
if (parts) {
|
|
290
391
|
// `file:line:col` (no key/value label) so VS Code's terminal links it.
|
|
@@ -299,7 +400,7 @@ export async function validateOnce({
|
|
|
299
400
|
d.sourcePath,
|
|
300
401
|
projectRoot,
|
|
301
402
|
sourceFileCache,
|
|
302
|
-
|
|
403
|
+
d.keyError ? "key" : "value",
|
|
303
404
|
);
|
|
304
405
|
if (frame !== undefined) {
|
|
305
406
|
console.log(bar);
|
|
@@ -340,13 +441,17 @@ export async function validateOnce({
|
|
|
340
441
|
|
|
341
442
|
// Render the grouped report least- to most-actionable, top-to-bottom, so the
|
|
342
443
|
// most important things end up at the bottom nearest the prompt: valid files
|
|
343
|
-
// and skipped modules first, then
|
|
444
|
+
// and skipped modules first, then warning-only modules, then error-only
|
|
445
|
+
// modules, then fixable modules.
|
|
344
446
|
const allReports = [...reports.values()];
|
|
447
|
+
const hasErrors = (r: ModuleReport) =>
|
|
448
|
+
r.diagnostics.some((d) => d.severity !== "warning");
|
|
449
|
+
const warningModules = allReports.filter((r) => !hasErrors(r));
|
|
345
450
|
const fixableModules = allReports.filter((r) =>
|
|
346
451
|
r.diagnostics.some((d) => d.severity === "fixable"),
|
|
347
452
|
);
|
|
348
453
|
const errorModules = allReports.filter(
|
|
349
|
-
(r) => !r.diagnostics.some((d) => d.severity === "fixable"),
|
|
454
|
+
(r) => hasErrors(r) && !r.diagnostics.some((d) => d.severity === "fixable"),
|
|
350
455
|
);
|
|
351
456
|
|
|
352
457
|
for (const v of valid) {
|
|
@@ -365,7 +470,11 @@ export async function validateOnce({
|
|
|
365
470
|
if (allReports.length > 0) {
|
|
366
471
|
console.log("");
|
|
367
472
|
}
|
|
368
|
-
for (const report of [
|
|
473
|
+
for (const report of [
|
|
474
|
+
...warningModules,
|
|
475
|
+
...errorModules,
|
|
476
|
+
...fixableModules,
|
|
477
|
+
]) {
|
|
369
478
|
renderModule(report);
|
|
370
479
|
}
|
|
371
480
|
|
|
@@ -373,32 +482,51 @@ export async function validateOnce({
|
|
|
373
482
|
(n, r) => n + r.diagnostics.filter((d) => d.severity === "fixable").length,
|
|
374
483
|
0,
|
|
375
484
|
);
|
|
485
|
+
const errorFiles = allReports.filter(hasErrors).length;
|
|
486
|
+
const warningFiles = allReports.filter((r) =>
|
|
487
|
+
r.diagnostics.some((d) => d.severity === "warning"),
|
|
488
|
+
).length;
|
|
489
|
+
const plural = (n: number, word: string) =>
|
|
490
|
+
`${n} ${word}${n === 1 ? "" : "s"}`;
|
|
491
|
+
const warnings = `${plural(totalWarnings, "warning")} in ${plural(warningFiles, "file")}`;
|
|
492
|
+
const warningPart =
|
|
493
|
+
totalWarnings > 0
|
|
494
|
+
? `${warnings}${strict ? ", which --strict counts as errors" : ""}`
|
|
495
|
+
: undefined;
|
|
496
|
+
const tail = [
|
|
497
|
+
...(valid.length > 0 ? [`${valid.length} valid`] : []),
|
|
498
|
+
...(skipped.length > 0 ? [`${skipped.length} skipped`] : []),
|
|
499
|
+
];
|
|
376
500
|
if (totalErrors > 0) {
|
|
377
|
-
let summary =
|
|
501
|
+
let summary = plural(totalErrors, "error");
|
|
378
502
|
if (fixableTotal > 0) {
|
|
379
503
|
summary += ` (${fixableTotal} fixable)`;
|
|
380
504
|
}
|
|
381
|
-
summary += ` across ${
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
505
|
+
summary += ` across ${plural(errorFiles, "file")}`;
|
|
506
|
+
console.log(
|
|
507
|
+
picocolors.red("✘"),
|
|
508
|
+
[summary, ...(warningPart ? [warningPart] : []), ...tail].join(" · "),
|
|
509
|
+
);
|
|
510
|
+
} else if (warningPart && strict) {
|
|
511
|
+
// Not "No validation errors" beside a ✘: under --strict this run failed,
|
|
512
|
+
// and the warnings are why.
|
|
513
|
+
console.log(
|
|
514
|
+
picocolors.red("✘"),
|
|
515
|
+
[`${warnings}, and --strict fails on warnings`, ...tail].join(" · "),
|
|
516
|
+
);
|
|
517
|
+
} else if (warningPart) {
|
|
518
|
+
console.log(
|
|
519
|
+
picocolors.green("✔"),
|
|
520
|
+
["No validation errors", warningPart, ...tail].join(" · "),
|
|
521
|
+
);
|
|
391
522
|
} else {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (skipped.length > 0) {
|
|
397
|
-
summary += ` · ${skipped.length} skipped`;
|
|
398
|
-
}
|
|
399
|
-
console.log(picocolors.green("✔"), summary);
|
|
523
|
+
console.log(
|
|
524
|
+
picocolors.green("✔"),
|
|
525
|
+
["No validation errors found", ...tail].join(" · "),
|
|
526
|
+
);
|
|
400
527
|
}
|
|
401
|
-
|
|
528
|
+
const errors = blockingOnly ? totalErrors - repairedOnSave : totalErrors;
|
|
529
|
+
return strict ? errors + totalWarnings : errors;
|
|
402
530
|
}
|
|
403
531
|
|
|
404
532
|
// Directory names anywhere in the path that should never be watched.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { validateOnce } from "./validate";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A local file in a project with remote files is a WARNING: `val validate`
|
|
8
|
+
* prints it and passes, `--strict` makes it fail. Every project made from a
|
|
9
|
+
* template starts this way, hosted ones included, and the site serves those
|
|
10
|
+
* files from public/ until they are uploaded.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const BASIC_FIXTURE = path.resolve(__dirname, "__fixtures__/basic");
|
|
14
|
+
|
|
15
|
+
const PNG = Buffer.from(
|
|
16
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX/AAD//" +
|
|
17
|
+
"/+l2Z/dAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9oIBRELLnJqTAAAAAxJREFUCNdjYGBgAAAABAABJzQnCgAAAABJRU5ErkJggg==",
|
|
18
|
+
"base64",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const IMAGES = Array.from(
|
|
22
|
+
{ length: 7 },
|
|
23
|
+
(_, i) => `/public/val/images/image-${i}_0000${i}.png`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const PROJECT: Record<string, string | Buffer> = {
|
|
27
|
+
"val.modules.ts": `import { modules } from "@valbuild/core";
|
|
28
|
+
import { config } from "./val.config";
|
|
29
|
+
|
|
30
|
+
export default modules(config, [
|
|
31
|
+
{ def: () => import("./content/images.val") },
|
|
32
|
+
]);
|
|
33
|
+
`,
|
|
34
|
+
"content/images.val.ts": `import { c, s } from "../val.config";
|
|
35
|
+
|
|
36
|
+
export default c.define(
|
|
37
|
+
"/content/images.val.ts",
|
|
38
|
+
s.imagedir("/public/val/images").remote(),
|
|
39
|
+
{
|
|
40
|
+
${IMAGES.map(
|
|
41
|
+
(image) =>
|
|
42
|
+
` "${image}": { width: 8, height: 8, mimeType: "image/png", alt: null },`,
|
|
43
|
+
).join("\n")}
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
`,
|
|
47
|
+
...Object.fromEntries(IMAGES.map((image) => [image.slice(1), PNG])),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe("val validate: a local file in a project with remote files", () => {
|
|
51
|
+
let root: string;
|
|
52
|
+
let output: string[];
|
|
53
|
+
let log: jest.SpyInstance;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
const tmpBase = path.join(__dirname, "..", ".tmp");
|
|
57
|
+
fs.mkdirSync(tmpBase, { recursive: true });
|
|
58
|
+
root = fs.mkdtempSync(path.join(tmpBase, "warnings-"));
|
|
59
|
+
for (const file of ["val.config.ts", "tsconfig.json"]) {
|
|
60
|
+
fs.copyFileSync(path.join(BASIC_FIXTURE, file), path.join(root, file));
|
|
61
|
+
}
|
|
62
|
+
for (const [file, content] of Object.entries(PROJECT)) {
|
|
63
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
64
|
+
fs.writeFileSync(path.join(root, file), content);
|
|
65
|
+
}
|
|
66
|
+
output = [];
|
|
67
|
+
log = jest.spyOn(console, "log").mockImplementation((...args) => {
|
|
68
|
+
output.push(args.join(" "));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
log.mockRestore();
|
|
74
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// eslint-disable-next-line no-control-regex
|
|
78
|
+
const plain = () => output.join("\n").replace(/\u001b\[[0-9;]*m/g, "");
|
|
79
|
+
|
|
80
|
+
test("is printed once, over every file it is about, and passes", async () => {
|
|
81
|
+
expect(await validateOnce({ projectRoot: root, fix: false })).toBe(0);
|
|
82
|
+
const text = plain();
|
|
83
|
+
expect(text).toContain("content/images.val.ts ⚠ 7 warnings");
|
|
84
|
+
expect(
|
|
85
|
+
text.match(/Local files in a project with remote files/g),
|
|
86
|
+
).toHaveLength(1);
|
|
87
|
+
expect(text).toContain(IMAGES[0]);
|
|
88
|
+
expect(text).toContain("… 2 more");
|
|
89
|
+
expect(text).toContain("→ val validate --fix uploads them");
|
|
90
|
+
expect(text).toContain("✔ No validation errors · 7 warnings in 1 file");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("fails under --strict", async () => {
|
|
94
|
+
expect(
|
|
95
|
+
await validateOnce({ projectRoot: root, fix: false, strict: true }),
|
|
96
|
+
).toBe(7);
|
|
97
|
+
const text = plain();
|
|
98
|
+
expect(text).toContain(
|
|
99
|
+
"✘ 7 warnings in 1 file, and --strict fails on warnings",
|
|
100
|
+
);
|
|
101
|
+
// Not "passed" and "failed" in one line.
|
|
102
|
+
expect(text).not.toContain("No validation errors");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("does not hold back val publish, unless --strict", async () => {
|
|
106
|
+
expect(
|
|
107
|
+
await validateOnce({ projectRoot: root, fix: false, blockingOnly: true }),
|
|
108
|
+
).toBe(0);
|
|
109
|
+
expect(
|
|
110
|
+
await validateOnce({
|
|
111
|
+
projectRoot: root,
|
|
112
|
+
fix: false,
|
|
113
|
+
blockingOnly: true,
|
|
114
|
+
strict: true,
|
|
115
|
+
}),
|
|
116
|
+
).toBe(7);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("fails under --fix when the files cannot be uploaded", async () => {
|
|
120
|
+
// Not logged in: there is no .val/pat.json in the project.
|
|
121
|
+
expect(
|
|
122
|
+
await validateOnce({ projectRoot: root, fix: true }),
|
|
123
|
+
).toBeGreaterThan(0);
|
|
124
|
+
const text = plain();
|
|
125
|
+
expect(text).toContain("not logged in");
|
|
126
|
+
expect(text).not.toContain("⚠");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("an image inside rich text is a warning too", async () => {
|
|
130
|
+
// A rich text image's `src` resolves to the rich text schema, not the
|
|
131
|
+
// image one, once the schema is serialized -- which the handler read as
|
|
132
|
+
// "neither image nor file" and reported as an error.
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(root, "val.modules.ts"),
|
|
135
|
+
`import { modules } from "@valbuild/core";
|
|
136
|
+
import { config } from "./val.config";
|
|
137
|
+
|
|
138
|
+
export default modules(config, [
|
|
139
|
+
{ def: () => import("./content/page.val") },
|
|
140
|
+
]);
|
|
141
|
+
`,
|
|
142
|
+
);
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(root, "content/page.val.ts"),
|
|
145
|
+
`import { c, s } from "../val.config";
|
|
146
|
+
|
|
147
|
+
export default c.define(
|
|
148
|
+
"/content/page.val.ts",
|
|
149
|
+
s.richtext({ img: s.image().remote() }),
|
|
150
|
+
[
|
|
151
|
+
{
|
|
152
|
+
tag: "p",
|
|
153
|
+
children: [
|
|
154
|
+
"Look: ",
|
|
155
|
+
{
|
|
156
|
+
tag: "img",
|
|
157
|
+
src: { path: "${IMAGES[0]}", width: 8, height: 8, mimeType: "image/png" },
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
);
|
|
163
|
+
`,
|
|
164
|
+
);
|
|
165
|
+
expect(await validateOnce({ projectRoot: root, fix: false })).toBe(0);
|
|
166
|
+
const text = plain();
|
|
167
|
+
expect(text).toContain("content/page.val.ts ⚠ 1 warning");
|
|
168
|
+
expect(text).not.toContain("neither image nor file");
|
|
169
|
+
});
|
|
170
|
+
});
|