@hubspot/cli 4.2.0 → 4.2.1-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,129 @@
1
+ const path = require('path');
2
+ const chalk = require('chalk');
3
+ const { i18n } = require('./lang');
4
+ const { logger } = require('@hubspot/cli-lib/logger');
5
+ const { handleKeypress } = require('@hubspot/cli-lib/lib/process');
6
+ const SpinniesManager = require('./SpinniesManager');
7
+ const DevServerManager = require('./DevServerManager');
8
+ const { EXIT_CODES } = require('./enums/exitCodes');
9
+ const { getProjectDetailUrl } = require('./projects');
10
+ const { uiAccountDescription, uiBetaMessage, uiLink, uiLine } = require('./ui');
11
+
12
+ const i18nKey = 'cli.lib.LocalDevManagerV2';
13
+
14
+ class LocalDevManagerV2 {
15
+ constructor(options) {
16
+ this.targetAccountId = options.targetAccountId;
17
+ this.projectConfig = options.projectConfig;
18
+ this.projectDir = options.projectDir;
19
+ this.extension = options.extension;
20
+ this.debug = options.debug || false;
21
+
22
+ this.projectSourceDir = path.join(
23
+ this.projectDir,
24
+ this.projectConfig.srcDir
25
+ );
26
+
27
+ if (!this.targetAccountId || !this.projectConfig || !this.projectDir) {
28
+ logger.log(i18n(`${i18nKey}.failedToInitialize`));
29
+ process.exit(EXIT_CODES.ERROR);
30
+ }
31
+ }
32
+
33
+ async start() {
34
+ console.clear();
35
+ SpinniesManager.removeAll();
36
+ SpinniesManager.init();
37
+
38
+ uiBetaMessage(i18n(`${i18nKey}.betaMessage`));
39
+ logger.log();
40
+ logger.log(
41
+ chalk.hex('#FF8F59')(
42
+ i18n(`${i18nKey}.running`, {
43
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
44
+ projectName: this.projectConfig.name,
45
+ })
46
+ )
47
+ );
48
+ logger.log(
49
+ uiLink(
50
+ i18n(`${i18nKey}.viewInHubSpotLink`),
51
+ getProjectDetailUrl(this.projectConfig.name, this.targetAccountId)
52
+ )
53
+ );
54
+ logger.log();
55
+ logger.log(i18n(`${i18nKey}.quitHelper`));
56
+ uiLine();
57
+ logger.log();
58
+
59
+ await this.devServerStart();
60
+
61
+ this.updateKeypressListeners();
62
+ }
63
+
64
+ async stop() {
65
+ SpinniesManager.add('cleanupMessage', {
66
+ text: i18n(`${i18nKey}.exitingStart`),
67
+ });
68
+
69
+ const cleanupSucceeded = await this.devServerCleanup();
70
+
71
+ if (!cleanupSucceeded) {
72
+ SpinniesManager.fail('cleanupMessage', {
73
+ text: i18n(`${i18nKey}.exitingFail`),
74
+ });
75
+ process.exit(EXIT_CODES.ERROR);
76
+ }
77
+
78
+ SpinniesManager.succeed('cleanupMessage', {
79
+ text: i18n(`${i18nKey}.exitingSucceed`),
80
+ });
81
+ process.exit(EXIT_CODES.SUCCESS);
82
+ }
83
+
84
+ updateKeypressListeners() {
85
+ handleKeypress(async key => {
86
+ if ((key.ctrl && key.name === 'c') || key.name === 'q') {
87
+ this.stop();
88
+ }
89
+ });
90
+ }
91
+
92
+ async devServerStart() {
93
+ try {
94
+ DevServerManager.safeLoadServer();
95
+ await DevServerManager.start({
96
+ accountId: this.targetAccountId,
97
+ debug: this.debug,
98
+ extension: this.extension,
99
+ projectConfig: this.projectConfig,
100
+ projectSourceDir: this.projectSourceDir,
101
+ });
102
+ } catch (e) {
103
+ if (this.debug) {
104
+ logger.error(e);
105
+ }
106
+ logger.error(
107
+ i18n(`${i18nKey}.devServer.startError`, { message: e.message })
108
+ );
109
+ process.exit(EXIT_CODES.ERROR);
110
+ }
111
+ }
112
+
113
+ async devServerCleanup() {
114
+ try {
115
+ await DevServerManager.cleanup();
116
+ return true;
117
+ } catch (e) {
118
+ if (this.debug) {
119
+ logger.error(e);
120
+ }
121
+ logger.error(
122
+ i18n(`${i18nKey}.devServer.cleanupError`, { message: e.message })
123
+ );
124
+ return false;
125
+ }
126
+ }
127
+ }
128
+
129
+ module.exports = LocalDevManagerV2;
@@ -1,105 +1,364 @@
1
- const Spinnies = require('spinnies');
1
+ /*
2
+ https://github.com/jbcarpanelli/spinnies
3
+
4
+ Copyright 2019 Juan Bautista Carpanelli (jcarpanelli)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9
+ **/
10
+
11
+ const readline = require('readline');
12
+ const chalk = require('chalk');
13
+ const cliCursor = require('cli-cursor');
14
+ const {
15
+ breakText,
16
+ cleanStream,
17
+ colorOptions,
18
+ getLinesLength,
19
+ purgeSpinnerOptions,
20
+ purgeSpinnersOptions,
21
+ SPINNERS,
22
+ terminalSupportsUnicode,
23
+ writeStream,
24
+ } = require('./spinniesUtils');
2
25
 
