@salesforce/sf-plugins-core 9.1.1 → 10.0.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.
@@ -0,0 +1,55 @@
1
+ /*
2
+ * Copyright (c) 2023, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import { inspect } from 'node:util';
8
+ import ansis from 'ansis';
9
+ function prettyPrint(obj) {
10
+ if (!obj)
11
+ return inspect(obj);
12
+ if (typeof obj === 'string')
13
+ return obj;
14
+ if (typeof obj === 'number')
15
+ return obj.toString();
16
+ if (typeof obj === 'boolean')
17
+ return obj.toString();
18
+ if (typeof obj === 'object') {
19
+ return Object.entries(obj)
20
+ .map(([key, value]) => `${key}: ${inspect(value)}`)
21
+ .join(', ');
22
+ }
23
+ return inspect(obj);
24
+ }
25
+ export default function styledObject(obj, keys) {
26
+ if (!obj)
27
+ return inspect(obj);
28
+ if (typeof obj === 'string')
29
+ return obj;
30
+ if (typeof obj === 'number')
31
+ return obj.toString();
32
+ if (typeof obj === 'boolean')
33
+ return obj.toString();
34
+ const output = [];
35
+ const keyLengths = Object.keys(obj).map((key) => key.toString().length);
36
+ const maxKeyLength = Math.max(...keyLengths) + 2;
37
+ const logKeyValue = (key, value) => `${ansis.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + prettyPrint(value);
38
+ for (const [key, value] of Object.entries(obj)) {
39
+ if (keys && !keys.includes(key))
40
+ continue;
41
+ if (Array.isArray(value)) {
42
+ if (value.length > 0) {
43
+ output.push(logKeyValue(key, value[0]));
44
+ for (const e of value.slice(1)) {
45
+ output.push(' '.repeat(maxKeyLength) + prettyPrint(e));
46
+ }
47
+ }
48
+ }
49
+ else if (value !== null && value !== undefined) {
50
+ output.push(logKeyValue(key, value));
51
+ }
52
+ }
53
+ return output.join('\n');
54
+ }
55
+ //# sourceMappingURL=styledObject.js.map
@@ -0,0 +1,22 @@
1
+ export declare function table<T extends Record<string, unknown>>(data: T[], columns: Columns<T>, options?: Options): void;
2
+ export type Column<T extends Record<string, unknown>> = {
3
+ extended: boolean;
4
+ header: string;
5
+ minWidth: number;
6
+ get(row: T): unknown;
7
+ };
8
+ export type Columns<T extends Record<string, unknown>> = {
9
+ [key: string]: Partial<Column<T>>;
10
+ };
11
+ export type Options = {
12
+ columns?: string;
13
+ extended?: boolean;
14
+ filter?: string;
15
+ 'no-header'?: boolean;
16
+ 'no-truncate'?: boolean;
17
+ rowStart?: string;
18
+ sort?: string;
19
+ title?: string;
20
+ printLine?(s: unknown): void;
21
+ };
22
+ //# sourceMappingURL=table.d.ts.map
@@ -0,0 +1,259 @@
1
+ /*
2
+ * Copyright (c) 2023, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import { inspect } from 'node:util';
8
+ import ansis from 'ansis';
9
+ import { orderBy } from 'natural-orderby';
10
+ import sliceAnsi from 'slice-ansi';
11
+ import sw from 'string-width';
12
+ import { ux } from '@oclif/core';
13
+ function sumBy(arr, fn) {
14
+ return arr.reduce((sum, i) => sum + fn(i), 0);
15
+ }
16
+ function capitalize(s) {
17
+ return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : '';
18
+ }
19
+ function termwidth(stream) {
20
+ if (!stream.isTTY) {
21
+ return 80;
22
+ }
23
+ const [width] = stream.getWindowSize();
24
+ if (width < 1) {
25
+ return 80;
26
+ }
27
+ if (width < 40) {
28
+ return 40;
29
+ }
30
+ return width;
31
+ }
32
+ const stdtermwidth = typeof process.env.OCLIF_COLUMNS === 'string'
33
+ ? Number.parseInt(process.env.OCLIF_COLUMNS, 10)
34
+ : termwidth(process.stdout);
35
+ class Table {
36
+ columns;
37
+ options;
38
+ data;
39
+ constructor(data, columns, options = {}) {
40
+ // assign columns
41
+ this.columns = Object.entries(columns).map(([key, col]) => {
42
+ const extended = col.extended ?? false;
43
+ // turn null and undefined into empty strings by default
44
+ const get = col.get ?? ((row) => row[key] ?? '');
45
+ const header = typeof col.header === 'string' ? col.header : capitalize(key.replaceAll('_', ' '));
46
+ const minWidth = Math.max(col.minWidth ?? 0, sw(header) + 1);
47
+ return {
48
+ extended,
49
+ get,
50
+ header,
51
+ key,
52
+ minWidth,
53
+ };
54
+ });
55
+ // assign options
56
+ // eslint-disable-next-line @typescript-eslint/unbound-method
57
+ const { columns: cols, extended, filter, printLine, sort, title } = options;
58
+ this.options = {
59
+ columns: cols,
60
+ extended,
61
+ filter,
62
+ 'no-header': options['no-header'] ?? false,
63
+ 'no-truncate': options['no-truncate'] ?? false,
64
+ printLine: printLine ?? ((s) => ux.stdout(s)),
65
+ rowStart: ' ',
66
+ sort,
67
+ title,
68
+ };
69
+ // build table rows from input array data
70
+ let rows = data.map((d) => Object.fromEntries(this.columns.map((col) => {
71
+ let val = col.get(d);
72
+ if (typeof val !== 'string')
73
+ val = inspect(val, { breakLength: Number.POSITIVE_INFINITY });
74
+ return [col.key, val];
75
+ })));
76
+ // filter rows
77
+ if (this.options.filter) {
78
+ // eslint-disable-next-line prefer-const
79
+ let [header, regex] = this.options.filter.split('=');
80
+ const isNot = header.startsWith('-');
81
+ if (isNot)
82
+ header = header.slice(1);
83
+ const col = this.findColumnFromHeader(header);
84
+ if (!col || !regex)
85
+ throw new Error('Filter flag has an invalid value');
86
+ rows = rows.filter((d) => {
87
+ const re = new RegExp(regex);
88
+ const val = d[col.key];
89
+ const match = val.match(re);
90
+ return isNot ? !match : match;
91
+ });
92
+ }
93
+ // sort rows
94
+ if (this.options.sort) {
95
+ const sorters = this.options.sort.split(',');
96
+ const sortHeaders = sorters.map((k) => (k.startsWith('-') ? k.slice(1) : k));
97
+ const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map((c) => (v) => v[c.key]);
98
+ const sortKeysOrder = sorters.map((k) => (k.startsWith('-') ? 'desc' : 'asc'));
99
+ rows = orderBy(rows, sortKeys, sortKeysOrder);
100
+ }
101
+ // and filter columns
102
+ if (this.options.columns) {
103
+ const filters = this.options.columns.split(',');
104
+ this.columns = this.filterColumnsFromHeaders(filters);
105
+ }
106
+ else if (!this.options.extended) {
107
+ // show extended columns/properties
108
+ this.columns = this.columns.filter((c) => !c.extended);
109
+ }
110
+ this.data = rows;
111
+ }
112
+ display() {
113
+ const { data, options } = this;
114
+ // column truncation
115
+ //
116
+ // find max width for each column
117
+ const columns = this.columns.map((c) => {
118
+ const maxWidth = Math.max(sw('.'.padEnd(c.minWidth - 1)), sw(c.header), getWidestColumnWith(data, c.key)) + 1;
119
+ return {
120
+ ...c,
121
+ maxWidth,
122
+ width: maxWidth,
123
+ };
124
+ });
125
+ // terminal width
126
+ const maxWidth = stdtermwidth - 2;
127
+ // truncation logic
128
+ const maybeShorten = () => {
129
+ // don't shorten if full mode
130
+ if (options['no-truncate'] ?? (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK))
131
+ return;
132
+ // don't shorten if there is enough screen width
133
+ const dataMaxWidth = sumBy(columns, (c) => c.width);
134
+ const overWidth = dataMaxWidth - maxWidth;
135
+ if (overWidth <= 0)
136
+ return;
137
+ // not enough room, short all columns to minWidth
138
+ for (const col of columns) {
139
+ col.width = col.minWidth;
140
+ }
141
+ // if sum(minWidth's) is greater than term width
142
+ // nothing can be done so
143
+ // display all as minWidth
144
+ const dataMinWidth = sumBy(columns, (c) => c.minWidth);
145
+ if (dataMinWidth >= maxWidth)
146
+ return;
147
+ // some wiggle room left, add it back to "needy" columns
148
+ let wiggleRoom = maxWidth - dataMinWidth;
149
+ const needyCols = columns
150
+ .map((c) => ({ key: c.key, needs: c.maxWidth - c.width }))
151
+ .sort((a, b) => a.needs - b.needs);
152
+ for (const { key, needs } of needyCols) {
153
+ if (!needs)
154
+ continue;
155
+ const col = columns.find((c) => key === c.key);
156
+ if (!col)
157
+ continue;
158
+ if (wiggleRoom > needs) {
159
+ col.width = col.width + needs;
160
+ wiggleRoom -= needs;
161
+ }
162
+ else if (wiggleRoom) {
163
+ col.width = col.width + wiggleRoom;
164
+ wiggleRoom = 0;
165
+ }
166
+ }
167
+ };
168
+ maybeShorten();
169
+ // print table title
170
+ if (options.title) {
171
+ options.printLine(options.title);
172
+ // print title divider
173
+ options.printLine(''.padEnd(columns.reduce((sum, col) => sum + col.width, 1), '='));
174
+ // TODO: avoid mutating the passed in options to prevent sideeffects where this table changes the options to other tables
175
+ options.rowStart = '| ';
176
+ }
177
+ // print headers
178
+ if (!options['no-header']) {
179
+ let headers = options.rowStart;
180
+ for (const col of columns) {
181
+ const header = col.header;
182
+ headers += header.padEnd(col.width);
183
+ }
184
+ if (headers)
185
+ options.printLine(ansis.bold(headers));
186
+ // print header dividers
187
+ let dividers = options.rowStart;
188
+ for (const col of columns) {
189
+ const divider = ''.padEnd(col.width - 1, '─') + ' ';
190
+ dividers += divider.padEnd(col.width);
191
+ }
192
+ if (dividers)
193
+ options.printLine(ansis.bold(dividers));
194
+ }
195
+ // print rows
196
+ for (const row of data) {
197
+ // find max number of lines
198
+ // for all cells in a row
199
+ // with multi-line strings
200
+ let numOfLines = 1;
201
+ for (const col of columns) {
202
+ const d = row[col.key];
203
+ const lines = d.split('\n').length;
204
+ if (lines > numOfLines)
205
+ numOfLines = lines;
206
+ }
207
+ // eslint-disable-next-line unicorn/no-new-array
208
+ const linesIndexess = [...new Array(numOfLines).keys()];
209
+ // print row
210
+ // including multi-lines
211
+ for (const i of linesIndexess) {
212
+ let l = options.rowStart;
213
+ for (const col of columns) {
214
+ const width = col.width;
215
+ let d = row[col.key];
216
+ d = d.split('\n')[i] || '';
217
+ const visualWidth = sw(d);
218
+ const colorWidth = d.length - visualWidth;
219
+ let cell = d.padEnd(width + colorWidth);
220
+ if (cell.length - colorWidth > width || visualWidth === width) {
221
+ // truncate the cell, preserving ANSI escape sequences, and keeping
222
+ // into account the width of fullwidth unicode characters
223
+ cell = sliceAnsi(cell, 0, width - 2) + '… ';
224
+ // pad with spaces; this is necessary in case the original string
225
+ // contained fullwidth characters which cannot be split
226
+ cell += ' '.repeat(width - sw(cell));
227
+ }
228
+ l += cell;
229
+ }
230
+ options.printLine(l);
231
+ }
232
+ }
233
+ }
234
+ filterColumnsFromHeaders(filters) {
235
+ const cols = [];
236
+ for (const f of [...new Set(filters)]) {
237
+ const c = this.columns.find((i) => i.header.toLowerCase() === f.toLowerCase());
238
+ if (c)
239
+ cols.push(c);
240
+ }
241
+ return cols;
242
+ }
243
+ findColumnFromHeader(header) {
244
+ return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase());
245
+ }
246
+ }
247
+ export function table(data, columns, options = {}) {
248
+ new Table(data, columns, options).display();
249
+ }
250
+ const getWidestColumnWith = (data, columnKey) => data.reduce((previous, current) => {
251
+ const d = current[columnKey];
252
+ if (typeof d !== 'string')
253
+ return previous;
254
+ // convert multi-line cell to single longest line
255
+ // for width calculations
256
+ const manyLines = d.split('\n');
257
+ return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r) => sw(r))) : sw(d));
258
+ }, 0);
259
+ //# sourceMappingURL=table.js.map
package/lib/ux/ux.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { ux } from '@oclif/core';
2
1
  import { AnyJson } from '@salesforce/ts-types';
3
2
  import { UxBase } from './base.js';
4
3
  import { Spinner } from './spinner.js';
4
+ import { Columns as TableColumns, Options as TableOptions } from './table.js';
5
5
  /**
6
6
  * UX methods for plugins. Automatically suppress console output if outputEnabled is set to false.
7
7
  *
@@ -31,6 +31,13 @@ export declare class Ux extends UxBase {
31
31
  * @param args Args to be used for formatting.
32
32
  */
