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

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,320 @@ 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.log();
68
+ logger.error(
69
+ i18n(`${i18nKey}.noDeployedBuild`, {
70
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
71
+ })
72
+ );
73
+ process.exit(EXIT_CODES.SUCCESS);
74
+ }
84
75
 
85
- this.uploadQueue = new PQueue({ concurrency: 10 });
76
+ const components = await findProjectComponents(this.projectSourceDir);
86
77
 
87
- if (this.debug) {
88
- this.uploadQueue.on('error', error => {
89
- logger.debug(error);
90
- });
78
+ // The project is empty, there is nothing to run locally
79
+ if (!components.length) {
80
+ logger.log();
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
+ );
88
+
89
+ // The project does not contain any components that support local development
90
+ if (!runnableComponents.length) {
91
+ logger.log();
92
+ logger.error(i18n(`${i18nKey}.noRunnableComponents`));
93
+ process.exit(EXIT_CODES.SUCCESS);
94
+ }
95
95
 
96
- logger.log(i18n(`${i18nKey}.header.betaMessage`));
97
96
  logger.log();
97
+ const setupSucceeded = await this.devServerSetup(runnableComponents);
98
98
 
99
- this.updateConsoleHeader();
99
+ if (setupSucceeded || !this.debug) {
100
+ console.clear();
101
+ }
102
+
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
144
 
121
- let exitCode = EXIT_CODES.SUCCESS;
145
+ const cleanupSucceeded = await this.devServerCleanup();
122
146
 
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
- if (exitCode === EXIT_CODES.SUCCESS) {
145
- SpinniesManager.succeed('cleanupMessage', {
146
- text: i18n(`${i18nKey}.exitingSucceed`),
147
- });
148
- } else {
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',
193
- });
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',
154
+ SpinniesManager.succeed('cleanupMessage', {
155
+ text: i18n(`${i18nKey}.exitingSucceed`),
205
156
  });
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');
273
-
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);
300
- }
301
-
302
- async createNewStagingBuild() {
303
- try {
304
- const { buildId } = await provisionBuild(
305
- this.targetAccountId,
306
- this.projectConfig.name,
307
- this.projectConfig.platformVersion
168
+ logUploadWarning(reason) {
169
+ // Avoid logging the warning to the console if it is currently the most
170
+ // recently logged warning. We do not want to spam the console with the same message.
171
+ if (!this.uploadWarnings[reason]) {
172
+ const currentDefaultAccount = getConfigDefaultAccount();
173
+ const defaultAccountId = getAccountId(currentDefaultAccount);
174
+
175
+ logger.log();
176
+ logger.warn(i18n(`${i18nKey}.uploadWarning.header`, { reason }));
177
+ logger.log(
178
+ i18n(`${i18nKey}.uploadWarning.stopDev`, {
179
+ command: uiCommandReference('hs project dev'),
180
+ })
308
181
  );
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
- });
182
+ if (this.targetAccountId !== defaultAccountId) {
183
+ logger.log(
184
+ i18n(`${i18nKey}.uploadWarning.runUploadWithAccount`, {
185
+ command: uiCommandReference(
186
+ `hs project upload --account=${this.targetAccountId}`
187
+ ),
188
+ })
189
+ );
190
+ } else {
191
+ logger.log(
192
+ i18n(`${i18nKey}.uploadWarning.runUpload`, {
193
+ command: uiCommandReference('hs project upload'),
194
+ })
195
+ );
318
196
  }
319
- this.stop();
320
- }
321
- }
197
+ logger.log(
198
+ i18n(`${i18nKey}.uploadWarning.restartDev`, {
199
+ command: uiCommandReference('hs project dev'),
200
+ })
201
+ );
322
202
 
323
- async startWatching() {
324
- if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
325
- await this.createNewStagingBuild();
203
+ this.mostRecentUploadWarning = reason;
204
+ this.uploadWarnings[reason] = true;
326
205
  }
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
206
  }
341
207
 
