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 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
- [![npm version](https://img.shields.io/npm/v/yini-cli.svg)](https://www.npmjs.com/package/yini-cli) [![All Test Suites](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [![Regression Tests](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [![CLI Test CI](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml) [![npm downloads](https://img.shields.io/npm/dm/yini-cli)](https://www.npmjs.com/package/yini-cli)
8
+ [![npm version](https://img.shields.io/npm/v/yini-cli.svg)](https://www.npmjs.com/package/yini-cli) [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![All Test Suites](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [![Regression Tests](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [![CLI Test CI](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml) [![npm downloads](https://img.shields.io/npm/dm/yini-cli)](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 = (file, optionOverwrite,
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
- let txt = '';
55
- if (reason) {
56
- txt = `${action.padEnd(6)} "${file}" (${reason})`;
57
- }
58
- else {
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
- console.warn(txt);
63
+ if (!options.quiet) {
64
+ console.warn(txt);
65
+ }
63
66
  }
64
67
  else {
65
- console.log(txt);
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 outp
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
- if (options.js && options.compact) {
115
- throw new Error('--js and --compact cannot be combined.');
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
- console.warn('Warning: --pretty is deprecated. Use --json instead.');
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
- if (errorCount > 0 &&
178
- !commandOptions.bestEffort &&
179
- (commandOptions.strict || !hasUsableOutput)) {
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
- console.log(output);
208
+ printStdout(commandOptions, output);
196
209
  }
197
210
  }
198
211
  catch (err) {
199
212
  const message = err instanceof Error ? err.message : String(err);
200
- console.error(`Error: ${message}`);
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 validateFile: (file: string, commandOptions?: IValidateCommandOptions) => never;
10
+ export declare const validateTargets: (targets: string[], options: IValidateCommandOptions) => never;