@oclif/table 0.3.9 → 0.4.2

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/lib/table.js CHANGED
@@ -8,38 +8,15 @@ import React from 'react';
8
8
  import stripAnsi from 'strip-ansi';
9
9
  import wrapAnsi from 'wrap-ansi';
10
10
  import { BORDER_SKELETONS } from './skeletons.js';
11
- import { allKeysInCollection, determineWidthOfWrappedText, getColumns, getHeadings, intersperse, maybeStripAnsi, shouldUsePlainTable, sortData, } from './utils.js';
12
- /**
13
- * Determines the configured width based on the provided width value.
14
- * If no width is provided, it returns the width of the current terminal.
15
- * If the provided width is a percentage, it calculates the width based on the percentage of the terminal width.
16
- * If the provided width is a number, it returns the provided width.
17
- * If the calculated width is greater than the terminal width, it returns the terminal width.
18
- *
19
- * @param providedWidth - The width value provided.
20
- * @returns The determined configured width.
21
- */
22
- function determineConfiguredWidth(providedWidth, columns = process.stdout.columns) {
23
- if (!providedWidth)
24
- return columns;
25
- const num = typeof providedWidth === 'string' && providedWidth.endsWith('%')
26
- ? Math.floor((Number.parseInt(providedWidth, 10) / 100) * columns)
27
- : typeof providedWidth === 'string'
28
- ? Number.parseInt(providedWidth, 10)
29
- : providedWidth;
30
- if (num > columns) {
31
- return columns;
32
- }
33
- return num;
34
- }
11
+ import { allKeysInCollection, determineConfiguredWidth, determineWidthOfWrappedText, getColumns, getHeadings, intersperse, maybeStripAnsi, shouldUsePlainTable, sortData, } from './utils.js';
35
12
  /**
36
13
  * Determine the width to use for the table.
37
14
  *
38
15
  * This allows us to use the minimum width required to display the table if the configured width is too small.
39
16
  */
