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 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
@@ -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?.[name];
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?.[negatedName];
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) {
@@ -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 value = values[String(name)];
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(String(name), definition);
48
+ parsed[name] = parseDefaultOptionValue(optionName, definition);
46
49
  continue;
47
50
  }
48
51
  if (definition.required === true) {
49
- throw createExpectedRequiredArgumentError(String(name));
52
+ throw createExpectedRequiredArgumentError(optionName);
50
53
  }
51
54
  parsed[name] = undefined;
52
55
  continue;
53
56
  }
54
- parsed[name] = parseOptionValue(String(name), definition, value);
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) {
@@ -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
- return Object.assign({}, schema, ...schemas);
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 `drain` waiting when a writable sink reports backpressure;
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 `drain` waiting when a writable sink reports backpressure;
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
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Allowed here:
5
5
  * - CSV row joining;
6
- * - CSV cell escaping for comma, quote, and newline values;
6
+ * - CSV cell escaping for comma, quote, carriage return, and newline values;
7
7
  *
8
8
  * This file must not contain application report mapping.
9
9
  */
@@ -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('"', '""')}"`;
@@ -9,5 +9,7 @@
9
9
  */
10
10
  /**
11
11
  * Renders an already prepared value as pretty JSON with a trailing newline.
12
+ *
13
+ * Throws when the value has no top-level JSON representation.
12
14
  */
13
15
  export declare function renderJson(value: unknown): string;
@@ -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
- return `${JSON.stringify(value, null, 2)}\n`;
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 Array.isArray(result['value']);
73
+ return isArrayOf(result['value'], isRecord);
74
74
  }
75
- if (result['type'] === 'table' || result['type'] === 'csv') {
76
- return Array.isArray(result['rows']);
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.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
  },