33
33
  log(message?: string, ...args: string[]): void;
34
+ /**
35
+ * Log a message to stderr. This will be automatically suppressed if output is disabled.
36
+ *
37
+ * @param message Message to log. Formatting is supported.
38
+ * @param args Args to be used for formatting.
39
+ */
40
+ logToStderr(message?: string, ...args: string[]): void;
34
41
  /**
35
42
  * Log a warning message to the console. This will be automatically suppressed if output is disabled.
36
43
  *
@@ -58,7 +65,7 @@ export declare class Ux extends UxBase {
58
65
  *
59
66
  * @param obj JSON to display
60
67
  */
61
- styledJSON(obj: AnyJson): void;
68
+ styledJSON(obj: AnyJson, theme?: Record<string, string>): void;
62
69
  /**
63
70
  * Display stylized object to the console. This will be automatically suppressed if output is disabled.
64
71
  *
@@ -76,8 +83,8 @@ export declare class Ux extends UxBase {
76
83
  export declare namespace Ux {
77
84
  namespace Table {
78
85
  type Data = Record<string, unknown>;
79
- type Columns<T extends Data> = ux.Table.table.Columns<T>;
80
- type Options = ux.Table.table.Options;
86
+ type Columns<T extends Data> = TableColumns<T>;
87
+ type Options = TableOptions;
81
88
  }
82
89
  }
83
90
  //# sourceMappingURL=ux.d.ts.map
package/lib/ux/ux.js CHANGED
@@ -4,9 +4,13 @@
4
4
  * Licensed under the BSD 3-Clause license.
5
5
  * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
+ import ansis from 'ansis';
7
8
  import { ux } from '@oclif/core';
9
+ import terminalLink from 'terminal-link';
8
10
  import { UxBase } from './base.js';
9
11
  import { Spinner } from './spinner.js';
12
+ import { table } from './table.js';
13
+ import styledObject from './styledObject.js';
10
14
  /**
11
15
  * UX methods for plugins. Automatically suppress console output if outputEnabled is set to false.
12
16
  *
@@ -38,7 +42,16 @@ export class Ux extends UxBase {
38
42
  * @param args Args to be used for formatting.
39
43
  */