40
- function determineWidthToUse(columns, configuredWidth) {
17
+ function determineWidthToUse(columns, maxWidth, width) {
41
18
  const tableWidth = columns.map((c) => c.width).reduce((a, b) => a + b, 0) + columns.length + 1;
42
- return tableWidth < configuredWidth ? configuredWidth : tableWidth;
19
+ return width ?? (tableWidth < maxWidth ? maxWidth : tableWidth);
43
20
  }
44
21
  function determineTruncatePosition(overflow) {
45
22
  switch (overflow) {
@@ -134,20 +111,22 @@ export function formatTextWithMargins({ horizontalAlignment, overflow, padding,
134
111
  };
135
112
  }
136
113
  function setup(props) {
137
- const { data, filter, horizontalAlignment = 'left', maxWidth, noStyle = false, overflow = 'truncate', padding = 1, sort, title, verticalAlignment = 'top', } = props;
114
+ const { data, filter, horizontalAlignment = 'left', maxWidth, noStyle = false, overflow = 'truncate', padding = 1, sort, title, verticalAlignment = 'top', width, } = props;
138
115
  const headerOptions = noStyle ? {} : { bold: true, color: 'blue', ...props.headerOptions };
139
116
  const borderStyle = noStyle ? 'none' : (props.borderStyle ?? 'all');
140
117
  const borderColor = noStyle ? undefined : props.borderColor;
141
118
  const borderProps = { color: borderColor };
142
119
  const titleOptions = noStyle ? {} : props.titleOptions;
143
120
  const processedData = maybeStripAnsi(sortData(filter ? data.filter((row) => filter(row)) : data, sort), noStyle);
121
+ const tableWidth = width ? determineConfiguredWidth(width) : undefined;
144
122
  const config = {
145
123
  borderStyle,
146
124
  columns: props.columns ?? allKeysInCollection(data),
147
125
  data: processedData,
148
126
  headerOptions,
149
127
  horizontalAlignment,
150
- maxWidth: determineConfiguredWidth(maxWidth),
128
+ maxWidth: tableWidth ?? determineConfiguredWidth(maxWidth),
129
+ width: tableWidth,
151
130
  overflow,
152
131
  padding,
153
132
  verticalAlignment,
@@ -215,7 +194,7 @@ function setup(props) {
215
194
  }
216
195
  export function Table(props) {
217
196
  const { columns, config, dataComponent, footerComponent, headerComponent, headerFooterComponent, headingComponent, headings, processedData, separatorComponent, title, titleOptions, } = setup(props);
218
- return (React.createElement(Box, { flexDirection: "column", width: determineWidthToUse(columns, config.maxWidth) },
197
+ return (React.createElement(Box, { flexDirection: "column", width: determineWidthToUse(columns, config.maxWidth, config.width) },
219
198
  title && React.createElement(Text, { ...titleOptions }, title),
220
199
  headerComponent({ columns, data: {}, key: 'header' }),
221
200
  headingComponent({ columns, data: headings, key: 'heading' }),
package/lib/types.d.ts CHANGED
@@ -4,6 +4,7 @@ export type CellProps = React.PropsWithChildren<{
4
4
  }>;
5
5
  export type HorizontalAlignment = 'left' | 'right' | 'center';
6
6
  export type VerticalAlignment = 'top' | 'center' | 'bottom';
7
+ export type Percentage = `${number}%`;
7
8
  export type ColumnProps<T> = {
8
9
  /**
9
10
  * Horizontal alignment of cell content. Overrides the horizontal alignment set in the table.
@@ -26,11 +27,14 @@ export type ColumnProps<T> = {
26
27
  * Vertical alignment of cell content. Overrides the vertical alignment set in the table.
27
28
  */
28
29
  verticalAlignment?: VerticalAlignment;
30
+ /**
31
+ * Set the width of the column. If not provided, it will default to the width of the content.
32
+ */
33
+ width?: Percentage | number;
29
34
  };
30
35
  export type AllColumnProps<T> = {
31
36
  [K in keyof T]: ColumnProps<K>;
32
37
  }[keyof T];
33
- export type Percentage = `${number}%`;
34
38
  type TextOptions = {
35
39
  color?: SupportedColor;
36
40
  backgroundColor?: SupportedColor;
@@ -70,15 +74,35 @@ export type TableOptions<T extends Record<string, unknown>> = {
70
74
  */
71
75
  padding?: number;
72
76
  /**
73
- * Width of the table. Can be a number (e.g. 80) or a percentage (e.g. '80%').
77
+ * Maximum width of the table. Can be a number (e.g. 80) or a percentage (e.g. '80%').
74
78
  *
75
- * If not provided, it will default to the width of the terminal (determined by `process.stdout.columns`).
79
+ * 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,
80
+ * it will wrap or truncate the content based on the `overflow` option. In other words, this property allows you to set the width
81
+ * at which wrapping or truncation occurs.
82
+ *
83
+ * If not provided, the maximum width will default to the terminal width.
76
84
  *
77
85
  * If you provide a number or percentage that is larger than the terminal width, it will default to the terminal width.
78
86
  *
79
87
  * 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.
80
88
  */
81
89
  maxWidth?: Percentage | number;
90
+ /**
91
+ * Exact width of the table. Can be a number (e.g. 80) or a percentage (e.g. '80%').
92
+ *
93
+ * By default, the table will only take up as much space as it needs to fit the content. If you set the `width` option, the table will
94
+ * always take up that amount of space, regardless of the content. If the content is too large, it will wrap or truncate based on the
95
+ * `overflow` option. If it's too small, it will add empty space evenly across the columns.
96
+ *
97
+ * Setting this property will override the `maxWidth` option.
98
+ *
99
+ * If not provided, it will default to the natural width of the table.
100
+ *
101
+ * If you provide a number or percentage that is larger than the terminal width, it will default to the terminal width.
102
+ *
103
+ * 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.
104
+ */
105
+ width?: Percentage | number;
82
106
  /**
83
107
  * Overflow behavior for cells. Defaults to 'truncate'.
84
108
  */
@@ -159,6 +183,7 @@ export type Config<T> = {
159
183
  borderStyle: BorderStyle;
160
184
  horizontalAlignment: HorizontalAlignment;
161
185
  verticalAlignment: VerticalAlignment;
186
+ width: number | undefined;
162
187
  };
163
188
  export type RowConfig = {
164
189
  /**
package/lib/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Column, Config, Sort } from './types.js';
1
+ import { Column, Config, Percentage, Sort } from './types.js';
2
2
  /**
3
3
  * Intersperses a list of elements with another element.
4
4
  *
@@ -11,6 +11,17 @@ export declare function intersperse<T, I>(intersperser: (index: number) => I, el
11
11
  export declare function sortData<T extends Record<string, unknown>>(data: T[], sort?: Sort<T> | undefined): T[];
12
12
  export declare function allKeysInCollection<T extends Record<string, unknown>>(data: T[]): (keyof T)[];
13
13
  export declare function determineWidthOfWrappedText(text: string): number;
14
+ /**
15
+ * Determines the configured width based on the provided width value.
16
+ * If no width is provided, it returns the width of the current terminal.
17
+ * If the provided width is a percentage, it calculates the width based on the percentage of the terminal width.
18
+ * If the provided width is a number, it returns the provided width.
19
+ * If the calculated width is greater than the terminal width, it returns the terminal width.
20
+ *
21
+ * @param providedWidth - The width value provided.
22
+ * @returns The determined configured width.
23
+ */
24
+ export declare function determineConfiguredWidth(providedWidth: number | Percentage | undefined, columns?: number): number;
14
25
  export declare function getColumns<T extends Record<string, unknown>>(config: Config<T>, headings: Partial<T>): Column<T>[];
15
26
  export declare function getHeadings<T extends Record<string, unknown>>(config: Config<T>): Partial<T>;
16
27
  export declare function maybeStripAnsi<T extends Record<string, unknown>[]>(data: T, noStyle: boolean): T;
package/lib/utils.js CHANGED
@@ -42,8 +42,31 @@ export function determineWidthOfWrappedText(text) {
42
42
  const lines = text.split('\n');
43
43
  return lines.reduce((max, line) => Math.max(max, line.length), 0);
44
44
  }
45
+ /**
46
+ * Determines the configured width based on the provided width value.
47
+ * If no width is provided, it returns the width of the current terminal.
48
+ * If the provided width is a percentage, it calculates the width based on the percentage of the terminal width.
49
+ * If the provided width is a number, it returns the provided width.
50
+ * If the calculated width is greater than the terminal width, it returns the terminal width.
51
+ *
52
+ * @param providedWidth - The width value provided.
53
+ * @returns The determined configured width.
54
+ */
55
+ export function determineConfiguredWidth(providedWidth, columns = process.stdout.columns) {
56
+ if (!providedWidth)
57
+ return columns;
58
+ const num = typeof providedWidth === 'string' && providedWidth.endsWith('%')
59
+ ? Math.floor((Number.parseInt(providedWidth, 10) / 100) * columns)
60
+ : typeof providedWidth === 'string'
61
+ ? Number.parseInt(providedWidth, 10)
62
+ : providedWidth;
63
+ if (num > columns) {
64
+ return columns;
65
+ }
66
+ return num;
67
+ }
45
68
  export function getColumns(config, headings) {
46
- const { columns, horizontalAlignment, maxWidth, overflow, verticalAlignment } = config;
69
+ const { columns, horizontalAlignment, maxWidth, overflow, verticalAlignment, width } = config;
47
70
  const widths = columns.map((propsOrKey) => {
48
71
  const props = typeof propsOrKey === 'object' ? propsOrKey : { key: propsOrKey };
49
72
  const { key } = props;
@@ -53,10 +76,16 @@ export function getColumns(config, headings) {
53
76
  const value = data[key];
54
77
  if (value === undefined || value === null)
55
78
  return 0;
79
+ // Some terminals don't play nicely with zero-width characters, so we replace them with spaces.
80
+ // https://github.com/sindresorhus/terminal-link/issues/18
81
+ // https://github.com/Shopify/cli/pull/995
56
82
  return determineWidthOfWrappedText(stripAnsi(String(value).replaceAll('​', ' ')));
57
83
  });
58
84
  const header = String(headings[key]).length;
59
- const width = Math.max(...data, header) + padding * 2;
85
+ // If a column width is provided, use that. Otherwise, use the width of the largest cell in the column.
86
+ const columnWidth = props.width
87
+ ? determineConfiguredWidth(props.width, width ?? maxWidth)
88
+ : Math.max(...data, header) + padding * 2;
60
89
  return {
61
90
  column: key,
62
91
  horizontalAlignment: props.horizontalAlignment ?? horizontalAlignment,
@@ -64,7 +93,7 @@ export function getColumns(config, headings) {
64
93
  overflow: props.overflow ?? overflow,
65
94
  padding,
66
95
  verticalAlignment: props.verticalAlignment ?? verticalAlignment,
67
- width,
96
+ width: columnWidth,
68
97
  };
69
98
  });
70
99
  const numberOfBorders = widths.length + 1;
@@ -93,6 +122,18 @@ export function getColumns(config, headings) {
93
122
  seen.clear();
94
123
  // At most, reduce the width to the padding + 3
95
124
  reduceColumnWidths((col) => col.padding * 2 + 3);
125
+ // If the table width was provided AND it's greater than the calculated table width, expand the columns to fill the width
126
+ if (width && width > tableWidth) {
127
+ const extraWidth = width - tableWidth;
128
+ const extraWidthPerColumn = Math.floor(extraWidth / widths.length);
129
+ for (const w of widths) {
130
+ w.width += extraWidthPerColumn;
131
+ // if it's the last column, add all the remaining width
132
+ if (w === widths.at(-1)) {
133
+ w.width += extraWidth - extraWidthPerColumn * widths.length;
134
+ }
135
+ }
136
+ }
96
137
  return widths;
97
138
  }
98
139
  export function getHeadings(config) {
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.3.9",
4
+ "version": "0.4.2",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/table/issues",
7
7
  "dependencies": {
@@ -9,7 +9,7 @@
9
9
  "@types/react": "^18.3.12",
10
10
  "change-case": "^5.4.4",
11
11
  "cli-truncate": "^4.0.0",
12
- "ink": "^5.1.0",
12
+ "ink": "5.0.1",
13
13
  "natural-orderby": "^3.0.2",
14
14
  "object-hash": "^3.0.0",
15
15
  "react": "^18.3.1",
@@ -19,13 +19,13 @@
19
19
  "devDependencies": {
20
20
  "@commitlint/config-conventional": "^19",
21
21
  "@oclif/prettier-config": "^0.2.1",
22
- "@oclif/test": "^4.1.3",
22
+ "@oclif/test": "^4.1.6",
23
23
  "@types/chai": "^4.3.16",
24
24
  "@types/mocha": "^10.0.10",
25
25
  "@types/node": "^18",
26
26
  "@types/object-hash": "^3.0.6",
27
27
  "@types/sinon": "^17.0.3",
28
- "ansis": "^3.3.2",
28
+ "ansis": "^3.5.2",
29
29
  "chai": "^4.5.0",
30
30
  "commitlint": "^19",
31
31
  "eslint": "^8.57.0",
@@ -34,7 +34,7 @@
34
34
  "eslint-config-prettier": "^9.1.0",
35
35
  "eslint-config-xo": "^0.45.0",
36
36
  "eslint-config-xo-react": "^0.27.0",
37
- "eslint-plugin-react": "^7.37.2",
37
+ "eslint-plugin-react": "^7.37.3",
38
38
  "eslint-plugin-react-hooks": "^4.6.2",
39
39
  "husky": "^9.1.7",
40
40
  "ink-testing-library": "^4.0.0",