icore 2.0.0 → 2.0.1
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/CHANGELOG.md +13 -0
- package/dist/argv/parser.js +10 -4
- package/dist/options/parser.js +14 -11
- package/dist/options/schema.js +2 -1
- package/dist/output/text-writer.d.ts +3 -2
- package/dist/output/text-writer.js +42 -2
- package/dist/presentation/renderers/csv.d.ts +1 -1
- package/dist/presentation/renderers/csv.js +2 -2
- package/dist/presentation/renderers/json.d.ts +2 -0
- package/dist/presentation/renderers/json.js +7 -1
- package/dist/presentation/result-renderer.js +28 -3
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,19 @@ Version boundaries from `1.0.12` through `1.0.19` were checked against npm
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Built package output automatically before npm and Yarn packing.
|
|
20
|
+
- Preserved option names that overlap properties inherited from
|
|
21
|
+
`Object.prototype`.
|
|
22
|
+
- Rejected malformed nested records, table rows, and CSV rows in presentation
|
|
23
|
+
result guards.
|
|
24
|
+
- Quoted CSV cells containing carriage returns.
|
|
25
|
+
- Rejected backpressured writes when EventEmitter-compatible sinks error or
|
|
26
|
+
close before `drain`.
|
|
27
|
+
- Rejected top-level values without a JSON representation instead of emitting
|
|
28
|
+
invalid JSON text.
|
|
29
|
+
|
|
17
30
|
## [2.0.0] - 2026-07-14
|
|
18
31
|
|
|
19
32
|
### Added
|
package/dist/argv/parser.js
CHANGED
|
@@ -18,7 +18,7 @@ const icore_error_1 = require("../errors/icore-error");
|
|
|
18
18
|
*/
|
|
19
19
|
function parseArgv(args, schema) {
|
|
20
20
|
const positionals = [];
|
|
21
|
-
const options =
|
|
21
|
+
const options = Object.create(null);
|
|
22
22
|
const aliases = buildShortAliasMap(schema);
|
|
23
23
|
let parseOptions = true;
|
|
24
24
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -68,10 +68,10 @@ function parseArgv(args, schema) {
|
|
|
68
68
|
if (name === '') {
|
|
69
69
|
throw createUnexpectedArgumentError(arg);
|
|
70
70
|
}
|
|
71
|
-
const definition = schema
|
|
71
|
+
const definition = getOwnOptionDefinition(schema, name);
|
|
72
72
|
if (separatorIndex === -1 && definition === undefined && name.startsWith('no-')) {
|
|
73
73
|
const negatedName = name.slice(3);
|
|
74
|
-
const negatedDefinition = schema
|
|
74
|
+
const negatedDefinition = getOwnOptionDefinition(schema, negatedName);
|
|
75
75
|
if (negatedDefinition?.type === 'boolean') {
|
|
76
76
|
if (Object.hasOwn(options, negatedName)) {
|
|
77
77
|
throw createDuplicateArgumentError(negatedName);
|
|
@@ -106,9 +106,15 @@ function parseArgv(args, schema) {
|
|
|
106
106
|
}
|
|
107
107
|
return {
|
|
108
108
|
positionals,
|
|
109
|
-
options
|
|
109
|
+
options: { ...options }
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
function getOwnOptionDefinition(schema, name) {
|
|
113
|
+
if (schema === undefined || !Object.hasOwn(schema, name)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return schema[name];
|
|
117
|
+
}
|
|
112
118
|
function buildShortAliasMap(schema) {
|
|
113
119
|
const aliases = new Map();
|
|
114
120
|
if (schema === undefined) {
|
package/dist/options/parser.js
CHANGED
|
@@ -26,8 +26,8 @@ function parseOptions(schema, values) {
|
|
|
26
26
|
* metadata.
|
|
27
27
|
*/
|
|
28
28
|
function parseOptionsDetailed(schema, values) {
|
|
29
|
-
const parsed =
|
|
30
|
-
const provided =
|
|
29
|
+
const parsed = Object.create(null);
|
|
30
|
+
const provided = Object.create(null);
|
|
31
31
|
for (const name of Object.keys(values)) {
|
|
32
32
|
if (!Object.hasOwn(schema, name)) {
|
|
33
33
|
throw createUnexpectedArgumentError(name);
|
|
@@ -35,27 +35,30 @@ function parseOptionsDetailed(schema, values) {
|
|
|
35
35
|
}
|
|
36
36
|
for (const name of Object.keys(schema)) {
|
|
37
37
|
const definition = schema[name];
|
|
38
|
-
const
|
|
38
|
+
const optionName = String(name);
|
|
39
|
+
const value = Object.hasOwn(values, optionName)
|
|
40
|
+
? values[optionName]
|
|
41
|
+
: undefined;
|
|
39
42
|
if (definition === undefined) {
|
|
40
43
|
continue;
|
|
41
44
|
}
|
|
42
45
|
provided[name] = value !== undefined;
|
|
43
46
|
if (value === undefined) {
|
|
44
47
|
if ('default' in definition) {
|
|
45
|
-
parsed[name] = parseDefaultOptionValue(
|
|
48
|
+
parsed[name] = parseDefaultOptionValue(optionName, definition);
|
|
46
49
|
continue;
|
|
47
50
|
}
|
|
48
51
|
if (definition.required === true) {
|
|
49
|
-
throw createExpectedRequiredArgumentError(
|
|
52
|
+
throw createExpectedRequiredArgumentError(optionName);
|
|
50
53
|
}
|
|
51
54
|
parsed[name] = undefined;
|
|
52
55
|
continue;
|
|
53
56
|
}
|
|
54
|
-
parsed[name] = parseOptionValue(
|
|
57
|
+
parsed[name] = parseOptionValue(optionName, definition, value);
|
|
55
58
|
}
|
|
56
59
|
return {
|
|
57
|
-
options: parsed,
|
|
58
|
-
provided: provided
|
|
60
|
+
options: { ...parsed },
|
|
61
|
+
provided: { ...provided }
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
/**
|
|
@@ -63,8 +66,8 @@ function parseOptionsDetailed(schema, values) {
|
|
|
63
66
|
* remaining raw options untouched.
|
|
64
67
|
*/
|
|
65
68
|
function parseOptionsSubsetDetailed(schema, values) {
|
|
66
|
-
const subsetValues =
|
|
67
|
-
const rest =
|
|
69
|
+
const subsetValues = Object.create(null);
|
|
70
|
+
const rest = Object.create(null);
|
|
68
71
|
for (const [name, value] of Object.entries(values)) {
|
|
69
72
|
if (Object.hasOwn(schema, name)) {
|
|
70
73
|
subsetValues[name] = value;
|
|
@@ -74,7 +77,7 @@ function parseOptionsSubsetDetailed(schema, values) {
|
|
|
74
77
|
}
|
|
75
78
|
return {
|
|
76
79
|
...parseOptionsDetailed(schema, subsetValues),
|
|
77
|
-
rest
|
|
80
|
+
rest: { ...rest }
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
function parseOptionValue(name, definition, value) {
|
package/dist/options/schema.js
CHANGED
|
@@ -19,5 +19,6 @@ exports.mergeOptionsSchema = mergeOptionsSchema;
|
|
|
19
19
|
* Later schemas override earlier schemas with the same option name.
|
|
20
20
|
*/
|
|
21
21
|
function mergeOptionsSchema(schema, ...schemas) {
|
|
22
|
-
|
|
22
|
+
const merged = Object.assign(Object.create(null), schema, ...schemas);
|
|
23
|
+
return { ...merged };
|
|
23
24
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Allowed here:
|
|
6
6
|
* - the minimal text writer contract;
|
|
7
7
|
* - promise-returning sink support;
|
|
8
|
-
* - Node-style
|
|
8
|
+
* - Node-style backpressure and writable lifecycle handling;
|
|
9
9
|
*
|
|
10
10
|
* This file must not contain stdout/stderr selection or command semantics.
|
|
11
11
|
*/
|
|
@@ -24,6 +24,7 @@ export type BackpressureTextSink = {
|
|
|
24
24
|
* Creates a text writer that preserves writable-stream backpressure.
|
|
25
25
|
*
|
|
26
26
|
* Promise-returning sinks are awaited, and Node-style sinks returning `false`
|
|
27
|
-
* are resumed after their next `drain` event.
|
|
27
|
+
* are resumed after their next `drain` event. EventEmitter-compatible sinks
|
|
28
|
+
* reject when they emit `error` or `close` before draining.
|
|
28
29
|
*/
|
|
29
30
|
export declare function createBackpressureTextWriter(sink: BackpressureTextSink): TextWriter;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Allowed here:
|
|
7
7
|
* - the minimal text writer contract;
|
|
8
8
|
* - promise-returning sink support;
|
|
9
|
-
* - Node-style
|
|
9
|
+
* - Node-style backpressure and writable lifecycle handling;
|
|
10
10
|
*
|
|
11
11
|
* This file must not contain stdout/stderr selection or command semantics.
|
|
12
12
|
*/
|
|
@@ -16,7 +16,8 @@ exports.createBackpressureTextWriter = createBackpressureTextWriter;
|
|
|
16
16
|
* Creates a text writer that preserves writable-stream backpressure.
|
|
17
17
|
*
|
|
18
18
|
* Promise-returning sinks are awaited, and Node-style sinks returning `false`
|
|
19
|
-
* are resumed after their next `drain` event.
|
|
19
|
+
* are resumed after their next `drain` event. EventEmitter-compatible sinks
|
|
20
|
+
* reject when they emit `error` or `close` before draining.
|
|
20
21
|
*/
|
|
21
22
|
function createBackpressureTextWriter(sink) {
|
|
22
23
|
return {
|
|
@@ -32,6 +33,10 @@ function createBackpressureTextWriter(sink) {
|
|
|
32
33
|
if (typeof sink.once !== 'function') {
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
36
|
+
if (isEventedBackpressureTextSink(sink)) {
|
|
37
|
+
await waitForEventedDrain(sink);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
35
40
|
await waitForDrain(sink.once.bind(sink));
|
|
36
41
|
}
|
|
37
42
|
};
|
|
@@ -47,3 +52,38 @@ function waitForDrain(onceDrain) {
|
|
|
47
52
|
onceDrain('drain', resolve);
|
|
48
53
|
});
|
|
49
54
|
}
|
|
55
|
+
function waitForEventedDrain(sink) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let settled = false;
|
|
58
|
+
function cleanup() {
|
|
59
|
+
sink.off('drain', handleDrain);
|
|
60
|
+
sink.off('error', handleError);
|
|
61
|
+
sink.off('close', handleClose);
|
|
62
|
+
}
|
|
63
|
+
function settle(action) {
|
|
64
|
+
if (settled) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
settled = true;
|
|
68
|
+
cleanup();
|
|
69
|
+
action();
|
|
70
|
+
}
|
|
71
|
+
function handleDrain() {
|
|
72
|
+
settle(resolve);
|
|
73
|
+
}
|
|
74
|
+
function handleError(error) {
|
|
75
|
+
settle(() => reject(error));
|
|
76
|
+
}
|
|
77
|
+
function handleClose() {
|
|
78
|
+
settle(() => reject(new Error('Writable sink closed before drain')));
|
|
79
|
+
}
|
|
80
|
+
sink.once('error', handleError);
|
|
81
|
+
sink.once('close', handleClose);
|
|
82
|
+
sink.once('drain', handleDrain);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function isEventedBackpressureTextSink(sink) {
|
|
86
|
+
const candidate = sink;
|
|
87
|
+
return (typeof sink.once === 'function'
|
|
88
|
+
&& typeof candidate.off === 'function');
|
|
89
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Allowed here:
|
|
6
6
|
* - CSV row joining;
|
|
7
|
-
* - CSV cell escaping for comma, quote, and newline values;
|
|
7
|
+
* - CSV cell escaping for comma, quote, carriage return, and newline values;
|
|
8
8
|
*
|
|
9
9
|
* This file must not contain application report mapping.
|
|
10
10
|
*/
|
|
@@ -28,7 +28,7 @@ function renderCsv(rows) {
|
|
|
28
28
|
}
|
|
29
29
|
function renderCsvValue(value) {
|
|
30
30
|
const text = String(value);
|
|
31
|
-
if (!/[",\n]/.test(text)) {
|
|
31
|
+
if (!/[",\r\n]/.test(text)) {
|
|
32
32
|
return text;
|
|
33
33
|
}
|
|
34
34
|
return `"${text.replaceAll('"', '""')}"`;
|
|
@@ -12,7 +12,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.renderJson = renderJson;
|
|
13
13
|
/**
|
|
14
14
|
* Renders an already prepared value as pretty JSON with a trailing newline.
|
|
15
|
+
*
|
|
16
|
+
* Throws when the value has no top-level JSON representation.
|
|
15
17
|
*/
|
|
16
18
|
function renderJson(value) {
|
|
17
|
-
|
|
19
|
+
const json = JSON.stringify(value, null, 2);
|
|
20
|
+
if (json === undefined) {
|
|
21
|
+
throw new TypeError('Expected a JSON-serializable value');
|
|
22
|
+
}
|
|
23
|
+
return `${json}\n`;
|
|
18
24
|
}
|
|
@@ -70,10 +70,13 @@ function isPresentationResult(value) {
|
|
|
70
70
|
return result['value'] === null || isRecord(result['value']);
|
|
71
71
|
}
|
|
72
72
|
if (result['type'] === 'records') {
|
|
73
|
-
return
|
|
73
|
+
return isArrayOf(result['value'], isRecord);
|
|
74
74
|
}
|
|
75
|
-
if (result['type'] === 'table'
|
|
76
|
-
return
|
|
75
|
+
if (result['type'] === 'table') {
|
|
76
|
+
return isArrayOf(result['rows'], isTextTableRow);
|
|
77
|
+
}
|
|
78
|
+
if (result['type'] === 'csv') {
|
|
79
|
+
return isArrayOf(result['rows'], isCsvRow);
|
|
77
80
|
}
|
|
78
81
|
return false;
|
|
79
82
|
}
|
|
@@ -182,3 +185,25 @@ function assertNever(value) {
|
|
|
182
185
|
function isRecord(value) {
|
|
183
186
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
184
187
|
}
|
|
188
|
+
function isTextTableRow(value) {
|
|
189
|
+
return isArrayOf(value, (cell) => typeof cell === 'string');
|
|
190
|
+
}
|
|
191
|
+
function isCsvRow(value) {
|
|
192
|
+
return isArrayOf(value, isCsvCell);
|
|
193
|
+
}
|
|
194
|
+
function isCsvCell(value) {
|
|
195
|
+
return (typeof value === 'string'
|
|
196
|
+
|| typeof value === 'number'
|
|
197
|
+
|| typeof value === 'boolean');
|
|
198
|
+
}
|
|
199
|
+
function isArrayOf(value, isValue) {
|
|
200
|
+
if (!Array.isArray(value)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
for (const element of value) {
|
|
204
|
+
if (!isValue(element)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icore",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Declarative command line interface and terminal presentation mechanics for Node.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"command-line",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"types": "dist/index.d.ts",
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc",
|
|
36
|
+
"prepack": "tsc",
|
|
36
37
|
"test": "fwa --prune",
|
|
37
38
|
"lint": "eslint"
|
|
38
39
|
},
|