yini-cli 1.3.0-beta → 1.3.2-beta
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/parseCommand.d.ts +9 -0
- package/dist/commands/parseCommand.js +110 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -256,6 +256,6 @@ In this project on GitHub, the `libs` directory contains third party software an
|
|
|
256
256
|
---
|
|
257
257
|
|
|
258
258
|
**^YINI ≡**
|
|
259
|
-
>
|
|
259
|
+
> Readable like INI. Structured like JSON. No indentation surprises.
|
|
260
260
|
|
|
261
261
|
[yini-lang.org](https://yini-lang.org/?utm_source=github&utm_medium=referral&utm_campaign=yini_cli&utm_content=readme_footer) · [YINI on GitHub](https://github.com/YINI-lang)
|
|
@@ -13,4 +13,13 @@ export interface IParseCommandOptions extends IGlobalOptions {
|
|
|
13
13
|
bestEffort?: boolean;
|
|
14
14
|
overwrite?: boolean;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Will return true if:
|
|
18
|
+
* * --overwrite was not explicitly given
|
|
19
|
+
* * destination exists
|
|
20
|
+
* * destination is newer than source
|
|
21
|
+
* @returns
|
|
22
|
+
*/
|
|
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;
|
|
16
25
|
export declare const parseFile: (file: string, commandOptions: IParseCommandOptions) => void;
|
|
@@ -3,8 +3,68 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import YINI from 'yini-parser';
|
|
5
5
|
import { getSerializer } from '../serializers/index.js';
|
|
6
|
-
import { debugPrint, printObject
|
|
6
|
+
import { debugPrint, printObject } from '../utils/print.js';
|
|
7
7
|
// -------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Will return true if:
|
|
10
|
+
* * --overwrite was not explicitly given
|
|
11
|
+
* * destination exists
|
|
12
|
+
* * destination is newer than source
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export const shouldSkipBecauseDestNewer = (file, optionOverwrite,
|
|
16
|
+
// optionVerbose?: boolean | undefined,
|
|
17
|
+
outputFile = '') => {
|
|
18
|
+
if (outputFile && optionOverwrite === undefined) {
|
|
19
|
+
const resolved = path.resolve(outputFile);
|
|
20
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
21
|
+
const srcStat = fs.statSync(file);
|
|
22
|
+
const destStat = fs.statSync(resolved);
|
|
23
|
+
if (destStat.mtimeMs > srcStat.mtimeMs) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
export const isAllowWriteOutput = (srcPath, destPath, newContent, overwrite) => {
|
|
31
|
+
if (!fs.existsSync(destPath)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (overwrite === true) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (overwrite === false) {
|
|
38
|
+
throw new Error(`File "${destPath}" already exists. Overwriting disabled (--no-overwrite).`);
|
|
39
|
+
}
|
|
40
|
+
const srcStat = fs.statSync(srcPath);
|
|
41
|
+
const destStat = fs.statSync(destPath);
|
|
42
|
+
if (destStat.mtimeMs > srcStat.mtimeMs) {
|
|
43
|
+
reportAction('skip', destPath, `newer than source "${srcPath}"`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const existing = fs.readFileSync(destPath, 'utf-8');
|
|
47
|
+
if (existing === newContent) {
|
|
48
|
+
reportAction('skip', destPath, 'output unchanged');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const reportAction = (action, file, reason) => {
|
|
54
|
+
let txt = '';
|
|
55
|
+
if (reason) {
|
|
56
|
+
txt = `${action.padEnd(6)} "${file}" (${reason})`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
txt = `${action.padEnd(6)} "${file}"`;
|
|
60
|
+
}
|
|
61
|
+
if (action === 'skip') {
|
|
62
|
+
console.warn(txt);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(txt);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
8
68
|
/*
|
|
9
69
|
TODO / SHOULD-DO:
|
|
10
70
|
|
|
@@ -67,48 +127,68 @@ const resolveOutputFormat = (options) => {
|
|
|
67
127
|
}
|
|
68
128
|
return 'json';
|
|
69
129
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*/
|
|
130
|
+
/**
|
|
131
|
+
* Returns true if the parsed result appears to contain usable output data.
|
|
132
|
+
*
|
|
133
|
+
* This is used by the CLI to distinguish between:
|
|
134
|
+
* - parse runs that reported errors but still recovered meaningful data, and
|
|
135
|
+
* - parse runs that failed so badly that no useful parsed structure was produced.
|
|
136
|
+
*
|
|
137
|
+
* A value is considered meaningful only if it is:
|
|
138
|
+
* - not null or undefined,
|
|
139
|
+
* - an object,
|
|
140
|
+
* - and contains at least one own top-level property.
|
|
141
|
+
*
|
|
142
|
+
* @param value The parsed result data to inspect.
|
|
143
|
+
* @returns True if the parsed result looks usable for output; otherwise false.
|
|
144
|
+
*/
|
|
145
|
+
const hasMeaningfulParsedData = (value) => {
|
|
146
|
+
if (value == null)
|
|
147
|
+
return false;
|
|
148
|
+
if (typeof value !== 'object')
|
|
149
|
+
return false;
|
|
150
|
+
return Object.keys(value).length > 0;
|
|
151
|
+
};
|
|
85
152
|
const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
|
|
86
|
-
let preferredFailLevel = commandOptions.bestEffort
|
|
87
|
-
? 'ignore-errors'
|
|
88
|
-
: 'auto';
|
|
89
|
-
let includeMetaData = false;
|
|
90
153
|
debugPrint('File = ' + file);
|
|
91
154
|
debugPrint('outputFormat = ' + outputFormat);
|
|
92
155
|
const parseOptions = {
|
|
93
156
|
strictMode: commandOptions.strict ?? false,
|
|
94
|
-
failLevel:
|
|
95
|
-
includeMetadata:
|
|
157
|
+
failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
|
|
158
|
+
includeMetadata: true,
|
|
159
|
+
includeDiagnostics: true,
|
|
96
160
|
};
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
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;
|
|
100
171
|
}
|
|
101
172
|
try {
|
|
102
|
-
const
|
|
173
|
+
const parsedWithMeta = YINI.parseFile(file, parseOptions);
|
|
174
|
+
const errorCount = parsedWithMeta?.meta?.diagnostics?.errors?.errorCount ?? 0;
|
|
175
|
+
const parsedData = parsedWithMeta.result;
|
|
176
|
+
const hasUsableOutput = hasMeaningfulParsedData(parsedData);
|
|
177
|
+
if (errorCount > 0 &&
|
|
178
|
+
!commandOptions.bestEffort &&
|
|
179
|
+
(commandOptions.strict || !hasUsableOutput)) {
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
103
182
|
const serializer = getSerializer(outputFormat);
|
|
104
|
-
const output = serializer.serialize(
|
|
183
|
+
const output = serializer.serialize(parsedData);
|
|
105
184
|
if (outputFile) {
|
|
106
185
|
const resolved = path.resolve(outputFile);
|
|
107
|
-
|
|
108
|
-
|
|
186
|
+
if (!isAllowWriteOutput(file, resolved, output, commandOptions.overwrite)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
109
189
|
fs.writeFileSync(resolved, output, 'utf-8');
|
|
110
190
|
if (commandOptions.verbose) {
|
|
111
|
-
|
|
191
|
+
reportAction('write', resolved);
|
|
112
192
|
}
|
|
113
193
|
}
|
|
114
194
|
else {
|
|
@@ -121,21 +201,3 @@ const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
|
|
|
121
201
|
process.exit(1);
|
|
122
202
|
}
|
|
123
203
|
};
|
|
124
|
-
const enforceWritePolicy = (srcPath, destPath, overwrite) => {
|
|
125
|
-
if (!fs.existsSync(destPath)) {
|
|
126
|
-
return; // File does not exist, OK to write.
|
|
127
|
-
}
|
|
128
|
-
const srcStat = fs.statSync(srcPath);
|
|
129
|
-
const destStat = fs.statSync(destPath);
|
|
130
|
-
const destIsNewer = destStat.mtimeMs >= srcStat.mtimeMs;
|
|
131
|
-
if (overwrite === true) {
|
|
132
|
-
return; // Explicit overwrite, OK.
|
|
133
|
-
}
|
|
134
|
-
if (overwrite === false) {
|
|
135
|
-
throw new Error(`File "${destPath}" already exists. Overwriting disabled (--no-overwrite).`);
|
|
136
|
-
}
|
|
137
|
-
// Default policy (overwrite undefined).
|
|
138
|
-
if (destIsNewer) {
|
|
139
|
-
throw new Error(`Destination file "${destPath}" is newer than source. Use --overwrite to force.`);
|
|
140
|
-
}
|
|
141
|
-
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yini-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2-beta",
|
|
4
4
|
"description": "CLI tool for validating and converting YINI configuration files - an INI-like format with real structure, nested sections and strict or lenient modes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yini",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"commander": "^14.0.1",
|
|
75
75
|
"xmlbuilder2": "^4.0.3",
|
|
76
76
|
"yaml": "^2.8.2",
|
|
77
|
-
"yini-parser": "^1.4.
|
|
77
|
+
"yini-parser": "^1.4.1-beta"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@eslint/js": "^9.31.0",
|