40
44
  log(message, ...args) {
41
- this.maybeNoop(() => ux.log(message, ...args));
45
+ this.maybeNoop(() => ux.stdout(message, ...args));
46
+ }
47
+ /**
48
+ * Log a message to stderr. This will be automatically suppressed if output is disabled.
49
+ *
50
+ * @param message Message to log. Formatting is supported.
51
+ * @param args Args to be used for formatting.
52
+ */
53
+ logToStderr(message, ...args) {
54
+ this.maybeNoop(() => ux.stderr(message, ...args));
42
55
  }
43
56
  /**
44
57
  * Log a warning message to the console. This will be automatically suppressed if output is disabled.
@@ -56,7 +69,7 @@ export class Ux extends UxBase {
56
69
  * @param options Options for how the table should be displayed
57
70
  */
58
71
  table(data, columns, options) {
59
- this.maybeNoop(() => ux.table(data, columns, { 'no-truncate': true, ...options }));
72
+ this.maybeNoop(() => table(data, columns, { 'no-truncate': true, ...options }));
60
73
  }
61
74
  /**
62
75
  * Display a url to the console. This will be automatically suppressed if output is disabled.
@@ -66,15 +79,25 @@ export class Ux extends UxBase {
66
79
  * @param params
67
80
  */
68
81
  url(text, uri, params = {}) {
69
- this.maybeNoop(() => ux.url(text, uri, params));
82
+ this.maybeNoop(() => ux.stdout(terminalLink(text, uri, { fallback: () => uri, ...params })));
70
83
  }
