v8r 4.2.0 → 4.3.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.3.0](https://www.npmjs.com/package/v8r/v/4.3.0) - 2025-04-21
4
+
5
+ * Add ignore patern files. v8r now looks for ignore patterns in `.v8rignore` by default.
6
+ More info: https://chris48s.github.io/v8r/ignoring-files/
7
+ * Include the prop name in `additionalProperty` log message.
8
+ * Allow config file to contain `$schema` key.
9
+ * Fix: Clear the cache on init if TTL is 0.
10
+
11
+ ## 📦 [4.2.1](https://www.npmjs.com/package/v8r/v/4.2.1) - 2024-12-14
12
+
13
+ * Upgrade to flat-cache 6.
14
+ This release revamps how cache is stored and invalidated internally
15
+ but should have no user-visible impact
16
+
3
17
  ## 📦 [4.2.0](https://www.npmjs.com/package/v8r/v/4.2.0) - 2024-10-24
4
18
 
5
19
  * Add `V8R_CONFIG_FILE` environment variable.
@@ -65,6 +65,14 @@
65
65
  "description": "Exit with code 0 even if an error was encountered. True means a non-zero exit code is only issued if validation could be completed successfully and one or more files were invalid",
66
66
  "type": "boolean"
67
67
  },
68
+ "ignorePatternFiles": {
69
+ "description": "A list of files containing glob patterns to ignore",
70
+ "uniqueItems": true,
71
+ "type": "array",
72
+ "items": {
73
+ "type": "string"
74
+ }
75
+ },
68
76
  "patterns": {
69
77
  "type": "array",
70
78
  "description": "One or more filenames or glob patterns describing local file or files to validate",
@@ -87,6 +95,9 @@
87
95
  "type": "string",
88
96
  "pattern": "^(package:|file:)"
89
97
  }
98
+ },
99
+ "$schema": {
100
+ "type": "string"
90
101
  }
91
102
  }
92
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v8r",
3
- "version": "4.2.0",
3
+ "version": "4.3.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\"",
@@ -33,10 +33,11 @@
33
33
  "chalk": "^5.0.0",
34
34
  "cosmiconfig": "^9.0.0",
35
35
  "decamelize": "^6.0.0",
36
- "flat-cache": "^5.0.0",
36
+ "flat-cache": "^6.1.4",
37
37
  "glob": "^10.1.0",
38
38
  "global-agent": "^3.0.0",
39
39
  "got": "^13.0.0",
40
+ "ignore": "^7.0.0",
40
41
  "is-url": "^1.2.4",
41
42
  "js-yaml": "^4.0.0",
42
43
  "json5": "^2.2.0",
@@ -47,13 +48,13 @@
47
48
  "devDependencies": {
48
49
  "c8": "^10.1.2",
49
50
  "eslint": "^9.9.0",
50
- "eslint-config-prettier": "^9.0.0",
51
+ "eslint-config-prettier": "^10.1.2",
51
52
  "eslint-plugin-jsdoc": "^50.2.2",
52
53
  "eslint-plugin-mocha": "^10.0.3",
53
54
  "eslint-plugin-prettier": "^5.0.0",
54
- "mocha": "^10.7.3",
55
+ "mocha": "^11.0.1",
55
56
  "mock-cwd": "^1.0.0",
56
- "nock": "^13.0.4",
57
+ "nock": "^14.0.4",
57
58
  "prettier": "^3.0.0",
58
59
  "prettier-plugin-jsdoc": "^1.3.0"
59
60
  },