342
- async handleWatchEvent(filePath, event) {
343
- const changeInfo = {
344
- event,
345
- filePath,
346
- remotePath: path.relative(this.projectSourceDir, filePath),
347
- };
208
+ monitorConsoleOutput() {
209
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
348
210
 
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 });
211
+ process.stdout.write = function(chunk, encoding, callback) {
212
+ // Reset the most recently logged warning
213
+ if (
214
+ this.mostRecentUploadWarning &&
215
+ this.uploadWarnings[this.mostRecentUploadWarning]
216
+ ) {
217
+ delete this.uploadWarnings[this.mostRecentUploadWarning];
218
+ }
359
219
 
360
- if (!this.uploadQueue.isPaused) {
361
- this.flushStandbyChanges();
362
- }
220
+ return originalStdoutWrite(chunk, encoding, callback);
221
+ }.bind(this);
363
222
  }
364
223
 
365
- handlePreventedUpload(changeInfo) {
366
- const { remotePath } = changeInfo;
224
+ compareLocalProjectToDeployed(runnableComponents) {
225
+ const deployedComponentNames = this.deployedBuild.subbuildStatuses.map(
226
+ subbuildStatus => subbuildStatus.buildName
227
+ );
367
228
 
368
- if (this.uploadPermission === UPLOAD_PERMISSIONS.never) {
369
- this.updateDevModeStatus('noUploadsAllowed');
229
+ let missingComponents = [];
370
230
 
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');
231
+ runnableComponents.forEach(({ type, config, path }) => {
232
+ if (type === COMPONENT_TYPES.app) {
233
+ const cardConfigs = getAppCardConfigs(config, path);
381
234
 
382
- const addedToQueue = this.addChangeToStandbyQueue({
383
- ...changeInfo,
384
- supported: false,
385
- });
235
+ if (!deployedComponentNames.includes(config.name)) {
236
+ missingComponents.push(
237
+ `${i18n(`${i18nKey}.uploadWarning.appLabel`)} ${config.name}`
238
+ );
239
+ }
386
240
 
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,
241
+ cardConfigs.forEach(cardConfig => {
242
+ if (
243
+ cardConfig.data &&
244
+ cardConfig.data.title &&
245
+ !deployedComponentNames.includes(cardConfig.data.title)
246
+ ) {
247
+ missingComponents.push(
248
+ `${i18n(`${i18nKey}.uploadWarning.appLabel`)} ${
249
+ cardConfig.data.title
250
+ }`
251
+ );
252
+ }
408
253
  });
409
254
  }
255
+ });
256
+
257
+ if (missingComponents.length) {
258
+ this.logUploadWarning(
259
+ i18n(`${i18nKey}.uploadWarning.missingComponents`, {
260
+ missingComponents: missingComponents.join(', '),
261
+ })
262
+ );
410
263
  }
411
264
  }
412
265
 
413
- addChangeToStandbyQueue(changeInfo) {
414
- const { event, filePath } = changeInfo;
266
+ startWatching(runnableComponents) {
267
+ this.watcher = chokidar.watch(this.projectDir, {
268
+ ignoreInitial: true,
269
+ });
415
270
 
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',
271
+ const configPaths = runnableComponents
272
+ .filter(({ type }) => type === COMPONENT_TYPES.app)
273
+ .map(component => {
274
+ const appConfigPath = path.join(component.path, APP_COMPONENT_CONFIG);
275
+ return appConfigPath;
433
276
  });
434
- return false;
435
- }
436
277
 
437
- const existingIndex = this.standbyChanges.findIndex(
438
- standyChangeInfo => standyChangeInfo.filePath === filePath
439
- );
278
+ const projectConfigPath = path.join(this.projectDir, PROJECT_CONFIG_FILE);
279
+ configPaths.push(projectConfigPath);
440
280
 
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;
281
+ this.watcher.on('add', filePath => {
282
+ this.handleWatchEvent(filePath, WATCH_EVENTS.add, configPaths);
283
+ });
284
+ this.watcher.on('change', filePath => {
285
+ this.handleWatchEvent(filePath, WATCH_EVENTS.change, configPaths);
286
+ });
287
+ this.watcher.on('unlink', filePath => {
288
+ this.handleWatchEvent(filePath, WATCH_EVENTS.unlink, configPaths);
289
+ });
290
+ this.watcher.on('unlinkDir', filePath => {
291
+ this.handleWatchEvent(filePath, WATCH_EVENTS.unlinkDir, configPaths);
292
+ });
448
293
  }
449
294
 
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
- }
295
+ async stopWatching() {
296
+ await this.watcher.close();
500
297
  }
501
298
 
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);
299
+ handleWatchEvent(filePath, event, configPaths) {
300
+ if (configPaths.includes(filePath)) {
301
+ this.logUploadWarning(
302
+ i18n(`${i18nKey}.uploadWarning.configEdit`, {
303
+ path: path.relative(this.projectDir, filePath),
304
+ })
305
+ );
306
+ } else {
307
+ this.devServerFileChange(filePath, event);
511
308
  }
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
309
  }
