Skip to content

Commit 6d425a4

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 6d425a4

5 files changed

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

0 commit comments

Comments
 (0)