v8r 0.11.0 → 0.13.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 +12 -0
- package/README.md +82 -1
- package/config-schema.json +92 -0
- package/package.json +7 -4
- package/src/ajv.js +5 -10
- package/src/cache.js +5 -5
- package/src/catalogs.js +119 -0
- package/src/cli.js +75 -175
- package/src/config.js +177 -0
- package/src/glob.js +2 -2
- package/src/index.js +2 -2
- package/src/io.js +10 -0
- package/src/logger.js +64 -0
- package/src/output-formatters.js +19 -0
- package/src/logging.js +0 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 📦 [0.13.0](https://www.npmjs.com/package/v8r/v/0.13.0) - 2022-06-11
|
|
4
|
+
|
|
5
|
+
* Overhaul of CLI output/machine-readable output. Validation results are sent to stdout. Log messages are now sent to stderr only. Pass `--format [text|json] (default: text)` to specify what is sent to stdout.
|
|
6
|
+
|
|
7
|
+
## 📦 [0.12.0](https://www.npmjs.com/package/v8r/v/0.12.0) - 2022-05-07
|
|
8
|
+
|
|
9
|
+
* Add config file. See https://github.com/chris48s/v8r/blob/main/README.md#configuration for more details.
|
|
10
|
+
|
|
11
|
+
## 📦 [0.11.1](https://www.npmjs.com/package/v8r/v/0.11.1) - 2022-03-03
|
|
12
|
+
|
|
13
|
+
* Fix: call minimatch with `{dot: true}`, fixes [#174](https://github.com/chris48s/v8r/issues/174)
|
|
14
|
+
|
|
3
15
|
## 📦 [0.11.0](https://www.npmjs.com/package/v8r/v/0.11.0) - 2022-02-27
|
|
4
16
|
|
|
5
17
|
* Drop compatibility with node 12, now requires node `^14.13.1 || >=15.0.0`
|
package/README.md
CHANGED
|
@@ -77,6 +77,83 @@ $ v8r feature.geojson -c my-catalog.json
|
|
|
77
77
|
|
|
78
78
|
This can be used to specify different custom schemas for multiple file patterns.
|
|
79
79
|
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
v8r uses CosmiConfig to search for a configuration. This means you can specify your configuration in any of the following places:
|
|
83
|
+
|
|
84
|
+
- `package.json`
|
|
85
|
+
- `.v8rrc`
|
|
86
|
+
- `.v8rrc.json`
|
|
87
|
+
- `.v8rrc.yaml`
|
|
88
|
+
- `.v8rrc.yml`
|
|
89
|
+
- `.v8rrc.js`
|
|
90
|
+
- `.v8rrc.cjs`
|
|
91
|
+
- `v8r.config.js`
|
|
92
|
+
- `v8r.config.cjs`
|
|
93
|
+
|
|
94
|
+
v8r only searches for a config file in the current working directory.
|
|
95
|
+
|
|
96
|
+
Example yaml config file:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
# - One or more filenames or glob patterns describing local file or files to validate
|
|
100
|
+
# - overridden by passing one or more positional arguments
|
|
101
|
+
patterns: ['*json']
|
|
102
|
+
|
|
103
|
+
# - Level of verbose logging. 0 is standard, higher numbers are more verbose
|
|
104
|
+
# - overridden by passing --verbose / -v
|
|
105
|
+
# - default = 0
|
|
106
|
+
verbose: 2
|
|
107
|
+
|
|
108
|
+
# - Exit with code 0 even if an error was encountered. True means a non-zero exit
|
|
109
|
+
# code is only issued if validation could be completed successfully and one or
|
|
110
|
+
# more files were invalid
|
|
111
|
+
# - overridden by passing --ignore-errors
|
|
112
|
+
# - default = false
|
|
113
|
+
ignoreErrors: true
|
|
114
|
+
|
|
115
|
+
# - Remove cached HTTP responses older than cacheTtl seconds old.
|
|
116
|
+
# Specifying 0 clears and disables cache completely
|
|
117
|
+
# - overridden by passing --cache-ttl
|
|
118
|
+
# - default = 600
|
|
119
|
+
cacheTtl: 86400
|
|
120
|
+
|
|
121
|
+
# - Output format for validation results
|
|
122
|
+
# - overridden by passing --format
|
|
123
|
+
# - default = text
|
|
124
|
+
format: "json"
|
|
125
|
+
|
|
126
|
+
# - A custom schema catalog.
|
|
127
|
+
# This catalog will be searched ahead of any custom catalogs passed using
|
|
128
|
+
# --catalogs or SchemaStore.org
|
|
129
|
+
# The format of this is subtly different to the format of a catalog
|
|
130
|
+
# passed via --catalogs (which matches the SchemaStore.org format)
|
|
131
|
+
customCatalog:
|
|
132
|
+
schemas:
|
|
133
|
+
- name: Custom Schema # The name of the schema (required)
|
|
134
|
+
description: Custom Schema # A description of the schema (optional)
|
|
135
|
+
|
|
136
|
+
# A Minimatch glob expression for matching up file names with a schema (required)
|
|
137
|
+
fileMatch: ["*.geojson"]
|
|
138
|
+
|
|
139
|
+
# A URL or local file path for the schema location (required)
|
|
140
|
+
# Unlike the SchemaStore.org format, which has a `url` key,
|
|
141
|
+
# custom catalogs defined in v8r config files have a `location` key
|
|
142
|
+
# which can refer to either a URL or local file.
|
|
143
|
+
# Relative paths are interpreted as relative to the config file location.
|
|
144
|
+
location: foo/bar/geojson-schema.json
|
|
145
|
+
|
|
146
|
+
# A custom parser to use for files matching fileMatch
|
|
147
|
+
# instead of trying to infer the correct parser from the filename (optional)
|
|
148
|
+
# This property is specific to custom catalogs defined in v8r config files
|
|
149
|
+
parser: json5
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The config file format is specified more formally in a JSON Schema:
|
|
153
|
+
|
|
154
|
+
- [machine-readable JSON](config-schema.json)
|
|
155
|
+
- [human-readable HTML](https://json-schema-viewer.vercel.app/view?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchris48s%2Fv8r%2Fmain%2Fconfig-schema.json&show_breadcrumbs=on&template_name=flat)
|
|
156
|
+
|
|
80
157
|
## Exit codes
|
|
81
158
|
|
|
82
159
|
* v8r always exits with code `0` when:
|
|
@@ -91,6 +168,10 @@ This can be used to specify different custom schemas for multiple file patterns.
|
|
|
91
168
|
|
|
92
169
|
This behaviour can be modified using the `--ignore-errors` flag. When invoked with `--ignore-errors` v8r will exit with code `0` even if one of these errors was encountered while attempting validation. A non-zero exit code will only be issued if validation could be completed successfully and the file was invalid.
|
|
93
170
|
|
|
171
|
+
* v8r always exits with code `97` when:
|
|
172
|
+
* There was an error loading a config file
|
|
173
|
+
* A config file was loaded but failed validation
|
|
174
|
+
|
|
94
175
|
* v8r always exits with code `98` when:
|
|
95
176
|
* An input glob pattern was invalid
|
|
96
177
|
* An input glob pattern was valid but did not match any files
|
|
@@ -127,4 +208,4 @@ This can be used to specify different custom schemas for multiple file patterns.
|
|
|
127
208
|
|
|
128
209
|
### ❓ Can `v8r` validate against a local schema?
|
|
129
210
|
|
|
130
|
-
💡 Yes. The `--schema` flag can be either a path to a local file or a URL.
|
|
211
|
+
💡 Yes. The `--schema` flag can be either a path to a local file or a URL. You can also use a [config file](#configuration) to include local schemas in a custom catalog.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "JSON schema for v8r config files",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"cacheTtl": {
|
|
8
|
+
"description": "Remove cached HTTP responses older than cacheTtl seconds old. Specifying 0 clears and disables cache completely",
|
|
9
|
+
"type": "integer",
|
|
10
|
+
"minimum": 0
|
|
11
|
+
},
|
|
12
|
+
"customCatalog": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"description": "A custom schema catalog. This catalog will be searched ahead of any custom catalogs passed using --catalogs or SchemaStore.org",
|
|
15
|
+
"required": [
|
|
16
|
+
"schemas"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schemas": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"description": "A list of JSON schema references.",
|
|
22
|
+
"items": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"required": [
|
|
25
|
+
"name",
|
|
26
|
+
"fileMatch",
|
|
27
|
+
"location"
|
|
28
|
+
],
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"description": {
|
|
32
|
+
"description": "A description of the schema",
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"fileMatch": {
|
|
36
|
+
"description": "A Minimatch glob expression for matching up file names with a schema.",
|
|
37
|
+
"uniqueItems": true,
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"location": {
|
|
44
|
+
"description": "A URL or local file path for the schema location",
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"name": {
|
|
48
|
+
"description": "The name of the schema",
|
|
49
|
+
"type": "string"
|
|
50
|
+
},
|
|
51
|
+
"parser": {
|
|
52
|
+
"description": "A custom parser to use for files matching fileMatch instead of trying to infer the correct parser from the filename",
|
|
53
|
+
"type": "string",
|
|
54
|
+
"enum": [
|
|
55
|
+
"json",
|
|
56
|
+
"yaml",
|
|
57
|
+
"json5"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"format": {
|
|
66
|
+
"description": "Output format for validation results",
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": [
|
|
69
|
+
"text",
|
|
70
|
+
"json"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"ignoreErrors": {
|
|
74
|
+
"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",
|
|
75
|
+
"type": "boolean"
|
|
76
|
+
},
|
|
77
|
+
"patterns": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"description": "One or more filenames or glob patterns describing local file or files to validate",
|
|
80
|
+
"minItems": 1,
|
|
81
|
+
"uniqueItems": true,
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"verbose": {
|
|
87
|
+
"description": "Level of verbose logging. 0 is standard, higher numbers are more verbose",
|
|
88
|
+
"type": "integer",
|
|
89
|
+
"minimum": 0
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "v8r",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "A command-line JSON and YAML validator that's on your wavelength",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "V8R_CACHE_NAME=v8r-test c8 --reporter=text mocha \"src/**/*.spec.js\"",
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
"ajv-draft-04": "^1.0.0",
|
|
28
28
|
"ajv-formats": "^2.1.1",
|
|
29
29
|
"chalk": "^5.0.0",
|
|
30
|
+
"cosmiconfig": "^7.0.1",
|
|
31
|
+
"decamelize": "^6.0.0",
|
|
30
32
|
"flat-cache": "^3.0.4",
|
|
31
|
-
"glob": "^
|
|
33
|
+
"glob": "^8.0.1",
|
|
32
34
|
"got": "^12.0.1",
|
|
33
35
|
"is-url": "^1.2.4",
|
|
34
36
|
"js-yaml": "^4.0.0",
|
|
@@ -44,12 +46,13 @@
|
|
|
44
46
|
"eslint-config-prettier": "^8.1.0",
|
|
45
47
|
"eslint-plugin-mocha": "^10.0.3",
|
|
46
48
|
"eslint-plugin-prettier": "^4.0.0",
|
|
47
|
-
"mocha": "^
|
|
49
|
+
"mocha": "^10.0.0",
|
|
50
|
+
"mock-cwd": "^1.0.0",
|
|
48
51
|
"nock": "^13.0.4",
|
|
49
52
|
"prettier": "^2.1.2"
|
|
50
53
|
},
|
|
51
54
|
"engines": {
|
|
52
|
-
"node": "
|
|
55
|
+
"node": ">=14.13.1"
|
|
53
56
|
},
|
|
54
57
|
"type": "module",
|
|
55
58
|
"keywords": [
|
package/src/ajv.js
CHANGED
|
@@ -9,9 +9,9 @@ import Ajv2019 from "ajv/dist/2019.js";
|
|
|
9
9
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
10
10
|
import addFormats from "ajv-formats";
|
|
11
11
|
|
|
12
|
-
function _ajvFactory(schema, cache) {
|
|
12
|
+
function _ajvFactory(schema, strictMode, cache) {
|
|
13
13
|
const resolver = (url) => cache.fetch(url);
|
|
14
|
-
const opts = { allErrors: true, loadSchema: resolver, strict:
|
|
14
|
+
const opts = { allErrors: true, loadSchema: resolver, strict: strictMode };
|
|
15
15
|
|
|
16
16
|
if (
|
|
17
17
|
typeof schema["$schema"] === "string" ||
|
|
@@ -53,17 +53,12 @@ function _ajvFactory(schema, cache) {
|
|
|
53
53
|
*/
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async function validate(data, schema, cache) {
|
|
57
|
-
const ajv = _ajvFactory(schema, cache);
|
|
56
|
+
async function validate(data, schema, strictMode, cache) {
|
|
57
|
+
const ajv = _ajvFactory(schema, strictMode, cache);
|
|
58
58
|
addFormats(ajv);
|
|
59
59
|
const validateFn = await ajv.compileAsync(schema);
|
|
60
60
|
const valid = validateFn(data);
|
|
61
|
-
|
|
62
|
-
console.log("\nErrors:");
|
|
63
|
-
console.log(validateFn.errors);
|
|
64
|
-
console.log("");
|
|
65
|
-
}
|
|
66
|
-
return valid;
|
|
61
|
+
return { valid, errors: validateFn.errors ? validateFn.errors : [] };
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
export { _ajvFactory, validate };
|
package/src/cache.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import got from "got";
|
|
2
|
-
import
|
|
2
|
+
import logger from "./logger.js";
|
|
3
3
|
|
|
4
4
|
class Cache {
|
|
5
5
|
constructor(flatCache, ttl) {
|
|
@@ -13,10 +13,10 @@ class Cache {
|
|
|
13
13
|
Object.entries(this.cache.all()).forEach(
|
|
14
14
|
function ([url, cachedResponse]) {
|
|
15
15
|
if (!("timestamp" in cachedResponse) || !("body" in cachedResponse)) {
|
|
16
|
-
|
|
16
|
+
logger.debug(`Cache error: deleting malformed response`);
|
|
17
17
|
this.cache.removeKey(url);
|
|
18
18
|
} else if (Date.now() > cachedResponse.timestamp + this.ttl) {
|
|
19
|
-
|
|
19
|
+
logger.debug(`Cache stale: deleting cached response from ${url}`);
|
|
20
20
|
this.cache.removeKey(url);
|
|
21
21
|
}
|
|
22
22
|
this.cache.save(true);
|
|
@@ -53,12 +53,12 @@ class Cache {
|
|
|
53
53
|
this.expire();
|
|
54
54
|
const cachedResponse = this.cache.getKey(url);
|
|
55
55
|
if (cachedResponse !== undefined) {
|
|
56
|
-
|
|
56
|
+
logger.debug(`Cache hit: using cached response from ${url}`);
|
|
57
57
|
return cachedResponse.body;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
try {
|
|
61
|
-
|
|
61
|
+
logger.debug(`Cache miss: calling ${url}`);
|
|
62
62
|
const resp = await got(url);
|
|
63
63
|
const parsedBody = JSON.parse(resp.body);
|
|
64
64
|
if (this.ttl > 0) {
|
package/src/catalogs.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import minimatch from "minimatch";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { validate } from "./ajv.js";
|
|
4
|
+
import { getFromUrlOrFile } from "./io.js";
|
|
5
|
+
import logger from "./logger.js";
|
|
6
|
+
|
|
7
|
+
const SCHEMASTORE_CATALOG_URL =
|
|
8
|
+
"https://www.schemastore.org/api/json/catalog.json";
|
|
9
|
+
const SCHEMASTORE_CATALOG_SCHEMA_URL =
|
|
10
|
+
"https://json.schemastore.org/schema-catalog.json";
|
|
11
|
+
|
|
12
|
+
function coerceMatch(inMatch) {
|
|
13
|
+
const outMatch = {};
|
|
14
|
+
outMatch.location = inMatch.url || inMatch.location;
|
|
15
|
+
for (const [key, value] of Object.entries(inMatch)) {
|
|
16
|
+
if (!["location", "url"].includes(key)) {
|
|
17
|
+
outMatch[key] = value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return outMatch;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCatalogs(config) {
|
|
24
|
+
let catalogs = [];
|
|
25
|
+
if (config.customCatalog) {
|
|
26
|
+
catalogs.push({
|
|
27
|
+
location: config.configFileRelativePath,
|
|
28
|
+
catalog: config.customCatalog,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (config.catalogs) {
|
|
32
|
+
catalogs = catalogs.concat(
|
|
33
|
+
config.catalogs.map(function (loc) {
|
|
34
|
+
return { location: loc };
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
catalogs.push({ location: SCHEMASTORE_CATALOG_URL });
|
|
39
|
+
return catalogs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function getMatchForFilename(catalogs, filename, cache) {
|
|
43
|
+
for (const [i, rec] of catalogs.entries()) {
|
|
44
|
+
const catalogLocation = rec.location;
|
|
45
|
+
const catalog =
|
|
46
|
+
rec.catalog || (await getFromUrlOrFile(catalogLocation, cache));
|
|
47
|
+
|
|
48
|
+
if (!rec.catalog) {
|
|
49
|
+
const catalogSchema = await getFromUrlOrFile(
|
|
50
|
+
SCHEMASTORE_CATALOG_SCHEMA_URL,
|
|
51
|
+
cache
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Validate the catalog
|
|
55
|
+
const strictMode = false;
|
|
56
|
+
const { valid } = await validate(
|
|
57
|
+
catalog,
|
|
58
|
+
catalogSchema,
|
|
59
|
+
strictMode,
|
|
60
|
+
cache
|
|
61
|
+
);
|
|
62
|
+
if (!valid || catalog.schemas === undefined) {
|
|
63
|
+
throw new Error(`Malformed catalog at ${catalogLocation}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { schemas } = catalog;
|
|
68
|
+
const matches = getSchemaMatchesForFilename(schemas, filename);
|
|
69
|
+
logger.debug(`Searching for schema in ${catalogLocation} ...`);
|
|
70
|
+
if (matches.length === 1) {
|
|
71
|
+
logger.info(`Found schema in ${catalogLocation} ...`);
|
|
72
|
+
return coerceMatch(matches[0]); // Match found. We're done.
|
|
73
|
+
}
|
|
74
|
+
if (matches.length === 0 && i < catalogs.length - 1) {
|
|
75
|
+
continue; // No match found. Try the next catalog in the array.
|
|
76
|
+
}
|
|
77
|
+
if (matches.length > 1) {
|
|
78
|
+
// We found >1 matches in the same catalog. This is always a hard error.
|
|
79
|
+
const matchesLog = matches
|
|
80
|
+
.map(function (match) {
|
|
81
|
+
let outStr = "";
|
|
82
|
+
outStr += ` ${match.name}\n`;
|
|
83
|
+
if (match.description) {
|
|
84
|
+
outStr += ` ${match.description}\n`;
|
|
85
|
+
}
|
|
86
|
+
outStr += ` ${match.url || match.location}\n`;
|
|
87
|
+
return outStr;
|
|
88
|
+
})
|
|
89
|
+
.join("\n");
|
|
90
|
+
logger.info(
|
|
91
|
+
`Found multiple possible schemas for ${filename}. Possible matches:\n${matchesLog}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
// Either we found >1 matches in the same catalog or we found 0 matches
|
|
95
|
+
// in the last catalog and there are no more catalogs left to try.
|
|
96
|
+
throw new Error(`Could not find a schema to validate ${filename}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getSchemaMatchesForFilename(schemas, filename) {
|
|
101
|
+
const matches = [];
|
|
102
|
+
schemas.forEach(function (schema) {
|
|
103
|
+
if ("fileMatch" in schema) {
|
|
104
|
+
if (schema.fileMatch.includes(path.basename(filename))) {
|
|
105
|
+
matches.push(schema);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const glob of schema.fileMatch) {
|
|
109
|
+
if (minimatch(path.normalize(filename), glob, { dot: true })) {
|
|
110
|
+
matches.push(schema);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return matches;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { getCatalogs, getMatchForFilename, getSchemaMatchesForFilename };
|
package/src/cli.js
CHANGED
|
@@ -1,97 +1,27 @@
|
|
|
1
1
|
import flatCache from "flat-cache";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import isUrl from "is-url";
|
|
4
|
-
import minimatch from "minimatch";
|
|
5
|
-
import { createRequire } from "module";
|
|
6
3
|
import os from "os";
|
|
7
4
|
import path from "path";
|
|
8
|
-
import yargs from "yargs";
|
|
9
|
-
import { hideBin } from "yargs/helpers";
|
|
10
5
|
import { validate } from "./ajv.js";
|
|
11
6
|
import { Cache } from "./cache.js";
|
|
7
|
+
import { getCatalogs, getMatchForFilename } from "./catalogs.js";
|
|
8
|
+
import { getConfig } from "./config.js";
|
|
12
9
|
import { getFiles } from "./glob.js";
|
|
13
|
-
import
|
|
10
|
+
import { getFromUrlOrFile } from "./io.js";
|
|
11
|
+
import logger from "./logger.js";
|
|
12
|
+
import { logErrors, resultsToJson } from "./output-formatters.js";
|
|
14
13
|
import { parseFile } from "./parser.js";
|
|
15
14
|
|
|
16
|
-
const SCHEMASTORE_CATALOG_URL =
|
|
17
|
-
"https://www.schemastore.org/api/json/catalog.json";
|
|
18
|
-
|
|
19
|
-
const SCHEMASTORE_CATALOG_SCHEMA_URL =
|
|
20
|
-
"https://json.schemastore.org/schema-catalog.json";
|
|
21
|
-
|
|
22
15
|
const EXIT = {
|
|
23
16
|
VALID: 0,
|
|
24
17
|
ERROR: 1,
|
|
25
|
-
|
|
18
|
+
INVALID_CONFIG: 97,
|
|
19
|
+
NOT_FOUND: 98,
|
|
26
20
|
INVALID: 99,
|
|
27
21
|
};
|
|
28
22
|
|
|
29
23
|
const CACHE_DIR = path.join(os.tmpdir(), "flat-cache");
|
|
30
24
|
|
|
31
|
-
async function getFromUrlOrFile(location, cache) {
|
|
32
|
-
return isUrl(location)
|
|
33
|
-
? await cache.fetch(location)
|
|
34
|
-
: JSON.parse(await fs.promises.readFile(location, "utf8"));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function getSchemaUrlForFilename(catalogs, filename, cache) {
|
|
38
|
-
for (const [i, catalogLocation] of catalogs.entries()) {
|
|
39
|
-
const catalog = await getFromUrlOrFile(catalogLocation, cache);
|
|
40
|
-
const catalogSchema = await getFromUrlOrFile(
|
|
41
|
-
SCHEMASTORE_CATALOG_SCHEMA_URL,
|
|
42
|
-
cache
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// Validate the catalog
|
|
46
|
-
const valid = await validate(catalog, catalogSchema, cache);
|
|
47
|
-
if (!valid || catalog.schemas === undefined) {
|
|
48
|
-
throw new Error(`Malformed catalog at ${catalogLocation}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const { schemas } = catalog;
|
|
52
|
-
const matches = getSchemaMatchesForFilename(schemas, filename);
|
|
53
|
-
logging.debug(`Searching for schema in ${catalogLocation} ...`);
|
|
54
|
-
if (matches.length === 1) {
|
|
55
|
-
logging.info(`Found schema in ${catalogLocation} ...`);
|
|
56
|
-
return matches[0].url; // Match found. We're done.
|
|
57
|
-
}
|
|
58
|
-
if (matches.length === 0 && i < catalogs.length - 1) {
|
|
59
|
-
continue; // No match found. Try the next catalog in the array.
|
|
60
|
-
}
|
|
61
|
-
if (matches.length > 1) {
|
|
62
|
-
// We found >1 matches in the same catalog. This is always a hard error.
|
|
63
|
-
const matchesLog = matches
|
|
64
|
-
.map((match) => ` ${match.description}: ${match.url}`)
|
|
65
|
-
.join("\n");
|
|
66
|
-
logging.info(
|
|
67
|
-
`Found multiple possible schemas for ${filename}. Possible matches:\n${matchesLog}`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
// Either we found >1 matches in the same catalog or we found 0 matches
|
|
71
|
-
// in the last catalog and there are no more catalogs left to try.
|
|
72
|
-
throw new Error(`Could not find a schema to validate ${filename}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getSchemaMatchesForFilename(schemas, filename) {
|
|
77
|
-
const matches = [];
|
|
78
|
-
schemas.forEach(function (schema) {
|
|
79
|
-
if ("fileMatch" in schema) {
|
|
80
|
-
if (schema.fileMatch.includes(path.basename(filename))) {
|
|
81
|
-
matches.push(schema);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
for (const glob of schema.fileMatch) {
|
|
85
|
-
if (minimatch(path.normalize(filename), glob)) {
|
|
86
|
-
matches.push(schema);
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
return matches;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
25
|
function secondsToMilliseconds(seconds) {
|
|
96
26
|
return seconds * 1000;
|
|
97
27
|
}
|
|
@@ -103,45 +33,53 @@ function getFlatCache() {
|
|
|
103
33
|
return flatCache.load("v8r", CACHE_DIR);
|
|
104
34
|
}
|
|
105
35
|
|
|
106
|
-
async function validateFile(filename,
|
|
107
|
-
|
|
36
|
+
async function validateFile(filename, config, cache) {
|
|
37
|
+
logger.info(`Processing ${filename}`);
|
|
38
|
+
let result = {
|
|
39
|
+
fileLocation: filename,
|
|
40
|
+
schemaLocation: null,
|
|
41
|
+
valid: null,
|
|
42
|
+
errors: [],
|
|
43
|
+
code: null,
|
|
44
|
+
};
|
|
108
45
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
args.schema ||
|
|
116
|
-
(await getSchemaUrlForFilename(
|
|
117
|
-
(args.catalogs || []).concat([SCHEMASTORE_CATALOG_URL]),
|
|
118
|
-
filename,
|
|
119
|
-
cache
|
|
120
|
-
));
|
|
46
|
+
const catalogs = getCatalogs(config);
|
|
47
|
+
const catalogMatch = config.schema
|
|
48
|
+
? {}
|
|
49
|
+
: await getMatchForFilename(catalogs, filename, cache);
|
|
50
|
+
const schemaLocation = config.schema || catalogMatch.location;
|
|
51
|
+
result.schemaLocation = schemaLocation;
|
|
121
52
|
const schema = await getFromUrlOrFile(schemaLocation, cache);
|
|
122
|
-
|
|
53
|
+
logger.info(
|
|
123
54
|
`Validating ${filename} against schema from ${schemaLocation} ...`
|
|
124
55
|
);
|
|
125
56
|
|
|
126
|
-
const
|
|
57
|
+
const data = parseFile(
|
|
58
|
+
await fs.promises.readFile(filename, "utf8"),
|
|
59
|
+
catalogMatch.parser ? `.${catalogMatch.parser}` : path.extname(filename)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const strictMode = config.verbose >= 2 ? "log" : false;
|
|
63
|
+
const { valid, errors } = await validate(data, schema, strictMode, cache);
|
|
64
|
+
result.valid = valid;
|
|
65
|
+
result.errors = errors;
|
|
127
66
|
if (valid) {
|
|
128
|
-
|
|
67
|
+
logger.success(`${filename} is valid\n`);
|
|
129
68
|
} else {
|
|
130
|
-
|
|
69
|
+
logger.error(`${filename} is invalid\n`);
|
|
131
70
|
}
|
|
132
71
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
return EXIT.INVALID;
|
|
72
|
+
result.code = valid ? EXIT.VALID : EXIT.INVALID;
|
|
73
|
+
return result;
|
|
137
74
|
} catch (e) {
|
|
138
|
-
|
|
139
|
-
|
|
75
|
+
logger.error(`${e.message}\n`);
|
|
76
|
+
result.code = EXIT.ERROR;
|
|
77
|
+
return result;
|
|
140
78
|
}
|
|
141
79
|
}
|
|
142
80
|
|
|
143
|
-
function
|
|
144
|
-
const codes = Object.values(results);
|
|
81
|
+
function resultsToStatusCode(results, ignoreErrors) {
|
|
82
|
+
const codes = Object.values(results).map((result) => result.code);
|
|
145
83
|
if (codes.includes(EXIT.INVALID)) {
|
|
146
84
|
return EXIT.INVALID;
|
|
147
85
|
}
|
|
@@ -152,98 +90,60 @@ function mergeResults(results, ignoreErrors) {
|
|
|
152
90
|
}
|
|
153
91
|
|
|
154
92
|
function Validator() {
|
|
155
|
-
return async function (
|
|
93
|
+
return async function (config) {
|
|
156
94
|
let filenames = [];
|
|
157
|
-
for (const pattern of
|
|
95
|
+
for (const pattern of config.patterns) {
|
|
158
96
|
const matches = await getFiles(pattern);
|
|
159
97
|
if (matches.length === 0) {
|
|
160
|
-
|
|
161
|
-
return EXIT.
|
|
98
|
+
logger.error(`Pattern '${pattern}' did not match any files`);
|
|
99
|
+
return EXIT.NOT_FOUND;
|
|
162
100
|
}
|
|
163
101
|
filenames = filenames.concat(matches);
|
|
164
102
|
}
|
|
165
103
|
|
|
166
|
-
const ttl = secondsToMilliseconds(
|
|
104
|
+
const ttl = secondsToMilliseconds(config.cacheTtl || 0);
|
|
167
105
|
const cache = new Cache(getFlatCache(), ttl);
|
|
168
106
|
|
|
169
107
|
const results = Object.fromEntries(filenames.map((key) => [key, null]));
|
|
170
108
|
for (const [filename] of Object.entries(results)) {
|
|
171
|
-
results[filename] = await validateFile(filename,
|
|
109
|
+
results[filename] = await validateFile(filename, config, cache);
|
|
110
|
+
|
|
111
|
+
if (results[filename].valid === false && config.format === "text") {
|
|
112
|
+
logErrors(filename, results[filename].errors);
|
|
113
|
+
}
|
|
114
|
+
// else: silence is golden
|
|
115
|
+
|
|
172
116
|
cache.resetCounters();
|
|
173
117
|
}
|
|
174
|
-
|
|
118
|
+
|
|
119
|
+
if (config.format === "json") {
|
|
120
|
+
resultsToJson(results);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return resultsToStatusCode(results, config.ignoreErrors);
|
|
175
124
|
};
|
|
176
125
|
}
|
|
177
126
|
|
|
178
|
-
async function cli(
|
|
179
|
-
|
|
127
|
+
async function cli(config) {
|
|
128
|
+
if (!config) {
|
|
129
|
+
try {
|
|
130
|
+
config = await getConfig(process.argv);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
logger.error(e.message);
|
|
133
|
+
return EXIT.INVALID_CONFIG;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logger.setVerbosity(config.verbose);
|
|
138
|
+
logger.debug(`Merged args/config: ${JSON.stringify(config, null, 2)}`);
|
|
139
|
+
|
|
180
140
|
try {
|
|
181
141
|
const validate = new Validator();
|
|
182
|
-
return await validate(
|
|
142
|
+
return await validate(config);
|
|
183
143
|
} catch (e) {
|
|
184
|
-
|
|
144
|
+
logger.error(e.message);
|
|
185
145
|
return EXIT.ERROR;
|
|
186
|
-
} finally {
|
|
187
|
-
logging.cleanup();
|
|
188
146
|
}
|
|
189
147
|
}
|
|
190
148
|
|
|
191
|
-
|
|
192
|
-
return yargs(hideBin(argv))
|
|
193
|
-
.command(
|
|
194
|
-
"$0 <patterns..>",
|
|
195
|
-
"Validate local json/yaml files against schema(s)",
|
|
196
|
-
(yargs) => {
|
|
197
|
-
yargs.positional("patterns", {
|
|
198
|
-
describe:
|
|
199
|
-
"One or more filenames or glob patterns describing local file or files to validate",
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
)
|
|
203
|
-
.version(
|
|
204
|
-
// Workaround for https://github.com/yargs/yargs/issues/1934
|
|
205
|
-
// TODO: remove once fixed
|
|
206
|
-
createRequire(import.meta.url)("../package.json").version
|
|
207
|
-
)
|
|
208
|
-
.option("verbose", {
|
|
209
|
-
alias: "v",
|
|
210
|
-
type: "boolean",
|
|
211
|
-
description: "Run with verbose logging. Can be stacked e.g: -vv -vvv",
|
|
212
|
-
})
|
|
213
|
-
.count("verbose")
|
|
214
|
-
.option("schema", {
|
|
215
|
-
alias: "s",
|
|
216
|
-
type: "string",
|
|
217
|
-
describe:
|
|
218
|
-
"Local path or URL of a schema to validate against. " +
|
|
219
|
-
"If not supplied, we will attempt to find an appropriate schema on " +
|
|
220
|
-
"schemastore.org using the filename. If passed with glob pattern(s) " +
|
|
221
|
-
"matching multiple files, all matching files will be validated " +
|
|
222
|
-
"against this schema",
|
|
223
|
-
})
|
|
224
|
-
.option("catalogs", {
|
|
225
|
-
type: "string",
|
|
226
|
-
alias: "c",
|
|
227
|
-
array: true,
|
|
228
|
-
describe:
|
|
229
|
-
"Local path or URL of custom catalogs to use prior to schemastore.org",
|
|
230
|
-
})
|
|
231
|
-
.conflicts("schema", "catalogs")
|
|
232
|
-
.option("ignore-errors", {
|
|
233
|
-
type: "boolean",
|
|
234
|
-
default: false,
|
|
235
|
-
describe:
|
|
236
|
-
"Exit with code 0 even if an error was encountered. Passing this flag " +
|
|
237
|
-
"means a non-zero exit code is only issued if validation could be " +
|
|
238
|
-
"completed successfully and one or more files were invalid",
|
|
239
|
-
})
|
|
240
|
-
.option("cache-ttl", {
|
|
241
|
-
type: "number",
|
|
242
|
-
default: 600,
|
|
243
|
-
describe:
|
|
244
|
-
"Remove cached HTTP responses older than <cache-ttl> seconds old. " +
|
|
245
|
-
"Passing 0 clears and disables cache completely",
|
|
246
|
-
}).argv;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export { cli, parseArgs };
|
|
149
|
+
export { cli };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
// TODO: once JSON modules is stable these requires could become imports
|
|
3
|
+
// https://nodejs.org/api/esm.html#esm_experimental_json_modules
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
import Ajv2019 from "ajv/dist/2019.js";
|
|
7
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
8
|
+
import decamelize from "decamelize";
|
|
9
|
+
import isUrl from "is-url";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import yargs from "yargs";
|
|
12
|
+
import { hideBin } from "yargs/helpers";
|
|
13
|
+
import logger from "./logger.js";
|
|
14
|
+
import { logErrors } from "./output-formatters.js";
|
|
15
|
+
|
|
16
|
+
function validateConfig(configFile) {
|
|
17
|
+
const ajv = new Ajv2019({ allErrors: true, strict: false });
|
|
18
|
+
const schema = require("../config-schema.json");
|
|
19
|
+
const validateFn = ajv.compile(schema);
|
|
20
|
+
const valid = validateFn(configFile.config);
|
|
21
|
+
if (!valid) {
|
|
22
|
+
logErrors(
|
|
23
|
+
configFile.filepath ? configFile.filepath : "",
|
|
24
|
+
validateFn.errors
|
|
25
|
+
);
|
|
26
|
+
throw new Error("Malformed config file");
|
|
27
|
+
}
|
|
28
|
+
return valid;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function preProcessConfig(configFile) {
|
|
32
|
+
if (!configFile?.config?.customCatalog?.schemas) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const schema of configFile.config.customCatalog.schemas) {
|
|
36
|
+
if (!path.isAbsolute(schema.location) && !isUrl(schema.location)) {
|
|
37
|
+
schema.location = path.join(
|
|
38
|
+
path.dirname(configFile.filepath),
|
|
39
|
+
schema.location
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getCosmiConfig(cosmiconfigOptions) {
|
|
46
|
+
cosmiconfigOptions.stopDir = process.cwd();
|
|
47
|
+
const configFile = (await cosmiconfig("v8r", cosmiconfigOptions).search(
|
|
48
|
+
process.cwd()
|
|
49
|
+
)) || { config: {} };
|
|
50
|
+
if (configFile.filepath) {
|
|
51
|
+
logger.info(`Loaded config file from ${getRelativeFilePath(configFile)}`);
|
|
52
|
+
} else {
|
|
53
|
+
logger.info(`No config file found`);
|
|
54
|
+
}
|
|
55
|
+
validateConfig(configFile);
|
|
56
|
+
preProcessConfig(configFile);
|
|
57
|
+
return configFile;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mergeConfigs(args, config) {
|
|
61
|
+
const mergedConfig = { ...args };
|
|
62
|
+
mergedConfig.cacheName = config?.config?.cacheName;
|
|
63
|
+
mergedConfig.customCatalog = config?.config?.customCatalog;
|
|
64
|
+
mergedConfig.configFileRelativePath = undefined;
|
|
65
|
+
if (config.filepath) {
|
|
66
|
+
mergedConfig.configFileRelativePath = getRelativeFilePath(config);
|
|
67
|
+
}
|
|
68
|
+
return mergedConfig;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getRelativeFilePath(config) {
|
|
72
|
+
return path.relative(process.cwd(), config.filepath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseArgs(argv, config) {
|
|
76
|
+
const parser = yargs(hideBin(argv));
|
|
77
|
+
|
|
78
|
+
let command = "$0 <patterns..>";
|
|
79
|
+
const patternsOpts = {
|
|
80
|
+
describe:
|
|
81
|
+
"One or more filenames or glob patterns describing local file or files to validate",
|
|
82
|
+
};
|
|
83
|
+
if (Object.keys(config.config).includes("patterns")) {
|
|
84
|
+
command = "$0 [patterns..]";
|
|
85
|
+
patternsOpts.default = config.config.patterns;
|
|
86
|
+
patternsOpts.defaultDescription = `${JSON.stringify(
|
|
87
|
+
config.config.patterns
|
|
88
|
+
)} (from config file ${getRelativeFilePath(config)})`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
parser
|
|
92
|
+
.command(
|
|
93
|
+
command,
|
|
94
|
+
"Validate local json/yaml files against schema(s)",
|
|
95
|
+
(yargs) => {
|
|
96
|
+
yargs.positional("patterns", patternsOpts);
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
.version(
|
|
100
|
+
// Workaround for https://github.com/yargs/yargs/issues/1934
|
|
101
|
+
// TODO: remove once fixed
|
|
102
|
+
require("../package.json").version
|
|
103
|
+
)
|
|
104
|
+
.option("verbose", {
|
|
105
|
+
alias: "v",
|
|
106
|
+
type: "boolean",
|
|
107
|
+
description: "Run with verbose logging. Can be stacked e.g: -vv -vvv",
|
|
108
|
+
})
|
|
109
|
+
.count("verbose")
|
|
110
|
+
.option("schema", {
|
|
111
|
+
alias: "s",
|
|
112
|
+
type: "string",
|
|
113
|
+
describe:
|
|
114
|
+
"Local path or URL of a schema to validate against. " +
|
|
115
|
+
"If not supplied, we will attempt to find an appropriate schema on " +
|
|
116
|
+
"schemastore.org using the filename. If passed with glob pattern(s) " +
|
|
117
|
+
"matching multiple files, all matching files will be validated " +
|
|
118
|
+
"against this schema",
|
|
119
|
+
})
|
|
120
|
+
.option("catalogs", {
|
|
121
|
+
type: "string",
|
|
122
|
+
alias: "c",
|
|
123
|
+
array: true,
|
|
124
|
+
describe:
|
|
125
|
+
"Local path or URL of custom catalogs to use prior to schemastore.org",
|
|
126
|
+
})
|
|
127
|
+
.conflicts("schema", "catalogs")
|
|
128
|
+
.option("ignore-errors", {
|
|
129
|
+
type: "boolean",
|
|
130
|
+
default: false,
|
|
131
|
+
describe:
|
|
132
|
+
"Exit with code 0 even if an error was encountered. Passing this flag " +
|
|
133
|
+
"means a non-zero exit code is only issued if validation could be " +
|
|
134
|
+
"completed successfully and one or more files were invalid",
|
|
135
|
+
})
|
|
136
|
+
.option("cache-ttl", {
|
|
137
|
+
type: "number",
|
|
138
|
+
default: 600,
|
|
139
|
+
describe:
|
|
140
|
+
"Remove cached HTTP responses older than <cache-ttl> seconds old. " +
|
|
141
|
+
"Passing 0 clears and disables cache completely",
|
|
142
|
+
})
|
|
143
|
+
.option("format", {
|
|
144
|
+
type: "string",
|
|
145
|
+
choices: ["text", "json"],
|
|
146
|
+
default: "text",
|
|
147
|
+
describe: "Output format for validation results",
|
|
148
|
+
})
|
|
149
|
+
.example([
|
|
150
|
+
["$0 file.json", "Validate a single file"],
|
|
151
|
+
["$0 file1.json file2.json", "Validate multiple files"],
|
|
152
|
+
[
|
|
153
|
+
"$0 'dir/*.yml' 'dir/*.yaml'",
|
|
154
|
+
"Specify files to validate with glob patterns",
|
|
155
|
+
],
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
for (const [key, value] of Object.entries(config.config)) {
|
|
159
|
+
if (["cacheTtl", "format", "ignoreErrors", "verbose"].includes(key)) {
|
|
160
|
+
parser.default(
|
|
161
|
+
decamelize(key, { separator: "-" }),
|
|
162
|
+
value,
|
|
163
|
+
`${value} (from config file ${getRelativeFilePath(config)})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return parser.argv;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function getConfig(argv, cosmiconfigOptions = {}) {
|
|
172
|
+
const config = await getCosmiConfig(cosmiconfigOptions);
|
|
173
|
+
const args = parseArgs(argv, config);
|
|
174
|
+
return mergeConfigs(args, config);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { getConfig, parseArgs, preProcessConfig, validateConfig };
|
package/src/glob.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import glob from "glob";
|
|
2
|
-
import
|
|
2
|
+
import logger from "./logger.js";
|
|
3
3
|
|
|
4
4
|
const globPromise = function (pattern, options) {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
@@ -13,7 +13,7 @@ async function getFiles(pattern) {
|
|
|
13
13
|
try {
|
|
14
14
|
return await globPromise(pattern, { dot: true });
|
|
15
15
|
} catch (e) {
|
|
16
|
-
|
|
16
|
+
logger.error(e.message);
|
|
17
17
|
return [];
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/index.js
CHANGED
package/src/io.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import isUrl from "is-url";
|
|
3
|
+
|
|
4
|
+
async function getFromUrlOrFile(location, cache) {
|
|
5
|
+
return isUrl(location)
|
|
6
|
+
? await cache.fetch(location)
|
|
7
|
+
: JSON.parse(await fs.promises.readFile(location, "utf8"));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { getFromUrlOrFile };
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
class Logger {
|
|
4
|
+
constructor(verbosity = 0) {
|
|
5
|
+
this.stderr = [];
|
|
6
|
+
this.stdout = [];
|
|
7
|
+
this.verbosity = verbosity;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
setVerbosity(verbosity) {
|
|
11
|
+
this.verbosity = verbosity;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
writeOut(message) {
|
|
15
|
+
process.stdout.write(message + "\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
writeErr(message) {
|
|
19
|
+
process.stderr.write(message + "\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resetStderr() {
|
|
23
|
+
this.stderr = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
resetStdout() {
|
|
27
|
+
this.stdout = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
log(message) {
|
|
31
|
+
this.stdout.push(message);
|
|
32
|
+
this.writeOut(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
info(message) {
|
|
36
|
+
const formatedMessage = chalk.blue.bold("ℹ ") + message;
|
|
37
|
+
this.stderr.push(formatedMessage);
|
|
38
|
+
this.writeErr(formatedMessage);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
debug(message) {
|
|
42
|
+
const formatedMessage = chalk.blue.bold("ℹ ") + message;
|
|
43
|
+
this.stderr.push(formatedMessage);
|
|
44
|
+
if (this.verbosity === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.writeErr(formatedMessage);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
error(message) {
|
|
51
|
+
const formatedMessage = chalk.red.bold("✖ ") + message;
|
|
52
|
+
this.stderr.push(formatedMessage);
|
|
53
|
+
this.writeErr(formatedMessage);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
success(message) {
|
|
57
|
+
const formatedMessage = chalk.green.bold("✔ ") + message;
|
|
58
|
+
this.stderr.push(formatedMessage);
|
|
59
|
+
this.writeErr(formatedMessage);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const logger = new Logger();
|
|
64
|
+
export default logger;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import logger from "./logger.js";
|
|
3
|
+
|
|
4
|
+
function logErrors(filename, errors) {
|
|
5
|
+
const ajv = new Ajv();
|
|
6
|
+
logger.log(
|
|
7
|
+
ajv.errorsText(errors, {
|
|
8
|
+
separator: "\n",
|
|
9
|
+
dataVar: filename + "#",
|
|
10
|
+
})
|
|
11
|
+
);
|
|
12
|
+
logger.log("");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resultsToJson(results) {
|
|
16
|
+
logger.log(JSON.stringify({ results }, null, 2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { logErrors, resultsToJson };
|
package/src/logging.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
|
|
3
|
-
const origWarn = console.warn;
|
|
4
|
-
const origInfo = console.info;
|
|
5
|
-
const origDebug = console.debug;
|
|
6
|
-
|
|
7
|
-
function init(verbosity) {
|
|
8
|
-
if (verbosity === 0) {
|
|
9
|
-
console.warn = function () {};
|
|
10
|
-
console.info = function () {};
|
|
11
|
-
console.debug = function () {};
|
|
12
|
-
}
|
|
13
|
-
// TODO: implement multiple log levels if/when you need them
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function cleanup() {
|
|
17
|
-
console.warn = origWarn;
|
|
18
|
-
console.info = origInfo;
|
|
19
|
-
console.debug = origDebug;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function info(message) {
|
|
23
|
-
console.log(chalk.blue.bold("ℹ ") + message);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function debug(message) {
|
|
27
|
-
console.debug(chalk.blue.bold("ℹ ") + message);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function error(message) {
|
|
31
|
-
console.error(chalk.red.bold("✖ ") + message);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function success(message) {
|
|
35
|
-
console.log(chalk.green.bold("✔ ") + message);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const logging = { cleanup, init, info, debug, error, success };
|
|
39
|
-
export default logging;
|