v8r 3.1.0 → 4.0.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 +41 -2
- package/README.md +8 -228
- package/config-schema.json +13 -13
- package/package.json +12 -11
- package/src/{config.js → bootstrap.js} +72 -27
- package/src/cli.js +58 -25
- package/src/config-validators.js +54 -0
- package/src/glob.js +3 -1
- package/src/output-formatters.js +4 -10
- package/src/parser.js +19 -19
- package/src/plugins/output-json.js +17 -0
- package/src/plugins/output-text.js +18 -0
- package/src/plugins/parser-json.js +25 -0
- package/src/plugins/parser-json5.js +22 -0
- package/src/plugins/parser-toml.js +22 -0
- package/src/plugins/parser-yaml.js +22 -0
- package/src/plugins.js +223 -0
- package/src/public.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 📦 [4.0.0](https://www.npmjs.com/package/v8r/v/4.0.0) - 2024-08-19
|
|
4
|
+
|
|
5
|
+
* **Breaking:** Change to the JSON output format. The `results` key is now an array instead of an object.
|
|
6
|
+
In v8r <4, `results` was an object mapping filename to result object. For example:
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"results": {
|
|
10
|
+
"./package.json": {
|
|
11
|
+
"fileLocation": "./package.json",
|
|
12
|
+
"schemaLocation": "https://json.schemastore.org/package.json",
|
|
13
|
+
"valid": true,
|
|
14
|
+
"errors": [],
|
|
15
|
+
"code": 0
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
In v8r >=4 `results` is now an array of result objects. For example:
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"results": [
|
|
25
|
+
{
|
|
26
|
+
"fileLocation": "./package.json",
|
|
27
|
+
"schemaLocation": "https://json.schemastore.org/package.json",
|
|
28
|
+
"valid": true,
|
|
29
|
+
"errors": [],
|
|
30
|
+
"code": 0
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
* Plugin system: It is now possible to extend the functionality of v8r by using or writing plugins. See https://chris48s.github.io/v8r/category/plugins/ for further information
|
|
36
|
+
* Documentation improvements
|
|
37
|
+
|
|
38
|
+
## 📦 [3.1.1](https://www.npmjs.com/package/v8r/v/3.1.1) - 2024-08-03
|
|
39
|
+
|
|
40
|
+
* Allow 'toml' as an allowed value for parser in custom catalog
|
|
41
|
+
|
|
3
42
|
## 📦 [3.1.0](https://www.npmjs.com/package/v8r/v/3.1.0) - 2024-06-03
|
|
4
43
|
|
|
5
44
|
* Add ability to configure a proxy using global-agent
|
|
6
45
|
|
|
7
46
|
## 📦 [3.0.0](https://www.npmjs.com/package/v8r/v/3.0.0) - 2024-01-25
|
|
8
47
|
|
|
9
|
-
* Drop compatibility with node 16
|
|
48
|
+
* **Breaking:** Drop compatibility with node 16
|
|
10
49
|
* Add ability to validate Toml documents
|
|
11
50
|
|
|
12
51
|
## 📦 [2.1.0](https://www.npmjs.com/package/v8r/v/2.1.0) - 2023-10-23
|
|
@@ -15,7 +54,7 @@
|
|
|
15
54
|
|
|
16
55
|
## 📦 [2.0.0](https://www.npmjs.com/package/v8r/v/2.0.0) - 2023-05-02
|
|
17
56
|
|
|
18
|
-
* Drop compatibility with node 14
|
|
57
|
+
* **Breaking:** Drop compatibility with node 14
|
|
19
58
|
* Upgrade glob and minimatch to latest versions
|
|
20
59
|
* Tested on node 20
|
|
21
60
|
|
package/README.md
CHANGED
|
@@ -1,233 +1,13 @@
|
|
|
1
1
|
# v8r
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-

|
|
5
|
-

|
|
6
|
-

|
|
7
|
-

|
|
3
|
+
[](https://github.com/chris48s/v8r/actions/workflows/build.yml?query=branch%3Amain)
|
|
4
|
+
[](https://app.codecov.io/gh/chris48s/v8r)
|
|
5
|
+
[](https://www.npmjs.com/package/v8r)
|
|
6
|
+
[](https://www.npmjs.com/package/v8r)
|
|
7
|
+
[](https://www.npmjs.com/package/v8r)
|
|
8
8
|
|
|
9
|
-
v8r is a command-line
|
|
9
|
+
v8r is a command-line validator that uses [Schema Store](https://www.schemastore.org/) to detect a suitable schema for your input files based on the filename.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
📦 Install the package from [NPM](https://www.npmjs.com/package/v8r)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npx v8r@latest <filename>
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Local install:
|
|
19
|
-
```bash
|
|
20
|
-
npm install -g v8r
|
|
21
|
-
v8r <filename>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Usage Examples
|
|
25
|
-
|
|
26
|
-
### Validating files
|
|
27
|
-
|
|
28
|
-
v8r can validate JSON or YAML files. You can pass filenames or glob patterns:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
# single filename
|
|
32
|
-
$ v8r package.json
|
|
33
|
-
|
|
34
|
-
# multiple files
|
|
35
|
-
$ v8r file1.json file2.json
|
|
36
|
-
|
|
37
|
-
# glob patterns
|
|
38
|
-
$ v8r 'dir/*.yml' 'dir/*.yaml'
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
[DigitalOcean's Glob Tool](https://www.digitalocean.com/community/tools/glob) can be used to help construct glob patterns
|
|
42
|
-
|
|
43
|
-
### Manually specifying a schema
|
|
44
|
-
|
|
45
|
-
By default, v8r queries [Schema Store](https://www.schemastore.org/) to detect a suitable schema based on the filename.
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
# if v8r can't auto-detect a schema for your file..
|
|
49
|
-
$ v8r feature.geojson
|
|
50
|
-
✖ Could not find a schema to validate feature.geojson
|
|
51
|
-
|
|
52
|
-
# ..you can specify one using the --schema flag
|
|
53
|
-
$ v8r feature.geojson --schema https://json.schemastore.org/geojson
|
|
54
|
-
ℹ Validating feature.geojson against schema from https://json.schemastore.org/geojson ...
|
|
55
|
-
✔ feature.geojson is valid
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Using a custom catlog
|
|
59
|
-
|
|
60
|
-
Using the `--schema` flag will validate all files matched by the glob pattern against that schema. You can also define a custom [schema catalog](https://json.schemastore.org/schema-catalog.json). v8r will search any custom catalogs before falling back to [Schema Store](https://www.schemastore.org/).
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
$ cat > my-catalog.json <<EOF
|
|
64
|
-
{ "\$schema": "https://json.schemastore.org/schema-catalog.json",
|
|
65
|
-
"version": 1,
|
|
66
|
-
"schemas": [ { "name": "geojson",
|
|
67
|
-
"description": "geojson",
|
|
68
|
-
"url": "https://json.schemastore.org/geojson.json",
|
|
69
|
-
"fileMatch": ["*.geojson"] } ] }
|
|
70
|
-
EOF
|
|
71
|
-
|
|
72
|
-
$ v8r feature.geojson -c my-catalog.json
|
|
73
|
-
ℹ Found schema in my-catalog.json ...
|
|
74
|
-
ℹ Validating feature.geojson against schema from https://json.schemastore.org/geojson ...
|
|
75
|
-
✔ feature.geojson is valid
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
This can be used to specify different custom schemas for multiple file patterns.
|
|
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
|
-
|
|
157
|
-
## Configuring a Proxy
|
|
158
|
-
|
|
159
|
-
It is possible to configure a proxy via [global-agent](https://www.npmjs.com/package/global-agent) using the `GLOBAL_AGENT_HTTP_PROXY` environment variable:
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
export GLOBAL_AGENT_HTTP_PROXY=http://myproxy:8888
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Exit codes
|
|
166
|
-
|
|
167
|
-
* v8r always exits with code `0` when:
|
|
168
|
-
* The input glob pattern(s) matched one or more files, all input files were validated against a schema, and all input files were **valid**
|
|
169
|
-
* `v8r` was called with `--help` or `--version` flags
|
|
170
|
-
|
|
171
|
-
* By default v8r exits with code `1` when an error was encountered trying to validate one or more input files. For example:
|
|
172
|
-
* No suitable schema could be found
|
|
173
|
-
* An error was encountered during an HTTP request
|
|
174
|
-
* An input file was not JSON or yaml
|
|
175
|
-
* etc
|
|
176
|
-
|
|
177
|
-
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.
|
|
178
|
-
|
|
179
|
-
* v8r always exits with code `97` when:
|
|
180
|
-
* There was an error loading a config file
|
|
181
|
-
* A config file was loaded but failed validation
|
|
182
|
-
|
|
183
|
-
* v8r always exits with code `98` when:
|
|
184
|
-
* An input glob pattern was invalid
|
|
185
|
-
* An input glob pattern was valid but did not match any files
|
|
186
|
-
|
|
187
|
-
* v8r always exits with code `99` when:
|
|
188
|
-
* The input glob pattern matched one or more files, one or more input files were validated against a schema and the input file was **invalid**
|
|
189
|
-
|
|
190
|
-
## Versioning
|
|
191
|
-
|
|
192
|
-
v8r follows [semantic versioning](https://semver.org/). For this project, the "API" is defined as:
|
|
193
|
-
|
|
194
|
-
- CLI flags and options
|
|
195
|
-
- CLI exit codes
|
|
196
|
-
- The configuration file format
|
|
197
|
-
- The native JSON output format
|
|
198
|
-
|
|
199
|
-
A "breaking change" also includes:
|
|
200
|
-
|
|
201
|
-
- Dropping compatibility with a major Node JS version
|
|
202
|
-
- Dropping compatibility with a JSON Schema draft
|
|
203
|
-
|
|
204
|
-
## FAQ
|
|
205
|
-
|
|
206
|
-
### ❓ How does `v8r` decide what schema to validate against if I don't supply one?
|
|
207
|
-
|
|
208
|
-
💡 `v8r` queries the [Schema Store catalog](https://www.schemastore.org/) to try and find a suitable schema based on the name of the input file.
|
|
209
|
-
|
|
210
|
-
### ❓ My file is valid, but it doesn't validate against one of the suggested schemas.
|
|
211
|
-
|
|
212
|
-
💡 `v8r` is a fairly thin layer of glue between [Schema Store](https://www.schemastore.org/) (where the schemas come from) and [ajv](https://www.npmjs.com/package/ajv) (the validation engine). It is likely that this kind of problem is either an issue with the schema or validation engine.
|
|
213
|
-
|
|
214
|
-
* Schema store issue tracker: https://github.com/SchemaStore/schemastore/issues
|
|
215
|
-
* Ajv issue tracker: https://github.com/ajv-validator/ajv/issues
|
|
216
|
-
|
|
217
|
-
### ❓ What JSON schema versions are compatible?
|
|
218
|
-
|
|
219
|
-
💡 `v8r` works with JSON schema drafts:
|
|
220
|
-
|
|
221
|
-
* draft-04
|
|
222
|
-
* draft-06
|
|
223
|
-
* draft-07
|
|
224
|
-
* draft 2019-09
|
|
225
|
-
* draft 2020-12
|
|
226
|
-
|
|
227
|
-
### ❓ Will 100% of the schemas on schemastore.org work with this tool?
|
|
228
|
-
|
|
229
|
-
💡 No. There are some with [known issues](https://github.com/chris48s/v8r/issues/18)
|
|
230
|
-
|
|
231
|
-
### ❓ Can `v8r` validate against a local schema?
|
|
232
|
-
|
|
233
|
-
💡 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.
|
|
13
|
+
📚 Jump into the [Documentation](https://chris48s.github.io/v8r) to get started
|
package/config-schema.json
CHANGED
|
@@ -49,13 +49,8 @@
|
|
|
49
49
|
"type": "string"
|
|
50
50
|
},
|
|
51
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
|
-
]
|
|
52
|
+
"description": "A custom parser to use for files matching fileMatch instead of trying to infer the correct parser from the filename. 'json', 'json5', 'toml' and 'yaml' are always valid. Plugins may define additional values which are valid here.",
|
|
53
|
+
"type": "string"
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
}
|
|
@@ -63,12 +58,8 @@
|
|
|
63
58
|
}
|
|
64
59
|
},
|
|
65
60
|
"format": {
|
|
66
|
-
"description": "Output format for validation results",
|
|
67
|
-
"type": "string"
|
|
68
|
-
"enum": [
|
|
69
|
-
"text",
|
|
70
|
-
"json"
|
|
71
|
-
]
|
|
61
|
+
"description": "Output format for validation results. 'text' and 'json' are always valid. Plugins may define additional values which are valid here.",
|
|
62
|
+
"type": "string"
|
|
72
63
|
},
|
|
73
64
|
"ignoreErrors": {
|
|
74
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",
|
|
@@ -87,6 +78,15 @@
|
|
|
87
78
|
"description": "Level of verbose logging. 0 is standard, higher numbers are more verbose",
|
|
88
79
|
"type": "integer",
|
|
89
80
|
"minimum": 0
|
|
81
|
+
},
|
|
82
|
+
"plugins": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"description": "An array of strings describing v8r plugins to load",
|
|
85
|
+
"uniqueItems": true,
|
|
86
|
+
"items": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"pattern": "^(package:|file:)"
|
|
89
|
+
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "v8r",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A command-line JSON and
|
|
3
|
+
"version": "4.0.0",
|
|
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\"",
|
|
7
|
-
"lint": "eslint \"**/*.{js,cjs}\"",
|
|
7
|
+
"lint": "eslint \"**/*.{js,cjs,mjs}\"",
|
|
8
8
|
"coverage": "c8 report --reporter=cobertura",
|
|
9
|
-
"prettier": "prettier --write \"**/*.{js,cjs}\"",
|
|
10
|
-
"prettier:check": "prettier --check \"**/*.{js,cjs}\"",
|
|
9
|
+
"prettier": "prettier --write \"**/*.{js,cjs,mjs}\"",
|
|
10
|
+
"prettier:check": "prettier --check \"**/*.{js,cjs,mjs}\"",
|
|
11
11
|
"v8r": "src/index.js"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
14
|
"v8r": "src/index.js"
|
|
15
15
|
},
|
|
16
|
-
"
|
|
17
|
-
"exports": "./src/index.js",
|
|
16
|
+
"exports": "./src/public.js",
|
|
18
17
|
"files": [
|
|
19
18
|
"src/**/!(*.spec).js",
|
|
20
19
|
"config-schema.json",
|
|
@@ -46,15 +45,17 @@
|
|
|
46
45
|
"yargs": "^17.0.1"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"c8": "^
|
|
50
|
-
"eslint": "^9.
|
|
48
|
+
"c8": "^10.1.2",
|
|
49
|
+
"eslint": "^9.9.0",
|
|
51
50
|
"eslint-config-prettier": "^9.0.0",
|
|
51
|
+
"eslint-plugin-jsdoc": "^50.2.2",
|
|
52
52
|
"eslint-plugin-mocha": "^10.0.3",
|
|
53
53
|
"eslint-plugin-prettier": "^5.0.0",
|
|
54
|
-
"mocha": "^10.
|
|
54
|
+
"mocha": "^10.7.3",
|
|
55
55
|
"mock-cwd": "^1.0.0",
|
|
56
56
|
"nock": "^13.0.4",
|
|
57
|
-
"prettier": "^3.0.0"
|
|
57
|
+
"prettier": "^3.0.0",
|
|
58
|
+
"prettier-plugin-jsdoc": "^1.3.0"
|
|
58
59
|
},
|
|
59
60
|
"engines": {
|
|
60
61
|
"node": ">=18"
|
|
@@ -3,30 +3,19 @@ import { createRequire } from "module";
|
|
|
3
3
|
// https://nodejs.org/api/esm.html#esm_experimental_json_modules
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
|
|
6
|
-
import Ajv2019 from "ajv/dist/2019.js";
|
|
7
6
|
import { cosmiconfig } from "cosmiconfig";
|
|
8
7
|
import decamelize from "decamelize";
|
|
9
8
|
import isUrl from "is-url";
|
|
10
9
|
import path from "path";
|
|
11
10
|
import yargs from "yargs";
|
|
12
11
|
import { hideBin } from "yargs/helpers";
|
|
12
|
+
import {
|
|
13
|
+
validateConfigAgainstSchema,
|
|
14
|
+
validateConfigDocumentParsers,
|
|
15
|
+
validateConfigOutputFormats,
|
|
16
|
+
} from "./config-validators.js";
|
|
13
17
|
import logger from "./logger.js";
|
|
14
|
-
import {
|
|
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
|
-
}
|
|
18
|
+
import { loadAllPlugins, resolveUserPlugins } from "./plugins.js";
|
|
30
19
|
|
|
31
20
|
function preProcessConfig(configFile) {
|
|
32
21
|
if (!configFile?.config?.customCatalog?.schemas) {
|
|
@@ -52,8 +41,6 @@ async function getCosmiConfig(cosmiconfigOptions) {
|
|
|
52
41
|
} else {
|
|
53
42
|
logger.info(`No config file found`);
|
|
54
43
|
}
|
|
55
|
-
validateConfig(configFile);
|
|
56
|
-
preProcessConfig(configFile);
|
|
57
44
|
return configFile;
|
|
58
45
|
}
|
|
59
46
|
|
|
@@ -72,7 +59,7 @@ function getRelativeFilePath(config) {
|
|
|
72
59
|
return path.relative(process.cwd(), config.filepath);
|
|
73
60
|
}
|
|
74
61
|
|
|
75
|
-
function parseArgs(argv, config) {
|
|
62
|
+
function parseArgs(argv, config, documentFormats, outputFormats) {
|
|
76
63
|
const parser = yargs(hideBin(argv));
|
|
77
64
|
|
|
78
65
|
let command = "$0 <patterns..>";
|
|
@@ -91,7 +78,7 @@ function parseArgs(argv, config) {
|
|
|
91
78
|
parser
|
|
92
79
|
.command(
|
|
93
80
|
command,
|
|
94
|
-
|
|
81
|
+
`Validate local ${documentFormats.join("/")} files against schema(s)`,
|
|
95
82
|
(yargs) => {
|
|
96
83
|
yargs.positional("patterns", patternsOpts);
|
|
97
84
|
},
|
|
@@ -142,7 +129,7 @@ function parseArgs(argv, config) {
|
|
|
142
129
|
})
|
|
143
130
|
.option("format", {
|
|
144
131
|
type: "string",
|
|
145
|
-
choices:
|
|
132
|
+
choices: outputFormats,
|
|
146
133
|
default: "text",
|
|
147
134
|
describe: "Output format for validation results",
|
|
148
135
|
})
|
|
@@ -168,10 +155,68 @@ function parseArgs(argv, config) {
|
|
|
168
155
|
return parser.argv;
|
|
169
156
|
}
|
|
170
157
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
158
|
+
function getDocumentFormats(loadedPlugins) {
|
|
159
|
+
let documentFormats = [];
|
|
160
|
+
for (const plugin of loadedPlugins) {
|
|
161
|
+
documentFormats = documentFormats.concat(plugin.registerInputFileParsers());
|
|
162
|
+
}
|
|
163
|
+
return documentFormats;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getOutputFormats(loadedPlugins) {
|
|
167
|
+
let outputFormats = [];
|
|
168
|
+
for (const plugin of loadedPlugins) {
|
|
169
|
+
outputFormats = outputFormats.concat(plugin.registerOutputFormats());
|
|
170
|
+
}
|
|
171
|
+
return outputFormats;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function bootstrap(argv, config, cosmiconfigOptions = {}) {
|
|
175
|
+
if (config) {
|
|
176
|
+
// special case for unit testing purposes
|
|
177
|
+
// this allows us to inject an incomplete config and bypass the validation
|
|
178
|
+
const { allLoadedPlugins, loadedCorePlugins, loadedUserPlugins } =
|
|
179
|
+
await loadAllPlugins(config.plugins || []);
|
|
180
|
+
return {
|
|
181
|
+
config,
|
|
182
|
+
allLoadedPlugins,
|
|
183
|
+
loadedCorePlugins,
|
|
184
|
+
loadedUserPlugins,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// load the config file and validate it against the schema
|
|
189
|
+
const configFile = await getCosmiConfig(cosmiconfigOptions);
|
|
190
|
+
validateConfigAgainstSchema(configFile);
|
|
191
|
+
|
|
192
|
+
// load both core and user plugins
|
|
193
|
+
let plugins = resolveUserPlugins(configFile.config.plugins || []);
|
|
194
|
+
const { allLoadedPlugins, loadedCorePlugins, loadedUserPlugins } =
|
|
195
|
+
await loadAllPlugins(plugins);
|
|
196
|
+
const documentFormats = getDocumentFormats(allLoadedPlugins);
|
|
197
|
+
const outputFormats = getOutputFormats(allLoadedPlugins);
|
|
198
|
+
|
|
199
|
+
// now we have documentFormats and outputFormats
|
|
200
|
+
// we can finish validating and processing the config
|
|
201
|
+
validateConfigDocumentParsers(configFile, documentFormats);
|
|
202
|
+
validateConfigOutputFormats(configFile, outputFormats);
|
|
203
|
+
preProcessConfig(configFile);
|
|
204
|
+
|
|
205
|
+
// parse command line arguments
|
|
206
|
+
const args = parseArgs(argv, configFile, documentFormats, outputFormats);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
config: mergeConfigs(args, configFile),
|
|
210
|
+
allLoadedPlugins,
|
|
211
|
+
loadedCorePlugins,
|
|
212
|
+
loadedUserPlugins,
|
|
213
|
+
};
|
|
175
214
|
}
|
|
176
215
|
|
|
177
|
-
export {
|
|
216
|
+
export {
|
|
217
|
+
bootstrap,
|
|
218
|
+
getDocumentFormats,
|
|
219
|
+
getOutputFormats,
|
|
220
|
+
parseArgs,
|
|
221
|
+
preProcessConfig,
|
|
222
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -4,19 +4,18 @@ import os from "os";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import isUrl from "is-url";
|
|
6
6
|
import { validate } from "./ajv.js";
|
|
7
|
+
import { bootstrap } from "./bootstrap.js";
|
|
7
8
|
import { Cache } from "./cache.js";
|
|
8
9
|
import { getCatalogs, getMatchForFilename } from "./catalogs.js";
|
|
9
|
-
import { getConfig } from "./config.js";
|
|
10
10
|
import { getFiles } from "./glob.js";
|
|
11
11
|
import { getFromUrlOrFile } from "./io.js";
|
|
12
12
|
import logger from "./logger.js";
|
|
13
|
-
import {
|
|
14
|
-
import { parseDocument } from "./parser.js";
|
|
13
|
+
import { parseFile } from "./parser.js";
|
|
15
14
|
|
|
16
15
|
const EXIT = {
|
|
17
16
|
VALID: 0,
|
|
18
17
|
ERROR: 1,
|
|
19
|
-
|
|
18
|
+
INVALID_CONFIG_OR_PLUGIN: 97,
|
|
20
19
|
NOT_FOUND: 98,
|
|
21
20
|
INVALID: 99,
|
|
22
21
|
};
|
|
@@ -34,7 +33,7 @@ function getFlatCache() {
|
|
|
34
33
|
return flatCache.load("v8r", CACHE_DIR);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
async function validateFile(filename, config, cache) {
|
|
36
|
+
async function validateFile(filename, config, plugins, cache) {
|
|
38
37
|
logger.info(`Processing ${filename}`);
|
|
39
38
|
let result = {
|
|
40
39
|
fileLocation: filename,
|
|
@@ -55,9 +54,11 @@ async function validateFile(filename, config, cache) {
|
|
|
55
54
|
`Validating ${filename} against schema from ${schemaLocation} ...`,
|
|
56
55
|
);
|
|
57
56
|
|
|
58
|
-
const data =
|
|
57
|
+
const data = parseFile(
|
|
58
|
+
plugins,
|
|
59
59
|
await fs.promises.readFile(filename, "utf8"),
|
|
60
|
-
|
|
60
|
+
filename,
|
|
61
|
+
catalogMatch.parser,
|
|
61
62
|
);
|
|
62
63
|
|
|
63
64
|
const strictMode = config.verbose >= 2 ? "log" : false;
|
|
@@ -101,7 +102,7 @@ function resultsToStatusCode(results, ignoreErrors) {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function Validator() {
|
|
104
|
-
return async function (config) {
|
|
105
|
+
return async function (config, plugins) {
|
|
105
106
|
let filenames = [];
|
|
106
107
|
for (const pattern of config.patterns) {
|
|
107
108
|
const matches = await getFiles(pattern);
|
|
@@ -115,20 +116,32 @@ function Validator() {
|
|
|
115
116
|
const ttl = secondsToMilliseconds(config.cacheTtl || 0);
|
|
116
117
|
const cache = new Cache(getFlatCache(), ttl);
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
for (const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
let results = [];
|
|
120
|
+
for (const filename of filenames) {
|
|
121
|
+
const result = await validateFile(filename, config, plugins, cache);
|
|
122
|
+
results.push(result);
|
|
123
|
+
|
|
124
|
+
for (const plugin of plugins) {
|
|
125
|
+
const message = plugin.getSingleResultLogMessage(
|
|
126
|
+
result,
|
|
127
|
+
filename,
|
|
128
|
+
config.format,
|
|
129
|
+
);
|
|
130
|
+
if (message != null) {
|
|
131
|
+
logger.log(message);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
124
134
|
}
|
|
125
|
-
// else: silence is golden
|
|
126
135
|
|
|
127
136
|
cache.resetCounters();
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
for (const plugin of plugins) {
|
|
140
|
+
const message = plugin.getAllResultsLogMessage(results, config.format);
|
|
141
|
+
if (message != null) {
|
|
142
|
+
logger.log(message);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
return resultsToStatusCode(results, config.ignoreErrors);
|
|
@@ -136,21 +149,41 @@ function Validator() {
|
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
async function cli(config) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
let allLoadedPlugins, loadedCorePlugins, loadedUserPlugins;
|
|
153
|
+
try {
|
|
154
|
+
({ config, allLoadedPlugins, loadedCorePlugins, loadedUserPlugins } =
|
|
155
|
+
await bootstrap(process.argv, config));
|
|
156
|
+
} catch (e) {
|
|
157
|
+
logger.error(e.message);
|
|
158
|
+
return EXIT.INVALID_CONFIG_OR_PLUGIN;
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
logger.setVerbosity(config.verbose);
|
|
149
162
|
logger.debug(`Merged args/config: ${JSON.stringify(config, null, 2)}`);
|
|
150
163
|
|
|
164
|
+
/*
|
|
165
|
+
Note there is a bit of a chicken and egg problem here.
|
|
166
|
+
We have to load the plugins before we can load the config
|
|
167
|
+
but this logger.debug() needs to happen AFTER we call logger.setVerbosity().
|
|
168
|
+
*/
|
|
169
|
+
logger.debug(
|
|
170
|
+
`Loaded user plugins: ${JSON.stringify(
|
|
171
|
+
loadedUserPlugins.map((plugin) => plugin.constructor.name),
|
|
172
|
+
null,
|
|
173
|
+
2,
|
|
174
|
+
)}`,
|
|
175
|
+
);
|
|
176
|
+
logger.debug(
|
|
177
|
+
`Loaded core plugins: ${JSON.stringify(
|
|
178
|
+
loadedCorePlugins.map((plugin) => plugin.constructor.name),
|
|
179
|
+
null,
|
|
180
|
+
2,
|
|
181
|
+
)}`,
|
|
182
|
+
);
|
|
183
|
+
|
|
151
184
|
try {
|
|
152
185
|
const validate = new Validator();
|
|
153
|
-
return await validate(config);
|
|
186
|
+
return await validate(config, allLoadedPlugins);
|
|
154
187
|
} catch (e) {
|
|
155
188
|
logger.error(e.message);
|
|
156
189
|
return EXIT.ERROR;
|
|
@@ -0,0 +1,54 @@
|
|
|
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 logger from "./logger.js";
|
|
8
|
+
import { formatErrors } from "./output-formatters.js";
|
|
9
|
+
|
|
10
|
+
function validateConfigAgainstSchema(configFile) {
|
|
11
|
+
const ajv = new Ajv2019({ allErrors: true, strict: false });
|
|
12
|
+
const schema = require("../config-schema.json");
|
|
13
|
+
const validateFn = ajv.compile(schema);
|
|
14
|
+
const valid = validateFn(configFile.config);
|
|
15
|
+
if (!valid) {
|
|
16
|
+
logger.log(
|
|
17
|
+
formatErrors(
|
|
18
|
+
configFile.filepath ? configFile.filepath : "",
|
|
19
|
+
validateFn.errors,
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
throw new Error("Malformed config file");
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateConfigDocumentParsers(configFile, documentFormats) {
|
|
28
|
+
for (const schema of configFile.config?.customCatalog?.schemas || []) {
|
|
29
|
+
if (schema?.parser != null && !documentFormats.includes(schema?.parser)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Malformed config file: "${schema.parser}" not in ${JSON.stringify(documentFormats)}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateConfigOutputFormats(configFile, outputFormats) {
|
|
39
|
+
if (
|
|
40
|
+
configFile.config?.format != null &&
|
|
41
|
+
!outputFormats.includes(configFile.config?.format)
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Malformed config file: "${configFile.config.format}" not in ${JSON.stringify(outputFormats)}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
validateConfigAgainstSchema,
|
|
52
|
+
validateConfigDocumentParsers,
|
|
53
|
+
validateConfigOutputFormats,
|
|
54
|
+
};
|
package/src/glob.js
CHANGED
|
@@ -3,7 +3,9 @@ import logger from "./logger.js";
|
|
|
3
3
|
|
|
4
4
|
async function getFiles(pattern) {
|
|
5
5
|
try {
|
|
6
|
-
|
|
6
|
+
let matches = await glob(pattern, { dot: true, dotRelative: true });
|
|
7
|
+
matches.sort((a, b) => a.localeCompare(b));
|
|
8
|
+
return matches;
|
|
7
9
|
} catch (e) {
|
|
8
10
|
logger.error(e.message);
|
|
9
11
|
return [];
|
package/src/output-formatters.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import logger from "./logger.js";
|
|
3
2
|
|
|
4
|
-
function
|
|
3
|
+
function formatErrors(filename, errors) {
|
|
5
4
|
const ajv = new Ajv();
|
|
6
|
-
|
|
5
|
+
return (
|
|
7
6
|
ajv.errorsText(errors, {
|
|
8
7
|
separator: "\n",
|
|
9
8
|
dataVar: filename + "#",
|
|
10
|
-
})
|
|
9
|
+
}) + "\n"
|
|
11
10
|
);
|
|
12
|
-
logger.log("");
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
logger.log(JSON.stringify({ results }, null, 2));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { logErrors, resultsToJson };
|
|
13
|
+
export { formatErrors };
|
package/src/parser.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "path";
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
|
-
import {
|
|
3
|
+
import { Document } from "./plugins.js";
|
|
4
4
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return yaml.load(contents);
|
|
17
|
-
case ".toml":
|
|
18
|
-
return parse(contents);
|
|
19
|
-
default:
|
|
20
|
-
throw new Error(`Unsupported format ${format}`);
|
|
5
|
+
function parseFile(plugins, contents, filename, parser) {
|
|
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
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return result.document;
|
|
15
|
+
}
|
|
21
16
|
}
|
|
17
|
+
|
|
18
|
+
const errorMessage = parser
|
|
19
|
+
? `Unsupported format ${parser}`
|
|
20
|
+
: `Unsupported format ${path.extname(filename).slice(1)}`;
|
|
21
|
+
throw new Error(errorMessage);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function parseSchema(contents, location) {
|
|
@@ -33,4 +33,4 @@ function parseSchema(contents, location) {
|
|
|
33
33
|
return JSON.parse(contents);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export {
|
|
36
|
+
export { parseFile, parseSchema };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BasePlugin } from "../plugins.js";
|
|
2
|
+
|
|
3
|
+
class JsonOutput extends BasePlugin {
|
|
4
|
+
static name = "v8r-plugin-json-output";
|
|
5
|
+
|
|
6
|
+
registerOutputFormats() {
|
|
7
|
+
return ["json"];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getAllResultsLogMessage(results, format) {
|
|
11
|
+
if (format === "json") {
|
|
12
|
+
return JSON.stringify({ results }, null, 2);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default JsonOutput;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BasePlugin } from "../plugins.js";
|
|
2
|
+
import { formatErrors } from "../output-formatters.js";
|
|
3
|
+
|
|
4
|
+
class TextOutput extends BasePlugin {
|
|
5
|
+
static name = "v8r-plugin-text-output";
|
|
6
|
+
|
|
7
|
+
registerOutputFormats() {
|
|
8
|
+
return ["text"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getSingleResultLogMessage(result, fileLocation, format) {
|
|
12
|
+
if (result.valid === false && format === "text") {
|
|
13
|
+
return formatErrors(fileLocation, result.errors);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default TextOutput;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BasePlugin, Document } from "../plugins.js";
|
|
2
|
+
|
|
3
|
+
class JsonParser extends BasePlugin {
|
|
4
|
+
static name = "v8r-plugin-json-parser";
|
|
5
|
+
|
|
6
|
+
registerInputFileParsers() {
|
|
7
|
+
return ["json"];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
parseInputFile(contents, fileLocation, parser) {
|
|
11
|
+
if (parser === "json") {
|
|
12
|
+
return new Document(JSON.parse(contents));
|
|
13
|
+
} else if (parser == null) {
|
|
14
|
+
if (
|
|
15
|
+
fileLocation.endsWith(".json") ||
|
|
16
|
+
fileLocation.endsWith(".geojson") ||
|
|
17
|
+
fileLocation.endsWith(".jsonld")
|
|
18
|
+
) {
|
|
19
|
+
return new Document(JSON.parse(contents));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default JsonParser;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import JSON5 from "json5";
|
|
2
|
+
import { BasePlugin, Document } from "../plugins.js";
|
|
3
|
+
|
|
4
|
+
class Json5Parser extends BasePlugin {
|
|
5
|
+
static name = "v8r-plugin-json5-parser";
|
|
6
|
+
|
|
7
|
+
registerInputFileParsers() {
|
|
8
|
+
return ["json5"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
parseInputFile(contents, fileLocation, parser) {
|
|
12
|
+
if (parser === "json5") {
|
|
13
|
+
return new Document(JSON5.parse(contents));
|
|
14
|
+
} else if (parser == null) {
|
|
15
|
+
if (fileLocation.endsWith(".json5") || fileLocation.endsWith(".jsonc")) {
|
|
16
|
+
return new Document(JSON5.parse(contents));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default Json5Parser;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
import { BasePlugin, Document } from "../plugins.js";
|
|
3
|
+
|
|
4
|
+
class TomlParser extends BasePlugin {
|
|
5
|
+
static name = "v8r-plugin-toml-parser";
|
|
6
|
+
|
|
7
|
+
registerInputFileParsers() {
|
|
8
|
+
return ["toml"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
parseInputFile(contents, fileLocation, parser) {
|
|
12
|
+
if (parser === "toml") {
|
|
13
|
+
return new Document(parse(contents));
|
|
14
|
+
} else if (parser == null) {
|
|
15
|
+
if (fileLocation.endsWith(".toml")) {
|
|
16
|
+
return new Document(parse(contents));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default TomlParser;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
import { BasePlugin, Document } from "../plugins.js";
|
|
3
|
+
|
|
4
|
+
class YamlParser extends BasePlugin {
|
|
5
|
+
static name = "v8r-plugin-yaml-parser";
|
|
6
|
+
|
|
7
|
+
registerInputFileParsers() {
|
|
8
|
+
return ["yaml"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
parseInputFile(contents, fileLocation, parser) {
|
|
12
|
+
if (parser === "yaml") {
|
|
13
|
+
return new Document(yaml.load(contents));
|
|
14
|
+
} else if (parser == null) {
|
|
15
|
+
if (fileLocation.endsWith(".yaml") || fileLocation.endsWith(".yml")) {
|
|
16
|
+
return new Document(yaml.load(contents));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default YamlParser;
|
package/src/plugins.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all v8r plugins.
|
|
5
|
+
*
|
|
6
|
+
* @abstract
|
|
7
|
+
*/
|
|
8
|
+
class BasePlugin {
|
|
9
|
+
/**
|
|
10
|
+
* Name of the plugin. All plugins must declare a name starting with
|
|
11
|
+
* `v8r-plugin-`.
|
|
12
|
+
*
|
|
13
|
+
* @type {string}
|
|
14
|
+
* @static
|
|
15
|
+
*/
|
|
16
|
+
static name = "untitled plugin";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Use the `registerInputFileParsers` hook to tell v8r about additional file
|
|
20
|
+
* formats that can be parsed. Any parsers registered with this hook become
|
|
21
|
+
* valid values for the `parser` property in custom schemas.
|
|
22
|
+
*
|
|
23
|
+
* @returns {string[]} File parsers to register
|
|
24
|
+
*/
|
|
25
|
+
registerInputFileParsers() {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Use the `parseInputFile` hook to tell v8r how to parse files.
|
|
31
|
+
*
|
|
32
|
+
* If `parseInputFile` returns anything other than undefined, that return
|
|
33
|
+
* value will be used and no further plugins will be invoked. If
|
|
34
|
+
* `parseInputFile` returns undefined, v8r will move on to the next plugin in
|
|
35
|
+
* the stack.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} contents - The unparsed file content.
|
|
38
|
+
* @param {string} fileLocation - The file path. Filenames are resolved and
|
|
39
|
+
* normalised by [glob](https://www.npmjs.com/package/glob) using the
|
|
40
|
+
* `dotRelative` option. This means relative paths in the current directory
|
|
41
|
+
* will be prefixed with `./` (or `.\` on Windows) even if this was not
|
|
42
|
+
* present in the input filename or pattern.
|
|
43
|
+
* @param {string | undefined} parser - If the user has specified a parser to
|
|
44
|
+
* use for this file in a custom schema, this will be passed to
|
|
45
|
+
* `parseInputFile` in the `parser` param.
|
|
46
|
+
* @returns {Document | undefined} Parsed file contents
|
|
47
|
+
*/
|
|
48
|
+
// eslint-disable-next-line no-unused-vars
|
|
49
|
+
parseInputFile(contents, fileLocation, parser) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Use the `registerOutputFormats` hook to tell v8r about additional output
|
|
55
|
+
* formats that can be generated. Any formats registered with this hook become
|
|
56
|
+
* valid values for the `format` property in the config file and the
|
|
57
|
+
* `--format` command line argument.
|
|
58
|
+
*
|
|
59
|
+
* @returns {string[]} Output formats to register
|
|
60
|
+
*/
|
|
61
|
+
registerOutputFormats() {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Use the `getSingleResultLogMessage` hook to provide a log message for v8r
|
|
67
|
+
* to output after processing a single file.
|
|
68
|
+
*
|
|
69
|
+
* If `getSingleResultLogMessage` returns anything other than undefined, that
|
|
70
|
+
* return value will be used and no further plugins will be invoked. If
|
|
71
|
+
* `getSingleResultLogMessage` returns undefined, v8r will move on to the next
|
|
72
|
+
* plugin in the stack.
|
|
73
|
+
*
|
|
74
|
+
* Any message returned from this function will be written to stdout.
|
|
75
|
+
*
|
|
76
|
+
* @param {ValidationResult} result - Result of attempting to validate this
|
|
77
|
+
* document.
|
|
78
|
+
* @param {string} fileLocation - The document file path. Filenames are
|
|
79
|
+
* resolved and normalised by [glob](https://www.npmjs.com/package/glob)
|
|
80
|
+
* using the `dotRelative` option. This means relative paths in the current
|
|
81
|
+
* directory will be prefixed with `./` (or `.\` on Windows) even if this
|
|
82
|
+
* was not present in the input filename or pattern.
|
|
83
|
+
* @param {string} format - The user's requested output format as specified in
|
|
84
|
+
* the config file or via the `--format` command line argument.
|
|
85
|
+
* @returns {string | undefined} Log message
|
|
86
|
+
*/
|
|
87
|
+
// eslint-disable-next-line no-unused-vars
|
|
88
|
+
getSingleResultLogMessage(result, fileLocation, format) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Use the `getAllResultsLogMessage` hook to provide a log message for v8r to
|
|
94
|
+
* output after processing all files.
|
|
95
|
+
*
|
|
96
|
+
* If `getAllResultsLogMessage` returns anything other than undefined, that
|
|
97
|
+
* return value will be used and no further plugins will be invoked. If
|
|
98
|
+
* `getAllResultsLogMessage` returns undefined, v8r will move on to the next
|
|
99
|
+
* plugin in the stack.
|
|
100
|
+
*
|
|
101
|
+
* Any message returned from this function will be written to stdout.
|
|
102
|
+
*
|
|
103
|
+
* @param {ValidationResult[]} results - Results of attempting to validate
|
|
104
|
+
* these documents.
|
|
105
|
+
* @param {string} format - The user's requested output format as specified in
|
|
106
|
+
* the config file or via the `--format` command line argument.
|
|
107
|
+
* @returns {string | undefined} Log message
|
|
108
|
+
*/
|
|
109
|
+
// eslint-disable-next-line no-unused-vars
|
|
110
|
+
getAllResultsLogMessage(results, format) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class Document {
|
|
116
|
+
/**
|
|
117
|
+
* Document is a thin wrapper class for a document we want to validate after
|
|
118
|
+
* parsing a file
|
|
119
|
+
*
|
|
120
|
+
* @param {any} document - The object to be wrapped
|
|
121
|
+
*/
|
|
122
|
+
constructor(document) {
|
|
123
|
+
this.document = document;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validatePlugin(plugin) {
|
|
128
|
+
if (
|
|
129
|
+
typeof plugin.name !== "string" ||
|
|
130
|
+
!plugin.name.startsWith("v8r-plugin-")
|
|
131
|
+
) {
|
|
132
|
+
throw new Error(`Plugin ${plugin.name} does not declare a valid name`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!(plugin.prototype instanceof BasePlugin)) {
|
|
136
|
+
throw new Error(`Plugin ${plugin.name} does not extend BasePlugin`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const prop of Object.getOwnPropertyNames(BasePlugin.prototype)) {
|
|
140
|
+
const method = plugin.prototype[prop];
|
|
141
|
+
const argCount = plugin.prototype[prop].length;
|
|
142
|
+
if (typeof method !== "function") {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Error loading plugin ${plugin.name}: must have a method called ${method}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const expectedArgs = BasePlugin.prototype[prop].length;
|
|
148
|
+
if (expectedArgs !== argCount) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Error loading plugin ${plugin.name}: ${prop} must take exactly ${expectedArgs} arguments`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveUserPlugins(userPlugins) {
|
|
157
|
+
let plugins = [];
|
|
158
|
+
for (let plugin of userPlugins) {
|
|
159
|
+
if (plugin.startsWith("package:")) {
|
|
160
|
+
plugins.push(plugin.slice(8));
|
|
161
|
+
}
|
|
162
|
+
if (plugin.startsWith("file:")) {
|
|
163
|
+
plugins.push(path.resolve(process.cwd(), plugin.slice(5)));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return plugins;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function loadPlugins(plugins) {
|
|
170
|
+
let loadedPlugins = [];
|
|
171
|
+
for (const plugin of plugins) {
|
|
172
|
+
loadedPlugins.push(await import(plugin));
|
|
173
|
+
}
|
|
174
|
+
loadedPlugins = loadedPlugins.map((plugin) => plugin.default);
|
|
175
|
+
loadedPlugins.forEach((plugin) => validatePlugin(plugin));
|
|
176
|
+
loadedPlugins = loadedPlugins.map((plugin) => new plugin());
|
|
177
|
+
return loadedPlugins;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function loadAllPlugins(userPlugins) {
|
|
181
|
+
const loadedUserPlugins = await loadPlugins(userPlugins);
|
|
182
|
+
|
|
183
|
+
const corePlugins = [
|
|
184
|
+
"./plugins/parser-json.js",
|
|
185
|
+
"./plugins/parser-json5.js",
|
|
186
|
+
"./plugins/parser-toml.js",
|
|
187
|
+
"./plugins/parser-yaml.js",
|
|
188
|
+
"./plugins/output-text.js",
|
|
189
|
+
"./plugins/output-json.js",
|
|
190
|
+
];
|
|
191
|
+
const loadedCorePlugins = await loadPlugins(corePlugins);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
allLoadedPlugins: loadedUserPlugins.concat(loadedCorePlugins),
|
|
195
|
+
loadedCorePlugins,
|
|
196
|
+
loadedUserPlugins,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @typedef {object} ValidationResult
|
|
202
|
+
* @property {string} fileLocation - Path of the document that was validated.
|
|
203
|
+
* Filenames are resolved and normalised by
|
|
204
|
+
* [glob](https://www.npmjs.com/package/glob) using the `dotRelative` option.
|
|
205
|
+
* This means relative paths in the current directory will be prefixed with
|
|
206
|
+
* `./` (or `.\` on Windows) even if this was not present in the input
|
|
207
|
+
* filename or pattern.
|
|
208
|
+
* @property {string | null} schemaLocation - Location of the schema used to
|
|
209
|
+
* validate this file if one could be found. `null` if no schema was found.
|
|
210
|
+
* @property {boolean | null} valid - Result of the validation (true/false) if a
|
|
211
|
+
* schema was found. `null` if no schema was found and no validation could be
|
|
212
|
+
* performed.
|
|
213
|
+
* @property {ErrorObject[]} errors - An array of [AJV Error
|
|
214
|
+
* Objects](https://ajv.js.org/api.html#error-objects) describing any errors
|
|
215
|
+
* encountered when validating this document.
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @external ErrorObject
|
|
220
|
+
* @see https://ajv.js.org/api.html#error-objects
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
export { BasePlugin, Document, loadAllPlugins, resolveUserPlugins };
|
package/src/public.js
ADDED