v8r 4.0.1 → 4.2.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/ajv.js +1 -1
- package/src/bootstrap.js +21 -27
- package/src/catalogs.js +1 -1
- package/src/cli.js +92 -43
- package/src/config-validators.js +1 -1
- package/src/io.js +2 -2
- package/src/output-formatters.js +10 -3
- package/src/parser.js +14 -8
- package/src/plugins/output-text.js +2 -2
- package/src/plugins/parser-yaml.js +2 -2
- package/src/plugins.js +10 -3
- package/src/test-helpers.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 📦 [4.2.0](https://www.npmjs.com/package/v8r/v/4.2.0) - 2024-10-24
|
|
4
|
+
|
|
5
|
+
* Add `V8R_CONFIG_FILE` environment variable.
|
|
6
|
+
This allows loading a config file from a location other than the directory v8r is invoked from.
|
|
7
|
+
More info: https://chris48s.github.io/v8r/configuration/
|
|
8
|
+
|
|
9
|
+
## 📦 [4.1.0](https://www.npmjs.com/package/v8r/v/4.1.0) - 2024-08-25
|
|
10
|
+
|
|
11
|
+
* v8r can now parse and validate files that contain multiple yaml documents
|
|
12
|
+
More info: https://chris48s.github.io/v8r/usage-examples/#files-containing-multiple-documents
|
|
13
|
+
* The `parseInputFile()` plugin hook may now conditionally return an array of `Document` objects
|
|
14
|
+
* The `ValidationResult` object now contains a `documentIndex` property.
|
|
15
|
+
This identifies the document when a multi-doc file has been validated.
|
|
16
|
+
|
|
3
17
|
## 📦 [4.0.1](https://www.npmjs.com/package/v8r/v/4.0.1) - 2024-08-19
|
|
4
18
|
|
|
5
19
|
* De-duplicate and sort files before validating
|
package/package.json
CHANGED
package/src/ajv.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRequire } from "module";
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
2
|
// TODO: once JSON modules is stable these requires could become imports
|
|
3
3
|
// https://nodejs.org/api/esm.html#esm_experimental_json_modules
|
|
4
4
|
const require = createRequire(import.meta.url);
|
package/src/bootstrap.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createRequire } from "module";
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
2
|
// TODO: once JSON modules is stable these requires could become imports
|
|
3
3
|
// https://nodejs.org/api/esm.html#esm_experimental_json_modules
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
6
8
|
import { cosmiconfig } from "cosmiconfig";
|
|
7
9
|
import decamelize from "decamelize";
|
|
8
|
-
import isUrl from "is-url";
|
|
9
|
-
import path from "path";
|
|
10
10
|
import yargs from "yargs";
|
|
11
11
|
import { hideBin } from "yargs/helpers";
|
|
12
12
|
import {
|
|
@@ -17,27 +17,28 @@ import {
|
|
|
17
17
|
import logger from "./logger.js";
|
|
18
18
|
import { loadAllPlugins, resolveUserPlugins } from "./plugins.js";
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
schema.location = path.join(
|
|
27
|
-
path.dirname(configFile.filepath),
|
|
28
|
-
schema.location,
|
|
29
|
-
);
|
|
20
|
+
async function getCosmiConfig(cosmiconfigOptions) {
|
|
21
|
+
let configFile;
|
|
22
|
+
|
|
23
|
+
if (process.env.V8R_CONFIG_FILE) {
|
|
24
|
+
if (!fs.existsSync(process.env.V8R_CONFIG_FILE)) {
|
|
25
|
+
throw new Error(`File ${process.env.V8R_CONFIG_FILE} does not exist.`);
|
|
30
26
|
}
|
|
27
|
+
configFile = await cosmiconfig("v8r", cosmiconfigOptions).load(
|
|
28
|
+
process.env.V8R_CONFIG_FILE,
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
cosmiconfigOptions.stopDir = process.cwd();
|
|
32
|
+
configFile = (await cosmiconfig("v8r", cosmiconfigOptions).search(
|
|
33
|
+
process.cwd(),
|
|
34
|
+
)) || { config: {} };
|
|
31
35
|
}
|
|
32
|
-
}
|
|
33
36
|
|
|
34
|
-
async function getCosmiConfig(cosmiconfigOptions) {
|
|
35
|
-
cosmiconfigOptions.stopDir = process.cwd();
|
|
36
|
-
const configFile = (await cosmiconfig("v8r", cosmiconfigOptions).search(
|
|
37
|
-
process.cwd(),
|
|
38
|
-
)) || { config: {} };
|
|
39
37
|
if (configFile.filepath) {
|
|
40
38
|
logger.info(`Loaded config file from ${getRelativeFilePath(configFile)}`);
|
|
39
|
+
logger.info(
|
|
40
|
+
`Patterns and relative paths will be resolved relative to current working directory: ${process.cwd()}`,
|
|
41
|
+
);
|
|
41
42
|
} else {
|
|
42
43
|
logger.info(`No config file found`);
|
|
43
44
|
}
|
|
@@ -200,7 +201,6 @@ async function bootstrap(argv, config, cosmiconfigOptions = {}) {
|
|
|
200
201
|
// we can finish validating and processing the config
|
|
201
202
|
validateConfigDocumentParsers(configFile, documentFormats);
|
|
202
203
|
validateConfigOutputFormats(configFile, outputFormats);
|
|
203
|
-
preProcessConfig(configFile);
|
|
204
204
|
|
|
205
205
|
// parse command line arguments
|
|
206
206
|
const args = parseArgs(argv, configFile, documentFormats, outputFormats);
|
|
@@ -213,10 +213,4 @@ async function bootstrap(argv, config, cosmiconfigOptions = {}) {
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
export {
|
|
217
|
-
bootstrap,
|
|
218
|
-
getDocumentFormats,
|
|
219
|
-
getOutputFormats,
|
|
220
|
-
parseArgs,
|
|
221
|
-
preProcessConfig,
|
|
222
|
-
};
|
|
216
|
+
export { bootstrap, getDocumentFormats, getOutputFormats, parseArgs };
|
package/src/catalogs.js
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import flatCache from "flat-cache";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import path from "path";
|
|
5
5
|
import isUrl from "is-url";
|
|
6
6
|
import { validate } from "./ajv.js";
|
|
7
7
|
import { bootstrap } from "./bootstrap.js";
|
|
@@ -10,6 +10,7 @@ import { getCatalogs, getMatchForFilename } from "./catalogs.js";
|
|
|
10
10
|
import { getFiles } from "./glob.js";
|
|
11
11
|
import { getFromUrlOrFile } from "./io.js";
|
|
12
12
|
import logger from "./logger.js";
|
|
13
|
+
import { getDocumentLocation } from "./output-formatters.js";
|
|
13
14
|
import { parseFile } from "./parser.js";
|
|
14
15
|
|
|
15
16
|
const EXIT = {
|
|
@@ -33,61 +34,122 @@ function getFlatCache() {
|
|
|
33
34
|
return flatCache.load("v8r", CACHE_DIR);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async function
|
|
37
|
-
|
|
37
|
+
async function validateDocument(
|
|
38
|
+
fileLocation,
|
|
39
|
+
documentIndex,
|
|
40
|
+
document,
|
|
41
|
+
schemaLocation,
|
|
42
|
+
schema,
|
|
43
|
+
strictMode,
|
|
44
|
+
cache,
|
|
45
|
+
resolver,
|
|
46
|
+
) {
|
|
38
47
|
let result = {
|
|
39
|
-
fileLocation
|
|
40
|
-
|
|
48
|
+
fileLocation,
|
|
49
|
+
documentIndex,
|
|
50
|
+
schemaLocation,
|
|
41
51
|
valid: null,
|
|
42
52
|
errors: [],
|
|
43
53
|
code: null,
|
|
44
54
|
};
|
|
55
|
+
try {
|
|
56
|
+
const { valid, errors } = await validate(
|
|
57
|
+
document,
|
|
58
|
+
schema,
|
|
59
|
+
strictMode,
|
|
60
|
+
cache,
|
|
61
|
+
resolver,
|
|
62
|
+
);
|
|
63
|
+
result.valid = valid;
|
|
64
|
+
result.errors = errors;
|
|
65
|
+
|
|
66
|
+
const documentLocation = getDocumentLocation(result);
|
|
67
|
+
if (valid) {
|
|
68
|
+
logger.success(`${documentLocation} is valid\n`);
|
|
69
|
+
} else {
|
|
70
|
+
logger.error(`${documentLocation} is invalid\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
result.code = valid ? EXIT.VALID : EXIT.INVALID;
|
|
74
|
+
return result;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
logger.error(`${e.message}\n`);
|
|
77
|
+
result.code = EXIT.ERROR;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function validateFile(filename, config, plugins, cache) {
|
|
83
|
+
logger.info(`Processing ${filename}`);
|
|
84
|
+
|
|
85
|
+
let schema, schemaLocation, documents, strictMode, resolver;
|
|
86
|
+
|
|
45
87
|
try {
|
|
46
88
|
const catalogs = getCatalogs(config);
|
|
47
89
|
const catalogMatch = config.schema
|
|
48
90
|
? {}
|
|
49
91
|
: await getMatchForFilename(catalogs, filename, cache);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const schema = await getFromUrlOrFile(schemaLocation, cache);
|
|
92
|
+
schemaLocation = config.schema || catalogMatch.location;
|
|
93
|
+
schema = await getFromUrlOrFile(schemaLocation, cache);
|
|
53
94
|
logger.info(
|
|
54
95
|
`Validating ${filename} against schema from ${schemaLocation} ...`,
|
|
55
96
|
);
|
|
56
97
|
|
|
57
|
-
|
|
98
|
+
documents = parseFile(
|
|
58
99
|
plugins,
|
|
59
100
|
await fs.promises.readFile(filename, "utf8"),
|
|
60
101
|
filename,
|
|
61
102
|
catalogMatch.parser,
|
|
62
103
|
);
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
strictMode = config.verbose >= 2 ? "log" : false;
|
|
106
|
+
resolver = isUrl(schemaLocation)
|
|
66
107
|
? (location) => getFromUrlOrFile(location, cache)
|
|
67
108
|
: (location) =>
|
|
68
109
|
getFromUrlOrFile(location, cache, path.dirname(schemaLocation));
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
} catch (e) {
|
|
111
|
+
logger.error(`${e.message}\n`);
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
fileLocation: filename,
|
|
115
|
+
documentIndex: null,
|
|
116
|
+
schemaLocation: schemaLocation || null,
|
|
117
|
+
valid: null,
|
|
118
|
+
errors: [],
|
|
119
|
+
code: EXIT.ERROR,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let results = [];
|
|
125
|
+
for (let i = 0; i < documents.length; i++) {
|
|
126
|
+
const documentIndex = documents.length === 1 ? null : i;
|
|
127
|
+
const result = await validateDocument(
|
|
128
|
+
filename,
|
|
129
|
+
documentIndex,
|
|
130
|
+
documents[i],
|
|
131
|
+
schemaLocation,
|
|
71
132
|
schema,
|
|
72
133
|
strictMode,
|
|
73
134
|
cache,
|
|
74
135
|
resolver,
|
|
75
136
|
);
|
|
76
|
-
result.valid = valid;
|
|
77
|
-
result.errors = errors;
|
|
78
|
-
if (valid) {
|
|
79
|
-
logger.success(`${filename} is valid\n`);
|
|
80
|
-
} else {
|
|
81
|
-
logger.error(`${filename} is invalid\n`);
|
|
82
|
-
}
|
|
83
137
|
|
|
84
|
-
result
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
138
|
+
results.push(result);
|
|
139
|
+
|
|
140
|
+
for (const plugin of plugins) {
|
|
141
|
+
const message = plugin.getSingleResultLogMessage(
|
|
142
|
+
result,
|
|
143
|
+
filename,
|
|
144
|
+
config.format,
|
|
145
|
+
);
|
|
146
|
+
if (message != null) {
|
|
147
|
+
logger.log(message);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
90
151
|
}
|
|
152
|
+
return results;
|
|
91
153
|
}
|
|
92
154
|
|
|
93
155
|
function resultsToStatusCode(results, ignoreErrors) {
|
|
@@ -122,21 +184,8 @@ function Validator() {
|
|
|
122
184
|
|
|
123
185
|
let results = [];
|
|
124
186
|
for (const filename of filenames) {
|
|
125
|
-
const
|
|
126
|
-
results.
|
|
127
|
-
|
|
128
|
-
for (const plugin of plugins) {
|
|
129
|
-
const message = plugin.getSingleResultLogMessage(
|
|
130
|
-
result,
|
|
131
|
-
filename,
|
|
132
|
-
config.format,
|
|
133
|
-
);
|
|
134
|
-
if (message != null) {
|
|
135
|
-
logger.log(message);
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
187
|
+
const fileResults = await validateFile(filename, config, plugins, cache);
|
|
188
|
+
results = results.concat(fileResults);
|
|
140
189
|
cache.resetCounters();
|
|
141
190
|
}
|
|
142
191
|
|
package/src/config-validators.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRequire } from "module";
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
2
|
// TODO: once JSON modules is stable these requires could become imports
|
|
3
3
|
// https://nodejs.org/api/esm.html#esm_experimental_json_modules
|
|
4
4
|
const require = createRequire(import.meta.url);
|
package/src/io.js
CHANGED
package/src/output-formatters.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
function getDocumentLocation(result) {
|
|
4
|
+
if (result.documentIndex == null) {
|
|
5
|
+
return result.fileLocation;
|
|
6
|
+
}
|
|
7
|
+
return `${result.fileLocation}[${result.documentIndex}]`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatErrors(location, errors) {
|
|
4
11
|
const ajv = new Ajv();
|
|
5
12
|
return (
|
|
6
13
|
ajv.errorsText(errors, {
|
|
7
14
|
separator: "\n",
|
|
8
|
-
dataVar:
|
|
15
|
+
dataVar: location + "#",
|
|
9
16
|
}) + "\n"
|
|
10
17
|
);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
export { formatErrors };
|
|
20
|
+
export { formatErrors, getDocumentLocation };
|
package/src/parser.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import path from "path";
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
3
|
import { Document } from "./plugins.js";
|
|
4
4
|
|
|
5
5
|
function parseFile(plugins, contents, filename, parser) {
|
|
6
6
|
for (const plugin of plugins) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const parsedFile = plugin.parseInputFile(contents, filename, parser);
|
|
8
|
+
|
|
9
|
+
if (parsedFile != null) {
|
|
10
|
+
const maybeDocuments = Array.isArray(parsedFile)
|
|
11
|
+
? parsedFile
|
|
12
|
+
: [parsedFile];
|
|
13
|
+
for (const doc of maybeDocuments) {
|
|
14
|
+
if (!(doc instanceof Document)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Plugin ${plugin.constructor.name} returned an unexpected type from parseInputFile hook. Expected Document, got ${typeof doc}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
return
|
|
20
|
+
return maybeDocuments.map((md) => md.document);
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BasePlugin } from "../plugins.js";
|
|
2
|
-
import { formatErrors } from "../output-formatters.js";
|
|
2
|
+
import { formatErrors, getDocumentLocation } from "../output-formatters.js";
|
|
3
3
|
|
|
4
4
|
class TextOutput extends BasePlugin {
|
|
5
5
|
static name = "v8r-plugin-text-output";
|
|
@@ -10,7 +10,7 @@ class TextOutput extends BasePlugin {
|
|
|
10
10
|
|
|
11
11
|
getSingleResultLogMessage(result, fileLocation, format) {
|
|
12
12
|
if (result.valid === false && format === "text") {
|
|
13
|
-
return formatErrors(
|
|
13
|
+
return formatErrors(getDocumentLocation(result), result.errors);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -10,10 +10,10 @@ class YamlParser extends BasePlugin {
|
|
|
10
10
|
|
|
11
11
|
parseInputFile(contents, fileLocation, parser) {
|
|
12
12
|
if (parser === "yaml") {
|
|
13
|
-
return
|
|
13
|
+
return yaml.loadAll(contents).map((doc) => new Document(doc));
|
|
14
14
|
} else if (parser == null) {
|
|
15
15
|
if (fileLocation.endsWith(".yaml") || fileLocation.endsWith(".yml")) {
|
|
16
|
-
return
|
|
16
|
+
return yaml.loadAll(contents).map((doc) => new Document(doc));
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/plugins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import path from "path";
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all v8r plugins.
|
|
@@ -32,7 +32,8 @@ class BasePlugin {
|
|
|
32
32
|
* If `parseInputFile` returns anything other than undefined, that return
|
|
33
33
|
* value will be used and no further plugins will be invoked. If
|
|
34
34
|
* `parseInputFile` returns undefined, v8r will move on to the next plugin in
|
|
35
|
-
* the stack.
|
|
35
|
+
* the stack. The result of successfully parsing a file can either be a single
|
|
36
|
+
* Document object or an array of Document objects.
|
|
36
37
|
*
|
|
37
38
|
* @param {string} contents - The unparsed file content.
|
|
38
39
|
* @param {string} fileLocation - The file path. Filenames are resolved and
|
|
@@ -43,7 +44,7 @@ class BasePlugin {
|
|
|
43
44
|
* @param {string | undefined} parser - If the user has specified a parser to
|
|
44
45
|
* use for this file in a custom schema, this will be passed to
|
|
45
46
|
* `parseInputFile` in the `parser` param.
|
|
46
|
-
* @returns {Document | undefined} Parsed file contents
|
|
47
|
+
* @returns {Document | Document[] | undefined} Parsed file contents
|
|
47
48
|
*/
|
|
48
49
|
// eslint-disable-next-line no-unused-vars
|
|
49
50
|
parseInputFile(contents, fileLocation, parser) {
|
|
@@ -205,6 +206,12 @@ async function loadAllPlugins(userPlugins) {
|
|
|
205
206
|
* This means relative paths in the current directory will be prefixed with
|
|
206
207
|
* `./` (or `.\` on Windows) even if this was not present in the input
|
|
207
208
|
* filename or pattern.
|
|
209
|
+
* @property {number | null} documentIndex - Some file formats allow multiple
|
|
210
|
+
* documents to be embedded in one file (e.g:
|
|
211
|
+
* [yaml](https://www.yaml.info/learn/document.html)). In these cases,
|
|
212
|
+
* `documentIndex` identifies is used to identify the sub document within the
|
|
213
|
+
* file. `documentIndex` will be `null` when there is a one-to-one
|
|
214
|
+
* relationship between file and document.
|
|
208
215
|
* @property {string | null} schemaLocation - Location of the schema used to
|
|
209
216
|
* validate this file if one could be found. `null` if no schema was found.
|
|
210
217
|
* @property {boolean | null} valid - Result of the validation (true/false) if a
|
package/src/test-helpers.js
CHANGED
|
@@ -4,6 +4,7 @@ import logger from "./logger.js";
|
|
|
4
4
|
const origWriteOut = logger.writeOut;
|
|
5
5
|
const origWriteErr = logger.writeErr;
|
|
6
6
|
const testCacheName = process.env.V8R_CACHE_NAME;
|
|
7
|
+
const env = process.env;
|
|
7
8
|
|
|
8
9
|
function setUp() {
|
|
9
10
|
flatCache.clearCacheById(testCacheName);
|
|
@@ -11,6 +12,7 @@ function setUp() {
|
|
|
11
12
|
logger.resetStderr();
|
|
12
13
|
logger.writeOut = function () {};
|
|
13
14
|
logger.writeErr = function () {};
|
|
15
|
+
process.env = { ...env };
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function tearDown() {
|
|
@@ -19,6 +21,7 @@ function tearDown() {
|
|
|
19
21
|
logger.resetStderr();
|
|
20
22
|
logger.writeOut = origWriteOut;
|
|
21
23
|
logger.writeErr = origWriteErr;
|
|
24
|
+
process.env = env;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function isString(el) {
|