523
310
 
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
-
311
+ async devServerSetup(components) {
539
312
  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,
313
+ await DevServerManager.setup({
314
+ components,
315
+ debug: this.debug,
316
+ onUploadRequired: this.logUploadWarning.bind(this),
561
317
  });
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
- });
318
+ return true;
319
+ } catch (e) {
320
+ if (this.debug) {
321
+ logger.error(e);
584
322
  }
585
- } else {
586
- const result = await pollProjectBuildAndDeploy(
587
- this.targetAccountId,
588
- this.projectConfig,
589
- null,
590
- this.currentStagedBuildId,
591
- true
323
+ logger.error(
324
+ i18n(`${i18nKey}.devServer.setupError`, { message: e.message })
592
325
  );
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
- }
620
- }
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
- })
653
- );
654
- this.standbyChanges = [];
326
+ return false;
655
327
  }
656
328
  }
657
329
 
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
330
  async devServerStart() {
670
331
  try {
671
- // Set this to true manually for now
672
- DevServerManager.initialized = true;
673
-
674
332
  await DevServerManager.start({
675
333
  accountId: this.targetAccountId,
676
- debug: this.debug,
677
- spinniesLogger: this.handleServerLog,
678
334
  projectConfig: this.projectConfig,
679
- projectSourceDir: this.projectSourceDir,
680
335
  });
681
336
  } catch (e) {
682
337
  if (this.debug) {
683
338
  logger.error(e);
684
339
  }
685
- SpinniesManager.add(null, {
686
- text: i18n(`${i18nKey}.devServer.startError`),
687
- status: 'non-spinnable',
688
- });
340
+ logger.error(
341
+ i18n(`${i18nKey}.devServer.startError`, { message: e.message })
342
+ );
343
+ process.exit(EXIT_CODES.ERROR);
344
+ }
345
+ }
346
+
347
+ devServerFileChange(filePath, event) {
348
+ try {
349
+ DevServerManager.fileChange({ filePath, event });
350
+ } catch (e) {
351
+ if (this.debug) {
352
+ logger.error(e);
353
+ }
354
+ logger.error(
355
+ i18n(`${i18nKey}.devServer.fileChangeError`, {
356
+ message: e.message,
357
+ })
358
+ );
689
359
  }
690
360
  }
691
361
 
692
362
  async devServerCleanup() {
693
363
  try {
694
364
  await DevServerManager.cleanup();
365
+ return true;
695
366
  } catch (e) {
696
367
  if (this.debug) {
697
368
  logger.error(e);
698
369
  }
699
- SpinniesManager.add(null, {
700
- text: i18n(`${i18nKey}.devServer.cleanupError`),
701
- status: 'non-spinnable',
702
- });
370
+ logger.error(
371
+ i18n(`${i18nKey}.devServer.cleanupError`, { message: e.message })
372
+ );
373
+ return false;
703
374
  }
704
375
  }
705
376
  }
706
377
 
707
- module.exports = { LocalDevManager, UPLOAD_PERMISSIONS };
378
+ module.exports = LocalDevManager;