yini-cli 1.2.0-beta → 1.3.0-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
@@ -5,7 +5,7 @@
5
5
 
6
6
  *YINI is an INI-inspired and human-readable text format for representing structured information. It is designed to be clear, predictable, and easy for humans to read and write. It supports nesting, comments, and a formally defined syntax. It is suitable for configuration files, application settings, and general data-storage use cases.*
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)
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)
9
9
 
10
10
  This tool is designed for teams and developers working with human-edited configuration files who require explicit structure without indentation-based semantics.
11
11
 
@@ -7,6 +7,8 @@ export interface IParseCommandOptions extends IGlobalOptions {
7
7
  json?: boolean;
8
8
  compact?: boolean;
9
9
  js?: boolean;
10
+ yaml?: boolean;
11
+ xml?: boolean;
10
12
  output?: string;
11
13
  bestEffort?: boolean;
12
14
  overwrite?: boolean;
@@ -1,7 +1,9 @@
1
+ // src/commands/parseCommand.ts
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import YINI from 'yini-parser';
4
- import { debugPrint, printObject, toPrettyJS, toPrettyJSON, } from '../utils/print.js';
5
+ import { getSerializer } from '../serializers/index.js';
6
+ import { debugPrint, printObject, } from '../utils/print.js';
5
7
  // -------------------------------------------------------------------------
6
8
  /*
7
9
  TODO / SHOULD-DO:
@@ -41,45 +43,52 @@ import { debugPrint, printObject, toPrettyJS, toPrettyJSON, } from '../utils/pri
41
43
  export const parseFile = (file, commandOptions) => {
42
44
  const outputFile = commandOptions.output || '';
43
45
  // const isStrictMode = !!commandOptions.strict
44
- const outputStyle = resolveOutputStyle(commandOptions);
46
+ const outputFormat = resolveOutputFormat(commandOptions);
45
47
  debugPrint('file = ' + file);
46
48
  debugPrint('output = ' + commandOptions.output);
47
49
  debugPrint('commandOptions:');
48
50
  printObject(commandOptions);
49
- doParseFile(file, commandOptions, outputStyle, outputFile);
51
+ doParseFile(file, commandOptions, outputFormat, outputFile);
50
52
  };
51
- const resolveOutputStyle = (options) => {
53
+ const resolveOutputFormat = (options) => {
52
54
  if (options.js && options.compact) {
53
55
  throw new Error('--js and --compact cannot be combined.');
54
56
  }
55
57
  if (options.compact)
56
- return 'JSON-compact';
58
+ return 'json-compact';
57
59
  if (options.js)
58
- return 'JS-style';
60
+ return 'js';
61
+ if (options.yaml)
62
+ return 'yaml';
63
+ if (options.xml)
64
+ return 'xml';
59
65
  if (options.pretty) {
60
66
  console.warn('Warning: --pretty is deprecated. Use --json instead.');
61
67
  }
62
- return 'Pretty-JSON';
68
+ return 'json';
63
69
  };
64
- const renderOutput = (parsed, style) => {
70
+ /*
71
+ const renderOutput = (parsed: unknown, style: TOutputStyle): string => {
65
72
  switch (style) {
66
73
  case 'JS-style':
67
- return toPrettyJS(parsed);
74
+ return toPrettyJS(parsed)
75
+
68
76
  case 'JSON-compact':
69
- return JSON.stringify(parsed);
77
+ return JSON.stringify(parsed)
78
+
70
79
  case 'Pretty-JSON':
71
80
  default:
72
- return toPrettyJSON(parsed);
81
+ return toPrettyJSON(parsed)
73
82
  }
74
- };
75
- const doParseFile = (file, commandOptions, outputStyle, outputFile = '') => {
76
- // let strictMode = !!commandOptions.strict
83
+ }
84
+ */
85
+ const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
77
86
  let preferredFailLevel = commandOptions.bestEffort
78
87
  ? 'ignore-errors'
79
88
  : 'auto';
80
89
  let includeMetaData = false;
81
90
  debugPrint('File = ' + file);
82
- debugPrint('outputStyle = ' + outputStyle);
91
+ debugPrint('outputFormat = ' + outputFormat);
83
92
  const parseOptions = {
84
93
  strictMode: commandOptions.strict ?? false,
85
94
  failLevel: preferredFailLevel,
@@ -91,7 +100,8 @@ const doParseFile = (file, commandOptions, outputStyle, outputFile = '') => {
91
100
  }
92
101
  try {
93
102
  const parsed = YINI.parseFile(file, parseOptions);
94
- const output = renderOutput(parsed, outputStyle);
103
+ const serializer = getSerializer(outputFormat);
104
+ const output = serializer.serialize(parsed);
95
105
  if (outputFile) {
96
106
  const resolved = path.resolve(outputFile);
97
107
  enforceWritePolicy(file, resolved, commandOptions.overwrite);
@@ -1,7 +1,6 @@
1
- import assert from 'node:assert';
2
1
  import { exit } from 'node:process';
3
2
  import YINI from 'yini-parser';
4
- const IS_DEBUG = true; // For local debugging purposes, etc.
3
+ const IS_DEBUG = false; // For local debugging purposes, etc.
5
4
  /*
6
5
  TODO / SHOULD-DO:
7
6
 
@@ -140,18 +139,8 @@ const IS_DEBUG = true; // For local debugging purposes, etc.
140
139
  export const validateFile = (file, commandOptions = {}) => {
141
140
  let parsedResult = undefined;
142
141
  let isCatchedError = true;
143
- // let failLevel: PreferredFailLevel = 'auto'
144
- // if (commandOptions.failFast) {
145
- // failLevel = 'warnings-and-errors'
146
- // }
147
- // if (commandOptions.bestEffort) {
148
- // failLevel = 'ignore-errors'
149
- // }
150
142
  const parseOptions = {
151
143
  strictMode: commandOptions.strict ?? false,
152
- // failLevel: 'errors',
153
- // failLevel: commandOptions.force ? 'ignore-errors' : 'errors',
154
- // failLevel: 'ignore-errors',
155
144
  failLevel: 'ignore-errors',
156
145
  includeMetadata: true,
157
146
  includeDiagnostics: true,
@@ -163,6 +152,10 @@ export const validateFile = (file, commandOptions = {}) => {
163
152
  }
164
153
  catch (err) {
165
154
  isCatchedError = true;
155
+ if (!commandOptions.silent) {
156
+ const message = err instanceof Error ? err.message : String(err);
157
+ console.error(`Error: ${message}`);
158
+ }
166
159
  }
167
160
  let metadata = null;
168
161
  let errors = 0;
@@ -171,9 +164,13 @@ export const validateFile = (file, commandOptions = {}) => {
171
164
  let infos = 0;
172
165
  if (!isCatchedError && parsedResult?.meta) {
173
166
  metadata = parsedResult?.meta;
174
- assert(metadata); // Make sure there is metadata!
167
+ // assert(metadata) // Make sure there is metadata!
175
168
  // printObject(metadata, true)
176
- assert(metadata.diagnostics);
169
+ // assert(metadata.diagnostics)
170
+ if (!metadata?.diagnostics) {
171
+ console.error('Internal error: Missing diagnostics metadata');
172
+ exit(1);
173
+ }
177
174
  const diag = metadata.diagnostics;
178
175
  errors = diag.errors.errorCount;
179
176
  warnings = diag.warnings.warningCount;
@@ -183,11 +180,10 @@ export const validateFile = (file, commandOptions = {}) => {
183
180
  IS_DEBUG && console.log();
184
181
  IS_DEBUG && console.log('isCatchedError = ' + isCatchedError);
185
182
  IS_DEBUG && console.log('TEMP OUTPUT');
186
- IS_DEBUG && console.log('isCatchedError = ' + isCatchedError);
187
183
  IS_DEBUG && console.log(' errors = ' + errors);
188
184
  IS_DEBUG && console.log('warnings = ' + warnings);
189
185
  IS_DEBUG && console.log(' notices = ' + notices);
190
- IS_DEBUG && console.log(' infor = ' + infos);
186
+ IS_DEBUG && console.log(' infos = ' + infos);
191
187
  IS_DEBUG && console.log('metadata = ' + metadata);
192
188
  IS_DEBUG &&
193
189
  console.log('includeMetadata = ' +
@@ -244,17 +240,18 @@ export const validateFile = (file, commandOptions = {}) => {
244
240
  // if (commandOptions.details) {
245
241
  if (errors || warnings) {
246
242
  if (!metadata) {
247
- console.error('Internal Error: No meta data found');
243
+ console.error('Internal error: No metadata available');
244
+ exit(1);
248
245
  }
249
- assert(metadata); // Make sure there is metadata!
246
+ // assert(metadata) // Make sure there is metadata!
250
247
  console.log();
251
248
  printIssuesFound(file, metadata);
252
249
  }
253
250
  if (commandOptions.stats) {
254
251
  if (!metadata) {
255
- console.error('Internal Error: No meta data found');
252
+ console.error('Internal error: No metadata available');
253
+ exit(1);
256
254
  }
257
- assert(metadata); // Make sure there is metadata!
258
255
  console.log();
259
256
  console.log(formatToStatsReport(file, metadata).trim());
260
257
  }
@@ -278,7 +275,7 @@ mode, errors, warnings, notices, infos) => {
278
275
  break;
279
276
  case 'Passed-with-Warnings':
280
277
  // result = '⚠️ Validation finished'
281
- result = '✔ Validation successful';
278
+ result = '✔ Validation successful (with warnings)';
282
279
  break;
283
280
  case 'Failed':
284
281
  // result = '✖ Validation failed'
@@ -355,7 +352,10 @@ const formatToStatsReport = (fileWithPath, metadata) => {
355
352
  // console.log('formatToStatsReport(..)')
356
353
  // printObject(metadata)
357
354
  // console.log()
358
- assert(metadata.diagnostics);
355
+ if (!metadata?.diagnostics) {
356
+ console.error('Internal error: Missing diagnostics');
357
+ exit(1);
358
+ }
359
359
  const diag = metadata.diagnostics;
360
360
  const issuesCount = diag.errors.errorCount +
361
361
  diag.warnings.warningCount +
@@ -408,7 +408,10 @@ const printIssuesFound = (fileWithPath, metadata) => {
408
408
  // printObject(metadata)
409
409
  // console.log(toPrettyJSON(metadata))
410
410
  // console.log()
411
- assert(metadata.diagnostics);
411
+ if (!metadata?.diagnostics) {
412
+ console.error('Internal error: Missing diagnostics metadata');
413
+ exit(1);
414
+ }
412
415
  const diag = metadata.diagnostics;
413
416
  IS_DEBUG && console.log('*** Issues Found');
414
417
  IS_DEBUG && console.log('*** ------------');
package/dist/index.js CHANGED
@@ -75,6 +75,8 @@ const parseCmd = program
75
75
  .option('--json', 'Output as formatted JSON (default).')
76
76
  .option('--compact', 'Output compact JSON (no whitespace).')
77
77
  .option('--js', 'Output as JavaScript.')
78
+ .option('--yaml', 'Output as YAML.')
79
+ .option('--xml', 'Output as XML.')
78
80
  // File handling options.
79
81
  .option('-o, --output <file>', 'Write output to <file>. By default, an existing file is only overwritten if it is older than the input YINI file.')
80
82
  .option('--overwrite', 'Always overwrite the output file, even if it is newer than the input YINI file.')
@@ -0,0 +1,3 @@
1
+ import { Serializer } from './types.js';
2
+ export type TOutputFormat = 'json' | 'json-compact' | 'js' | 'yaml' | 'xml';
3
+ export declare function getSerializer(format: TOutputFormat): Serializer;
@@ -0,0 +1,53 @@
1
+ // src/serializers/index.ts
2
+ /*
3
+
4
+ NOTE:
5
+
6
+ Supported formats are a small, intentionally selected set of commonly
7
+ used output formats:
8
+
9
+ - JSON: Output as formatted JSON (default) for easy reading and broad
10
+ compatibility with tools and APIs.
11
+ - compact: Output compact JSON (no whitespace) for efficient piping,
12
+ scripting, and smaller output size.
13
+ - JS: Output as JavaScript for direct use in Node.js or embedding the
14
+ result as a JavaScript object.
15
+ - YAML: Output as YAML for environments and tools that commonly use
16
+ YAML-based configuration.
17
+ - XML: Output as XML for interoperability with systems and tooling that
18
+ rely on XML formats.
19
+
20
+ These formats cover the most common scenarios for inspecting parsed YINI data
21
+ and exchanging it with other tools and systems.
22
+
23
+ The CLI focuses on providing a small number of widely useful formats while
24
+ keeping the command simple and predictable.
25
+
26
+ Additional conversions can be implemented in separate tools built on top of
27
+ the YINI CLI — for example a dedicated utility such as yini-convert.
28
+
29
+ This keeps the core CLI focused while allowing additional functionality to be
30
+ added in complementary tools if needed.
31
+
32
+ */
33
+ import { JsonSerializer } from './jsonSerializer.js';
34
+ import { JSSerializer } from './jSSerializer.js';
35
+ import { XmlSerializer } from './xmlSerializer.js';
36
+ import { YamlSerializer } from './yamlSerializer.js';
37
+ export function getSerializer(format) {
38
+ switch (format) {
39
+ case 'json':
40
+ case 'json-compact':
41
+ return new JsonSerializer(format);
42
+ case 'js':
43
+ return new JSSerializer();
44
+ case 'yaml':
45
+ return new YamlSerializer();
46
+ case 'xml':
47
+ return new XmlSerializer();
48
+ default: {
49
+ const exhaustiveCheck = format;
50
+ throw new Error(`Unsupported output format: ${exhaustiveCheck}`);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ import { Serializer } from './types.js';
2
+ export type ToPrettyJSOptions = {
3
+ indent?: number;
4
+ exportDefault?: boolean;
5
+ trailingSemicolon?: boolean;
6
+ };
7
+ export declare class JSSerializer implements Serializer {
8
+ readonly format = "json";
9
+ serialize(data: unknown): string;
10
+ }
11
+ /**
12
+ * A clean and stable toPrettyJS(..) without relying on util.inspect(..).
13
+ * - Deterministic output (unlike util.inspect, which can vary across Node versions).
14
+ * - Exactly 4 spaces.
15
+ * - Clean single quotes.
16
+ * - Proper escaping, and safe quoting of non-identifier keys.
17
+ */
18
+ export declare function getPrettyJS(value: unknown, opts?: ToPrettyJSOptions): string;
@@ -0,0 +1,84 @@
1
+ export class JSSerializer {
2
+ constructor() {
3
+ this.format = 'json';
4
+ }
5
+ serialize(data) {
6
+ return getPrettyJS(data);
7
+ }
8
+ }
9
+ /**
10
+ * A clean and stable toPrettyJS(..) without relying on util.inspect(..).
11
+ * - Deterministic output (unlike util.inspect, which can vary across Node versions).
12
+ * - Exactly 4 spaces.
13
+ * - Clean single quotes.
14
+ * - Proper escaping, and safe quoting of non-identifier keys.
15
+ */
16
+ export function getPrettyJS(value, opts = {}) {
17
+ const indent = opts.indent ?? 4;
18
+ const exportDefault = opts.exportDefault ?? false;
19
+ const trailingSemicolon = opts.trailingSemicolon ?? exportDefault;
20
+ const pad = (level) => ' '.repeat(level * indent);
21
+ const isPlainObject = (v) => v !== null &&
22
+ typeof v === 'object' &&
23
+ Object.getPrototypeOf(v) === Object.prototype;
24
+ const isValidIdentifier = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key);
25
+ const escapeString = (s) => s
26
+ .replace(/\\/g, '\\\\')
27
+ .replace(/'/g, "\\'")
28
+ .replace(/\r/g, '\\r')
29
+ .replace(/\n/g, '\\n')
30
+ .replace(/\t/g, '\\t')
31
+ .replace(/\u2028/g, '\\u2028')
32
+ .replace(/\u2029/g, '\\u2029');
33
+ const formatKey = (key) => isValidIdentifier(key) ? key : `'${escapeString(key)}'`;
34
+ const format = (v, level) => {
35
+ if (v === null)
36
+ return 'null';
37
+ const t = typeof v;
38
+ if (t === 'string')
39
+ return `'${escapeString(v)}'`;
40
+ if (t === 'number')
41
+ return Number.isFinite(v) ? String(v) : 'null';
42
+ if (t === 'boolean')
43
+ return v ? 'true' : 'false';
44
+ if (t === 'bigint')
45
+ return `${v}n`;
46
+ if (t === 'undefined')
47
+ return 'undefined';
48
+ if (t === 'function')
49
+ return '[Function]';
50
+ if (t === 'symbol')
51
+ return 'Symbol()';
52
+ if (v instanceof Date)
53
+ return `'${v.toISOString()}'`;
54
+ if (Array.isArray(v)) {
55
+ if (v.length === 0)
56
+ return '[]';
57
+ const inner = v
58
+ .map((item) => `${pad(level + 1)}${format(item, level + 1)}`)
59
+ .join(',\n');
60
+ return `[\n${inner}\n${pad(level)}]`;
61
+ }
62
+ if (isPlainObject(v)) {
63
+ const keys = Object.keys(v);
64
+ if (keys.length === 0)
65
+ return '{}';
66
+ const inner = keys
67
+ .map((k) => `${pad(level + 1)}${formatKey(k)}: ${format(v[k], level + 1)}`)
68
+ .join(',\n');
69
+ return `{\n${inner}\n${pad(level)}}`;
70
+ }
71
+ // Fallback for Map/Set/class instances etc.
72
+ try {
73
+ // JSON fallback keeps it stable-ish for "weird" objects
74
+ return `'${escapeString(JSON.stringify(v))}'`;
75
+ }
76
+ catch {
77
+ return "'[Unserializable]'";
78
+ }
79
+ };
80
+ const body = format(value, 0);
81
+ if (!exportDefault)
82
+ return body;
83
+ return `export default ${body}${trailingSemicolon ? ';' : ''}\n`;
84
+ }
@@ -0,0 +1,7 @@
1
+ import { Serializer } from './types.js';
2
+ export declare class JsonSerializer implements Serializer {
3
+ readonly format: 'json' | 'json-compact';
4
+ constructor(format?: 'json' | 'json-compact');
5
+ serialize(data: unknown): string;
6
+ }
7
+ export declare const getPrettyJSON: (obj: unknown) => string;
@@ -0,0 +1,14 @@
1
+ export class JsonSerializer {
2
+ constructor(format = 'json') {
3
+ this.format = format;
4
+ }
5
+ serialize(data) {
6
+ return this.format === 'json'
7
+ ? getPrettyJSON(data)
8
+ : JSON.stringify(data); // Compact.
9
+ }
10
+ }
11
+ export const getPrettyJSON = (obj) => {
12
+ const str = JSON.stringify(obj, null, 4);
13
+ return str;
14
+ };
@@ -0,0 +1,4 @@
1
+ export interface Serializer {
2
+ readonly format: string;
3
+ serialize(data: unknown): string;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { Serializer } from './types.js';
2
+ export declare class XmlSerializer implements Serializer {
3
+ readonly format = "xml";
4
+ serialize(data: unknown): string;
5
+ }
@@ -0,0 +1,12 @@
1
+ // src/serializers/xmlSerializer.ts
2
+ import { create } from 'xmlbuilder2';
3
+ export class XmlSerializer {
4
+ constructor() {
5
+ this.format = 'xml';
6
+ }
7
+ serialize(data) {
8
+ const plainJSON = JSON.parse(JSON.stringify(data));
9
+ const doc = create({ version: '1.0' }).ele({ yini: plainJSON });
10
+ return doc.end({ prettyPrint: true });
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import { Serializer } from './types.js';
2
+ export declare class YamlSerializer implements Serializer {
3
+ readonly format = "yaml";
4
+ serialize(data: unknown): string;
5
+ }
@@ -0,0 +1,9 @@
1
+ import YAML from 'yaml';
2
+ export class YamlSerializer {
3
+ constructor() {
4
+ this.format = 'yaml';
5
+ }
6
+ serialize(data) {
7
+ return YAML.stringify(data);
8
+ }
9
+ }
@@ -1,8 +1,4 @@
1
- type ToPrettyJSOptions = {
2
- indent?: number;
3
- exportDefault?: boolean;
4
- trailingSemicolon?: boolean;
5
- };
1
+ import { ToPrettyJSOptions } from '../serializers/jSSerializer.js';
6
2
  export declare const debugPrint: (str?: any) => void;
7
3
  export declare const devPrint: (str?: any) => void;
8
4
  export declare const toJSON: (obj: any) => string;
@@ -19,12 +15,4 @@ export declare const printJSON: (obj: any, isForce?: boolean) => void;
19
15
  * @note This function relies on util.inspect(..).
20
16
  */
21
17
  export declare const printObject: (obj: any, isForce?: boolean, isColors?: boolean) => void;
22
- /**
23
- * A clean and stable toPrettyJS(..) without relying on util.inspect(..).
24
- * - Deterministic output (unlike util.inspect, which can vary across Node versions).
25
- * - Exactly 4 spaces.
26
- * - Clean single quotes.
27
- * - Proper escaping, and safe quoting of non-identifier keys.
28
- */
29
18
  export declare function toPrettyJS(value: unknown, opts?: ToPrettyJSOptions): string;
30
- export {};
@@ -4,6 +4,13 @@
4
4
  */
5
5
  import util from 'util';
6
6
  import { isDebug, isDev, isProdEnv, isTestEnv } from '../config/env.js';
7
+ import { getPrettyJSON } from '../serializers/jsonSerializer.js';
8
+ import { getPrettyJS } from '../serializers/jSSerializer.js';
9
+ // type ToPrettyJSOptions = {
10
+ // indent?: number // default: 4
11
+ // exportDefault?: boolean // default: false -> if true: "export default ...;"
12
+ // trailingSemicolon?: boolean // default: true when exportDefault
13
+ // }
7
14
  export const debugPrint = (str = '') => {
8
15
  isDebug() && console.debug('DEBUG: ' + str);
9
16
  };
@@ -15,8 +22,9 @@ export const toJSON = (obj) => {
15
22
  return str;
16
23
  };
17
24
  export const toPrettyJSON = (obj) => {
18
- const str = JSON.stringify(obj, null, 4);
19
- return str;
25
+ // const str = JSON.stringify(obj, null, 4)
26
+ // return str
27
+ return getPrettyJSON(obj);
20
28
  };
21
29
  /** Pretty-prints a JavaScript object as formatted JSON to the console.
22
30
  * Strict JSON, all keys are enclosed in ", etc.
@@ -43,79 +51,79 @@ export const printObject = (obj, isForce = false, isColors = true) => {
43
51
  }
44
52
  console.log(util.inspect(obj, { depth: null, colors: isColors }));
45
53
  };
46
- /**
47
- * A clean and stable toPrettyJS(..) without relying on util.inspect(..).
48
- * - Deterministic output (unlike util.inspect, which can vary across Node versions).
49
- * - Exactly 4 spaces.
50
- * - Clean single quotes.
51
- * - Proper escaping, and safe quoting of non-identifier keys.
52
- */
54
+ // /**
55
+ // * A clean and stable toPrettyJS(..) without relying on util.inspect(..).
56
+ // * - Deterministic output (unlike util.inspect, which can vary across Node versions).
57
+ // * - Exactly 4 spaces.
58
+ // * - Clean single quotes.
59
+ // * - Proper escaping, and safe quoting of non-identifier keys.
60
+ // */
61
+ // export function toPrettyJS(
62
+ // value: unknown,
63
+ // opts: ToPrettyJSOptions = {},
64
+ // ): string {
65
+ // const indent = opts.indent ?? 4
66
+ // const exportDefault = opts.exportDefault ?? false
67
+ // const trailingSemicolon = opts.trailingSemicolon ?? exportDefault
68
+ // const pad = (level: number) => ' '.repeat(level * indent)
69
+ // const isPlainObject = (v: any): v is Record<string, unknown> =>
70
+ // v !== null &&
71
+ // typeof v === 'object' &&
72
+ // Object.getPrototypeOf(v) === Object.prototype
73
+ // const isValidIdentifier = (key: string) =>
74
+ // /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
75
+ // const escapeString = (s: string) =>
76
+ // s
77
+ // .replace(/\\/g, '\\\\')
78
+ // .replace(/'/g, "\\'")
79
+ // .replace(/\r/g, '\\r')
80
+ // .replace(/\n/g, '\\n')
81
+ // .replace(/\t/g, '\\t')
82
+ // .replace(/\u2028/g, '\\u2028')
83
+ // .replace(/\u2029/g, '\\u2029')
84
+ // const formatKey = (key: string) =>
85
+ // isValidIdentifier(key) ? key : `'${escapeString(key)}'`
86
+ // const format = (v: unknown, level: number): string => {
87
+ // if (v === null) return 'null'
88
+ // const t = typeof v
89
+ // if (t === 'string') return `'${escapeString(v as string)}'`
90
+ // if (t === 'number') return Number.isFinite(v) ? String(v) : 'null'
91
+ // if (t === 'boolean') return v ? 'true' : 'false'
92
+ // if (t === 'bigint') return `${v}n`
93
+ // if (t === 'undefined') return 'undefined'
94
+ // if (t === 'function') return '[Function]'
95
+ // if (t === 'symbol') return 'Symbol()'
96
+ // if (v instanceof Date) return `'${v.toISOString()}'`
97
+ // if (Array.isArray(v)) {
98
+ // if (v.length === 0) return '[]'
99
+ // const inner = v
100
+ // .map((item) => `${pad(level + 1)}${format(item, level + 1)}`)
101
+ // .join(',\n')
102
+ // return `[\n${inner}\n${pad(level)}]`
103
+ // }
104
+ // if (isPlainObject(v)) {
105
+ // const keys = Object.keys(v)
106
+ // if (keys.length === 0) return '{}'
107
+ // const inner = keys
108
+ // .map(
109
+ // (k) =>
110
+ // `${pad(level + 1)}${formatKey(k)}: ${format((v as any)[k], level + 1)}`,
111
+ // )
112
+ // .join(',\n')
113
+ // return `{\n${inner}\n${pad(level)}}`
114
+ // }
115
+ // // Fallback for Map/Set/class instances etc.
116
+ // try {
117
+ // // JSON fallback keeps it stable-ish for "weird" objects
118
+ // return `'${escapeString(JSON.stringify(v))}'`
119
+ // } catch {
120
+ // return "'[Unserializable]'"
121
+ // }
122
+ // }
123
+ // const body = format(value, 0)
124
+ // if (!exportDefault) return body
125
+ // return `export default ${body}${trailingSemicolon ? ';' : ''}\n`
126
+ // }
53
127
  export function toPrettyJS(value, opts = {}) {
54
- const indent = opts.indent ?? 4;
55
- const exportDefault = opts.exportDefault ?? false;
56
- const trailingSemicolon = opts.trailingSemicolon ?? exportDefault;
57
- const pad = (level) => ' '.repeat(level * indent);
58
- const isPlainObject = (v) => v !== null &&
59
- typeof v === 'object' &&
60
- Object.getPrototypeOf(v) === Object.prototype;
61
- const isValidIdentifier = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key);
62
- const escapeString = (s) => s
63
- .replace(/\\/g, '\\\\')
64
- .replace(/'/g, "\\'")
65
- .replace(/\r/g, '\\r')
66
- .replace(/\n/g, '\\n')
67
- .replace(/\t/g, '\\t')
68
- .replace(/\u2028/g, '\\u2028')
69
- .replace(/\u2029/g, '\\u2029');
70
- const formatKey = (key) => isValidIdentifier(key) ? key : `'${escapeString(key)}'`;
71
- const format = (v, level) => {
72
- if (v === null)
73
- return 'null';
74
- const t = typeof v;
75
- if (t === 'string')
76
- return `'${escapeString(v)}'`;
77
- if (t === 'number')
78
- return Number.isFinite(v) ? String(v) : 'null';
79
- if (t === 'boolean')
80
- return v ? 'true' : 'false';
81
- if (t === 'bigint')
82
- return `${v}n`;
83
- if (t === 'undefined')
84
- return 'undefined';
85
- if (t === 'function')
86
- return '[Function]';
87
- if (t === 'symbol')
88
- return 'Symbol()';
89
- if (v instanceof Date)
90
- return `'${v.toISOString()}'`;
91
- if (Array.isArray(v)) {
92
- if (v.length === 0)
93
- return '[]';
94
- const inner = v
95
- .map((item) => `${pad(level + 1)}${format(item, level + 1)}`)
96
- .join(',\n');
97
- return `[\n${inner}\n${pad(level)}]`;
98
- }
99
- if (isPlainObject(v)) {
100
- const keys = Object.keys(v);
101
- if (keys.length === 0)
102
- return '{}';
103
- const inner = keys
104
- .map((k) => `${pad(level + 1)}${formatKey(k)}: ${format(v[k], level + 1)}`)
105
- .join(',\n');
106
- return `{\n${inner}\n${pad(level)}}`;
107
- }
108
- // Fallback for Map/Set/class instances etc.
109
- try {
110
- // JSON fallback keeps it stable-ish for "weird" objects
111
- return `'${escapeString(JSON.stringify(v))}'`;
112
- }
113
- catch {
114
- return "'[Unserializable]'";
115
- }
116
- };
117
- const body = format(value, 0);
118
- if (!exportDefault)
119
- return body;
120
- return `export default ${body}${trailingSemicolon ? ';' : ''}\n`;
128
+ return getPrettyJS(value);
121
129
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yini-cli",
3
- "version": "1.2.0-beta",
4
- "description": "CLI tool for YINI text-based format - an INI-like config format with real structure, comments, nested sections, no YAML foot-guns, and less noise than JSON.",
3
+ "version": "1.3.0-beta",
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",
7
7
  "cli",
@@ -72,7 +72,9 @@
72
72
  "author": "Marko K. Seppänen",
73
73
  "dependencies": {
74
74
  "commander": "^14.0.1",
75
- "yini-parser": "^1.3.2-beta"
75
+ "xmlbuilder2": "^4.0.3",
76
+ "yaml": "^2.8.2",
77
+ "yini-parser": "^1.4.0-beta"
76
78
  },
77
79
  "devDependencies": {
78
80
  "@eslint/js": "^9.31.0",