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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v8r",
3
- "version": "4.0.1",
3
+ "version": "4.2.0",
4
4
  "description": "A command-line JSON, YAML and TOML validator that's on your wavelength",
5
5
  "scripts": {
6
6
  "test": "V8R_CACHE_NAME=v8r-test c8 --reporter=text mocha \"src/**/*.spec.js\"",
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 preProcessConfig(configFile) {
21
- if (!configFile?.config?.customCatalog?.schemas) {
22
- return;
23
- }
24
- for (const schema of configFile.config.customCatalog.schemas) {
25
- if (!path.isAbsolute(schema.location) && !isUrl(schema.location)) {
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
@@ -1,5 +1,5 @@
1
+ import path from "node:path";
1
2
  import { minimatch } from "minimatch";
2
- import path from "path";
3
3
  import { validate } from "./ajv.js";
4
4
  import { getFromUrlOrFile } from "./io.js";
5
5
  import logger from "./logger.js";
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 validateFile(filename, config, plugins, cache) {
37
- logger.info(`Processing ${filename}`);
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: filename,
40
- schemaLocation: null,
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
- const schemaLocation = config.schema || catalogMatch.location;
51
- result.schemaLocation = schemaLocation;
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
- const data = parseFile(
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
- const strictMode = config.verbose >= 2 ? "log" : false;
65
- const resolver = isUrl(schemaLocation)
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
- const { valid, errors } = await validate(
70
- data,
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.code = valid ? EXIT.VALID : EXIT.INVALID;
85
- return result;
86
- } catch (e) {
87
- logger.error(`${e.message}\n`);
88
- result.code = EXIT.ERROR;
89
- return result;
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 result = await validateFile(filename, config, plugins, cache);
126
- results.push(result);
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
 
@@ -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
@@ -1,5 +1,5 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
3
  import isUrl from "is-url";
4
4
  import { parseSchema } from "./parser.js";
5
5
 
@@ -1,13 +1,20 @@
1
1
  import Ajv from "ajv";
2
2
 
3
- function formatErrors(filename, errors) {
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: filename + "#",
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 result = plugin.parseInputFile(contents, filename, parser);
8
- if (result != null) {
9
- if (!(result instanceof Document)) {
10
- throw new Error(
11
- `Plugin ${plugin.constructor.name} returned an unexpected type from parseInputFile hook. Expected Document, got ${typeof result}`,
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 result.document;
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(fileLocation, result.errors);
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 new Document(yaml.load(contents));
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 new Document(yaml.load(contents));
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
@@ -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) {