Skip to content

Commit 3e76ebc

Browse files
committed
Merge branch 'feature/cli-errors' into 'master'
refactor: move for generating pretty schema validation error to CLI See merge request html-validate/html-validate!905
2 parents d59c210 + ed25da9 commit 3e76ebc

13 files changed

+271
-210
lines changed

etc/browser.api.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,10 +1095,10 @@ export { SchemaObject }
10951095
// @public (undocumented)
10961096
export class SchemaValidationError extends UserError {
10971097
constructor(filename: string | null, message: string, obj: unknown, schema: SchemaObject, errors: ErrorObject[]);
1098-
// (undocumented)
1099-
filename: string | null;
1100-
// (undocumented)
1101-
prettyError(): string;
1098+
readonly errors: ErrorObject[];
1099+
readonly filename: string | null;
1100+
readonly obj: unknown;
1101+
readonly schema: SchemaObject;
11021102
}
11031103

11041104
// @public (undocumented)

etc/index.api.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,10 +1188,10 @@ export { SchemaObject }
11881188
// @public (undocumented)
11891189
export class SchemaValidationError extends UserError {
11901190
constructor(filename: string | null, message: string, obj: unknown, schema: SchemaObject, errors: ErrorObject[]);
1191-
// (undocumented)
1192-
filename: string | null;
1193-
// (undocumented)
1194-
prettyError(): string;
1191+
readonly errors: ErrorObject[];
1192+
readonly filename: string | null;
1193+
readonly obj: unknown;
1194+
readonly schema: SchemaObject;
11951195
}
11961196