71
84
  /**
72
85
  * Display stylized JSON to the console. This will be automatically suppressed if output is disabled.
73
86
  *
74
87
  * @param obj JSON to display
75
88
  */
76
- styledJSON(obj) {
77
- this.maybeNoop(() => ux.styledJSON(obj));
89
+ styledJSON(obj, theme) {
90
+ // Default theme if sf's theme.json does not have the json property set. This will allow us
91
+ // to ship sf-plugins-core before the theme.json is updated.
92
+ const defaultTheme = {
93
+ key: 'blueBright',
94
+ string: 'greenBright',
95
+ number: 'blue',
96
+ boolean: 'redBright',
97
+ null: 'blackBright',
98
+ };
99
+ const mergedTheme = { ...defaultTheme, ...theme };
100
+ this.maybeNoop(() => ux.stdout(ux.colorizeJson(obj, { theme: mergedTheme })));
78
101
  }
79
102
  /**
80
103
  * Display stylized object to the console. This will be automatically suppressed if output is disabled.
@@ -83,7 +106,7 @@ export class Ux extends UxBase {
83
106
  * @param keys Keys of object to display
84
107
  */
85
108
  styledObject(obj, keys) {
86
- this.maybeNoop(() => ux.styledObject(obj, keys));
109
+ this.maybeNoop(() => ux.stdout(styledObject(obj, keys)));
87
110
  }
88
111
  /**
89
112
  * Display stylized header to the console. This will be automatically suppressed if output is disabled.
@@ -91,7 +114,7 @@ export class Ux extends UxBase {
91
114
  * @param text header to display
92
115
  */
93
116
  styledHeader(text) {
94
- this.maybeNoop(() => ux.styledHeader(text));
117
+ this.maybeNoop(() => ux.stdout(ansis.dim('=== ') + ansis.bold(text) + '\n'));
95
118
  }
96
119
  }