package/src/bootstrap.js CHANGED
@@ -76,13 +76,51 @@ function parseArgs(argv, config, documentFormats, outputFormats) {
76
76
  )} (from config file ${getRelativeFilePath(config)})`;
77
77
  }
78
78
 
79
+ const ignoreFilesOpts = {
80
+ describe: "A list of files containing glob patterns to ignore",
81
+ };
82
+ let ignoreFilesDefault = [".v8rignore"];
83
+ ignoreFilesOpts.defaultDescription = `${JSON.stringify(ignoreFilesDefault)}`;
84
+ if (Object.keys(config.config).includes("ignorePatternFiles")) {
85
+ ignoreFilesDefault = config.config.ignorePatternFiles;
86
+ ignoreFilesOpts.defaultDescription = `${JSON.stringify(
87
+ ignoreFilesDefault,
88
+ )} (from config file ${getRelativeFilePath(config)})`;
89
+ }
90
+
79
91
  parser
80
92
  .command(
93
+ // command
81
94
  command,
95
+
96
+ // description
82
97
  `Validate local ${documentFormats.join("/")} files against schema(s)`,
98
+
99
+ // builder
83
100
  (yargs) => {
84
101
  yargs.positional("patterns", patternsOpts);
85
102
  },
103
+
104
+ // handler
105
+ (args) => {
106
+ /*
107
+ Yargs doesn't allow .conflicts() with an argument that has a default
108
+ value (it considers the arg "set" even if we just use the default)
109
+ so we need to apply the default values here.
110
+ */
111
+ if (args.ignorePatternFiles === undefined) {
112
+ args.ignorePatternFiles = args["ignore-pattern-files"] =
113
+ ignoreFilesDefault;
114
+ }
115
+
116
+ if (args.ignore === false) {
117
+ args.ignorePatternFiles = args["ignore-pattern-files"] = [];
118
+ }
119
+
120
+ if (args.ignore === undefined) {
121
+ args.ignore = true;
122
+ }
123
+ },
86
124
  )
87
125
  .version(
88
126
  // Workaround for https://github.com/yargs/yargs/issues/1934
@@ -110,7 +148,7 @@ function parseArgs(argv, config, documentFormats, outputFormats) {
110
148
  alias: "c",
111
149
  array: true,
112
150
  describe:
113
- "Local path or URL of custom catalogs to use prior to schemastore.org",
151
+ "A list of local paths or URLs of custom catalogs to use prior to schemastore.org",
114
152
  })
115
153
  .conflicts("schema", "catalogs")
116
154
  .option("ignore-errors", {
@@ -121,6 +159,17 @@ function parseArgs(argv, config, documentFormats, outputFormats) {
121
159
  "means a non-zero exit code is only issued if validation could be " +
122
160
  "completed successfully and one or more files were invalid",
123
161
  })
162
+ .option("ignore-pattern-files", {
163
+ type: "string",
164
+ array: true,
165
+ describe: "A list of files containing glob patterns to ignore",
166
+ ...ignoreFilesOpts,
167
+ })
168
+ .option("no-ignore", {
169
+ type: "boolean",
170
+ describe: "Disable all ignore files",
171
+ })
172
+ .conflicts("ignore-pattern-files", "no-ignore")
124
173
  .option("cache-ttl", {
125
174
  type: "number",
126
175
  default: 600,
package/src/cache.js CHANGED
@@ -3,26 +3,14 @@ import logger from "./logger.js";
3
3
  import { parseSchema } from "./parser.js";
4
4
 
5
5
  class Cache {
6
- constructor(flatCache, ttl) {
6
+ constructor(flatCache) {
7
7
  this.cache = flatCache;
8
- this.ttl = ttl;
8
+ this.ttl = this.cache._cache.ttl || 0;
9
9
  this.callCounter = {};
10
10
  this.callLimit = 10;
11
- }
12
-
13
- expire() {
14
- Object.entries(this.cache.all()).forEach(
15
- function ([url, cachedResponse]) {
16
- if (!("timestamp" in cachedResponse) || !("body" in cachedResponse)) {
17
- logger.debug(`Cache error: deleting malformed response`);
18
- this.cache.removeKey(url);
19
- } else if (Date.now() > cachedResponse.timestamp + this.ttl) {
20
- logger.debug(`Cache stale: deleting cached response from ${url}`);
21
- this.cache.removeKey(url);
22
- }
23
- this.cache.save(true);
24
- }.bind(this),
25
- );
11
+ if (this.ttl === 0) {
12
+ this.cache.clear();
13
+ }
26
14
  }
27
15
 
28
16
  limitDepth(url) {
@@ -51,7 +39,6 @@ class Cache {
51
39
 
52
40
  async fetch(url) {
53
41
  this.limitDepth(url);
54
- this.expire();
55
42
  const cachedResponse = this.cache.getKey(url);
56
43
  if (cachedResponse !== undefined) {
57
44
  logger.debug(`Cache hit: using cached response from ${url}`);
@@ -63,7 +50,7 @@ class Cache {
63
50
  const resp = await got(url);
64
51
  const parsedBody = parseSchema(resp.body, url);
65
52
  if (this.ttl > 0) {
66
- this.cache.setKey(url, { timestamp: Date.now(), body: parsedBody });
53
+ this.cache.setKey(url, { body: parsedBody });
67
54
  this.cache.save(true);
68
55
  }
69
56
  return parsedBody;
package/src/cli.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import flatCache from "flat-cache";
4
+ import { FlatCache } from "flat-cache";
5
5
  import isUrl from "is-url";
6
6
  import { validate } from "./ajv.js";
7
7
  import { bootstrap } from "./bootstrap.js";
8
8
  import { Cache } from "./cache.js";
9
9
  import { getCatalogs, getMatchForFilename } from "./catalogs.js";
10
- import { getFiles } from "./glob.js";
10
+ import { getFiles, NotFound } from "./glob.js";
11
11
  import { getFromUrlOrFile } from "./io.js";
12
12
  import logger from "./logger.js";
13
13
  import { getDocumentLocation } from "./output-formatters.js";
@@ -27,11 +27,15 @@ function secondsToMilliseconds(seconds) {
27
27
  return seconds * 1000;
28
28
  }
29
29
 
30
- function getFlatCache() {
30
+ function getFlatCache(ttl) {
31
+ let cache;
31
32
  if (process.env.V8R_CACHE_NAME) {
32
- return flatCache.load(process.env.V8R_CACHE_NAME);
33
+ cache = new FlatCache({ cacheId: process.env.V8R_CACHE_NAME, ttl: ttl });
34
+ } else {
35
+ cache = new FlatCache({ cacheId: "v8rv2", cacheDir: CACHE_DIR, ttl: ttl });
33
36
  }
34
- return flatCache.load("v8r", CACHE_DIR);
37
+ cache.load();
38
+ return cache;
35
39
  }
36
40
 
37
41
  async function validateDocument(
@@ -166,21 +170,18 @@ function resultsToStatusCode(results, ignoreErrors) {
166
170
  function Validator() {
167
171
  return async function (config, plugins) {
168
172
  let filenames = [];
169
- for (const pattern of config.patterns) {
170
- const matches = await getFiles(pattern);
171
- if (matches.length === 0) {
172
- logger.error(`Pattern '${pattern}' did not match any files`);
173
+ try {
174
+ filenames = await getFiles(config.patterns, config.ignorePatternFiles);
175
+ } catch (e) {
176
+ if (e instanceof NotFound) {
177
+ logger.error(e.message);
173
178
  return EXIT.NOT_FOUND;
174
179
  }
175
- filenames = filenames.concat(matches);
180
+ throw e;
176
181
  }
177
182
 
178
- // de-dupe and sort
179
- filenames = [...new Set(filenames)];
180
- filenames.sort((a, b) => a.localeCompare(b));
181
-
182
183
  const ttl = secondsToMilliseconds(config.cacheTtl || 0);
183
- const cache = new Cache(getFlatCache(), ttl);
184
+ const cache = new Cache(getFlatCache(ttl));
184
185
 
185
186
  let results = [];
186
187
  for (const filename of filenames) {
package/src/glob.js CHANGED
@@ -1,13 +1,90 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { glob } from "glob";
4
+ import ignore from "ignore";
2
5
  import logger from "./logger.js";
3
6
 
4
- async function getFiles(pattern) {
7
+ class NotFound extends Error {}
8
+
9
+ async function getMatches(pattern) {
5
10
  try {
6
- return await glob(pattern, { dot: true, dotRelative: true });
11
+ return await glob(pattern, { dot: true });
7
12
  } catch (e) {
8
13
  logger.error(e.message);
9
14
  return [];
10
15
  }
11
16
  }
12
17
 
13
- export { getFiles };
18
+ async function exists(path) {
19
+ try {
20
+ await fs.promises.access(path);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ async function filterIgnores(filenames, ignorePatterns) {
28
+ const ig = ignore();
29
+ for (const patterns of ignorePatterns) {
30
+ ig.add(patterns);
31
+ }
32
+ return ig.filter(filenames);
33
+ }
34
+
35
+ async function readIgnoreFiles(filenames) {
36
+ let content = [];
37
+ for (const filename of filenames) {
38
+ const abspath = path.join(process.cwd(), filename);
39
+ if (await exists(abspath)) {
40
+ content.push(await fs.promises.readFile(abspath, "utf8"));
41
+ }
42
+ }
43
+ return content;
44
+ }
45
+
46
+ function getSeparator() {
47
+ return process.platform == "win32" ? "\\" : "/";
48
+ }
49
+
50
+ async function getFiles(patterns, ignorePatternFiles) {
51
+ let filenames = [];
52
+
53
+ // find all the files matching input globs
54
+ for (const pattern of patterns) {
55
+ const matches = await getMatches(pattern);
56
+ if (matches.length === 0) {
57
+ throw new NotFound(`Pattern '${pattern}' did not match any files`);
58
+ }
59
+ filenames = filenames.concat(matches);
60
+ }
61
+
62
+ // de-dupe
63
+ filenames = [...new Set(filenames)];
64
+
65
+ // process ignores
66
+ const ignorePatterns = await readIgnoreFiles(ignorePatternFiles);
67
+ let filteredFilenames = await filterIgnores(filenames, ignorePatterns);
68
+
69
+ const diff = filenames.filter((x) => filteredFilenames.indexOf(x) < 0);
70
+ if (diff.length > 0) {
71
+ logger.debug(
72
+ `Ignoring file(s):\n ${diff.join("\n ")}\nbased on ignore patterns in\n ${ignorePatternFiles.join("\n ")}`,
73
+ );
74
+ }
75
+
76
+ // finally, sort
77
+ filteredFilenames.sort((a, b) => a.localeCompare(b));
78
+
79
+ if (filteredFilenames.length === 0) {
80
+ throw new NotFound(`Could not find any files to validate`);
81
+ }
82
+
83
+ const sep = getSeparator();
84
+ filteredFilenames = filteredFilenames.map((fn) =>
85
+ !fn.startsWith(".." + sep) ? "." + sep + fn : fn,
86
+ );
87
+ return filteredFilenames;
88
+ }
89
+
90
+ export { getFiles, NotFound };
@@ -9,8 +9,25 @@ function getDocumentLocation(result) {
9
9
 
10
10
  function formatErrors(location, errors) {
11
11
  const ajv = new Ajv();
12
+ let formattedErrors = [];
13
+
14
+ if (errors) {
15
+ formattedErrors = errors.map(function (error) {
16
+ if (
17
+ error.keyword === "additionalProperties" &&
18
+ typeof error.params.additionalProperty === "string"
19
+ ) {
20
+ return {
21
+ ...error,
22
+ message: `${error.message}, found additional property '${error.params.additionalProperty}'`,
23
+ };
24
+ }
25
+ return error;
26
+ });
27
+ }
28
+
12
29
  return (
13
- ajv.errorsText(errors, {
30
+ ajv.errorsText(formattedErrors, {
14
31
  separator: "\n",
15
32
  dataVar: location + "#",
16
33
  }) + "\n"
package/src/plugins.js CHANGED
@@ -37,10 +37,9 @@ class BasePlugin {
37
37
  *
38
38
  * @param {string} contents - The unparsed file content.
39
39
  * @param {string} fileLocation - The file path. Filenames are resolved and
40
- * normalised by [glob](https://www.npmjs.com/package/glob) using the
41
- * `dotRelative` option. This means relative paths in the current directory
42
- * will be prefixed with `./` (or `.\` on Windows) even if this was not
43
- * present in the input filename or pattern.
40
+ * normalised using dot-relative notation. This means relative paths in the
41
+ * current directory will be prefixed with `./` (or `.\` on Windows) even if
42
+ * this was not present in the input filename or pattern.
44
43
  * @param {string | undefined} parser - If the user has specified a parser to
45
44
  * use for this file in a custom schema, this will be passed to
46
45
  * `parseInputFile` in the `parser` param.
@@ -77,10 +76,9 @@ class BasePlugin {
77
76
  * @param {ValidationResult} result - Result of attempting to validate this
78
77
  * document.
79
78
  * @param {string} fileLocation - The document file path. Filenames are
80
- * resolved and normalised by [glob](https://www.npmjs.com/package/glob)
81
- * using the `dotRelative` option. This means relative paths in the current
82
- * directory will be prefixed with `./` (or `.\` on Windows) even if this
83
- * was not present in the input filename or pattern.
79
+ * resolved and normalised using dot-relative notation. This means relative
80
+ * paths in the current directory will be prefixed with `./` (or `.\` on
81
+ * Windows) even if this was not present in the input filename or pattern.
84
82
  * @param {string} format - The user's requested output format as specified in
85
83
  * the config file or via the `--format` command line argument.
86
84
  * @returns {string | undefined} Log message
@@ -201,11 +199,10 @@ async function loadAllPlugins(userPlugins) {
201
199
  /**
202
200
  * @typedef {object} ValidationResult
203
201
  * @property {string} fileLocation - Path of the document that was validated.
204
- * Filenames are resolved and normalised by
205
- * [glob](https://www.npmjs.com/package/glob) using the `dotRelative` option.
206
- * This means relative paths in the current directory will be prefixed with
207
- * `./` (or `.\` on Windows) even if this was not present in the input
208
- * filename or pattern.
202
+ * Filenames are resolved and normalised using dot-relative notation. This
203
+ * means relative paths in the current directory will be prefixed with `./`
204
+ * (or `.\` on Windows) even if this was not present in the input filename or
205
+ * pattern.
209
206
  * @property {number | null} documentIndex - Some file formats allow multiple
210
207
  * documents to be embedded in one file (e.g:
211
208
  * [yaml](https://www.yaml.info/learn/document.html)). In these cases,
@@ -1,4 +1,4 @@
1
- import flatCache from "flat-cache";
1
+ import { clearCacheById } from "flat-cache";
2
2
  import logger from "./logger.js";
3
3
 
4
4
  const origWriteOut = logger.writeOut;
@@ -7,7 +7,7 @@ const testCacheName = process.env.V8R_CACHE_NAME;
7
7
  const env = process.env;
8
8
 
9
9
  function setUp() {
10
- flatCache.clearCacheById(testCacheName);
10
+ clearCacheById(testCacheName);
11
11
  logger.resetStdout();
12
12
  logger.resetStderr();
13
13
  logger.writeOut = function () {};
@@ -16,7 +16,7 @@ function setUp() {
16
16
  }
17
17
 
18
18
  function tearDown() {
19
- flatCache.clearCacheById(testCacheName);
19
+ clearCacheById(testCacheName);
20
20
  logger.resetStdout();
21
21
  logger.resetStderr();
22
22
  logger.writeOut = origWriteOut;