@oclif/table 0.4.14 → 0.5.0

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 CHANGED
@@ -4,20 +4,278 @@
4
4
  [![Downloads/week](https://img.shields.io/npm/dw/@oclif/table.svg)](https://npmjs.org/package/@oclif/table)
5
5
  [![License](https://img.shields.io/npm/l/@oclif/table.svg)](https://github.com/oclif/table/blob/main/LICENSE)
6
6
 
7
- # Description
7
+ ### Description
8
8
 
9
- Print tables to the terminal using [ink](https://www.npmjs.com/package/ink)
9
+ Print beautiful, flexible tables to the terminal using [Ink](https://www.npmjs.com/package/ink). This library powers many oclif/Salesforce CLIs and is built for real-world, production output: wide columns, multi-line cells, smart wrapping/truncation, theming, and great CI behavior.
10
10
 
11
- # Examples
11
+ ### Features
12
12
 
13
- You can see examples of how to use it in the [examples](./examples/) directory.
13
+ - Rich rendering via Ink with graceful fallback to plain text in CI
14
+ - Automatic column sizing with max/explicit width support (including percentages)
15
+ - Per-column alignment, padding, and overflow control (wrap, truncate, middle/start/end)
16
+ - Header formatting with `change-case` presets or custom function
17
+ - Multiple border styles (outline, headers-only, vertical/horizontal, none, and more)
18
+ - Sort and filter data, optionally per-column
19
+ - Print multiple tables side-by-side or stacked with layout controls
20
+ - Return a string (`makeTable`) or print directly (`printTable`/`printTables`)
21
+ - TypeScript-first API with excellent types
14
22
 
15
- You can run any of these with the following:
23
+ ---
16
24
 
25
+ ### Requirements
26
+
27
+ - Node.js >= 18
28
+ - ESM only (this package has `"type": "module"`). Use `import`, not `require`.
29
+
30
+ ### Installation
31
+
32
+ ```bash
33
+ npm install @oclif/table
34
+ # or
35
+ yarn add @oclif/table
36
+ # or
37
+ pnpm add @oclif/table
38
+ # or
39
+ bun add @oclif/table
40
+ ```
41
+
42
+ ### Quick start
43
+
44
+ ```js
45
+ import {printTable} from '@oclif/table'
46
+
47
+ const data = [
48
+ {name: 'Alice', role: 'Engineer', notes: 'Loves distributed systems'},
49
+ {name: 'Bob', role: 'Designer', notes: 'Enjoys typography and UI'},
50
+ ]
51
+
52
+ printTable({
53
+ title: 'Team',
54
+ data,
55
+ columns: [
56
+ {key: 'name', width: 16},
57
+ {key: 'role', width: 14, horizontalAlignment: 'center'},
58
+ {key: 'notes', overflow: 'wrap'},
59
+ ],
60
+ })
61
+ ```
62
+
63
+ To generate a string instead of printing to stdout:
64
+
65
+ ```js
66
+ import {makeTable} from '@oclif/table'
67
+
68
+ const output = makeTable({data, columns: ['name', 'role', 'notes']})
69
+ // do something with `output`
70
+ ```
71
+
72
+ To print multiple tables in one layout:
73
+
74
+ ```js
75
+ import {printTables} from '@oclif/table'
76
+
77
+ printTables(
78
+ [
79
+ {title: 'Developers', data, columns: ['name', 'role']},
80
+ {title: 'Notes', data, columns: ['notes']},
81
+ ],
82
+ {direction: 'row', columnGap: 4, margin: 1},
83
+ )
84
+ ```
85
+
86
+ ---
87
+
88
+ ### API
89
+
90
+ - `printTable(options)` — renders a single table to stdout.
91
+ - `makeTable(options): string` — renders a single table and returns the output string.
92
+ - `printTables(tables, options?)` — renders multiple tables with layout controls.
93
+
94
+ All three accept the same `TableOptions<T>` (documented below). `printTables` accepts an array of `TableOptions` plus optional container layout props.
95
+
96
+ #### TypeScript
97
+
98
+ ```ts
99
+ import type {TableOptions} from '@oclif/table'
100
+
101
+ type Row = {name: string; age: number}
102
+ const opts: TableOptions<Row> = {
103
+ data: [{name: 'Ada', age: 36}],
104
+ columns: [
105
+ {key: 'name', name: 'Full Name'},
106
+ {key: 'age', horizontalAlignment: 'right'},
107
+ ],
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ### Options reference
114
+
115
+ Below are the most important options with defaults and behavior. See examples in `./examples` for more patterns.
116
+
117
+ - `data` (required): array of rows, each a plain object.
118
+ - `columns`: list of keys or objects describing each column.
119
+ - String form: `'name'` (uses key as header)
120
+ - Object form: `{ key, name?, width?, padding?, overflow?, horizontalAlignment?, verticalAlignment? }`
121
+ - `name`: header label (defaults to key or formatted with `headerOptions.formatter`)
122
+ - `width`: number of characters or percentage string like `'50%'`
123
+ - `padding`: spaces added on both sides (default: table `padding`)
124
+ - `overflow`: `'wrap' | 'truncate' | 'truncate-start' | 'truncate-middle' | 'truncate-end'`
125
+ - `horizontalAlignment`: `'left' | 'center' | 'right'`
126
+ - `verticalAlignment`: `'top' | 'center' | 'bottom'`
127
+
128
+ - `padding` (number): cell padding for all columns. Default: `1`.
129
+
130
+ - `maxWidth` (number | percentage | 'none'): maximum table width. Defaults to terminal width.
131
+ - When the natural table width exceeds `maxWidth`, columns shrink and content wraps/truncates based on `overflow`.
132
+ - Use `'none'` to allow unlimited width (useful for very wide outputs; users can resize their terminals).
133
+
134
+ - `width` (number | percentage): exact table width. Overrides `maxWidth`. If wider than the natural width, columns expand evenly.
135
+
136
+ - `overflow`: cell overflow behavior. Default: `'truncate'`.
137
+ - `'wrap'` wraps long content
138
+ - `'truncate'` truncates the end
139
+ - `'truncate-start' | 'truncate-middle' | 'truncate-end'` choose truncation position
140
+
141
+ - `headerOptions`: style options for headers.
142
+ - `formatter`: either a function `(header: string) => string` or a `change-case` preset name:
143
+ `'camelCase' | 'capitalCase' | 'constantCase' | 'kebabCase' | 'pascalCase' | 'sentenceCase' | 'snakeCase'`
144
+ - Styling keys (also supported by `titleOptions`):
145
+ - `color`, `backgroundColor` (named color, hex `#rrggbb`, or `rgb(r,g,b)`)
146
+ - `bold`, `dimColor`, `italic`, `underline`, `strikethrough`, `inverse`
147
+ - Default header style is bold blue (unless `noStyle: true`).
148
+
149
+ - `borderStyle`: border preset. Default: `'all'`.
150
+ - Available: `'all' | 'headers-only' | 'headers-only-with-outline' | 'headers-only-with-underline' | 'horizontal' | 'horizontal-with-outline' | 'none' | 'outline' | 'vertical' | 'vertical-rows-with-outline' | 'vertical-with-outline'`
151
+
152
+ - `borderColor`: optional border color. When unset, the terminal's default color is used.
153
+
154
+ - `horizontalAlignment`: default column alignment. Default: `'left'`.
155
+
156
+ - `verticalAlignment`: default vertical alignment within cells. Default: `'top'`.
157
+
158
+ - `filter(row)`: predicate to include/exclude rows.
159
+
160
+ - `sort`: object mapping keys to `'asc' | 'desc' | (a,b)=>number`. Keys order defines multi-column priority.
161
+
162
+ - `title`: optional string printed above the table.
163
+
164
+ - `titleOptions`: style options for the title (same keys as `headerOptions` except `formatter`).
165
+
166
+ - `trimWhitespace`: when wrapping text, trim whitespace at line boundaries. Default: `true`.
167
+
168
+ - `noStyle`: disable all styling. Also strips ANSI color codes from your cell content for accurate width calculation.
169
+
170
+ #### Container options (for `printTables`)
171
+
172
+ `printTables(tables, containerOptions)` accepts:
173
+
174
+ - `direction`: `'row' | 'column'` (default `'row'`)
175
+ - `columnGap`, `rowGap`: spacing between tables
176
+ - `alignItems`: `'flex-start' | 'center' | 'flex-end'`
177
+ - `margin`, `marginLeft`, `marginRight`, `marginTop`, `marginBottom`
178
+
179
+ ---
180
+
181
+ ### Behavior and environment variables
182
+
183
+ - Terminal width detection: by default we use your terminal width. Override with `OCLIF_TABLE_COLUMN_OVERRIDE`.
184
+ - Example: `OCLIF_TABLE_COLUMN_OVERRIDE=120 node app.js`
185
+
186
+ - CI-safe output: in CI environments, we automatically fall back to a plain-text renderer (no Ink) for stability.
187
+ - To force Ink rendering in CI, set `OCLIF_TABLE_SKIP_CI_CHECK=1`.
188
+
189
+ - Very large data: by default, datasets with 10,000+ rows use the plain-text renderer to avoid memory issues.
190
+ - Override with `OCLIF_TABLE_LIMIT=<number>` (Salesforce CLIs may also honor `SF_TABLE_LIMIT`).
191
+
192
+ ---
193
+
194
+ ### Advanced examples
195
+
196
+ Header formatting with `change-case`:
197
+
198
+ ```js
199
+ import {printTable} from '@oclif/table'
200
+
201
+ printTable({
202
+ data: [{first_name: 'Ada', last_name: 'Lovelace'}],
203
+ columns: ['first_name', 'last_name'],
204
+ headerOptions: {formatter: 'capitalCase'},
205
+ })
17
206
  ```
207
+
208
+ Per-column sizing and overflow:
209
+
210
+ ```js
211
+ printTable({
212
+ data: [{name: 'A very very long name', notes: 'Multi-line\ncontent supported'}],
213
+ maxWidth: '80%',
214
+ columns: [
215
+ {key: 'name', width: 20, overflow: 'truncate-middle'},
216
+ {key: 'notes', overflow: 'wrap', horizontalAlignment: 'right'},
217
+ ],
218
+ })
219
+ ```
220
+
221
+ Switching borders and colors:
222
+
223
+ ```js
224
+ printTable({
225
+ title: 'Inventory',
226
+ titleOptions: {bold: true, color: 'cyan'},
227
+ data: [{item: 'Widget', qty: 10}],
228
+ columns: ['item', {key: 'qty', horizontalAlignment: 'right'}],
229
+ borderStyle: 'vertical-with-outline',
230
+ borderColor: 'green',
231
+ })
232
+ ```
233
+
234
+ ---
235
+
236
+ ### Examples directory
237
+
238
+ This repo contains many runnable examples in `./examples` (including edge cases like very wide/very tall tables). Run any with:
239
+
240
+ ```bash
18
241
  tsx examples/basic.ts
19
242
  ```
20
243
 
21
- # Contributing
244
+ ---
245
+
246
+ ### Testing
247
+
248
+ This package includes snapshot tests for all examples to ensure consistent output across different environments.
249
+
250
+ #### Running tests
251
+
252
+ ```bash
253
+ # Run all tests (including snapshot tests)
254
+ yarn test
255
+
256
+ # Run only snapshot tests
257
+ yarn test:examples
258
+
259
+ # Update snapshots after intentional changes
260
+ yarn test:examples:update
261
+ ```
262
+
263
+ #### How snapshot testing works
264
+
265
+ Examples are executed in an isolated child process with:
266
+
267
+ - Fixed terminal width (`OCLIF_TABLE_COLUMN_OVERRIDE=120`)
268
+ - Disabled CI detection unless an example sets it
269
+ - ESM loader for TypeScript (`ts-node/esm`)
270
+
271
+ Snapshots live in `test/__snapshots__/examples/*.txt` and mirror the `examples` directory structure.
272
+
273
+ ---
274
+
275
+ ### Contributing
22
276
 
23
277
  See the [contributing guide](./CONRTIBUTING.md).
278
+
279
+ ### License
280
+
281
+ MIT © Salesforce
package/lib/table.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { CellProps, ContainerProps, HorizontalAlignment, Overflow, TableOptions } from './types.js';
3
- export declare function formatTextWithMargins({ horizontalAlignment, overflow, padding, trimWhitespace, value, width }: {
3
+ export declare function formatTextWithMargins({ horizontalAlignment, overflow, padding, trimWhitespace, value, width, }: {
4
4
  overflow: Overflow;
5
5
  value: unknown;
6
6
  width: number;
@@ -50,7 +50,8 @@ export declare function makeTable<T extends Record<string, unknown>>(options: Ta
50
50
  * @template T - An array of records where each record represents a table.
51
51
  * @param {Object.<keyof T, TableOptions<T[keyof T]>>} tables - An object containing table options for each table.
52
52
  * @param {Omit<ContainerProps, 'children'>} [options] - Optional container properties excluding 'children'.
53
- * @throws {Error} Throws an error if the total number of rows across all tables exceeds 30,000.
53
+ * @throws {Error} Throws an error if the total number of rows across all tables exceeds 10,000.
54
+ * @throws {Error} Throws an error if any of the tables have `maxWidth: "none"`.
54
55
  */
55
56
  export declare function printTables<T extends Record<string, unknown>[]>(tables: {
56
57
  [P in keyof T]: TableOptions<T[P]>;
package/lib/table.js CHANGED
@@ -34,7 +34,7 @@ function determineTruncatePosition(overflow) {
34
34
  }
35
35
  }
36
36
  }
37
- export function formatTextWithMargins({ horizontalAlignment, overflow, padding, trimWhitespace = true, value, width }) {
37
+ export function formatTextWithMargins({ horizontalAlignment, overflow, padding, trimWhitespace = true, value, width, }) {
38
38
  function calculateMargins(spaces) {
39
39
  let marginLeft;
40
40
  let marginRight;
@@ -147,7 +147,7 @@ function setupTable(props) {
147
147
  borderProps,
148
148
  cell: Cell,
149
149
  skeleton: BORDER_SKELETONS[config.borderStyle].data,
150
- trimWhitespace
150
+ trimWhitespace,
151
151
  });
152
152
  const footerComponent = row({
153
153
  borderProps,
@@ -283,7 +283,7 @@ export function Skeleton(props) {
283
283
  *
284
284
  * Implementation inspired by https://github.com/vadimdemedes/ink/blob/master/test/helpers/create-stdout.ts
285
285
  */
286
- const createStdout = () => {
286
+ const createStdout = ({ columns }) => {
287
287
  // eslint-disable-next-line unicorn/prefer-event-target
288
288
  const stdout = new EventEmitter();
289
289
  // Override the rows so that ink doesn't clear the entire terminal when
@@ -292,7 +292,7 @@ const createStdout = () => {
292
292
  // https://github.com/vadimdemedes/ink/blob/v5.0.1/src/ink.tsx#L174
293
293
  // This might be a bad idea but it works.
294
294
  stdout.rows = 10_000;
295
- stdout.columns = getColumnWidth();
295
+ stdout.columns = columns;
296
296
  const frames = [];
297
297
  stdout.write = (data) => {
298
298
  frames.push(data);
@@ -308,8 +308,8 @@ const createStdout = () => {
308
308
  };
309
309
  class Output {
310
310
  stream;
311
- constructor() {
312
- this.stream = createStdout();
311
+ constructor(opts) {
312
+ this.stream = createStdout(opts);
313
313
  }
314
314
  printLastFrame() {
315
315
  process.stdout.write(`${this.stream.lastFrame()}\n`);
@@ -404,7 +404,7 @@ export function printTable(options) {
404
404
  renderPlainTable(options);
405
405
  return;
406
406
  }
407
- const output = new Output();
407
+ const output = new Output({ columns: options.maxWidth === 'none' ? Infinity : getColumnWidth() });
408
408
  const instance = render(React.createElement(Table, { ...options }), { stdout: output.stream });
409
409
  instance.unmount();
410
410
  output.printLastFrame();
@@ -417,7 +417,7 @@ export function printTable(options) {
417
417
  * @returns {string} The rendered table as a string.
418
418
  */
419
419
  export function makeTable(options) {
420
- const output = new Output();
420
+ const output = new Output({ columns: options.maxWidth === 'none' ? Infinity : getColumnWidth() });
421
421
  const instance = render(React.createElement(Table, { ...options }), { stdout: output.stream });
422
422
  instance.unmount();
423
423
  return output.stream.lastFrame() ?? '';
@@ -431,13 +431,17 @@ function Container(props) {
431
431
  * @template T - An array of records where each record represents a table.
432
432
  * @param {Object.<keyof T, TableOptions<T[keyof T]>>} tables - An object containing table options for each table.
433
433
  * @param {Omit<ContainerProps, 'children'>} [options] - Optional container properties excluding 'children'.
434
- * @throws {Error} Throws an error if the total number of rows across all tables exceeds 30,000.
434
+ * @throws {Error} Throws an error if the total number of rows across all tables exceeds 10,000.
435
+ * @throws {Error} Throws an error if any of the tables have `maxWidth: "none"`.
435
436
  */
436
437
  export function printTables(tables, options) {
437
438
  if (tables.reduce((acc, table) => acc + table.data.length, 0) > 10_000) {
438
439
  throw new Error('The data is too large to print multiple tables. Please use `printTable` instead.');
439
440
  }
440
- const output = new Output();
441
+ if (tables.some((table) => table.maxWidth === 'none')) {
442
+ throw new Error('printTables does not support `maxWidth: "none". Please use `printTable` instead.');
443
+ }
444
+ const output = new Output({ columns: getColumnWidth() });
441
445
  const leftMargin = options?.marginLeft ?? options?.margin ?? 0;
442
446
  const rightMargin = options?.marginRight ?? options?.margin ?? 0;
443
447
  const columns = getColumnWidth() - (leftMargin + rightMargin);
package/lib/types.d.ts CHANGED
@@ -75,7 +75,7 @@ export type TableOptions<T extends Record<string, unknown>> = {
75
75
  */
76
76
  padding?: number;
77
77
  /**
78
- * Maximum width of the table. Can be a number (e.g. 80) or a percentage (e.g. '80%').
78
+ * Maximum width of the table. Can be a number (e.g. 80) or a percentage (e.g. '80%'). Or set to 'none' for unlimited width.
79
79
  *
80
80
  * By default, the table will only take up as much space as it needs to fit the content. If it extends beyond the maximum width,
81
81
  * it will wrap or truncate the content based on the `overflow` option. In other words, this property allows you to set the width
@@ -86,8 +86,14 @@ export type TableOptions<T extends Record<string, unknown>> = {
86
86
  * If you provide a number or percentage that is larger than the terminal width, it will default to the terminal width.
87
87
  *
88
88
  * If you provide a number or percentage that is too small to fit the table, it will default to the minimum width of the table.
89
+ *
90
+ * If you provide 'none', the table will grow to its natural width, unbound by terminal width. This may render poorly in narrow terminals but
91
+ * it's useful because it will allow all the content to be visible without truncation or wrapping, which allows the user to resize their terminal
92
+ * to see all the content.
93
+ *
94
+ * @throws {Error} If you provide 'none' and you are using `printTables`.
89
95
  */
90
- maxWidth?: Percentage | number;
96
+ maxWidth?: Percentage | number | 'none';
91
97
  /**
92
98
  * Exact width of the table. Can be a number (e.g. 80) or a percentage (e.g. '80%').
93
99
  *
package/lib/utils.d.ts CHANGED
@@ -34,7 +34,7 @@ export declare function getColumnWidth(): number;
34
34
  * @param providedWidth - The width value provided.
35
35
  * @returns The determined configured width.
36
36
  */
37
- export declare function determineConfiguredWidth(providedWidth: number | Percentage | undefined, columns?: number): number;
37
+ export declare function determineConfiguredWidth(providedWidth: number | Percentage | 'none' | undefined, columns?: number): number;
38
38
  export declare function getColumns<T extends Record<string, unknown>>(config: Config<T>, headings: Partial<T>): Column<T>[];
39
39
  export declare function getHeadings<T extends Record<string, unknown>>(config: Config<T>): Partial<T>;
40
40
  export declare function maybeStripAnsi<T extends Record<string, unknown>[]>(data: T, noStyle: boolean): T;
package/lib/utils.js CHANGED
@@ -70,6 +70,8 @@ export function getColumnWidth() {
70
70
  export function determineConfiguredWidth(providedWidth, columns = getColumnWidth()) {
71
71
  if (!providedWidth)
72
72
  return columns;
73
+ if (providedWidth === 'none')
74
+ return Infinity;
73
75
  const num = typeof providedWidth === 'string' && providedWidth.endsWith('%')
74
76
  ? Math.floor((Number.parseInt(providedWidth, 10) / 100) * columns)
75
77
  : typeof providedWidth === 'string'
@@ -120,6 +122,9 @@ export function getColumns(config, headings) {
120
122
  // In that case, we don't want to reduce the column widths and just use the table's natural width.
121
123
  if (maxWidth === 0)
122
124
  return;
125
+ // The consumer has indicated they want an unbounded table
126
+ if (maxWidth === Infinity)
127
+ return;
123
128
  // If the table is too wide, reduce the width of the largest column as little as possible to fit the table.
124
129
  // If the table is still too wide, it will reduce the width of the next largest column and so on
125
130
  while (tableWidth > maxWidth) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oclif/table",
3
3
  "description": "Display table in terminal",
4
- "version": "0.4.14",
4
+ "version": "0.5.0",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/table/issues",
7
7
  "dependencies": {
@@ -17,10 +17,10 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@commitlint/config-conventional": "^19",
20
- "@eslint/compat": "^1.3.2",
20
+ "@eslint/compat": "^1.4.0",
21
21
  "@oclif/core": "^4",
22
22
  "@oclif/prettier-config": "^0.2.1",
23
- "@oclif/test": "^4.1.13",
23
+ "@oclif/test": "^4.1.14",
24
24
  "@types/chai": "^4.3.16",
25
25
  "@types/mocha": "^10.0.10",
26
26
  "@types/node": "^18",
@@ -29,8 +29,8 @@
29
29
  "ansis": "^3.17.0",
30
30
  "chai": "^4.5.0",
31
31
  "commitlint": "^19",
32
- "eslint": "^9.35.0",
33
- "eslint-config-oclif": "^6.0.102",
32
+ "eslint": "^9.38.0",
33
+ "eslint-config-oclif": "^6.0.110",
34
34
  "eslint-config-prettier": "^10.1.8",
35
35
  "eslint-config-xo": "^0.49.0",
36
36
  "eslint-config-xo-react": "^0.28.0",
@@ -75,7 +75,9 @@
75
75
  "posttest": "yarn lint",
76
76
  "prepack": "yarn run build",
77
77
  "prepare": "husky",
78
- "test": "mocha --forbid-only \"test/**/*.test.+(ts|tsx)\" --parallel"
78
+ "test": "mocha --forbid-only \"test/**/*.test.+(ts|tsx)\" --parallel",
79
+ "test:examples": "mocha --forbid-only \"test/examples.snapshot.test.ts\"",
80
+ "test:examples:update": "UPDATE_SNAPSHOTS=1 mocha --forbid-only \"test/examples.snapshot.test.ts\""
79
81
  },
80
82
  "types": "lib/index.d.ts",
81
83
  "type": "module"