@hubspot/cli 4.2.1-beta.2 → 4.2.1-beta.4

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.
@@ -1,40 +1,32 @@
1
- const chokidar = require('chokidar');
2
1
  const path = require('path');
3
- const { default: PQueue } = require('p-queue');
2
+ const chokidar = require('chokidar');
3
+ const chalk = require('chalk');
4
4
  const { i18n } = require('./lang');
5
5
  const { logger } = require('@hubspot/cli-lib/logger');
6
- const {
7
- isSpecifiedError,
8
- } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
9
6
  const { handleKeypress } = require('@hubspot/cli-lib/lib/process');
10
7
  const {
11
- logApiErrorInstance,
12
- ApiErrorContext,
13
- } = require('@hubspot/cli-lib/errorHandlers');
14
- const {
15
- PROJECT_BUILD_TEXT,
16
- PROJECT_DEPLOY_TEXT,
17
- ERROR_TYPES,
18
- } = require('@hubspot/cli-lib/lib/constants');
19
- const { isAllowedExtension } = require('@hubspot/cli-lib/path');
20
- const { shouldIgnoreFile } = require('@hubspot/cli-lib/ignoreRules');
21
- const {
22
- cancelStagedBuild,
23
- uploadFileToBuild,
24
- deleteFileFromBuild,
25
- provisionBuild,
26
- queueBuild,
27
- } = require('@hubspot/cli-lib/api/dfs');
8
+ getAccountId,
9
+ getConfigDefaultAccount,
10
+ } = require('@hubspot/cli-lib/lib/config');
11
+ const { PROJECT_CONFIG_FILE } = require('@hubspot/cli-lib/lib/constants');
28
12
  const SpinniesManager = require('./SpinniesManager');
29
13
  const DevServerManager = require('./DevServerManager');
30
14
  const { EXIT_CODES } = require('./enums/exitCodes');
31
- const { pollProjectBuildAndDeploy } = require('./projects');
32
- const { uiAccountDescription, uiLink } = require('./ui');
33
-
34
- const i18nKey = 'cli.lib.LocalDevManager';
35
-
36
- const BUILD_DEBOUNCE_TIME_LONG = 5000;
37
- const BUILD_DEBOUNCE_TIME_SHORT = 3500;
15
+ const { getProjectDetailUrl } = require('./projects');
16
+ const {
17
+ APP_COMPONENT_CONFIG,
18
+ COMPONENT_TYPES,
19
+ findProjectComponents,
20
+ getAppCardConfigs,
21
+ } = require('./projectStructure');
22
+ const {
23
+ UI_COLORS,
24
+ uiCommandReference,
25
+ uiAccountDescription,
26
+ uiBetaMessage,
27
+ uiLink,
28
+ uiLine,
29
+ } = require('./ui');
38
30
 
