yini-cli 1.3.1-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;
@@ -5,6 +5,51 @@ import YINI from 'yini-parser';
5
5
  import { getSerializer } from '../serializers/index.js';
6
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
+ };
8
53
  const reportAction = (action, file, reason) => {
9
54
  let txt = '';
10
55
  if (reason) {
@@ -82,69 +127,67 @@ const resolveOutputFormat = (options) => {
82
127
  }
83
128
  return 'json';
84
129
  };
85
- /*
86
- const renderOutput = (parsed: unknown, style: TOutputStyle): string => {
87
- switch (style) {
88
- case 'JS-style':
89
- return toPrettyJS(parsed)
90
-
91
- case 'JSON-compact':
92
- return JSON.stringify(parsed)
93
-
94
- case 'Pretty-JSON':
95
- default:
96
- return toPrettyJSON(parsed)
97
- }
98
- }
99
- */
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
+ };
100
152
  const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
101
- let preferredFailLevel = commandOptions.bestEffort
102
- ? 'ignore-errors'
103
- : 'auto';
104
- let includeMetaData = false;
105
153
  debugPrint('File = ' + file);
106
154
  debugPrint('outputFormat = ' + outputFormat);
107
155
  const parseOptions = {
108
156
  strictMode: commandOptions.strict ?? false,
109
157
  failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
110
- includeMetadata: false,
158
+ includeMetadata: true,
159
+ includeDiagnostics: true,
111
160
  };
112
- // If --best-effort then override fail-level.
113
- // if (commandOptions.bestEffort) {
114
- // parseOptions.failLevel = 'ignore-errors'
115
- // }
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
+ }
116
172
  try {
117
- 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
+ }
118
182
  const serializer = getSerializer(outputFormat);
119
- const output = serializer.serialize(parsed);
183
+ const output = serializer.serialize(parsedData);
120
184
  if (outputFile) {
121
185
  const resolved = path.resolve(outputFile);
122
- const canWrite = enforceWritePolicy(file, resolved, commandOptions.overwrite);
123
- if (!canWrite) {
124
- if (commandOptions.verbose) {
125
- console.log(`skip Skipping write to "${resolved}"`);
126
- }
186
+ if (!isAllowWriteOutput(file, resolved, output, commandOptions.overwrite)) {
127
187
  return;
128
188
  }
129
- // Double check, if the file was actually changed by comparing the contents.
130
- if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
131
- const existing = fs.readFileSync(resolved, 'utf-8');
132
- // Only write the output file if the content actually changed.
133
- // Prevents constantly showing meaningless rewrites, in some cases.
134
- if (existing === output) {
135
- if (commandOptions.verbose) {
136
- // console.log(
137
- // `skip Output unchanged. Skipping write: "${resolved}"`,
138
- // )
139
- reportAction('skip', resolved, 'output unchanged');
140
- }
141
- return;
142
- }
143
- }
144
- // Write JSON output to file instead of stdout.
145
189
  fs.writeFileSync(resolved, output, 'utf-8');
146
190
  if (commandOptions.verbose) {
147
- // console.log(`write Output written to file: "${outputFile}"`)
148
191
  reportAction('write', resolved);
149
192
  }
150
193
  }
@@ -158,29 +201,3 @@ const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
158
201
  process.exit(1);
159
202
  }
160
203
  };
161
- const enforceWritePolicy = (srcPath, destPath, overwrite) => {
162
- if (!fs.existsSync(destPath)) {
163
- return true; // File does not exist, OK to write.
164
- }
165
- const srcStat = fs.statSync(srcPath);
166
- const destStat = fs.statSync(destPath);
167
- // Only strictly newer triggers skip overwrite.
168
- const destIsNewer = destStat.mtimeMs > srcStat.mtimeMs;
169
- if (overwrite === true) {
170
- return true; // Explicit overwrite, OK.
171
- }
172
- if (overwrite === false) {
173
- throw new Error(`File "${destPath}" already exists. Overwriting disabled (--no-overwrite).`);
174
- }
175
- // Default policy (overwrite undefined).
176
- if (destIsNewer) {
177
- // console.warn(
178
- // // `Destination file "${destPath}" is newer than source. Use --overwrite to force.`,
179
- // //`Warning: destination file "${destPath}" is newer than source. Skipping write. Use --overwrite to force.`,
180
- // `Warning: destination "${destPath}" is newer than source "${srcPath}". Skipping write. Use --overwrite to force.`,
181
- // )
182
- reportAction('skip', destPath, `newer than source "${srcPath}"`);
183
- return false;
184
- }
185
- return true;
186
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yini-cli",
3
- "version": "1.3.1-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",