Skip to content

Commit 937bf27

Browse files
committed
util: add util.table()
Add `util.table(tabularData[, properties][, options])`, which renders tabular data with the same layout as `console.table` but returns it as a string instead of writing to a stream. This is useful for logging, writing to a file, or building output without a console. The row-building logic is extracted from `console.table` into a shared internal module so both APIs produce identical output. Signed-off-by: Daijiro Wachi <daijiro.wachi@gmail.com>
1 parent f6156ce commit 937bf27

5 files changed

Lines changed: 302 additions & 120 deletions

File tree

doc/api/util.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,48 @@ Returns the `string` after replacing any surrogate code points
28702870
(or equivalently, any unpaired surrogate code units) with the
28712871
Unicode "replacement character" U+FFFD.
28722872
2873+
## `util.table(tabularData[, properties][, options])`
2874+
2875+
<!-- YAML
2876+
added: REPLACEME
2877+
-->
2878+
2879+
* `tabularData` {any} The data to render. Arrays, objects, `Map`s and `Set`s
2880+
are rendered as tables; any other value is returned as inspected text.
2881+
* `properties` {string\[]} An alternate list of properties to use as the table's
2882+
columns. By default every own enumerable property of the rows is used.
2883+
* `options` {Object} Options forwarded to [`util.inspect()`][] when formatting
2884+
each cell.
2885+
* Returns: {string}
2886+
2887+
Returns tabular data formatted with the same layout as [`console.table()`][],
2888+
but as a string instead of writing it to a stream. This is useful for logging,
2889+
writing to a file, or building output without a console.
2890+
2891+
```mjs
2892+
import { table } from 'node:util';
2893+
2894+
console.log(table([
2895+
{ name: 'alice', age: 30 },
2896+
{ name: 'bob', age: 25 },
2897+
]));
2898+
// ┌─────────┬─────────┬─────┐
2899+
// │ (index) │ name │ age │
2900+
// ├─────────┼─────────┼─────┤
2901+
// │ 0 │ 'alice' │ 30 │
2902+
// │ 1 │ 'bob' │ 25 │
2903+
// └─────────┴─────────┴─────┘
2904+
```
2905+
2906+
```cjs
2907+
const { table } = require('node:util');
2908+
2909+
console.log(table([
2910+
{ name: 'alice', age: 30 },
2911+
{ name: 'bob', age: 25 },
2912+
]));
2913+
```
2914+
28732915
## `util.transferableAbortController()`
28742916
28752917
<!-- YAML
@@ -3914,6 +3956,7 @@ npx codemod@latest @nodejs/util-is
39143956
[`Runtime.ScriptId`]: https://chromedevtools.github.io/devtools-protocol/1-3/Runtime/#type-ScriptId
39153957
[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
39163958
[`console.error()`]: console.md#consoleerrordata-args
3959+
[`console.table()`]: console.md#consoletabletabulardata-properties
39173960
[`mime.toString()`]: #mimetostring
39183961
[`mimeParams.entries()`]: #mimeparamsentries
39193962
[`napi_create_external()`]: n-api.md#napi_create_external

lib/internal/console/constructor.js

Lines changed: 6 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
// console. It's exported for backwards compatibility.
55

66
const {
7-
ArrayFrom,
8-
ArrayIsArray,
97
ArrayPrototypeForEach,
10-
ArrayPrototypePush,
118
ArrayPrototypeUnshift,
129
Boolean,
1310
ErrorCaptureStackTrace,
@@ -17,8 +14,6 @@ const {
1714
ObjectDefineProperties,
1815
ObjectDefineProperty,
1916
ObjectKeys,
20-
ObjectPrototypeHasOwnProperty,
21-
ObjectValues,
2217
ReflectApply,
2318
ReflectConstruct,
2419
ReflectOwnKeys,
@@ -49,14 +44,12 @@ const {
4944
validateObject,
5045
validateOneOf,
5146
} = require('internal/validators');
52-
const { previewEntries } = internalBinding('util');
53-
const { Buffer: { isBuffer } } = require('buffer');
5447
const {
5548
inspect,
5649
formatWithOptions,
5750
} = require('internal/util/inspect');
5851
const {
59-
isTypedArray, isSet, isMap, isSetIterator, isMapIterator,
52+
isMap,
6053
} = require('internal/util/types');
6154
const {
6255
CHAR_UPPERCASE_C: kTraceCount,
@@ -76,7 +69,7 @@ const kTraceConsoleCategory = 'node,node.console';
7669
const kMaxGroupIndentation = 1000;
7770

7871
// Lazy loaded for startup performance.
79-
let cliTable;
72+
let buildTable;
8073

8174
let utilColors;
8275
function lazyUtilColors() {
@@ -558,120 +551,13 @@ const consoleMethods = {
558551
if (tabularData === null || typeof tabularData !== 'object')
559552
return this.log(tabularData);
560553

561-
cliTable ??= require('internal/cli_table');
562-
const final = (k, v) => this.log(cliTable(k, v));
563-
564-
const _inspect = (v) => {
565-
const depth = v !== null &&
566-
typeof v === 'object' &&
567-
!isArray(v) &&
568-
ObjectKeys(v).length > 2 ? -1 : 0;
569-
const opt = {
570-
depth,
571-
maxArrayLength: 3,
572-
breakLength: Infinity,
573-
...this[kGetInspectOptions](this._stdout),
574-
};
575-
return inspect(v, opt);
576-
};
577-
const getIndexArray = (length) => ArrayFrom(
578-
{ length }, (_, i) => _inspect(i));
579-
580-
const mapIter = isMapIterator(tabularData);
581-
let isKeyValue = false;
582-
let i = 0;
583-
if (mapIter) {
584-
const res = previewEntries(tabularData, true);
585-
tabularData = res[0];
586-
isKeyValue = res[1];
587-
}
588-
589-
if (isKeyValue || isMap(tabularData)) {
590-
const keys = [];
591-
const values = [];
592-
let length = 0;
593-
if (mapIter) {
594-
for (; i < tabularData.length / 2; ++i) {
595-
ArrayPrototypePush(keys, _inspect(tabularData[i * 2]));
596-
ArrayPrototypePush(values, _inspect(tabularData[i * 2 + 1]));
597-
length++;
598-
}
599-
} else {
600-
for (const { 0: k, 1: v } of tabularData) {
601-
ArrayPrototypePush(keys, _inspect(k));
602-
ArrayPrototypePush(values, _inspect(v));
603-
length++;
604-
}
605-
}
606-
return final([
607-
iterKey, keyKey, valuesKey,
608-
], [
609-
getIndexArray(length),
610-
keys,
611-
values,
612-
]);
613-
}
614-
615-
const setIter = isSetIterator(tabularData);
616-
if (setIter)
617-
tabularData = previewEntries(tabularData);
618-
619-
const setlike = setIter || mapIter || isSet(tabularData);
620-
if (setlike) {
621-
const values = [];
622-
let length = 0;
623-
for (const v of tabularData) {
624-
ArrayPrototypePush(values, _inspect(v));
625-
length++;
626-
}
627-
return final([iterKey, valuesKey], [getIndexArray(length), values]);
628-
}
629-
630-
const map = { __proto__: null };
631-
let hasPrimitives = false;
632-
const valuesKeyArray = [];
633-
const indexKeyArray = ObjectKeys(tabularData);
634-
635-
for (; i < indexKeyArray.length; i++) {
636-
const item = tabularData[indexKeyArray[i]];
637-
const primitive = item === null ||
638-
(typeof item !== 'function' && typeof item !== 'object');
639-
if (properties === undefined && primitive) {
640-
hasPrimitives = true;
641-
valuesKeyArray[i] = _inspect(item);
642-
} else {
643-
const keys = properties || ObjectKeys(item);
644-
for (const key of keys) {
645-
map[key] ??= [];
646-
if ((primitive && properties) ||
647-
!ObjectPrototypeHasOwnProperty(item, key))
648-
map[key][i] = '';
649-
else
650-
map[key][i] = _inspect(item[key]);
651-
}
652-
}
653-
}
654-
655-
const keys = ObjectKeys(map);
656-
const values = ObjectValues(map);
657-
if (hasPrimitives) {
658-
ArrayPrototypePush(keys, valuesKey);
659-
ArrayPrototypePush(values, valuesKeyArray);
660-
}
661-
ArrayPrototypeUnshift(keys, indexKey);
662-
ArrayPrototypeUnshift(values, indexKeyArray);
663-
664-
return final(keys, values);
554+
buildTable ??= require('internal/util/table');
555+
return this.log(
556+
buildTable(tabularData, properties, this[kGetInspectOptions](this._stdout)),
557+
);
665558
},
666559
};
667560

668-
const keyKey = 'Key';
669-
const valuesKey = 'Values';
670-
const indexKey = '(index)';
671-
const iterKey = '(iteration index)';
672-
673-
const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v);
674-
675561
function noop() {}
676562

677563
for (const method of ReflectOwnKeys(consoleMethods))

lib/internal/util/table.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use strict';
2+
3+
const {
4+
ArrayFrom,
5+
ArrayIsArray,
6+
ArrayPrototypePush,
7+
ArrayPrototypeUnshift,
8+
ObjectKeys,
9+
ObjectPrototypeHasOwnProperty,
10+
ObjectValues,
11+
SetPrototypeForEach,
12+
} = primordials;
13+
14+
const { previewEntries } = internalBinding('util');
15+
const { Buffer: { isBuffer } } = require('buffer');
16+
const { inspect } = require('internal/util/inspect');
17+
const {
18+
isTypedArray, isSet, isMap, isSetIterator, isMapIterator,
19+
} = require('internal/util/types');
20+
21+
const keyKey = 'Key';
22+
const valuesKey = 'Values';
23+
const indexKey = '(index)';
24+
const iterKey = '(iteration index)';
25+
26+
const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v);
27+
28+
/**
29+
* Builds the rows of a table from tabular data and renders them into a string.
30+
* Shared by `console.table` and `util.table`.
31+
* @param {any} tabularData The data to tabulate.
32+
* @param {string[]} [properties] The subset of properties to include as columns.
33+
* @param {object} [inspectOptions] Base options merged into each cell's inspect call.
34+
* @returns {string} The rendered table.
35+
*/
36+
function table(tabularData, properties, inspectOptions) {
37+
// Lazy require to avoid a cycle with the console bootstrap path.
38+
const cliTable = require('internal/cli_table');
39+
40+
const _inspect = (v) => {
41+
const depth = v !== null &&
42+
typeof v === 'object' &&
43+
!isArray(v) &&
44+
ObjectKeys(v).length > 2 ? -1 : 0;
45+
const opt = {
46+
depth,
47+
maxArrayLength: 3,
48+
breakLength: Infinity,
49+
...inspectOptions,
50+
};
51+
return inspect(v, opt);
52+
};
53+
const getIndexArray = (length) => ArrayFrom(
54+
{ length }, (_, i) => _inspect(i));
55+
56+
const mapIter = isMapIterator(tabularData);
57+
let isMapLike;
58+
if (mapIter) {
59+
const res = previewEntries(tabularData, true);
60+
tabularData = res[0];
61+
isMapLike = res[1];
62+
} else {
63+
isMapLike = isMap(tabularData);
64+
}
65+
66+
if (isMapLike) {
67+
const keys = [];
68+
const values = [];
69+
let length = 0;
70+
if (mapIter) {
71+
for (let i = 0; i < tabularData.length / 2; ++i) {
72+
ArrayPrototypePush(keys, _inspect(tabularData[i * 2]));
73+
ArrayPrototypePush(values, _inspect(tabularData[i * 2 + 1]));
74+
length++;
75+
}
76+
} else {
77+
for (const { 0: k, 1: v } of tabularData) {
78+
ArrayPrototypePush(keys, _inspect(k));
79+
ArrayPrototypePush(values, _inspect(v));
80+
length++;
81+
}
82+
}
83+
return cliTable([
84+
iterKey, keyKey, valuesKey,
85+
], [
86+
getIndexArray(length),
87+
keys,
88+
values,
89+
]);
90+
}
91+
92+
const setIter = isSetIterator(tabularData);
93+
if (setIter)
94+
tabularData = previewEntries(tabularData);
95+
96+
const setlike = setIter || mapIter || isSet(tabularData);
97+
if (setlike) {
98+
const values = [];
99+
if (setIter || mapIter) {
100+
// `previewEntries` already yielded an array, so read it by index.
101+
for (let j = 0; j < tabularData.length; j++)
102+
ArrayPrototypePush(values, _inspect(tabularData[j]));
103+
} else {
104+
// A real Set: iterate with `SetPrototypeForEach` to avoid the iterator
105+
// protocol and a second pass over the data.
106+
SetPrototypeForEach(tabularData,
107+
(v) => ArrayPrototypePush(values, _inspect(v)));
108+
}
109+
return cliTable([iterKey, valuesKey], [getIndexArray(values.length), values]);
110+
}
111+
112+
const map = { __proto__: null };
113+
let hasPrimitives = false;
114+
const valuesKeyArray = [];
115+
const indexKeyArray = ObjectKeys(tabularData);
116+
117+
for (let i = 0; i < indexKeyArray.length; i++) {
118+
const item = tabularData[indexKeyArray[i]];
119+
const primitive = item === null ||
120+
(typeof item !== 'function' && typeof item !== 'object');
121+
if (properties === undefined && primitive) {
122+
hasPrimitives = true;
123+
valuesKeyArray[i] = _inspect(item);
124+
} else {
125+
const keys = properties || ObjectKeys(item);
126+
for (let k = 0; k < keys.length; k++) {
127+
const key = keys[k];
128+
map[key] ??= [];
129+
if ((primitive && properties) ||
130+
!ObjectPrototypeHasOwnProperty(item, key))
131+
map[key][i] = '';
132+
else
133+
map[key][i] = _inspect(item[key]);
134+
}
135+
}
136+
}
137+
138+
const keys = ObjectKeys(map);
139+
const values = ObjectValues(map);
140+
if (hasPrimitives) {
141+
ArrayPrototypePush(keys, valuesKey);
142+
ArrayPrototypePush(values, valuesKeyArray);
143+
}
144+
ArrayPrototypeUnshift(keys, indexKey);
145+
ArrayPrototypeUnshift(values, indexKeyArray);
146+
147+
return cliTable(keys, values);
148+
}
149+
150+
module.exports = table;

0 commit comments

Comments
 (0)