3
- // Allows us to maintain a single instance of spinnies across multiple files
4
26
  class SpinniesManager {
5
27
  constructor() {
6
- this.spinnies = null;
7
- this.parentKey = null;
8
- this.categories = {};
28
+ this.resetState();
9
29
  }
10
30
 
11
- init(options) {
12
- if (!this.spinnies) {
13
- this.spinnies = new Spinnies(options);
31
+ init(options = {}) {
32
+ this.options = {
33
+ spinnerColor: 'greenBright',
34
+ succeedColor: 'green',
35
+ failColor: 'red',
36
+ spinner: terminalSupportsUnicode() ? SPINNERS.dots : SPINNERS.dashes,
37
+ disableSpins: false,
38
+ ...purgeSpinnersOptions(options),
39
+ };
40
+ this.spin =
41
+ !this.options.disableSpins &&
42
+ !process.env.CI &&
43
+ process.stderr &&
44
+ process.stderr.isTTY;
45
+
46
+ if (!this.hasAnySpinners()) {
47
+ this.resetState();
14
48
  }
49
+ this.bindSigint();
50
+ }
15
51
 
16
- return {
17
- add: this.add.bind(this),
18
- pick: this.spinnies.pick.bind(this.spinnies),
19
- remove: this.remove.bind(this),
20
- removeAll: this.removeAll.bind(this),
21
- update: this.spinnies.update.bind(this.spinnies),
22
- succeed: this.spinnies.succeed.bind(this.spinnies),
23
- fail: this.spinnies.fail.bind(this.spinnies),
24
- stopAll: this.spinnies.stopAll.bind(this.spinnies),
25
- hasActiveSpinners: this.spinnies.hasActiveSpinners.bind(this.spinnies),
26
- };
52
+ resetState() {
53
+ // Default Spinnies fields
54
+ this.spinners = {};
55
+ this.isCursorHidden = false;
56
+ if (this.currentInterval) {
57
+ clearInterval(this.currentInterval);
58
+ }
59
+ this.currentInterval = null;
60
+ this.stream = process.stderr;
61
+ this.lineCount = 0;
62
+ this.currentFrameIndex = 0;
63
+
64
+ // Custom fields
65
+ this.parentSpinnerName = null;
66
+ this.categories = {};
27
67
  }
28
68
 
29
- addKeyToCategory(key, category) {
69
+ addSpinnerToCategory(name, category) {
30
70
  if (!this.categories[category]) {
31
- this.categories[category] = [];
71
+ this.categories[category] = {};
32
72
  }
33
- this.categories[category].push(key);
73
+ this.categories[category][name] = true;
34
74
  }
35
75
 
36
- getCategoryForKey(key) {
37
- return Object.keys(this.categories).find(category =>
38
- this.categories[category].find(k => k === key)
76
+ getSpinnerCategory(name) {
77
+ return Object.keys(this.categories).find(
78
+ category => !!this.categories[category][name]
39
79
  );
40
80
  }
41
81
 
42
- removeKeyFromCategory(key) {
43
- const category = this.getCategoryForKey(key);
82
+ removeSpinnerFromCategory(name) {
83
+ const category = this.getSpinnerCategory(name);
44
84
  if (category) {
45
- const index = this.categories[category].indexOf(key);
46
- this.categories[category].splice(index, 1);
85
+ delete this.categories[category][name];
47
86
  }
48
87
  }
49
88
 
50
- add(key, options = {}) {
51
- const { category, isParent, noIndent, ...rest } = options;
52
- const originalIndent = rest.indent || 0;
89
+ pick(name) {
90
+ return this.spinners[name];
91
+ }
92
+
93
+ add(name, options = {}) {
94
+ const { category, isParent, noIndent, ...spinnerOptions } = options;
53
95
 
54
- // Support adding generic spinnies lines without specifying a key
55
- const uniqueKey = key || `${Date.now()}`;
96
+ // Support adding generic spinnies lines without specifying a name
97
+ const resolvedName = name || `${Date.now()}-${Math.random()}`;
56
98
 
57
99
  if (category) {
58
- this.addKeyToCategory(uniqueKey, category);
100
+ this.addSpinnerToCategory(resolvedName, category);
59
101
  }
60
102
 
61
- this.spinnies.add(uniqueKey, {
62
- ...rest,
63
- indent: this.parentKey && !noIndent ? originalIndent + 1 : originalIndent,
64
- });
103
+ if (!options.text) {
104
+ spinnerOptions.text = resolvedName;
105
+ }
106
+
107
+ const originalIndent = spinnerOptions.indent || 0;
108
+
109
+ const spinnerProperties = {
110
+ ...colorOptions(this.options),
111
+ succeedPrefix: this.options.succeedPrefix,
112
+ failPrefix: this.options.failPrefix,
113
+ status: 'spinning',
114
+ ...purgeSpinnerOptions(spinnerOptions),
115
+ indent:
116
+ this.parentSpinnerName && !noIndent
117
+ ? originalIndent + 1
118
+ : originalIndent,
119
+ };
120
+
121
+ this.spinners[resolvedName] = spinnerProperties;
122
+ this.updateSpinnerState();
65
123
 
66
124
  if (isParent) {
67
- this.parentKey = uniqueKey;
125
+ this.parentSpinnerName = resolvedName;
68
126
  }
69
127
 
70
- return uniqueKey;
128
+ return { name: resolvedName, ...spinnerProperties };
71
129
  }
72
130
 
73
- remove(key) {
74
- if (this.spinnies) {
75
- if (key === this.parentKey) {
76
- this.parentKey = null;
77
- }
78
- this.removeKeyFromCategory(key);
79
- this.spinnies.remove(key);
131
+ update(name, options = {}) {
132
+ const { status } = options;
133
+ this.setSpinnerProperties(name, options, status);
134
+ this.updateSpinnerState();
135
+
136
+ return this.spinners[name];
137
+ }
138
+
139
+ // TODO there is an issue here with the usage of "non-spinnable"
140
+ // The spinnies lib automatically removes any non-active spinners
141
+ // after adding a new spinner (add -> updateSpinnerState -> checkIfActiveSpinners)
142
+ // so "pick" is telling us that these newly-added spinners don't exist.
143
+ addOrUpdate(name, options = {}) {
144
+ const spinner = this.pick(name);
145
+
146
+ if (spinner) {
147
+ this.update(name, options);
148
+ } else {
149
+ this.add(name, options);
150
+ }
151
+ }
152
+
153
+ succeed(name, options = {}) {
154
+ this.setSpinnerProperties(name, options, 'succeed');
155
+ this.updateSpinnerState();
156
+
157
+ return this.spinners[name];
158
+ }
159
+
160
+ fail(name, options = {}) {
161
+ this.setSpinnerProperties(name, options, 'fail');
162
+ this.updateSpinnerState();
163
+
164
+ return this.spinners[name];
165
+ }
166
+
167
+ remove(name) {
168
+ if (typeof name !== 'string') {
169
+ throw Error('A spinner reference name must be specified');
170
+ }
171
+
172
+ if (name === this.parentSpinnerName) {
173
+ this.parentSpinnerName = null;
80
174
  }
175
+
176
+ this.removeSpinnerFromCategory(name);
177
+
178
+ const spinner = this.spinners[name];
179
+ delete this.spinners[name];
180
+ return spinner;
81
181
  }
82
182
 
83
183
  /**
84
184
  * Removes all spinnies instances
185
+ * @param {string} targetCategory - remove all spinnies with a matching category
85
186
  * @param {string} preserveCategory - do not remove spinnies with a matching category
86
187
  */
87
188
  removeAll({ preserveCategory = null, targetCategory = null } = {}) {
88
- if (this.spinnies) {
89
- Object.keys(this.spinnies.spinners).forEach(key => {
90
- if (targetCategory) {
91
- if (this.getCategoryForKey(key) === targetCategory) {
92
- this.remove(key);
189
+ Object.keys(this.spinners).forEach(name => {
190
+ if (targetCategory) {
191
+ if (this.getSpinnerCategory(name) === targetCategory) {
192
+ this.remove(name);
193
+ }
194
+ } else if (
195
+ !preserveCategory ||
196
+ this.getSpinnerCategory(name) !== preserveCategory
197
+ ) {
198
+ this.remove(name);
199
+ }
200
+ });
201
+ }
202
+
203
+ stopAll(newStatus = 'stopped') {
204
+ Object.keys(this.spinners).forEach(name => {
205
+ const { status: currentStatus } = this.spinners[name];
206
+ if (
207
+ currentStatus !== 'fail' &&
208
+ currentStatus !== 'succeed' &&
209
+ currentStatus !== 'non-spinnable'
210
+ ) {
211
+ if (newStatus === 'succeed' || newStatus === 'fail') {
212
+ this.spinners[name].status = newStatus;
213
+ this.spinners[name].color = this.options[`${newStatus}Color`];
214
+ } else {
215
+ this.spinners[name].status = 'stopped';
216
+ this.spinners[name].color = 'grey';
217
+ }
218
+ }
219
+ });
220
+ this.checkIfActiveSpinners();
221
+
222
+ return this.spinners;
223
+ }
224
+
225
+ hasAnySpinners() {
226
+ return !!Object.keys(this.spinners).length;
227
+ }
228
+
229
+ hasActiveSpinners() {
230
+ return !!Object.values(this.spinners).find(
231
+ ({ status }) => status === 'spinning'
232
+ );
233
+ }
234
+
235
+ setSpinnerProperties(name, options, status) {
236
+ if (typeof name !== 'string') {
237
+ throw Error('A spinner reference name must be specified');
238
+ }
239
+ if (!this.spinners[name]) {
240
+ throw Error(`No spinner initialized with name ${name}`);
241
+ }
242
+ options = purgeSpinnerOptions(options);
243
+ status = status || 'spinning';
244
+
245
+ this.spinners[name] = { ...this.spinners[name], ...options, status };
246
+ }
247
+
248
+ updateSpinnerState() {
249
+ if (this.spin) {
250
+ clearInterval(this.currentInterval);
251
+ this.currentInterval = this.loopStream();
252
+ if (!this.isCursorHidden) {
253
+ cliCursor.hide();
254
+ }
255
+ this.isCursorHidden = true;
256
+ this.checkIfActiveSpinners();
257
+ } else {
258
+ this.setRawStreamOutput();
259
+ }
260
+ }
261
+
262
+ loopStream() {
263
+ const { frames, interval } = this.options.spinner;
264
+ return setInterval(() => {
265
+ this.setStreamOutput(frames[this.currentFrameIndex]);
266
+ this.currentFrameIndex =
267
+ this.currentFrameIndex === frames.length - 1
268
+ ? 0
269
+ : ++this.currentFrameIndex;
270
+ }, interval);
271
+ }
272
+
273
+ setStreamOutput(frame = '') {
274
+ let output = '';
275
+ const linesLength = [];
276
+ const hasActiveSpinners = this.hasActiveSpinners();
277
+ Object.values(this.spinners).map(
278
+ ({
279
+ text,
280
+ status,
281
+ color,
282
+ spinnerColor,
283
+ succeedColor,
284
+ failColor,
285
+ succeedPrefix,
286
+ failPrefix,
287
+ indent,
288
+ }) => {
289
+ let line;
290
+ let prefixLength = indent || 0;
291
+ if (status === 'spinning') {
292
+ prefixLength += frame.length + 1;
293
+ text = breakText(text, prefixLength);
294
+ line = `${chalk[spinnerColor](frame)} ${
295
+ color ? chalk[color](text) : text
296
+ }`;
297
+ } else {
298
+ if (status === 'succeed') {
299
+ prefixLength += succeedPrefix.length + 1;
300
+ if (hasActiveSpinners) {
301
+ text = breakText(text, prefixLength);
302
+ }
303
+ line = `${chalk.green(succeedPrefix)} ${chalk[succeedColor](text)}`;
304
+ } else if (status === 'fail') {
305
+ prefixLength += failPrefix.length + 1;
306
+ if (hasActiveSpinners) {
307
+ text = breakText(text, prefixLength);
308
+ }
309
+ line = `${chalk.red(failPrefix)} ${chalk[failColor](text)}`;
310
+ } else {
311
+ if (hasActiveSpinners) {
312
+ text = breakText(text, prefixLength);
313
+ }
314
+ line = color ? chalk[color](text) : text;
93
315
  }
94
- } else if (
95
- !preserveCategory ||
96
- this.getCategoryForKey(key) !== preserveCategory
97
- ) {
98
- this.remove(key);
99
316
  }
100
- });
317
+ linesLength.push(...getLinesLength(text, prefixLength));
318
+ output += indent ? `${' '.repeat(indent)}${line}\n` : `${line}\n`;
319
+ }
320
+ );
321
+
322
+ if (!hasActiveSpinners) {
323
+ readline.clearScreenDown(this.stream);
324
+ }
325
+
326
+ writeStream(this.stream, output, linesLength);
327
+
328
+ if (hasActiveSpinners) {
329
+ cleanStream(this.stream, linesLength);
330
+ }
331
+
332
+ this.lineCount = linesLength.length;
333
+ }
334
+
335
+ setRawStreamOutput() {
336
+ Object.values(this.spinners).forEach(i => {
337
+ process.stderr.write(`- ${i.text}\n`);
338
+ });
339
+ }
340
+
341
+ checkIfActiveSpinners() {
342
+ if (!this.hasActiveSpinners()) {
343
+ if (this.spin) {
344
+ this.setStreamOutput();
345
+ readline.moveCursor(this.stream, 0, this.lineCount);
346
+ clearInterval(this.currentInterval);
347
+ this.isCursorHidden = false;
348
+ cliCursor.show();
349
+ }
350
+ this.spinners = {};
101
351
  }
102
352
  }
353
+
354
+ bindSigint() {
355
+ process.removeAllListeners('SIGINT');
356
+ process.on('SIGINT', () => {
357
+ cliCursor.show();
358
+ readline.moveCursor(process.stderr, 0, this.lineCount);
359
+ process.exit(0);
360
+ });
361
+ }
103
362
  }
104
363
 
105
364
  module.exports = new SpinniesManager();