39
31
  const WATCH_EVENTS = {
40
32
  add: 'add',
@@ -43,30 +35,22 @@ const WATCH_EVENTS = {
43
35
  unlinkDir: 'unlinkDir',
44
36
  };
45
37
 
46
- const UPLOAD_PERMISSIONS = {
47
- always: 'always',
48
- manual: 'manual',
49
- never: 'never',
50
- };
38
+ const i18nKey = 'cli.lib.LocalDevManager';
51
39
 
52
40
  class LocalDevManager {
53
41
  constructor(options) {
54
42
  this.targetAccountId = options.targetAccountId;
55
43
  this.projectConfig = options.projectConfig;
56
44
  this.projectDir = options.projectDir;
57
- this.uploadPermission =
58
- options.uploadPermission || UPLOAD_PERMISSIONS.always;
59
45
  this.debug = options.debug || false;
46
+ this.deployedBuild = options.deployedBuild;
47
+ this.watcher = null;
48
+ this.uploadWarnings = {};
60
49
 
61
50
  this.projectSourceDir = path.join(
62
51
  this.projectDir,
63
52
  this.projectConfig.srcDir
64
53
  );
65
- this.watcher = null;
66
- this.uploadQueue = null;
67
- this.standbyChanges = [];
68
- this.debouncedBuild = null;
69
- this.currentStagedBuildId = null;
70
54
 
71
55
  if (!this.targetAccountId || !this.projectConfig || !this.projectDir) {
72
56
  logger.log(i18n(`${i18nKey}.failedToInitialize`));
@@ -75,633 +59,315 @@ class LocalDevManager {
75
59
  }
76
60
 
77
61
  async start() {
62
+ SpinniesManager.stopAll();
78
63
  SpinniesManager.init();
79
64
 
80
- this.watcher = chokidar.watch(this.projectSourceDir, {
81
- ignoreInitial: true,
82
- ignored: file => shouldIgnoreFile(file),
83
- });
65
+ // Local dev currently relies on the existence of a deployed build in the target account
66
+ if (!this.deployedBuild) {
67
+ logger.error(
68
+ i18n(`${i18nKey}.noDeployedBuild`, {
69
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
70
+ uploadCommand: this.getUploadCommand(),
71
+ })
72
+ );
73
+ logger.log();
74
+ process.exit(EXIT_CODES.SUCCESS);
75
+ }
84
76
 
85
- this.uploadQueue = new PQueue({ concurrency: 10 });
77
+ const components = await findProjectComponents(this.projectSourceDir);
86
78
 
87
- if (this.debug) {
88
- this.uploadQueue.on('error', error => {
89
- logger.debug(error);
90
- });
79
+ // The project is empty, there is nothing to run locally
80
+ if (!components.length) {
81
+ logger.error(i18n(`${i18nKey}.noComponents`));
82
+ process.exit(EXIT_CODES.SUCCESS);
91
83
  }
92
84
 
93
- console.clear();
94
- SpinniesManager.removeAll();
85
+ const runnableComponents = components.filter(
86
+ component => component.runnable
87
+ );
95
88
 
96
- logger.log(i18n(`${i18nKey}.header.betaMessage`));
97
- logger.log();
89
+ // The project does not contain any components that support local development
90
+ if (!runnableComponents.length) {
91
+ logger.error(i18n(`${i18nKey}.noRunnableComponents`));
92
+ process.exit(EXIT_CODES.SUCCESS);
93
+ }
94
+
95
+ const setupSucceeded = await this.devServerSetup(runnableComponents);
96
+
97
+ if (!setupSucceeded) {
98
+ process.exit(EXIT_CODES.ERROR);
99
+ } else if (!this.debug) {
100
+ console.clear();
101
+ }
98
102
 
99
- this.updateConsoleHeader();
103
+ uiBetaMessage(i18n(`${i18nKey}.betaMessage`));
104
+ logger.log();
105
+ logger.log(
106
+ chalk.hex(UI_COLORS.SORBET)(
107
+ i18n(`${i18nKey}.running`, {
108
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
109
+ projectName: this.projectConfig.name,
110
+ })
111
+ )
112
+ );
113
+ logger.log(
114
+ uiLink(
115
+ i18n(`${i18nKey}.viewInHubSpotLink`),
116
+ getProjectDetailUrl(this.projectConfig.name, this.targetAccountId)
117
+ )
118
+ );
119
+ logger.log();
120
+ logger.log(i18n(`${i18nKey}.quitHelper`));
121
+ uiLine();
122
+ logger.log();
100
123
 
101
124
  await this.devServerStart();
102
125
 
103
- this.uploadQueue.start();
104
- await this.startWatching();
126
+ // Initialize project file watcher to detect configuration file changes
127
+ this.startWatching(runnableComponents);
105
128
 
106
129
  this.updateKeypressListeners();
107
130
 
108
- this.updateConsoleHeader();
131
+ this.monitorConsoleOutput();
132
+
133
+ // Verify that there are no mismatches between components in the local project
134
+ // and components in the deployed build of the project.
135
+ this.compareLocalProjectToDeployed(runnableComponents);
109
136
  }
110
137
 
111
138
  async stop() {
112
- this.clearConsoleContent();
113
-
114
139
  SpinniesManager.add('cleanupMessage', {
115
140
  text: i18n(`${i18nKey}.exitingStart`),
116
141
  });
117
142
 
118
143
  await this.stopWatching();
119
- await this.devServerCleanup();
120
-
121
- let exitCode = EXIT_CODES.SUCCESS;
122
-
123
- if (this.currentStagedBuildId) {
124
- try {
125
- await cancelStagedBuild(this.targetAccountId, this.projectConfig.name);
126
- } catch (err) {
127
- if (
128
- !isSpecifiedError(err, {
129
- subCategory: ERROR_TYPES.BUILD_NOT_IN_PROGRESS,
130
- })
131
- ) {
132
- logApiErrorInstance(
133
- err,
134
- new ApiErrorContext({
135
- accountId: this.targetAccountId,
136
- projectName: this.projectConfig.name,
137
- })
138
- );
139
- exitCode = EXIT_CODES.ERROR;
140
- }
141
- }
142
- }
143
144
 
144
- if (exitCode === EXIT_CODES.SUCCESS) {
145
- SpinniesManager.succeed('cleanupMessage', {
146
- text: i18n(`${i18nKey}.exitingSucceed`),
147
- });
148
- } else {
145
+ const cleanupSucceeded = await this.devServerCleanup();
146
+
147
+ if (!cleanupSucceeded) {
149
148
  SpinniesManager.fail('cleanupMessage', {
150
149
  text: i18n(`${i18nKey}.exitingFail`),
151
150
  });
151
+ process.exit(EXIT_CODES.ERROR);
152
152
  }
153
153
 
154
- process.exit(exitCode);
155
- }
156
-
157
- updateConsoleHeader() {
158
- SpinniesManager.addOrUpdate('devModeRunning', {
159
- text: i18n(`${i18nKey}.header.running`, {
160
- accountIdentifier: uiAccountDescription(this.targetAccountId),
161
- projectName: this.projectConfig.name,
162
- }),
163
- isParent: true,
164
- category: 'header',
165
- });
166
- SpinniesManager.addOrUpdate('devModeStatus', {
167
- text: i18n(`${i18nKey}.header.status.clean`),
168
- status: 'non-spinnable',
169
- indent: 1,
170
- category: 'header',
171
- });
172
-
173
- const viewText = DevServerManager.initialized
174
- ? uiLink(
175
- i18n(`${i18nKey}.header.viewInHubSpotLink`),
176
- DevServerManager.generateURL(`hs/project`),
177
- {
178
- inSpinnies: true,
179
- }
180
- )
181
- : ' ';
182
-
183
- SpinniesManager.addOrUpdate('viewInHubSpotLink', {
184
- text: viewText,
185
- status: 'non-spinnable',
186
- indent: 1,
187
- category: 'header',
188
- });
189
- SpinniesManager.addOrUpdate('spacer-1', {
190
- text: ' ',
191
- status: 'non-spinnable',
192
- category: 'header',
154
+ SpinniesManager.succeed('cleanupMessage', {
155
+ text: i18n(`${i18nKey}.exitingSucceed`),
193
156
  });
194
- SpinniesManager.addOrUpdate('quitHelper', {
195
- text: i18n(`${i18nKey}.header.quitHelper`),
196
- status: 'non-spinnable',
197
- indent: 1,
198
- category: 'header',
199
- });
200
- SpinniesManager.addOrUpdate('lineSeparator', {
201
- text: '-'.repeat(50),
202
- status: 'non-spinnable',
203
- noIndent: true,
204
- category: 'header',
205
- });
206
- }
207
-
208
- clearConsoleContent() {
209
- SpinniesManager.removeAll({ preserveCategory: 'header' });
157
+ process.exit(EXIT_CODES.SUCCESS);
210
158
  }
211
159
 
212
160
  updateKeypressListeners() {
213
161
  handleKeypress(async key => {
214
162
  if ((key.ctrl && key.name === 'c') || key.name === 'q') {
215
163
  this.stop();
216
- } else if (
217
- (key.name === 'y' || key.name === 'n') &&
218
- this.uploadPermission === UPLOAD_PERMISSIONS.manual &&
219
- this.hasAnyUnsupportedStandbyChanges()
220
- ) {
221
- SpinniesManager.remove('manualUploadRequired');
222
- SpinniesManager.remove('manualUploadExplanation1');
223
- SpinniesManager.remove('manualUploadExplanation2');
224
- SpinniesManager.remove('manualUploadPrompt');
225
-
226
- if (key.name === 'y') {
227
- SpinniesManager.add(null, {
228
- text: i18n(`${i18nKey}.upload.manualUploadConfirmed`),
229
- status: 'succeed',
230
- succeedColor: 'white',
231
- noIndent: true,
232
- });
233
- this.updateDevModeStatus('manualUpload');
234
- await this.createNewStagingBuild();
235
- this.flushStandbyChanges();
236
- await this.queueBuild();
237
- } else if (key.name === 'n') {
238
- SpinniesManager.add(null, {
239
- text: i18n(`${i18nKey}.upload.manualUploadSkipped`),
240
- status: 'fail',
241
- failColor: 'white',
242
- noIndent: true,
243
- });
244
- }
245
164
  }
246
165
  });
247
166
  }
248
167
 
249
- logBuildError(buildStatus = {}) {
250
- const subTasks = buildStatus[PROJECT_BUILD_TEXT.SUBTASK_KEY] || [];
251
- const failedSubTasks = subTasks.filter(task => task.status === 'FAILURE');
252
-
253
- if (failedSubTasks.length) {
254
- this.updateDevModeStatus('buildError');
255
-
256
- failedSubTasks.forEach(failedSubTask => {
257
- SpinniesManager.add(null, {
258
- text: failedSubTask.errorMessage,
259
- status: 'fail',
260
- failColor: 'white',
261
- indent: 1,
262
- });
263
- });
264
- }
265
- }
266
-
267
- logDeployError(deployStatus = {}) {
268
- const subTasks = deployStatus[PROJECT_DEPLOY_TEXT.SUBTASK_KEY] || [];
269
- const failedSubTasks = subTasks.filter(task => task.status === 'FAILURE');
270
-
271
- if (failedSubTasks.length) {
272
- this.updateDevModeStatus('deployError');
168
+ getUploadCommand() {
169
+ const currentDefaultAccount = getConfigDefaultAccount();
273
170
 
274
- failedSubTasks.forEach(failedSubTask => {
275
- SpinniesManager.add(null, {
276
- text: failedSubTask.errorMessage,
277
- status: 'fail',
278
- failColor: 'white',
279
- indent: 1,
280
- });
281
- });
282
- }
283
- }
284
-
285
- updateDevModeStatus(langKey) {
286
- SpinniesManager.update('devModeStatus', {
287
- text: i18n(`${i18nKey}.header.status.${langKey}`),
288
- status: 'non-spinnable',
289
- noIndent: true,
290
- });
291
- }
292
-
293
- async pauseUploadQueue() {
294
- this.uploadQueue.pause();
295
- await this.uploadQueue.onIdle();
296
- }
297
-
298
- hasAnyUnsupportedStandbyChanges() {
299
- return this.standbyChanges.some(({ supported }) => !supported);
171
+ return this.targetAccountId !== getAccountId(currentDefaultAccount)
172
+ ? uiCommandReference(
173
+ `hs project upload --account=${this.targetAccountId}`
174
+ )
175
+ : uiCommandReference('hs project upload');
300
176
  }
301
177
 
302
- async createNewStagingBuild() {
303
- try {
304
- const { buildId } = await provisionBuild(
305
- this.targetAccountId,
306
- this.projectConfig.name,
307
- this.projectConfig.platformVersion
178
+ logUploadWarning(reason) {
179
+ const warning = reason || i18n(`${i18nKey}.uploadWarning.defaultWarning`);
180
+
181
+ // Avoid logging the warning to the console if it is currently the most
182
+ // recently logged warning. We do not want to spam the console with the same message.
183
+ if (!this.uploadWarnings[warning]) {
184
+ logger.log();
185
+ logger.warn(i18n(`${i18nKey}.uploadWarning.header`, { warning }));
186
+ logger.log(
187
+ i18n(`${i18nKey}.uploadWarning.stopDev`, {
188
+ command: uiCommandReference('hs project dev'),
189
+ })
190
+ );
191
+ logger.log(
192
+ i18n(`${i18nKey}.uploadWarning.runUpload`, {
193
+ command: this.getUploadCommand(),
194
+ })
195
+ );
196
+ logger.log(
197
+ i18n(`${i18nKey}.uploadWarning.restartDev`, {
198
+ command: uiCommandReference('hs project dev'),
199
+ })
308
200
  );
309
- this.currentStagedBuildId = buildId;
310
- } catch (err) {
311
- logger.debug(err);
312
- if (isSpecifiedError(err, { subCategory: ERROR_TYPES.PROJECT_LOCKED })) {
313
- await cancelStagedBuild(this.targetAccountId, this.projectConfig.name);
314
- SpinniesManager.add(null, {
315
- text: i18n(`${i18nKey}.previousStagingBuildCancelled`),
316
- status: 'non-spinnable',
317
- });
318
- }
319
- this.stop();
320
- }
321
- }
322
201
 
323
- async startWatching() {
324
- if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
325
- await this.createNewStagingBuild();
202
+ this.mostRecentUploadWarning = warning;
203
+ this.uploadWarnings[warning] = true;
326
204
  }
327
-
328
- this.watcher.on('add', async filePath => {
329
- this.handleWatchEvent(filePath, WATCH_EVENTS.add);
330
- });
331
- this.watcher.on('change', async filePath => {
332
- this.handleWatchEvent(filePath, WATCH_EVENTS.change);
333
- });
334
- this.watcher.on('unlink', async filePath => {
335
- this.handleWatchEvent(filePath, WATCH_EVENTS.unlink);
336
- });
337
- this.watcher.on('unlinkDir', async filePath => {
338
- this.handleWatchEvent(filePath, WATCH_EVENTS.unlinkDir);
339
- });
340
205
  }
341
206
 
342
- async handleWatchEvent(filePath, event) {
343
- const changeInfo = {
344
- event,
345
- filePath,
346
- remotePath: path.relative(this.projectSourceDir, filePath),
347
- };
207
+ monitorConsoleOutput() {
208
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
348
209
 
349
- if (changeInfo.filePath.includes('dist')) {
350
- return;
351
- }
352
-
353
- if (this.uploadPermission !== UPLOAD_PERMISSIONS.always) {
354
- this.handlePreventedUpload(changeInfo);
355
- return;
356
- }
357
-
358
- this.addChangeToStandbyQueue({ ...changeInfo, supported: false });
210
+ process.stdout.write = function(chunk, encoding, callback) {
211
+ // Reset the most recently logged warning
212
+ if (
213
+ this.mostRecentUploadWarning &&
214
+ this.uploadWarnings[this.mostRecentUploadWarning]
215
+ ) {
216
+ delete this.uploadWarnings[this.mostRecentUploadWarning];
217
+ }
359
218
 
360
- if (!this.uploadQueue.isPaused) {
361
- this.flushStandbyChanges();
362
- }
219
+ return originalStdoutWrite(chunk, encoding, callback);
220
+ }.bind(this);
363
221
  }
364
222
 
365
- handlePreventedUpload(changeInfo) {
366
- const { remotePath } = changeInfo;
223
+ compareLocalProjectToDeployed(runnableComponents) {
224
+ const deployedComponentNames = this.deployedBuild.subbuildStatuses.map(
225
+ subbuildStatus => subbuildStatus.buildName
226
+ );
367
227
 
368
- if (this.uploadPermission === UPLOAD_PERMISSIONS.never) {
369
- this.updateDevModeStatus('noUploadsAllowed');
228
+ let missingComponents = [];
370
229
 
371
- SpinniesManager.add('noUploadsAllowed', {
372
- text: i18n(`${i18nKey}.upload.noUploadsAllowed`, {
373
- filePath: remotePath,
374
- }),
375
- status: 'fail',
376
- failColor: 'white',
377
- noIndent: true,
378
- });
379
- } else {
380
- this.updateDevModeStatus('manualUploadRequired');
230
+ runnableComponents.forEach(({ type, config, path }) => {
231
+ if (type === COMPONENT_TYPES.app) {
232
+ const cardConfigs = getAppCardConfigs(config, path);
381
233
 
382
- const addedToQueue = this.addChangeToStandbyQueue({
383
- ...changeInfo,
384
- supported: false,
385
- });
234
+ if (!deployedComponentNames.includes(config.name)) {
235
+ missingComponents.push(
236
+ `${i18n(`${i18nKey}.uploadWarning.appLabel`)} ${config.name}`
237
+ );
238
+ }
386
239
 
387
- if (addedToQueue) {
388
- SpinniesManager.add('manualUploadRequired', {
389
- text: i18n(`${i18nKey}.upload.manualUploadRequired`),
390
- status: 'fail',
391
- failColor: 'white',
392
- noIndent: true,
393
- });
394
- SpinniesManager.add('manualUploadExplanation1', {
395
- text: i18n(`${i18nKey}.upload.manualUploadExplanation1`),
396
- status: 'non-spinnable',
397
- indent: 1,
398
- });
399
- SpinniesManager.add('manualUploadExplanation2', {
400
- text: i18n(`${i18nKey}.upload.manualUploadExplanation2`),
401
- status: 'non-spinnable',
402
- indent: 1,
403
- });
404
- SpinniesManager.add('manualUploadPrompt', {
405
- text: i18n(`${i18nKey}.upload.manualUploadPrompt`),
406
- status: 'non-spinnable',
407
- indent: 1,
240
+ cardConfigs.forEach(cardConfig => {
241
+ if (
242
+ cardConfig.data &&
243
+ cardConfig.data.title &&
244
+ !deployedComponentNames.includes(cardConfig.data.title)
245
+ ) {
246
+ missingComponents.push(
247
+ `${i18n(`${i18nKey}.uploadWarning.uiExtensionLabel`)} ${
248
+ cardConfig.data.title
249
+ }`
250
+ );
251
+ }
408
252
  });
409
253
  }
254
+ });
255
+
256
+ if (missingComponents.length) {
257
+ this.logUploadWarning(
258
+ i18n(`${i18nKey}.uploadWarning.missingComponents`, {
259
+ missingComponents: missingComponents.join(', '),
260
+ })
261
+ );
410
262
  }
411
263
  }
412
264
 
413
- addChangeToStandbyQueue(changeInfo) {
414
- const { event, filePath } = changeInfo;
265
+ startWatching(runnableComponents) {
266
+ this.watcher = chokidar.watch(this.projectDir, {
267
+ ignoreInitial: true,
268
+ });
415
269
 
416
- if (event === WATCH_EVENTS.add || event === WATCH_EVENTS.change) {
417
- if (!isAllowedExtension(filePath, ['jsx', 'tsx'])) {
418
- SpinniesManager.add(null, {
419
- text: i18n(`${i18nKey}.upload.extensionNotAllowed`, {
420
- filePath,
421
- }),
422
- status: 'non-spinnable',
423
- });
424
- return false;
425
- }
426
- }
427
- if (shouldIgnoreFile(filePath, true)) {
428
- SpinniesManager.add(null, {
429
- text: i18n(`${i18nKey}.upload.fileIgnored`, {
430
- filePath,
431
- }),
432
- status: 'non-spinnable',
270
+ const configPaths = runnableComponents
271
+ .filter(({ type }) => type === COMPONENT_TYPES.app)
272
+ .map(component => {
273
+ const appConfigPath = path.join(component.path, APP_COMPONENT_CONFIG);
274
+ return appConfigPath;
433
275
  });
434
- return false;
435
- }
436
276
 
437
- const existingIndex = this.standbyChanges.findIndex(
438
- standyChangeInfo => standyChangeInfo.filePath === filePath
439
- );
277
+ const projectConfigPath = path.join(this.projectDir, PROJECT_CONFIG_FILE);
278
+ configPaths.push(projectConfigPath);
440
279
 
441
- if (existingIndex > -1) {
442
- // Make sure the most recent event to this file is the one that gets acted on
443
- this.standbyChanges[existingIndex].event = event;
444
- } else {
445
- this.standbyChanges.push(changeInfo);
446
- }
447
- return true;
280
+ this.watcher.on('add', filePath => {
281
+ this.handleWatchEvent(filePath, WATCH_EVENTS.add, configPaths);
282
+ });
283
+ this.watcher.on('change', filePath => {
284
+ this.handleWatchEvent(filePath, WATCH_EVENTS.change, configPaths);
285
+ });
286
+ this.watcher.on('unlink', filePath => {
287
+ this.handleWatchEvent(filePath, WATCH_EVENTS.unlink, configPaths);
288
+ });
289
+ this.watcher.on('unlinkDir', filePath => {
290
+ this.handleWatchEvent(filePath, WATCH_EVENTS.unlinkDir, configPaths);
291
+ });
448
292
  }
449
293
 
450
- async sendChanges(changeInfo) {
451
- const { event, filePath, remotePath } = changeInfo;
452
-
453
- try {
454
- if (event === WATCH_EVENTS.add || event === WATCH_EVENTS.change) {
455
- const { name: spinnerName } = SpinniesManager.add(null, {
456
- text: i18n(`${i18nKey}.upload.uploadingAddChange`, {
457
- filePath: remotePath,
458
- }),
459
- status: 'non-spinnable',
460
- });
461
- await uploadFileToBuild(
462
- this.targetAccountId,
463
- this.projectConfig.name,
464
- filePath,
465
- remotePath
466
- );
467
- SpinniesManager.update(spinnerName, {
468
- text: i18n(`${i18nKey}.upload.uploadedAddChange`, {
469
- filePath: remotePath,
470
- }),
471
- status: 'non-spinnable',
472
- });
473
- } else if (
474
- event === WATCH_EVENTS.unlink ||
475
- event === WATCH_EVENTS.unlinkDir
476
- ) {
477
- const { name: spinnerName } = SpinniesManager.add(null, {
478
- text: i18n(`${i18nKey}.upload.uploadingRemoveChange`, {
479
- filePath: remotePath,
480
- }),
481
- status: 'non-spinnable',
482
- });
483
- const path =
484
- event === WATCH_EVENTS.unlinkDir ? `${remotePath}/` : remotePath;
485
- await deleteFileFromBuild(
486
- this.targetAccountId,
487
- this.projectConfig.name,
488
- path
489
- );
490
- SpinniesManager.update(spinnerName, {
491
- text: i18n(`${i18nKey}.upload.uploadedRemoveChange`, {
492
- filePath: remotePath,
493
- }),
494
- status: 'non-spinnable',
495
- });
496
- }
497
- } catch (err) {
498
- logger.debug(err);
499
- }
294
+ async stopWatching() {
295
+ await this.watcher.close();
500
296
  }
501
297
 
502
- debounceQueueBuild(changeInfo) {
503
- const { event } = changeInfo;
504
-
505
- if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
506
- this.updateDevModeStatus('uploadPending');
507
- }
508
-
509
- if (this.debouncedBuild) {
510
- clearTimeout(this.debouncedBuild);
298
+ handleWatchEvent(filePath, event, configPaths) {
299
+ if (configPaths.includes(filePath)) {
300
+ this.logUploadWarning();
301
+ } else {
302
+ this.devServerFileChange(filePath, event);
511
303
  }
512
-
513
- const debounceWaitTime =
514
- event === WATCH_EVENTS.add
515
- ? BUILD_DEBOUNCE_TIME_LONG
516
- : BUILD_DEBOUNCE_TIME_SHORT;
517
-
518
- this.debouncedBuild = setTimeout(
519
- this.queueBuild.bind(this),
520
- debounceWaitTime
521
- );
522
304
  }
523
305
 
524
- async queueBuild() {
525
- SpinniesManager.add(null, { text: ' ', status: 'non-spinnable' });
526
-
527
- const { name: spinnerName } = SpinniesManager.add(null, {
528
- text: i18n(`${i18nKey}.upload.uploadingChanges`, {
529
- accountIdentifier: uiAccountDescription(this.targetAccountId),
530
- buildId: this.currentStagedBuildId,
531
- }),
532
- noIndent: true,
533
- });
534
-
535
- await this.pauseUploadQueue();
536
-
537
- let queueBuildError;
538
-
306
+ async devServerSetup(components) {
539
307
  try {
540
- await queueBuild(
541
- this.targetAccountId,
542
- this.projectConfig.name,
543
- this.projectConfig.platformVersion
544
- );
545
- } catch (err) {
546
- queueBuildError = err;
547
- }
548
-
549
- if (queueBuildError) {
550
- this.updateDevModeStatus('buildError');
551
-
552
- logger.debug(queueBuildError);
553
-
554
- SpinniesManager.fail(spinnerName, {
555
- text: i18n(`${i18nKey}.upload.uploadedChangesFailed`, {
556
- accountIdentifier: uiAccountDescription(this.targetAccountId),
557
- buildId: this.currentStagedBuildId,
558
- }),
559
- failColor: 'white',
560
- noIndent: true,
308
+ await DevServerManager.setup({
309
+ components,
310
+ debug: this.debug,
311
+ onUploadRequired: this.logUploadWarning.bind(this),
561
312
  });
562
-
563
- if (
564
- isSpecifiedError(queueBuildError, {
565
- subCategory: ERROR_TYPES.MISSING_PROJECT_PROVISION,
566
- })
567
- ) {
568
- SpinniesManager.add(null, {
569
- text: i18n(`${i18nKey}.cancelledFromUI`),
570
- status: 'non-spinnable',
571
- indent: 1,
572
- });
573
- this.stop();
574
- } else if (
575
- queueBuildError &&
576
- queueBuildError.error &&
577
- queueBuildError.error.message
578
- ) {
579
- SpinniesManager.add(null, {
580
- text: queueBuildError.error.message,
581
- status: 'non-spinnable',
582
- indent: 1,
583
- });
584
- }
585
- } else {
586
- const result = await pollProjectBuildAndDeploy(
587
- this.targetAccountId,
588
- this.projectConfig,
589
- null,
590
- this.currentStagedBuildId,
591
- true
592
- );
593
-
594
- if (result.succeeded) {
595
- this.updateDevModeStatus('clean');
596
-
597
- SpinniesManager.succeed(spinnerName, {
598
- text: i18n(`${i18nKey}.upload.uploadedChangesSucceeded`, {
599
- accountIdentifier: uiAccountDescription(this.targetAccountId),
600
- buildId: result.buildId,
601
- }),
602
- succeedColor: 'white',
603
- noIndent: true,
604
- });
605
- } else {
606
- SpinniesManager.fail(spinnerName, {
607
- text: i18n(`${i18nKey}.upload.uploadedChangesFailed`, {
608
- accountIdentifier: uiAccountDescription(this.targetAccountId),
609
- buildId: result.buildId,
610
- }),
611
- failColor: 'white',
612
- noIndent: true,
613
- });
614
-
615
- if (result.buildResult.status === 'FAILURE') {
616
- this.logBuildError(result.buildResult);
617
- } else if (result.deployResult.status === 'FAILURE') {
618
- this.logDeployError(result.deployResult);
619
- }
313
+ return true;
314
+ } catch (e) {
315
+ if (this.debug) {
316
+ logger.error(e);
620
317
  }
621
- }
622
-
623
- SpinniesManager.removeAll({ targetCategory: 'projectPollStatus' });
624
-
625
- if (
626
- !queueBuildError &&
627
- this.uploadPermission === UPLOAD_PERMISSIONS.always
628
- ) {
629
- await this.createNewStagingBuild();
630
- }
631
-
632
- this.uploadQueue.start();
633
-
634
- if (this.hasAnyUnsupportedStandbyChanges()) {
635
- this.flushStandbyChanges();
636
- }
637
- }
638
-
639
- flushStandbyChanges() {
640
- if (this.standbyChanges.length) {
641
- this.uploadQueue.addAll(
642
- this.standbyChanges.map(changeInfo => {
643
- return async () => {
644
- if (
645
- this.uploadPermission === UPLOAD_PERMISSIONS.always &&
646
- !this.uploadQueue.isPaused
647
- ) {
648
- this.debounceQueueBuild(changeInfo);
649
- }
650
- await this.sendChanges(changeInfo);
651
- };
652
- })
318
+ logger.error(
319
+ i18n(`${i18nKey}.devServer.setupError`, { message: e.message })
653
320
  );
654
- this.standbyChanges = [];
321
+ return false;
655
322
  }
656
323
  }
657
324
 
658
- async stopWatching() {
659
- await this.watcher.close();
660
- }
661
-
662
- handleServerLog(serverKey, ...args) {
663
- SpinniesManager.add(null, {
664
- text: `${args.join('')}`,
665
- status: 'non-spinnable',
666
- });
667
- }
668
-
669
325
  async devServerStart() {
670
326
  try {
671
- // Set this to true manually for now
672
- DevServerManager.initialized = true;
673
-
674
327
  await DevServerManager.start({
675
328
  accountId: this.targetAccountId,
676
- debug: this.debug,
677
- spinniesLogger: this.handleServerLog,
678
329
  projectConfig: this.projectConfig,
679
- projectSourceDir: this.projectSourceDir,
680
330
  });
681
331
  } catch (e) {
682
332
  if (this.debug) {
683
333
  logger.error(e);
684
334
  }
685
- SpinniesManager.add(null, {
686
- text: i18n(`${i18nKey}.devServer.startError`),
687
- status: 'non-spinnable',
688
- });
335
+ logger.error(
336
+ i18n(`${i18nKey}.devServer.startError`, { message: e.message })
337
+ );
338
+ process.exit(EXIT_CODES.ERROR);
339
+ }
340
+ }
341
+
342
+ devServerFileChange(filePath, event) {
343
+ try {
344
+ DevServerManager.fileChange({ filePath, event });
345
+ } catch (e) {
346
+ if (this.debug) {
347
+ logger.error(e);
348
+ }
349
+ logger.error(
350
+ i18n(`${i18nKey}.devServer.fileChangeError`, {
351
+ message: e.message,
352
+ })
353
+ );
689
354
  }
690
355
  }
691
356
 
692
357
  async devServerCleanup() {
693
358
  try {
694
359
  await DevServerManager.cleanup();
360
+ return true;
695
361
  } catch (e) {
696
362
  if (this.debug) {
697
363
  logger.error(e);
698
364
  }
699
- SpinniesManager.add(null, {
700
- text: i18n(`${i18nKey}.devServer.cleanupError`),
701
- status: 'non-spinnable',
702
- });
365
+ logger.error(
366
+ i18n(`${i18nKey}.devServer.cleanupError`, { message: e.message })
367
+ );
368
+ return false;
703
369
  }
704
370
  }
705
371
  }
706
372
 
707
- module.exports = { LocalDevManager, UPLOAD_PERMISSIONS };
373
+ module.exports = LocalDevManager;