yini-cli 1.3.4 → 1.4.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 +1 -1
- 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 +64 -51
- 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 +51 -31
- package/dist/utils/string.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
*YINI is an INI-inspired, human-friendly configuration format with real structure, nested sections, comments, and predictable parsing.*
|
|
7
7
|
|
|
8
|
-
[](https://www.npmjs.com/package/yini-cli) [](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) [](https://www.npmjs.com/package/yini-cli)
|
|
8
|
+
[](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) [](https://www.npmjs.com/package/yini-cli)
|
|
9
9
|
|
|
10
10
|
Designed for developers and teams who want human-edited configuration with explicit structure and no indentation-based semantics.
|
|
11
11
|
|
|
@@ -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;
|
|
@@ -4,6 +4,7 @@ import path from 'node: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.
|
|
@@ -152,52 +156,61 @@ const hasMeaningfulParsedData = (value) => {
|
|
|
152
156
|
const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
|
|
153
157
|
debugPrint('File = ' + file);
|
|
154
158
|
debugPrint('outputFormat = ' + outputFormat);
|
|
155
|
-
const parseOptions = {
|
|
156
|
-
strictMode: commandOptions.strict ?? false,
|
|
157
|
-
failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
|
|
158
|
-
includeMetadata: true,
|
|
159
|
-
includeDiagnostics: true,
|
|
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}"`);
|
|
169
|
-
}
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
159
|
try {
|
|
160
|
+
const strictMode = resolveStrictMode(commandOptions);
|
|
161
|
+
const parseOptions = {
|
|
162
|
+
strictMode: strictMode,
|
|
163
|
+
failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
|
|
164
|
+
includeMetadata: true,
|
|
165
|
+
includeDiagnostics: true,
|
|
166
|
+
};
|
|
167
|
+
// Check that output and input file are not the same.
|
|
168
|
+
if (outputFile) {
|
|
169
|
+
assertInputAndOutputAreDifferent(file, outputFile);
|
|
170
|
+
}
|
|
171
|
+
// Check early if should skip before parsing,
|
|
172
|
+
// saves a lot of time in some cases.
|
|
173
|
+
if (shouldSkipBecauseDestNewer(
|
|
174
|
+
// commandOptions,
|
|
175
|
+
file, commandOptions.overwrite,
|
|
176
|
+
// commandOptions.verbose,
|
|
177
|
+
outputFile)) {
|
|
178
|
+
if (commandOptions.verbose) {
|
|
179
|
+
const resolved = path.resolve(outputFile);
|
|
180
|
+
reportAction(commandOptions, 'skip', resolved, `newer than source "${file}"`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
173
184
|
const parsedWithMeta = YINI.parseFile(file, parseOptions);
|
|
174
185
|
const errorCount = parsedWithMeta?.meta?.diagnostics?.errors?.errorCount ?? 0;
|
|
175
186
|
const parsedData = parsedWithMeta.result;
|
|
176
187
|
const hasUsableOutput = hasMeaningfulParsedData(parsedData);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
const hasErrors = errorCount > 0;
|
|
189
|
+
const bestEffort = !!commandOptions.bestEffort;
|
|
190
|
+
// const strictMode = resolveStrictMode(commandOptions)
|
|
191
|
+
const shouldFailHard = hasErrors && !bestEffort && (strictMode || !hasUsableOutput);
|
|
192
|
+
if (shouldFailHard) {
|
|
180
193
|
process.exit(1);
|
|
181
194
|
}
|
|
182
195
|
const serializer = getSerializer(outputFormat);
|
|
183
196
|
const output = serializer.serialize(parsedData);
|
|
184
197
|
if (outputFile) {
|
|
185
198
|
const resolved = path.resolve(outputFile);
|
|
186
|
-
if (!isAllowWriteOutput(file, resolved, output, commandOptions.overwrite)) {
|
|
199
|
+
if (!isAllowWriteOutput(commandOptions, file, resolved, output, commandOptions.overwrite)) {
|
|
187
200
|
return;
|
|
188
201
|
}
|
|
189
202
|
fs.writeFileSync(resolved, output, 'utf-8');
|
|
190
203
|
if (commandOptions.verbose) {
|
|
191
|
-
reportAction('write', resolved);
|
|
204
|
+
reportAction(commandOptions, 'write', resolved);
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
else {
|
|
195
|
-
|
|
208
|
+
printStdout(commandOptions, output);
|
|
196
209
|
}
|
|
197
210
|
}
|
|
198
211
|
catch (err) {
|
|
199
212
|
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
-
|
|
213
|
+
printStderr(commandOptions, `Error: ${message}`);
|
|
201
214
|
process.exit(1);
|
|
202
215
|
}
|
|
203
216
|
};
|
|
@@ -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;
|