@salesforce/sf-plugins-core 9.1.1 → 10.0.0-beta.1

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,257 @@
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()[0];
24
+ if (width < 1) {
25
+ return 80;
26
+ }
27
+ if (width < 40) {
28
+ return 40;
29
+ }
30
+ return width;
31
+ }
32
+ const stdtermwidth = Number.parseInt(process.env.OCLIF_COLUMNS, 10) || termwidth(process.stdout);
33
+ class Table {
34
+ columns;
35
+ options;
36
+ data;
37
+ constructor(data, columns, options = {}) {
38
+ // assign columns
39
+ this.columns = Object.keys(columns).map((key) => {
40
+ const col = columns[key];
41
+ const extended = col.extended ?? false;
42
+ // turn null and undefined into empty strings by default
43
+ const get = col.get ?? ((row) => row[key] ?? '');
44
+ const header = typeof col.header === 'string' ? col.header : capitalize(key.replaceAll('_', ' '));
45
+ const minWidth = Math.max(col.minWidth ?? 0, sw(header) + 1);
46
+ return {
47
+ extended,
48
+ get,
49
+ header,
50
+ key,
51
+ minWidth,
52
+ };
53
+ });
54
+ // assign options
55
+ // eslint-disable-next-line @typescript-eslint/unbound-method
56
+ const { columns: cols, extended, filter, printLine, sort, title } = options;
57
+ this.options = {
58
+ columns: cols,
59
+ extended,
60
+ filter,
61
+ 'no-header': options['no-header'] ?? false,
62
+ 'no-truncate': options['no-truncate'] ?? false,
63
+ printLine: printLine ?? ((s) => ux.stdout(s)),
64
+ rowStart: ' ',
65
+ sort,
66
+ title,
67
+ };
68
+ // build table rows from input array data
69
+ let rows = data.map((d) => Object.fromEntries(this.columns.map((col) => {
70
+ let val = col.get(d);
71
+ if (typeof val !== 'string')
72
+ val = inspect(val, { breakLength: Number.POSITIVE_INFINITY });
73
+ return [col.key, val];
74
+ })));
75
+ // filter rows
76
+ if (this.options.filter) {
77
+ // eslint-disable-next-line prefer-const
78
+ let [header, regex] = this.options.filter.split('=');
79
+ const isNot = header.startsWith('-');
80
+ if (isNot)
81
+ header = header.slice(1);
82
+ const col = this.findColumnFromHeader(header);
83
+ if (!col || !regex)
84
+ throw new Error('Filter flag has an invalid value');
85
+ rows = rows.filter((d) => {
86
+ const re = new RegExp(regex);
87
+ const val = d[col.key];
88
+ const match = val.match(re);
89
+ return isNot ? !match : match;
90
+ });
91
+ }
92
+ // sort rows
93
+ if (this.options.sort) {
94
+ const sorters = this.options.sort.split(',');
95
+ const sortHeaders = sorters.map((k) => (k.startsWith('-') ? k.slice(1) : k));
96
+ const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map((c) => (v) => v[c.key]);
97
+ const sortKeysOrder = sorters.map((k) => (k.startsWith('-') ? 'desc' : 'asc'));
98
+ rows = orderBy(rows, sortKeys, sortKeysOrder);
99
+ }
100
+ // and filter columns
101
+ if (this.options.columns) {
102
+ const filters = this.options.columns.split(',');
103
+ this.columns = this.filterColumnsFromHeaders(filters);
104
+ }
105
+ else if (!this.options.extended) {
106
+ // show extended columns/properties
107
+ this.columns = this.columns.filter((c) => !c.extended);
108
+ }
109
+ this.data = rows;
110
+ }
111
+ display() {
112
+ const { data, options } = this;
113
+ // column truncation
114
+ //
115
+ // find max width for each column
116
+ const columns = this.columns.map((c) => {
117
+ const maxWidth = Math.max(sw('.'.padEnd(c.minWidth - 1)), sw(c.header), getWidestColumnWith(data, c.key)) + 1;
118
+ return {
119
+ ...c,
120
+ maxWidth,
121
+ width: maxWidth,
122
+ };
123
+ });
124
+ // terminal width
125
+ const maxWidth = stdtermwidth - 2;
126
+ // truncation logic
127
+ const shouldShorten = () => {
128
+ // don't shorten if full mode
129
+ if (options['no-truncate'] ?? (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK))
130
+ return;
131
+ // don't shorten if there is enough screen width
132
+ const dataMaxWidth = sumBy(columns, (c) => c.width);
133
+ const overWidth = dataMaxWidth - maxWidth;
134
+ if (overWidth <= 0)
135
+ return;
136
+ // not enough room, short all columns to minWidth
137
+ for (const col of columns) {
138
+ col.width = col.minWidth;
139
+ }
140
+ // if sum(minWidth's) is greater than term width
141
+ // nothing can be done so
142
+ // display all as minWidth
143
+ const dataMinWidth = sumBy(columns, (c) => c.minWidth);
144
+ if (dataMinWidth >= maxWidth)
145
+ return;
146
+ // some wiggle room left, add it back to "needy" columns
147
+ let wiggleRoom = maxWidth - dataMinWidth;
148
+ const needyCols = columns
149
+ .map((c) => ({ key: c.key, needs: c.maxWidth - c.width }))
150
+ .sort((a, b) => a.needs - b.needs);
151
+ for (const { key, needs } of needyCols) {
152
+ if (!needs)
153
+ continue;
154
+ const col = columns.find((c) => key === c.key);
155
+ if (!col)
156
+ continue;
157
+ if (wiggleRoom > needs) {
158
+ col.width = col.width + needs;
159
+ wiggleRoom -= needs;
160
+ }
161
+ else if (wiggleRoom) {
162
+ col.width = col.width + wiggleRoom;
163
+ wiggleRoom = 0;
164
+ }
165
+ }
166
+ };
167
+ shouldShorten();
168
+ // print table title
169
+ if (options.title) {
170
+ options.printLine(options.title);
171
+ // print title divider
172
+ options.printLine(''.padEnd(columns.reduce((sum, col) => sum + col.width, 1), '='));
173
+ options.rowStart = '| ';
174
+ }
175
+ // print headers
176
+ if (!options['no-header']) {
177
+ let headers = options.rowStart;
178
+ for (const col of columns) {
179
+ const header = col.header;
180
+ headers += header.padEnd(col.width);
181
+ }
182
+ if (headers)
183
+ options.printLine(ansis.bold(headers));
184
+ // print header dividers
185
+ let dividers = options.rowStart;
186
+ for (const col of columns) {
187
+ const divider = ''.padEnd(col.width - 1, '─') + ' ';
188
+ dividers += divider.padEnd(col.width);
189
+ }
190
+ if (dividers)
191
+ options.printLine(ansis.bold(dividers));
192
+ }
193
+ // print rows
194
+ for (const row of data) {
195
+ // find max number of lines
196
+ // for all cells in a row
197
+ // with multi-line strings
198
+ let numOfLines = 1;
199
+ for (const col of columns) {
200
+ const d = row[col.key];
201
+ const lines = d.split('\n').length;
202
+ if (lines > numOfLines)
203
+ numOfLines = lines;
204
+ }
205
+ // eslint-disable-next-line unicorn/no-new-array
206
+ const linesIndexess = [...new Array(numOfLines).keys()];
207
+ // print row
208
+ // including multi-lines
209
+ for (const i of linesIndexess) {
210
+ let l = options.rowStart;
211
+ for (const col of columns) {
212
+ const width = col.width;
213
+ let d = row[col.key];
214
+ d = d.split('\n')[i] || '';
215
+ const visualWidth = sw(d);
216
+ const colorWidth = d.length - visualWidth;
217
+ let cell = d.padEnd(width + colorWidth);
218
+ if (cell.length - colorWidth > width || visualWidth === width) {
219
+ // truncate the cell, preserving ANSI escape sequences, and keeping
220
+ // into account the width of fullwidth unicode characters
221
+ cell = sliceAnsi(cell, 0, width - 2) + '… ';
222
+ // pad with spaces; this is necessary in case the original string
223
+ // contained fullwidth characters which cannot be split
224
+ cell += ' '.repeat(width - sw(cell));
225
+ }
226
+ l += cell;
227
+ }
228
+ options.printLine(l);
229
+ }
230
+ }
231
+ }
232
+ filterColumnsFromHeaders(filters) {
233
+ const cols = [];
234
+ for (const f of [...new Set(filters)]) {
235
+ const c = this.columns.find((i) => i.header.toLowerCase() === f.toLowerCase());
236
+ if (c)
237
+ cols.push(c);
238
+ }
239
+ return cols;
240
+ }
241
+ findColumnFromHeader(header) {
242
+ return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase());
243
+ }
244
+ }
245
+ export function table(data, columns, options = {}) {
246
+ new Table(data, columns, options).display();
247
+ }
248
+ const getWidestColumnWith = (data, columnKey) => data.reduce((previous, current) => {
249
+ const d = current[columnKey];
250
+ if (typeof d !== 'string')
251
+ return previous;
252
+ // convert multi-line cell to single longest line
253
+ // for width calculations
254
+ const manyLines = d.split('\n');
255
+ return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r) => sw(r))) : sw(d));
256
+ }, 0);
257
+ //# 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-beta.1",
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
+ "@oclif/core": "^4",
49
50
  "@salesforce/core": "^7.3.9",
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",
62
+ "@types/cli-progress": "^3.11.5",
56
63
  "@salesforce/dev-scripts": "^9.1.2",
57
- "eslint-plugin-sf-plugin": "^1.18.4",
64
+ "eslint-plugin-sf-plugin": "^1.18.5",
58
65
  "ts-node": "^10.9.2",
59
66
  "typescript": "^5.4.5"
60
67
  },