yini-cli 1.2.1-beta → 1.3.1-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 +2 -0
- package/dist/commands/parseCommand.js +82 -27
- package/dist/index.js +2 -0
- package/dist/serializers/index.d.ts +3 -0
- package/dist/serializers/index.js +53 -0
- package/dist/serializers/jSSerializer.d.ts +18 -0
- package/dist/serializers/jSSerializer.js +84 -0
- package/dist/serializers/jsonSerializer.d.ts +7 -0
- package/dist/serializers/jsonSerializer.js +14 -0
- package/dist/serializers/types.d.ts +4 -0
- package/dist/serializers/types.js +1 -0
- package/dist/serializers/xmlSerializer.d.ts +5 -0
- package/dist/serializers/xmlSerializer.js +12 -0
- package/dist/serializers/yamlSerializer.d.ts +5 -0
- package/dist/serializers/yamlSerializer.js +9 -0
- package/dist/utils/print.d.ts +1 -13
- package/dist/utils/print.js +84 -76
- package/package.json +5 -3
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
|
-
[](https://www.npmjs.com/package/yini-cli) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/yini-cli) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml) [](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
|
|
|
@@ -1,8 +1,25 @@
|
|
|
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 {
|
|
5
|
+
import { getSerializer } from '../serializers/index.js';
|
|
6
|
+
import { debugPrint, printObject } from '../utils/print.js';
|
|
5
7
|
// -------------------------------------------------------------------------
|
|
8
|
+
const reportAction = (action, file, reason) => {
|
|
9
|
+
let txt = '';
|
|
10
|
+
if (reason) {
|
|
11
|
+
txt = `${action.padEnd(6)} "${file}" (${reason})`;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
txt = `${action.padEnd(6)} "${file}"`;
|
|
15
|
+
}
|
|
16
|
+
if (action === 'skip') {
|
|
17
|
+
console.warn(txt);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(txt);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
6
23
|
/*
|
|
7
24
|
TODO / SHOULD-DO:
|
|
8
25
|
|
|
@@ -41,64 +58,94 @@ import { debugPrint, printObject, toPrettyJS, toPrettyJSON, } from '../utils/pri
|
|
|
41
58
|
export const parseFile = (file, commandOptions) => {
|
|
42
59
|
const outputFile = commandOptions.output || '';
|
|
43
60
|
// const isStrictMode = !!commandOptions.strict
|
|
44
|
-
const
|
|
61
|
+
const outputFormat = resolveOutputFormat(commandOptions);
|
|
45
62
|
debugPrint('file = ' + file);
|
|
46
63
|
debugPrint('output = ' + commandOptions.output);
|
|
47
64
|
debugPrint('commandOptions:');
|
|
48
65
|
printObject(commandOptions);
|
|
49
|
-
doParseFile(file, commandOptions,
|
|
66
|
+
doParseFile(file, commandOptions, outputFormat, outputFile);
|
|
50
67
|
};
|
|
51
|
-
const
|
|
68
|
+
const resolveOutputFormat = (options) => {
|
|
52
69
|
if (options.js && options.compact) {
|
|
53
70
|
throw new Error('--js and --compact cannot be combined.');
|
|
54
71
|
}
|
|
55
72
|
if (options.compact)
|
|
56
|
-
return '
|
|
73
|
+
return 'json-compact';
|
|
57
74
|
if (options.js)
|
|
58
|
-
return '
|
|
75
|
+
return 'js';
|
|
76
|
+
if (options.yaml)
|
|
77
|
+
return 'yaml';
|
|
78
|
+
if (options.xml)
|
|
79
|
+
return 'xml';
|
|
59
80
|
if (options.pretty) {
|
|
60
81
|
console.warn('Warning: --pretty is deprecated. Use --json instead.');
|
|
61
82
|
}
|
|
62
|
-
return '
|
|
83
|
+
return 'json';
|
|
63
84
|
};
|
|
64
|
-
|
|
85
|
+
/*
|
|
86
|
+
const renderOutput = (parsed: unknown, style: TOutputStyle): string => {
|
|
65
87
|
switch (style) {
|
|
66
88
|
case 'JS-style':
|
|
67
|
-
return toPrettyJS(parsed)
|
|
89
|
+
return toPrettyJS(parsed)
|
|
90
|
+
|
|
68
91
|
case 'JSON-compact':
|
|
69
|
-
return JSON.stringify(parsed)
|
|
92
|
+
return JSON.stringify(parsed)
|
|
93
|
+
|
|
70
94
|
case 'Pretty-JSON':
|
|
71
95
|
default:
|
|
72
|
-
return toPrettyJSON(parsed)
|
|
96
|
+
return toPrettyJSON(parsed)
|
|
73
97
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
}
|
|
99
|
+
*/
|
|
100
|
+
const doParseFile = (file, commandOptions, outputFormat, outputFile = '') => {
|
|
77
101
|
let preferredFailLevel = commandOptions.bestEffort
|
|
78
102
|
? 'ignore-errors'
|
|
79
103
|
: 'auto';
|
|
80
104
|
let includeMetaData = false;
|
|
81
105
|
debugPrint('File = ' + file);
|
|
82
|
-
debugPrint('
|
|
106
|
+
debugPrint('outputFormat = ' + outputFormat);
|
|
83
107
|
const parseOptions = {
|
|
84
108
|
strictMode: commandOptions.strict ?? false,
|
|
85
|
-
failLevel:
|
|
86
|
-
includeMetadata:
|
|
109
|
+
failLevel: commandOptions.bestEffort ? 'ignore-errors' : 'auto',
|
|
110
|
+
includeMetadata: false,
|
|
87
111
|
};
|
|
88
112
|
// If --best-effort then override fail-level.
|
|
89
|
-
if (commandOptions.bestEffort) {
|
|
90
|
-
|
|
91
|
-
}
|
|
113
|
+
// if (commandOptions.bestEffort) {
|
|
114
|
+
// parseOptions.failLevel = 'ignore-errors'
|
|
115
|
+
// }
|
|
92
116
|
try {
|
|
93
117
|
const parsed = YINI.parseFile(file, parseOptions);
|
|
94
|
-
const
|
|
118
|
+
const serializer = getSerializer(outputFormat);
|
|
119
|
+
const output = serializer.serialize(parsed);
|
|
95
120
|
if (outputFile) {
|
|
96
121
|
const resolved = path.resolve(outputFile);
|
|
97
|
-
enforceWritePolicy(file, resolved, commandOptions.overwrite);
|
|
122
|
+
const canWrite = enforceWritePolicy(file, resolved, commandOptions.overwrite);
|
|
123
|
+
if (!canWrite) {
|
|
124
|
+
if (commandOptions.verbose) {
|
|
125
|
+
console.log(`skip Skipping write to "${resolved}"`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
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
|
+
}
|
|
98
144
|
// Write JSON output to file instead of stdout.
|
|
99
145
|
fs.writeFileSync(resolved, output, 'utf-8');
|
|
100
146
|
if (commandOptions.verbose) {
|
|
101
|
-
console.log(`Output written to file: "${outputFile}"`)
|
|
147
|
+
// console.log(`write Output written to file: "${outputFile}"`)
|
|
148
|
+
reportAction('write', resolved);
|
|
102
149
|
}
|
|
103
150
|
}
|
|
104
151
|
else {
|
|
@@ -113,19 +160,27 @@ const doParseFile = (file, commandOptions, outputStyle, outputFile = '') => {
|
|
|
113
160
|
};
|
|
114
161
|
const enforceWritePolicy = (srcPath, destPath, overwrite) => {
|
|
115
162
|
if (!fs.existsSync(destPath)) {
|
|
116
|
-
return; // File does not exist, OK to write.
|
|
163
|
+
return true; // File does not exist, OK to write.
|
|
117
164
|
}
|
|
118
165
|
const srcStat = fs.statSync(srcPath);
|
|
119
166
|
const destStat = fs.statSync(destPath);
|
|
120
|
-
|
|
167
|
+
// Only strictly newer triggers skip overwrite.
|
|
168
|
+
const destIsNewer = destStat.mtimeMs > srcStat.mtimeMs;
|
|
121
169
|
if (overwrite === true) {
|
|
122
|
-
return; // Explicit overwrite, OK.
|
|
170
|
+
return true; // Explicit overwrite, OK.
|
|
123
171
|
}
|
|
124
172
|
if (overwrite === false) {
|
|
125
173
|
throw new Error(`File "${destPath}" already exists. Overwriting disabled (--no-overwrite).`);
|
|
126
174
|
}
|
|
127
175
|
// Default policy (overwrite undefined).
|
|
128
176
|
if (destIsNewer) {
|
|
129
|
-
|
|
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;
|
|
130
184
|
}
|
|
185
|
+
return true;
|
|
131
186
|
};
|
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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|
package/dist/utils/print.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
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 {};
|
package/dist/utils/print.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "CLI tool for YINI
|
|
3
|
+
"version": "1.3.1-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
|
-
"
|
|
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",
|