97
120
  //# sourceMappingURL=ux.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sf-plugins-core",
3
- "version": "9.1.1",
3
+ "version": "10.0.0",
4
4
  "description": "Utils for writing Salesforce CLI plugins",
5
5
  "main": "lib/exported",
6
6
  "types": "lib/exported.d.ts",
@@ -20,6 +20,7 @@
20
20
  "prepack": "sf-prepack",
21
21
  "prepare": "sf-install",
22
22
  "test": "wireit",
23
+ "test:integration": "mocha test/**/*.integration.ts --timeout 30000",
23
24
  "test:only": "wireit"
24
25
  },
25
26
  "exports": {
@@ -45,16 +46,22 @@
45
46
  "dependencies": {
46
47
  "@inquirer/confirm": "^3.1.9",
47
48
  "@inquirer/password": "^2.1.9",
48
- "@oclif/core": "^3.26.6",
49
- "@salesforce/core": "^7.3.9",
49
+ "@oclif/core": "^4",
50
+ "@salesforce/core": "^7.3.10",
50
51
  "@salesforce/kit": "^3.1.2",
51
52
  "@salesforce/ts-types": "^2.0.9",
52
- "chalk": "^5.3.0"
53
+ "ansis": "^3.1.1",
54
+ "cli-progress": "^3.12.0",
55
+ "natural-orderby": "^3.0.2",
56
+ "slice-ansi": "^7.1.0",
57
+ "string-width": "^7.1.0",
58
+ "terminal-link": "^3.0.0"
53
59
  },
54
60
  "devDependencies": {
55
61
  "@inquirer/type": "^1.3.3",
56
62
  "@salesforce/dev-scripts": "^9.1.2",
57
- "eslint-plugin-sf-plugin": "^1.18.4",
63
+ "@types/cli-progress": "^3.11.5",
64
+ "eslint-plugin-sf-plugin": "^1.18.5",
58
65
  "ts-node": "^10.9.2",
59
66
  "typescript": "^5.4.5"
60
67
  },