11971197
// @public (undocumented)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`should handle function keyword: stderr 1`] = `
4+
"A configuration error was found in "src/cli/errors/__fixtures__/invalid.json":
5+
TYPE should be a function
6+
7+
1 | {
8+
> 2 | "foo": [1, 2, 3],
9+
| ^^^^^^^^^ 👈🏽 type should be a function
10+
3 | "bar": "baz"
11+
4 | }
12+
5 |
13+
"
14+
`;
15+
16+
exports[`should handle function keyword: stdout 1`] = `""`;
17+
18+
exports[`should handle regexp keyword: stderr 1`] = `
19+
"A configuration error was found in "src/cli/errors/__fixtures__/invalid.json":
20+
TYPE should be a regular expression
21+
22+
1 | {
23+
> 2 | "foo": [1, 2, 3],
24+
| ^^^^^^^^^ 👈🏽 type should be a regular expression
25+
3 | "bar": "baz"
26+
4 | }
27+
5 |
28+
"
29+
`;
30+
31+
exports[`should handle regexp keyword: stdout 1`] = `""`;
32+
33+
exports[`should handle when filename does not exist: stderr 1`] = `
34+
"A configuration error was found in "missing-file.json":
35+
TYPE must be string
36+
37+
1 | {
38+
> 2 | "foo": [
39+
| ^
40+
> 3 | 1,
41+
| ^^^^^^
42+
> 4 | 2,
43+
| ^^^^^^
44+
> 5 | 3
45+
| ^^^^^^
46+
> 6 | ],
47+
| ^^^^ 👈🏽 type must be string
48+
7 | "bar": "baz"
49+
8 | }
50+
"
51+
`;
52+
53+
exports[`should handle when filename does not exist: stdout 1`] = `""`;
54+
55+
exports[`should handle when filename is missing: stderr 1`] = `
56+
"A configuration error was found:
57+
TYPE must be string
58+
59+
1 | {
60+
> 2 | "foo": [
61+
| ^
62+
> 3 | 1,
63+
| ^^^^^^
64+
> 4 | 2,
65+
| ^^^^^^
66+
> 5 | 3
67+
| ^^^^^^
68+
> 6 | ],
69+
| ^^^^ 👈🏽 type must be string
70+
7 | "bar": "baz"
71+
8 | }
72+
"
73+
`;
74+
75+
exports[`should handle when filename is missing: stdout 1`] = `""`;
76+
77+
exports[`should present error in pretty format: stderr 1`] = `
78+
"A configuration error was found in "src/cli/errors/__fixtures__/invalid.json":
79+
TYPE must be string
80+
81+
1 | {
82+
> 2 | "foo": [1, 2, 3],
83+
| ^^^^^^^^^ 👈🏽 type must be string
84+
3 | "bar": "baz"
85+
4 | }
86+
5 |
87+
"
88+
`;
89+
90+
exports[`should present error in pretty format: stdout 1`] = `""`;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { Console } from "node:console";
4+
import { WritableStreamBuffer } from "stream-buffers";
5+
import Ajv, { type SchemaObject } from "ajv";
6+
import kleur from "kleur";
7+
import { SchemaValidationError } from "../../error";
8+
import { stripAnsi } from "../../jest/utils";
9+
import { ajvFunctionKeyword, ajvRegexpKeyword } from "../../schema/keywords";
10+
import { handleSchemaValidationError } from "./handle-schema-validation-error";
11+
12+
kleur.enabled = true;
13+
14+
let stdout: WritableStreamBuffer;
15+
let stderr: WritableStreamBuffer;
16+
let console: Console;
17+
18+
beforeEach(() => {
19+
stdout = new WritableStreamBuffer();
20+
stderr = new WritableStreamBuffer();
21+
console = new Console(stdout, stderr);
22+
});
23+
24+
function loadFixture(filename: string): { data: unknown; filePath: string } {
25+
const filePath = path.join(__dirname, "__fixtures__", filename);
26+
return { data: JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath };
27+
}
28+
29+
function errorFromFixture(
30+
filename: string | null,
31+
data: unknown,
32+
schema: SchemaObject,
33+
): SchemaValidationError {
34+
const ajv = new Ajv({ strict: true, strictTuples: true, strictTypes: true });
35+
ajv.addKeyword(ajvFunctionKeyword);
36+
ajv.addKeyword(ajvRegexpKeyword);
37+
const validator = ajv.compile(schema);
38+
validator(data);
39+
const errors = validator.errors ?? [];
40+
return new SchemaValidationError(filename, "Mock error", data, schema, errors);
41+
}
42+
43+
it("should present error in pretty format", () => {
44+
expect.assertions(2);
45+
const { data, filePath } = loadFixture("invalid.json");
46+
const error = errorFromFixture(filePath, data, {
47+
type: "object",
48+
properties: {
49+
foo: {
50+
type: "string",
51+
},
52+
},
53+
});
54+
handleSchemaValidationError(console, error);
55+
expect(stripAnsi(stdout.getContentsAsString("utf-8"))).toMatchSnapshot("stdout");
56+
expect(stripAnsi(stderr.getContentsAsString("utf-8"))).toMatchSnapshot("stderr");
57+
});
58+
59+
it("should handle when filename is missing", () => {
60+
expect.assertions(2);
61+
const { data } = loadFixture("invalid.json");
62+
const error = errorFromFixture(null, data, {
63+
type: "object",
64+
properties: {
65+
foo: {
66+
type: "string",
67+
},
68+
},
69+
});
70+
handleSchemaValidationError(console, error);
71+
expect(stripAnsi(stdout.getContentsAsString("utf-8"))).toMatchSnapshot("stdout");
72+
expect(stripAnsi(stderr.getContentsAsString("utf-8"))).toMatchSnapshot("stderr");
73+
});
74+
75+
it("should handle when filename does not exist", () => {
76+
expect.assertions(2);
77+
const { data } = loadFixture("invalid.json");
78+
const error = errorFromFixture("missing-file.json", data, {
79+
type: "object",
80+
properties: {
81+
foo: {
82+
type: "string",
83+
},
84+
},
85+
});
86+
handleSchemaValidationError(console, error);
87+
expect(stripAnsi(stdout.getContentsAsString("utf-8"))).toMatchSnapshot("stdout");
88+
expect(stripAnsi(stderr.getContentsAsString("utf-8"))).toMatchSnapshot("stderr");
89+
});
90+
91+
it("should handle function keyword", () => {
92+
expect.assertions(2);
93+
const { data, filePath } = loadFixture("invalid.json");
94+
const error = errorFromFixture(filePath, data, {
95+
type: "object",
96+
properties: {
97+
foo: {
98+
function: true,
99+
},
100+
},
101+
});
102+
handleSchemaValidationError(console, error);
103+
expect(stripAnsi(stdout.getContentsAsString("utf-8"))).toMatchSnapshot("stdout");
104+
expect(stripAnsi(stderr.getContentsAsString("utf-8"))).toMatchSnapshot("stderr");
105+
});
106+
107+
it("should handle regexp keyword", () => {
108+
expect.assertions(2);
109+
const { data, filePath } = loadFixture("invalid.json");
110+
const error = errorFromFixture(filePath, data, {
111+
type: "object",
112+
properties: {
113+
foo: {
114+
regexp: true,
115+
},
116+
},
117+
});
118+
handleSchemaValidationError(console, error);
119+
expect(stripAnsi(stdout.getContentsAsString("utf-8"))).toMatchSnapshot("stdout");
120+
expect(stripAnsi(stderr.getContentsAsString("utf-8"))).toMatchSnapshot("stderr");
121+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import betterAjvErrors from "@sidvind/better-ajv-errors";
4+
import kleur from "kleur";
5+
import { type SchemaValidationError } from "../../error";
6+
7+
function prettyError(err: SchemaValidationError): string {
8+
let json: string | undefined;
9+
if (err.filename && fs.existsSync(err.filename)) {
10+
json = fs.readFileSync(err.filename, "utf-8");
11+
}
12+
return betterAjvErrors(err.schema, err.obj, err.errors, {
13+
format: "cli",
14+
indent: 2,
15+
json,
16+
});
17+
}
18+
19+
/**
20+
* @internal
21+
*/
22+
export function handleSchemaValidationError(console: Console, err: SchemaValidationError): void {
23+
if (err.filename) {
24+
const filename = path.relative(process.cwd(), err.filename);
25+
console.error(kleur.red(`A configuration error was found in "${filename}":`));
26+
} else {
27+
console.error(kleur.red(`A configuration error was found:`));
28+
}
29+
console.group();
30+
{
31+
console.error(prettyError(err));
32+
}
33+
console.groupEnd();
34+
}

src/cli/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { handleSchemaValidationError } from "./handle-schema-validation-error";

src/cli/html-validate.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import minimist from "minimist";
66
import { SchemaValidationError, UserError } from "..";
77
import { name, version, bugs as pkgBugs } from "../generated/package";
88
import { CLI } from "./cli";
9+
import { handleSchemaValidationError } from "./errors";
910
import { Mode, modeToFlag } from "./mode";
1011
import { lint } from "./actions/lint";
1112
import { init } from "./actions/init";
@@ -73,20 +74,6 @@ function requiresFilename(mode: Mode): boolean {
7374
}
7475
}
7576

76-
function handleValidationError(err: SchemaValidationError): void {
77-
if (err.filename) {
78-
const filename = path.relative(process.cwd(), err.filename);
79-
console.log(kleur.red(`A configuration error was found in "${filename}":`));
80-
} else {
81-
console.log(kleur.red(`A configuration error was found:`));
82-
}
83-
console.group();
84-
{
85-
console.log(err.prettyError());
86-
}
87-
console.groupEnd();
88-
}
89-
9077
function handleUserError(err: UserError): void {
9178
const formatted = err.prettyFormat();
9279
if (formatted) {
@@ -276,7 +263,7 @@ async function run(): Promise<void> {
276263
process.exit(success ? 0 : 1);
277264
} catch (err) {
278265
if (err instanceof SchemaValidationError) {
279-
handleValidationError(err);
266+
handleSchemaValidationError(console, err);
280267
} else if (err instanceof UserError) {
281268
handleUserError(err);
282269
} else {

src/error/__snapshots__/schema-validation-error.spec.ts.snap

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)