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 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
- > YINI Clear, Structured Configuration Files.
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, } from '../utils/print.js';
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
- const renderOutput = (parsed: unknown, style: TOutputStyle): string => {
72
- switch (style) {
73
- case 'JS-style':
74
- return toPrettyJS(parsed)
75
-
76
- case 'JSON-compact':
77
- return JSON.stringify(parsed)
78
-
79
- case 'Pretty-JSON':
80
- default:
81
- return toPrettyJSON(parsed)
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: preferredFailLevel,
95
- includeMetadata: includeMetaData,
157
+ failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
158
+ includeMetadata: true,
159
+ includeDiagnostics: true,
96
160
  };
97
- // If --best-effort then override fail-level.
98
- if (commandOptions.bestEffort) {
99
- parseOptions.failLevel = 'ignore-errors';
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 parsed = YINI.parseFile(file, parseOptions);
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(parsed);
183
+ const output = serializer.serialize(parsedData);
105
184
  if (outputFile) {
106
185
  const resolved = path.resolve(outputFile);
107
- enforceWritePolicy(file, resolved, commandOptions.overwrite);
108
- // Write JSON output to file instead of stdout.
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
- console.log(`Output written to file: "${outputFile}"`);
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.0-beta",
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.0-beta"
77
+ "yini-parser": "^1.4.1-beta"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@eslint/js": "^9.31.0",