yini-cli 1.3.4 → 1.5.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/README.md +121 -22
- package/dist/commands/commonFunctions.d.ts +15 -0
- package/dist/commands/commonFunctions.js +76 -0
- package/dist/commands/parseCommand.d.ts +1 -1
- package/dist/commands/parseCommand.js +96 -53
- package/dist/commands/validateCommand.d.ts +6 -1
- package/dist/commands/validateCommand.js +440 -402
- package/dist/descriptions.js +6 -1
- package/dist/globalOptions/helpOption.js +9 -6
- package/dist/index.js +52 -32
- package/dist/utils/string.js +1 -0
- package/docs/CHANGELOG.md +130 -0
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# YINI CLI
|
|
2
|
-
> **Readable configuration without indentation pitfalls or JSON verbosity.**
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
The official CLI for validating, inspecting, and converting YINI configuration files to JSON or JavaScript, maintained by the YINI-lang project.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
YINI is an INI-inspired, human-readable configuration format with explicit structure, nested sections, comments, and predictable parsing.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
YINI is intended to emphasize clarity, readability, explicit structure, predictability, and deterministic parsing, while remaining simple, but not simplistic.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/yini-cli) [](https://www.typescriptlang.org/) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml)
|
|
11
10
|
|
|
12
11
|
## Quick Start
|
|
13
12
|
|
|
@@ -37,7 +36,7 @@ YINI CLI requires Node.js **v20 or later**.
|
|
|
37
36
|
|
|
38
37
|
3. **Test functionality**
|
|
39
38
|
Create a simple test file, for example: `config.yini`:
|
|
40
|
-
```
|
|
39
|
+
```ini
|
|
41
40
|
^ App
|
|
42
41
|
name = "My App Title"
|
|
43
42
|
version = "1.2.3"
|
|
@@ -82,11 +81,11 @@ Source: [config.yini](./samples/config.yini)
|
|
|
82
81
|
|
|
83
82
|
---
|
|
84
83
|
|
|
85
|
-
##
|
|
84
|
+
## YINI characteristics
|
|
86
85
|
- **Indentation-independent structure:** YINI is indentation-independent — whitespace never alters structural meaning.
|
|
87
|
-
- **Explicit nesting:**
|
|
86
|
+
- **Explicit nesting:** Section markers such as `^`, `^^`, and `^^^` define hierarchy explicitly.
|
|
88
87
|
- **Multiple data types:** Supports booleans (`true` / `false`, `yes` / `no`, etc.), numbers, lists, and inline objects, with explicit string syntax.
|
|
89
|
-
- **Comment support:** YINI supports multiple comment styles (`#`, `//`, `/* ... */`, and `;`)
|
|
88
|
+
- **Comment support:** YINI supports multiple comment styles (`#`, `//`, `/* ... */`, and `;`) for documenting configuration directly in the file.
|
|
90
89
|
- **Predictable parsing:** Well-defined rules with optional strict and lenient modes for different use cases.
|
|
91
90
|
|
|
92
91
|
---
|
|
@@ -128,10 +127,112 @@ yini parse --help
|
|
|
128
127
|
|
|
129
128
|
---
|
|
130
129
|
|
|
130
|
+
## Why YINI?
|
|
131
|
+
|
|
132
|
+
YINI is intended for configuration files where human readability, explicit structure, and predictable parsing are more important than minimal syntax or maximum flexibility.
|
|
133
|
+
|
|
134
|
+
Compared with common configuration formats:
|
|
135
|
+
- **INI:** YINI supports clearer nested sections and typed values.
|
|
136
|
+
- **JSON:** YINI supports comments and is easier to edit by hand.
|
|
137
|
+
- **YAML:** YINI does not use indentation to define structure.
|
|
138
|
+
- **TOML:** YINI uses explicit section markers for hierarchy instead of dotted table names.
|
|
139
|
+
|
|
140
|
+
The same small configuration can be written in several formats:
|
|
141
|
+
|
|
142
|
+
### YINI
|
|
143
|
+
```ini
|
|
144
|
+
^ Application
|
|
145
|
+
name = 'demo'
|
|
146
|
+
environment = 'dev'
|
|
147
|
+
|
|
148
|
+
^^ Server
|
|
149
|
+
host = 'localhost'
|
|
150
|
+
ports = [8080, 8081]
|
|
151
|
+
|
|
152
|
+
^^^ TLS
|
|
153
|
+
enabled = true
|
|
154
|
+
mode = 'optional'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `Application` contains the top-level application settings.
|
|
158
|
+
- `Server` is nested under `Application`.
|
|
159
|
+
- `TLS` is nested under `Server`.
|
|
160
|
+
- The section markers `^` make the nesting explicit. Indentation is optional and not required for structure.
|
|
161
|
+
- Strings can use either `'` or `"`.
|
|
162
|
+
|
|
163
|
+
### JSON
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"Application": {
|
|
167
|
+
"name": "demo",
|
|
168
|
+
"environment": "dev",
|
|
169
|
+
"Server": {
|
|
170
|
+
"host": "localhost",
|
|
171
|
+
"ports": [8080, 8081],
|
|
172
|
+
"TLS": {
|
|
173
|
+
"enabled": true,
|
|
174
|
+
"mode": "optional"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### YAML
|
|
182
|
+
```yaml
|
|
183
|
+
Application:
|
|
184
|
+
name: demo
|
|
185
|
+
environment: dev
|
|
186
|
+
Server:
|
|
187
|
+
host: localhost
|
|
188
|
+
ports:
|
|
189
|
+
- 8080
|
|
190
|
+
- 8081
|
|
191
|
+
TLS:
|
|
192
|
+
enabled: true
|
|
193
|
+
mode: optional
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### TOML
|
|
197
|
+
```toml
|
|
198
|
+
[Application]
|
|
199
|
+
name = "demo"
|
|
200
|
+
environment = "dev"
|
|
201
|
+
|
|
202
|
+
[Application.Server]
|
|
203
|
+
host = "localhost"
|
|
204
|
+
ports = [8080, 8081]
|
|
205
|
+
|
|
206
|
+
[Application.Server.TLS]
|
|
207
|
+
enabled = true
|
|
208
|
+
mode = "optional"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
YINI may not be the right choice when you need mature ecosystem support, existing schema tooling, or maximum compatibility with infrastructure that already expects JSON, YAML, or TOML. The format and parser are still alpha-stage and best suited for testing, experiments, and early integration feedback.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Feedback and bug reports
|
|
216
|
+
|
|
217
|
+
If you find a problem, please open an issue on GitHub:
|
|
218
|
+
|
|
219
|
+
- [Report a bug or issue](https://github.com/YINI-lang/yini-cli/issues)
|
|
220
|
+
|
|
221
|
+
When reporting parser behavior, it is helpful to include:
|
|
222
|
+
- The YINI input that caused the issue.
|
|
223
|
+
- The command and options used.
|
|
224
|
+
- The expected result.
|
|
225
|
+
- The actual result or error message.
|
|
226
|
+
- The installed `yini-cli` version.
|
|
227
|
+
- The Node.js version used.
|
|
228
|
+
- The operating system and version used.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
131
232
|
## A closer look at YINI
|
|
132
233
|
|
|
133
234
|
Here's a small example showing YINI structure and comments:
|
|
134
|
-
```
|
|
235
|
+
```ini
|
|
135
236
|
// This is a comment in YINI
|
|
136
237
|
|
|
137
238
|
^ App // Defines section (group) "App"
|
|
@@ -177,10 +278,8 @@ Here's a small example showing YINI structure and comments:
|
|
|
177
278
|
}
|
|
178
279
|
```
|
|
179
280
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- ▶️ See more on [YINI Homepage](https://yini-lang.org/?utm_source=github&utm_medium=referral&utm_campaign=yini_cli&utm_content=readme_middle).
|
|
183
|
-
- ▶️ Link to [Demo Apps](https://github.com/YINI-lang/yini-demo-apps/tree/main) with complete basic usage.
|
|
281
|
+
- [YINI Homepage](https://yini-lang.org/?utm_source=github&utm_medium=referral&utm_campaign=yini_cli&utm_content=readme_middle).
|
|
282
|
+
- [YINI Demo Apps](https://github.com/YINI-lang/yini-demo-apps/tree/main) with usage examples.
|
|
184
283
|
|
|
185
284
|
---
|
|
186
285
|
|
|
@@ -196,8 +295,8 @@ The `parse` command supports multiple output formats:
|
|
|
196
295
|
| `yini parse config.yini --js` | JavaScript object | JavaScript-style object (unquoted keys, single quotes). |
|
|
197
296
|
| `yini parse config.yini -o out.json` | File output | Writes formatted JSON to file (default format). |
|
|
198
297
|
|
|
199
|
-
|
|
200
|
-
|
|
298
|
+
> `--js` and `--compact` are mutually exclusive.
|
|
299
|
+
> `--output` can be combined with a style flag to control both formatting and destination.
|
|
201
300
|
|
|
202
301
|
### Output File Handling
|
|
203
302
|
|
|
@@ -237,20 +336,20 @@ Use `--overwrite` to force replacement.
|
|
|
237
336
|
---
|
|
238
337
|
|
|
239
338
|
## Contributing
|
|
240
|
-
|
|
339
|
+
Bug reports, feedback, and contributions are welcome.
|
|
241
340
|
|
|
242
|
-
|
|
341
|
+
GitHub Issues and Discussions are available for feedback and project discussion.
|
|
243
342
|
|
|
244
343
|
---
|
|
245
344
|
|
|
246
345
|
## License
|
|
247
|
-
This project is licensed under the Apache
|
|
248
|
-
|
|
249
|
-
In this project on GitHub, the `libs` directory contains third party software and each is licensed under its own license which is described in its own license file under the respective directory under `libs`.
|
|
346
|
+
This project is licensed under the Apache License 2.0 — see the [LICENSE](./LICENSE) file for details.
|
|
250
347
|
|
|
251
348
|
---
|
|
252
349
|
|
|
253
350
|
**^YINI ≡**
|
|
254
|
-
>
|
|
351
|
+
> YINI is a human-readable configuration format designed for clarity, readability, explicit structure, predictability, and deterministic parsing.
|
|
352
|
+
>
|
|
353
|
+
> See the specification for syntax and format details.
|
|
255
354
|
|
|
256
355
|
[yini-lang.org](https://yini-lang.org/?utm_source=github&utm_medium=referral&utm_campaign=yini_cli&utm_content=readme_footer) · [YINI-lang on GitHub](https://github.com/YINI-lang)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IGlobalOptions } from '../types.js';
|
|
2
|
+
export type TValidateRunMode = 'file' | 'directory';
|
|
3
|
+
export declare const printStdout: (options: IGlobalOptions, text: string) => void;
|
|
4
|
+
export declare const printStderr: (options: IGlobalOptions, text: string) => void;
|
|
5
|
+
export declare const printWarning: (options: IGlobalOptions, text: string) => void;
|
|
6
|
+
export declare const resolveStrictMode: (options: IGlobalOptions) => boolean;
|
|
7
|
+
export declare const resolveRunModeFromTargets: (targets: string[]) => TValidateRunMode;
|
|
8
|
+
export declare const getDisplayBaseDir: (targets: string[]) => string;
|
|
9
|
+
export declare const toDisplayPath: (filePath: string, baseDir: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
* @note Windows safe, since Windows paths are case-insensitive,
|
|
13
|
+
* so on Windows cases are normalized too.
|
|
14
|
+
*/
|
|
15
|
+
export declare const assertInputAndOutputAreDifferent: (srcPath: string, destPath: string) => void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/commands/commonFunctions.ts
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const printStdout = (options, text) => {
|
|
5
|
+
if (options.silent)
|
|
6
|
+
return;
|
|
7
|
+
console.log(text);
|
|
8
|
+
};
|
|
9
|
+
export const printStderr = (options, text) => {
|
|
10
|
+
if (options.silent)
|
|
11
|
+
return;
|
|
12
|
+
console.error(text);
|
|
13
|
+
};
|
|
14
|
+
export const printWarning = (options, text) => {
|
|
15
|
+
if (options.silent)
|
|
16
|
+
return;
|
|
17
|
+
if (options.quiet)
|
|
18
|
+
return;
|
|
19
|
+
console.warn(text);
|
|
20
|
+
};
|
|
21
|
+
export const resolveStrictMode = (options) => {
|
|
22
|
+
if (options.strict && options.lenient) {
|
|
23
|
+
throw new Error('--strict and --lenient cannot be used together.');
|
|
24
|
+
}
|
|
25
|
+
if (options.strict)
|
|
26
|
+
return true;
|
|
27
|
+
if (options.lenient)
|
|
28
|
+
return false;
|
|
29
|
+
return false; // Default = lenient
|
|
30
|
+
};
|
|
31
|
+
export const resolveRunModeFromTargets = (targets) => {
|
|
32
|
+
for (const target of targets) {
|
|
33
|
+
const resolved = path.resolve(target);
|
|
34
|
+
if (!fs.existsSync(resolved)) {
|
|
35
|
+
throw new Error(`Path does not exist: "${target}"`);
|
|
36
|
+
}
|
|
37
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
38
|
+
return 'directory';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return 'file';
|
|
42
|
+
};
|
|
43
|
+
export const getDisplayBaseDir = (targets) => {
|
|
44
|
+
if (!targets.length) {
|
|
45
|
+
return process.cwd();
|
|
46
|
+
}
|
|
47
|
+
if (targets.length === 1) {
|
|
48
|
+
const resolved = path.resolve(targets[0]);
|
|
49
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
return path.dirname(resolved);
|
|
53
|
+
}
|
|
54
|
+
return process.cwd();
|
|
55
|
+
};
|
|
56
|
+
export const toDisplayPath = (filePath, baseDir) => {
|
|
57
|
+
const relative = path.relative(baseDir, filePath);
|
|
58
|
+
if (!relative || relative.startsWith('..')) {
|
|
59
|
+
return filePath;
|
|
60
|
+
}
|
|
61
|
+
return relative;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
* @note Windows safe, since Windows paths are case-insensitive,
|
|
66
|
+
* so on Windows cases are normalized too.
|
|
67
|
+
*/
|
|
68
|
+
export const assertInputAndOutputAreDifferent = (srcPath, destPath) => {
|
|
69
|
+
const resolvedSrc = path.resolve(srcPath);
|
|
70
|
+
const resolvedDest = path.resolve(destPath);
|
|
71
|
+
const normalizedSrc = process.platform === 'win32' ? resolvedSrc.toLowerCase() : resolvedSrc;
|
|
72
|
+
const normalizedDest = process.platform === 'win32' ? resolvedDest.toLowerCase() : resolvedDest;
|
|
73
|
+
if (normalizedSrc === normalizedDest) {
|
|
74
|
+
throw new Error('Output file must be different from the input file.');
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -21,5 +21,5 @@ export interface IParseCommandOptions extends IGlobalOptions {
|
|
|
21
21
|
* @returns
|
|
22
22
|
*/
|
|
23
23
|
export declare const shouldSkipBecauseDestNewer: (file: string, optionOverwrite?: boolean | undefined, outputFile?: string) => boolean;
|
|
24
|
-
export declare const isAllowWriteOutput: (srcPath: string, destPath: string, newContent: string, overwrite?: boolean) => boolean;
|
|
24
|
+
export declare const isAllowWriteOutput: (commandOptions: IParseCommandOptions, srcPath: string, destPath: string, newContent: string, overwrite?: boolean) => boolean;
|
|
25
25
|
export declare const parseFile: (file: string, commandOptions: IParseCommandOptions) => void;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/commands/parseCommand.ts
|
|
2
|
-
import fs from '
|
|
3
|
-
import path from '
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
4
|
import YINI from 'yini-parser';
|
|
5
5
|
import { getSerializer } from '../serializers/index.js';
|
|
6
6
|
import { debugPrint, printObject } from '../utils/print.js';
|
|
7
|
+
import { assertInputAndOutputAreDifferent, printStderr, printStdout, printWarning, resolveStrictMode, } from './commonFunctions.js';
|
|
7
8
|
// -------------------------------------------------------------------------
|
|
8
9
|
/**
|
|
9
10
|
* Will return true if:
|
|
@@ -12,7 +13,9 @@ import { debugPrint, printObject } from '../utils/print.js';
|
|
|
12
13
|
* * destination is newer than source
|
|
13
14
|
* @returns
|
|
14
15
|
*/
|
|
15
|
-
export const shouldSkipBecauseDestNewer = (
|
|
16
|
+
export const shouldSkipBecauseDestNewer = (
|
|
17
|
+
// commandOptions: IParseCommandOptions,
|
|
18
|
+
file, optionOverwrite,
|
|
16
19
|
// optionVerbose?: boolean | undefined,
|
|
17
20
|
outputFile = '') => {
|
|
18
21
|
if (outputFile && optionOverwrite === undefined) {
|
|
@@ -27,7 +30,7 @@ outputFile = '') => {
|
|
|
27
30
|
}
|
|
28
31
|
return false;
|
|
29
32
|
};
|
|
30
|
-
export const isAllowWriteOutput = (srcPath, destPath, newContent, overwrite) => {
|
|
33
|
+
export const isAllowWriteOutput = (commandOptions, srcPath, destPath, newContent, overwrite) => {
|
|
31
34
|
if (!fs.existsSync(destPath)) {
|
|
32
35
|
return true;
|
|
33
36
|
}
|
|
@@ -40,29 +43,31 @@ export const isAllowWriteOutput = (srcPath, destPath, newContent, overwrite) =>
|
|
|
40
43
|
const srcStat = fs.statSync(srcPath);
|
|
41
44
|
const destStat = fs.statSync(destPath);
|
|
42
45
|
if (destStat.mtimeMs > srcStat.mtimeMs) {
|
|
43
|
-
reportAction('skip', destPath, `newer than source "${srcPath}"`);
|
|
46
|
+
reportAction(commandOptions, 'skip', destPath, `newer than source "${srcPath}"`);
|
|
44
47
|
return false;
|
|
45
48
|
}
|
|
46
49
|
const existing = fs.readFileSync(destPath, 'utf-8');
|
|
47
50
|
if (existing === newContent) {
|
|
48
|
-
reportAction('skip', destPath, 'output unchanged');
|
|
51
|
+
reportAction(commandOptions, 'skip', destPath, 'output unchanged');
|
|
49
52
|
return false;
|
|
50
53
|
}
|
|
51
54
|
return true;
|
|
52
55
|
};
|
|
53
|
-
const reportAction = (action, file, reason) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
txt = `${action.padEnd(6)} "${file}"`;
|
|
60
|
-
}
|
|
56
|
+
const reportAction = (options, action, file, reason) => {
|
|
57
|
+
if (options.silent)
|
|
58
|
+
return;
|
|
59
|
+
let txt = reason
|
|
60
|
+
? `${action.padEnd(6)} "${file}" (${reason})`
|
|
61
|
+
: `${action.padEnd(6)} "${file}"`;
|
|
61
62
|
if (action === 'skip') {
|
|
62
|
-
|
|
63
|
+
if (!options.quiet) {
|
|
64
|
+
console.warn(txt);
|
|
65
|
+
}
|
|
63
66
|
}
|
|
64
67
|
else {
|
|
65
|
-
|
|
68
|
+
if (options.verbose && !options.quiet) {
|
|
69
|
+
console.log(txt);
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
};
|
|
68
73
|
/*
|
|
@@ -89,7 +94,7 @@ const reportAction = (action, file, reason) => {
|
|
|
89
94
|
|
|
90
95
|
Execution control:
|
|
91
96
|
--fail-fast
|
|
92
|
-
--best-effort = ignore-errors within a file, attempt recovery and still emit
|
|
97
|
+
--best-effort = ignore-errors within a file, attempt recovery, and still emit output
|
|
93
98
|
--No for parse, --keep-going = continue to the next file when one fails
|
|
94
99
|
--max-errors <n>
|
|
95
100
|
--verbose
|
|
@@ -111,21 +116,20 @@ export const parseFile = (file, commandOptions) => {
|
|
|
111
116
|
doParseFile(file, commandOptions, outputFormat, outputFile);
|
|
112
117
|
};
|
|
113
118
|
const resolveOutputFormat = (options) => {
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const selected = [
|
|
120
|
+
options.json ? 'json' : null,
|
|
121
|
+
options.compact ? 'json-compact' : null,
|
|
122
|
+
options.js ? 'js' : null,
|
|
123
|
+
options.yaml ? 'yaml' : null,
|
|
124
|
+
options.xml ? 'xml' : null,
|
|
125
|
+
].filter(Boolean);
|
|
126
|
+
if (selected.length > 1) {
|
|
127
|
+
throw new Error('Choose only one output format: --json, --compact, --js, --yaml, or --xml.');
|
|
116
128
|
}
|
|
117
|
-
if (options.compact)
|
|
118
|
-
return 'json-compact';
|
|
119
|
-
if (options.js)
|
|
120
|
-
return 'js';
|
|
121
|
-
if (options.yaml)
|
|
122
|
-
return 'yaml';
|
|
123
|
-
if (options.xml)
|
|
124
|
-
return 'xml';
|
|
125
129
|
if (options.pretty) {
|
|
126
|
-
|
|
130
|
+
printWarning(options, 'Warning: --pretty is deprecated. Use --json instead.');
|
|
127
131
|
}
|
|
128
|
-
return 'json';
|
|
132
|
+
return selected[0] ?? 'json';
|
|
129
133
|
};
|
|
130
134
|
/**
|
|
131
135
|
* Returns true if the parsed result appears to contain usable output data.
|
|
@@ -149,55 +153,94 @@ const hasMeaningfulParsedData = (value) => {
|
|
|
149
153
|
return false;
|
|
150
154
|
return Object.keys(value).length > 0;
|
|
151
155
|
};
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
};
|
|
161
|
-
// Check early if should skip before parsing,
|
|
162
|
-
// saves a lot of time in some cases.
|
|
163
|
-
if (shouldSkipBecauseDestNewer(file, commandOptions.overwrite,
|
|
164
|
-
// commandOptions.verbose,
|
|
165
|
-
outputFile)) {
|
|
166
|
-
if (commandOptions.verbose) {
|
|
167
|
-
const resolved = path.resolve(outputFile);
|
|
168
|
-
reportAction('skip', resolved, `newer than source "${file}"`);
|
|
156
|
+
const formatParserDiagnostics = (parsed) => {
|
|
157
|
+
const errors = parsed.meta?.diagnostics?.errors?.payload ?? [];
|
|
158
|
+
return errors.map((diagnostic) => {
|
|
159
|
+
if (typeof diagnostic === 'string') {
|
|
160
|
+
return diagnostic;
|
|
161
|
+
}
|
|
162
|
+
if (!diagnostic || typeof diagnostic !== 'object') {
|
|
163
|
+
return String(diagnostic);
|
|
169
164
|
}
|
|
165
|
+
const issue = diagnostic;
|
|
166
|
+
const message = issue.message ?? issue.code ?? 'Unknown parser error';
|
|
167
|
+
const location = issue.line != null
|
|
168
|
+
? `line ${issue.line}${issue.column != null ? `:${issue.column}` : ''}: `
|
|
169
|
+
: '';
|
|
170
|
+
return `${location}${String(message)}`;
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const printParseFailure = (commandOptions, parsed) => {
|
|
174
|
+
const messages = formatParserDiagnostics(parsed);
|
|
175
|
+
if (messages.length === 0) {
|
|
176
|
+
printStderr(commandOptions, 'Syntax error: failed to parse YINI input.');
|
|
170
177
|
return;
|
|
171
178
|
}
|
|
179
|
+
for (const message of messages) {
|
|
180
|
+
const normalizedMessage = message.toLowerCase().includes('syntax error')
|
|
181
|
+
? message
|
|
182
|
+
: `Syntax error: ${message}`;
|
|
183
|
+
printStderr(commandOptions, normalizedMessage);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
|
|
187
|
+
debugPrint('File = ' + file);
|
|
188
|
+
debugPrint('outputFormat = ' + outputFormat);
|
|
172
189
|
try {
|
|
190
|
+
const strictMode = resolveStrictMode(commandOptions);
|
|
191
|
+
const parseOptions = {
|
|
192
|
+
strictMode: strictMode,
|
|
193
|
+
failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
|
|
194
|
+
includeMetadata: true,
|
|
195
|
+
includeDiagnostics: true,
|
|
196
|
+
};
|
|
197
|
+
// Check that output and input file are not the same.
|
|
198
|
+
if (outputFile) {
|
|
199
|
+
assertInputAndOutputAreDifferent(file, outputFile);
|
|
200
|
+
}
|
|
201
|
+
// Check early if should skip before parsing,
|
|
202
|
+
// saves a lot of time in some cases.
|
|
203
|
+
if (shouldSkipBecauseDestNewer(
|
|
204
|
+
// commandOptions,
|
|
205
|
+
file, commandOptions.overwrite,
|
|
206
|
+
// commandOptions.verbose,
|
|
207
|
+
outputFile)) {
|
|
208
|
+
if (commandOptions.verbose) {
|
|
209
|
+
const resolved = path.resolve(outputFile);
|
|
210
|
+
reportAction(commandOptions, 'skip', resolved, `newer than source "${file}"`);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
173
214
|
const parsedWithMeta = YINI.parseFile(file, parseOptions);
|
|
174
215
|
const errorCount = parsedWithMeta?.meta?.diagnostics?.errors?.errorCount ?? 0;
|
|
175
216
|
const parsedData = parsedWithMeta.result;
|
|
176
217
|
const hasUsableOutput = hasMeaningfulParsedData(parsedData);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
218
|
+
const hasErrors = errorCount > 0;
|
|
219
|
+
const bestEffort = !!commandOptions.bestEffort;
|
|
220
|
+
const shouldFailHard = hasErrors && !bestEffort && (strictMode || !hasUsableOutput);
|
|
221
|
+
if (shouldFailHard) {
|
|
222
|
+
printParseFailure(commandOptions, parsedWithMeta);
|
|
180
223
|
process.exit(1);
|
|
181
224
|
}
|
|
182
225
|
const serializer = getSerializer(outputFormat);
|
|
183
226
|
const output = serializer.serialize(parsedData);
|
|
184
227
|
if (outputFile) {
|
|
185
228
|
const resolved = path.resolve(outputFile);
|
|
186
|
-
if (!isAllowWriteOutput(file, resolved, output, commandOptions.overwrite)) {
|
|
229
|
+
if (!isAllowWriteOutput(commandOptions, file, resolved, output, commandOptions.overwrite)) {
|
|
187
230
|
return;
|
|
188
231
|
}
|
|
189
232
|
fs.writeFileSync(resolved, output, 'utf-8');
|
|
190
233
|
if (commandOptions.verbose) {
|
|
191
|
-
reportAction('write', resolved);
|
|
234
|
+
reportAction(commandOptions, 'write', resolved);
|
|
192
235
|
}
|
|
193
236
|
}
|
|
194
237
|
else {
|
|
195
|
-
|
|
238
|
+
printStdout(commandOptions, output);
|
|
196
239
|
}
|
|
197
240
|
}
|
|
198
241
|
catch (err) {
|
|
199
242
|
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
-
|
|
243
|
+
printStderr(commandOptions, `Error: ${message}`);
|
|
201
244
|
process.exit(1);
|
|
202
245
|
}
|
|
203
246
|
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { IGlobalOptions } from '../types.js';
|
|
2
2
|
export interface IValidateCommandOptions extends IGlobalOptions {
|
|
3
3
|
stats?: boolean;
|
|
4
|
+
warningsAsErrors?: boolean;
|
|
5
|
+
format?: 'json' | 'text';
|
|
6
|
+
failFast?: boolean;
|
|
7
|
+
recursive?: boolean;
|
|
8
|
+
maxErrors?: number;
|
|
4
9
|
}
|
|
5
|
-
export declare const
|
|
10
|
+
export declare const validateTargets: (targets: string[], options: IValidateCommandOptions) => never;
|