@hubspot/cli 4.2.1-beta.1 → 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,625 +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',
154
+ SpinniesManager.succeed('cleanupMessage', {
155
+ text: i18n(`${i18nKey}.exitingSucceed`),
165
156
  });
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',
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');
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
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
+ })
307
181
  );
308
- this.currentStagedBuildId = buildId;
309
- } catch (err) {
310
- logger.debug(err);
311
- if (isSpecifiedError(err, { subCategory: ERROR_TYPES.PROJECT_LOCKED })) {
312
- await cancelStagedBuild(this.targetAccountId, this.projectConfig.name);
313
- SpinniesManager.add(null, {
314
- text: i18n(`${i18nKey}.previousStagingBuildCancelled`),
315
- status: 'non-spinnable',
316
- });
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
+ );
317
196
  }
318
- this.stop();
319
- }
320
- }
197
+ logger.log(
198
+ i18n(`${i18nKey}.uploadWarning.restartDev`, {
199
+ command: uiCommandReference('hs project dev'),
200
+ })
201
+ );
321
202
 
322
- async startWatching() {
323
- if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
324
- await this.createNewStagingBuild();
203
+ this.mostRecentUploadWarning = reason;
204
+ this.uploadWarnings[reason] = true;
325
205
  }
326
-
327
- this.watcher.on('add', async filePath => {
328
- this.handleWatchEvent(filePath, WATCH_EVENTS.add);
329
- });
330
- this.watcher.on('change', async filePath => {
331
- this.handleWatchEvent(filePath, WATCH_EVENTS.change);
332
- });
333
- this.watcher.on('unlink', async filePath => {
334
- this.handleWatchEvent(filePath, WATCH_EVENTS.unlink);
335
- });
336
- this.watcher.on('unlinkDir', async filePath => {
337
- this.handleWatchEvent(filePath, WATCH_EVENTS.unlinkDir);
338
- });
339
206
  }
340
207
 
341
- async handleWatchEvent(filePath, event) {
342
- const changeInfo = {
343
- event,
344
- filePath,
345
- remotePath: path.relative(this.projectSourceDir, filePath),
346
- };
347
-
348
- if (changeInfo.filePath.includes('dist')) {
349
- return;
350
- }
208
+ monitorConsoleOutput() {
209
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
351
210
 
352
- if (this.uploadPermission !== UPLOAD_PERMISSIONS.always) {
353
- this.handlePreventedUpload(changeInfo);
354
- return;
355
- }
356
-
357
- 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
+ }
358
219
 
359
- if (!this.uploadQueue.isPaused) {
360
- this.flushStandbyChanges();
361
- }
220
+ return originalStdoutWrite(chunk, encoding, callback);
221
+ }.bind(this);
362
222
  }
363
223
 
364
- handlePreventedUpload(changeInfo) {
365
- const { remotePath } = changeInfo;
224
+ compareLocalProjectToDeployed(runnableComponents) {
225
+ const deployedComponentNames = this.deployedBuild.subbuildStatuses.map(
226
+ subbuildStatus => subbuildStatus.buildName
227
+ );
366
228
 
367
- if (this.uploadPermission === UPLOAD_PERMISSIONS.never) {
368
- this.updateDevModeStatus('noUploadsAllowed');
229
+ let missingComponents = [];
369
230
 
370
- SpinniesManager.add('noUploadsAllowed', {
371
- text: i18n(`${i18nKey}.upload.noUploadsAllowed`, {
372
- filePath: remotePath,
373
- }),
374
- status: 'fail',
375
- failColor: 'white',
376
- noIndent: true,
377
- });
378
- } else {
379
- this.updateDevModeStatus('manualUploadRequired');
231
+ runnableComponents.forEach(({ type, config, path }) => {
232
+ if (type === COMPONENT_TYPES.app) {
233
+ const cardConfigs = getAppCardConfigs(config, path);
380
234
 
381
- const addedToQueue = this.addChangeToStandbyQueue({
382
- ...changeInfo,
383
- supported: false,
384
- });
235
+ if (!deployedComponentNames.includes(config.name)) {
236
+ missingComponents.push(
237
+ `${i18n(`${i18nKey}.uploadWarning.appLabel`)} ${config.name}`
238
+ );
239
+ }
385
240
 
386
- if (addedToQueue) {
387
- SpinniesManager.add('manualUploadRequired', {
388
- text: i18n(`${i18nKey}.upload.manualUploadRequired`),
389
- status: 'fail',
390
- failColor: 'white',
391
- noIndent: true,
392
- });
393
- SpinniesManager.add('manualUploadExplanation1', {
394
- text: i18n(`${i18nKey}.upload.manualUploadExplanation1`),
395
- status: 'non-spinnable',
396
- indent: 1,
397
- });
398
- SpinniesManager.add('manualUploadExplanation2', {
399
- text: i18n(`${i18nKey}.upload.manualUploadExplanation2`),
400
- status: 'non-spinnable',
401
- indent: 1,
402
- });
403
- SpinniesManager.add('manualUploadPrompt', {
404
- text: i18n(`${i18nKey}.upload.manualUploadPrompt`),
405
- status: 'non-spinnable',
406
- 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
+ }
407
253
  });
408
254
  }
255
+ });
256
+
257
+ if (missingComponents.length) {
258
+ this.logUploadWarning(
259
+ i18n(`${i18nKey}.uploadWarning.missingComponents`, {
260
+ missingComponents: missingComponents.join(', '),
261
+ })
262
+ );
409
263
  }
410
264
  }
411
265
 
412
- addChangeToStandbyQueue(changeInfo) {
413
- const { event, filePath } = changeInfo;
266
+ startWatching(runnableComponents) {
267
+ this.watcher = chokidar.watch(this.projectDir, {
268
+ ignoreInitial: true,
269
+ });
414
270
 
415
- if (event === WATCH_EVENTS.add || event === WATCH_EVENTS.change) {
416
- if (!isAllowedExtension(filePath, ['jsx', 'tsx'])) {
417
- SpinniesManager.add(null, {
418
- text: i18n(`${i18nKey}.upload.extensionNotAllowed`, {
419
- filePath,
420
- }),
421
- status: 'non-spinnable',
422
- });
423
- return false;
424
- }
425
- }
426
- if (shouldIgnoreFile(filePath, true)) {
427
- SpinniesManager.add(null, {
428
- text: i18n(`${i18nKey}.upload.fileIgnored`, {
429
- filePath,
430
- }),
431
- 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;
432
276
  });
433
- return false;
434
- }
435
277
 
436
- const existingIndex = this.standbyChanges.findIndex(
437
- standyChangeInfo => standyChangeInfo.filePath === filePath
438
- );
278
+ const projectConfigPath = path.join(this.projectDir, PROJECT_CONFIG_FILE);
279
+ configPaths.push(projectConfigPath);
439
280
 
440
- if (existingIndex > -1) {
441
- // Make sure the most recent event to this file is the one that gets acted on
442
- this.standbyChanges[existingIndex].event = event;
443
- } else {
444
- this.standbyChanges.push(changeInfo);
445
- }
446
- 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
+ });
447
293
  }
448
294
 
449
- async sendChanges(changeInfo) {
450
- const { event, filePath, remotePath } = changeInfo;
451
-
452
- try {
453
- if (event === WATCH_EVENTS.add || event === WATCH_EVENTS.change) {
454
- const { name: spinnerName } = SpinniesManager.add(null, {
455
- text: i18n(`${i18nKey}.upload.uploadingAddChange`, {
456
- filePath: remotePath,
457
- }),
458
- status: 'non-spinnable',
459
- });
460
- await uploadFileToBuild(
461
- this.targetAccountId,
462
- this.projectConfig.name,
463
- filePath,
464
- remotePath
465
- );
466
- SpinniesManager.update(spinnerName, {
467
- text: i18n(`${i18nKey}.upload.uploadedAddChange`, {
468
- filePath: remotePath,
469
- }),
470
- status: 'non-spinnable',
471
- });
472
- } else if (
473
- event === WATCH_EVENTS.unlink ||
474
- event === WATCH_EVENTS.unlinkDir
475
- ) {
476
- const { name: spinnerName } = SpinniesManager.add(null, {
477
- text: i18n(`${i18nKey}.upload.uploadingRemoveChange`, {
478
- filePath: remotePath,
479
- }),
480
- status: 'non-spinnable',
481
- });
482
- const path =
483
- event === WATCH_EVENTS.unlinkDir ? `${remotePath}/` : remotePath;
484
- await deleteFileFromBuild(
485
- this.targetAccountId,
486
- this.projectConfig.name,
487
- path
488
- );
489
- SpinniesManager.update(spinnerName, {
490
- text: i18n(`${i18nKey}.upload.uploadedRemoveChange`, {
491
- filePath: remotePath,
492
- }),
493
- status: 'non-spinnable',
494
- });
495
- }
496
- } catch (err) {
497
- logger.debug(err);
498
- }
295
+ async stopWatching() {
296
+ await this.watcher.close();
499
297
  }
500
298
 
501
- debounceQueueBuild(changeInfo) {
502
- const { event } = changeInfo;
503
-
504
- if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
505
- this.updateDevModeStatus('uploadPending');
506
- }
507
-
508
- if (this.debouncedBuild) {
509
- 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);
510
308
  }
511
-
512
- const debounceWaitTime =
513
- event === WATCH_EVENTS.add
514
- ? BUILD_DEBOUNCE_TIME_LONG
515
- : BUILD_DEBOUNCE_TIME_SHORT;
516
-
517
- this.debouncedBuild = setTimeout(
518
- this.queueBuild.bind(this),
519
- debounceWaitTime
520
- );
521
309
  }
522
310
 
523
- async queueBuild() {
524
- SpinniesManager.add(null, { text: ' ', status: 'non-spinnable' });
525
-
526
- const { name: spinnerName } = SpinniesManager.add(null, {
527
- text: i18n(`${i18nKey}.upload.uploadingChanges`, {
528
- accountIdentifier: uiAccountDescription(this.targetAccountId),
529
- buildId: this.currentStagedBuildId,
530
- }),
531
- noIndent: true,
532
- });
533
-
534
- await this.pauseUploadQueue();
535
-
536
- let queueBuildError;
537
-
311
+ async devServerSetup(components) {
538
312
  try {
539
- await queueBuild(this.targetAccountId, this.projectConfig.name);
540
- } catch (err) {
541
- queueBuildError = err;
542
- }
543
-
544
- if (queueBuildError) {
545
- this.updateDevModeStatus('buildError');
546
-
547
- logger.debug(queueBuildError);
548
-
549
- SpinniesManager.fail(spinnerName, {
550
- text: i18n(`${i18nKey}.upload.uploadedChangesFailed`, {
551
- accountIdentifier: uiAccountDescription(this.targetAccountId),
552
- buildId: this.currentStagedBuildId,
553
- }),
554
- failColor: 'white',
555
- noIndent: true,
313
+ await DevServerManager.setup({
314
+ components,
315
+ debug: this.debug,
316
+ onUploadRequired: this.logUploadWarning.bind(this),
556
317
  });
557
-
558
- if (
559
- isSpecifiedError(queueBuildError, {
560
- subCategory: ERROR_TYPES.MISSING_PROJECT_PROVISION,
561
- })
562
- ) {
563
- SpinniesManager.add(null, {
564
- text: i18n(`${i18nKey}.cancelledFromUI`),
565
- status: 'non-spinnable',
566
- indent: 1,
567
- });
568
- this.stop();
569
- } else if (
570
- queueBuildError &&
571
- queueBuildError.error &&
572
- queueBuildError.error.message
573
- ) {
574
- SpinniesManager.add(null, {
575
- text: queueBuildError.error.message,
576
- status: 'non-spinnable',
577
- indent: 1,
578
- });
318
+ return true;
319
+ } catch (e) {
320
+ if (this.debug) {
321
+ logger.error(e);
579
322
  }
580
- } else {
581
- const result = await pollProjectBuildAndDeploy(
582
- this.targetAccountId,
583
- this.projectConfig,
584
- null,
585
- this.currentStagedBuildId,
586
- true
323
+ logger.error(
324
+ i18n(`${i18nKey}.devServer.setupError`, { message: e.message })
587
325
  );
588
-
589
- if (result.succeeded) {
590
- this.updateDevModeStatus('clean');
591
-
592
- SpinniesManager.succeed(spinnerName, {
593
- text: i18n(`${i18nKey}.upload.uploadedChangesSucceeded`, {
594
- accountIdentifier: uiAccountDescription(this.targetAccountId),
595
- buildId: result.buildId,
596
- }),
597
- succeedColor: 'white',
598
- noIndent: true,
599
- });
600
- } else {
601
- SpinniesManager.fail(spinnerName, {
602
- text: i18n(`${i18nKey}.upload.uploadedChangesFailed`, {
603
- accountIdentifier: uiAccountDescription(this.targetAccountId),
604
- buildId: result.buildId,
605
- }),
606
- failColor: 'white',
607
- noIndent: true,
608
- });
609
-
610
- if (result.buildResult.status === 'FAILURE') {
611
- this.logBuildError(result.buildResult);
612
- } else if (result.deployResult.status === 'FAILURE') {
613
- this.logDeployError(result.deployResult);
614
- }
615
- }
616
- }
617
-
618
- SpinniesManager.removeAll({ targetCategory: 'projectPollStatus' });
619
-
620
- if (
621
- !queueBuildError &&
622
- this.uploadPermission === UPLOAD_PERMISSIONS.always
623
- ) {
624
- await this.createNewStagingBuild();
625
- }
626
-
627
- this.uploadQueue.start();
628
-
629
- if (this.hasAnyUnsupportedStandbyChanges()) {
630
- this.flushStandbyChanges();
631
- }
632
- }
633
-
634
- flushStandbyChanges() {
635
- if (this.standbyChanges.length) {
636
- this.uploadQueue.addAll(
637
- this.standbyChanges.map(changeInfo => {
638
- return async () => {
639
- if (
640
- this.uploadPermission === UPLOAD_PERMISSIONS.always &&
641
- !this.uploadQueue.isPaused
642
- ) {
643
- this.debounceQueueBuild(changeInfo);
644
- }
645
- await this.sendChanges(changeInfo);
646
- };
647
- })
648
- );
649
- this.standbyChanges = [];
326
+ return false;
650
327
  }
651
328
  }
652
329
 
653
- async stopWatching() {
654
- await this.watcher.close();
655
- }
656
-
657
- handleServerLog(serverKey, ...args) {
658
- SpinniesManager.add(null, {
659
- text: `${args.join('')}`,
660
- status: 'non-spinnable',
661
- });
662
- }
663
-
664
330
  async devServerStart() {
665
331
  try {
666
332
  await DevServerManager.start({
667
333
  accountId: this.targetAccountId,
668
- debug: this.debug,
669
- spinniesLogger: this.handleServerLog,
670
334
  projectConfig: this.projectConfig,
671
- projectSourceDir: this.projectSourceDir,
672
335
  });
673
336
  } catch (e) {
674
337
  if (this.debug) {
675
338
  logger.error(e);
676
339
  }
677
- SpinniesManager.add(null, {
678
- text: i18n(`${i18nKey}.devServer.startError`),
679
- status: 'non-spinnable',
680
- });
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
+ );
681
359
  }
682
360
  }
683
361
 
684
362
  async devServerCleanup() {
685
363
  try {
686
364
  await DevServerManager.cleanup();
365
+ return true;
687
366
  } catch (e) {
688
367
  if (this.debug) {
689
368
  logger.error(e);
690
369
  }
691
- SpinniesManager.add(null, {
692
- text: i18n(`${i18nKey}.devServer.cleanupError`),
693
- status: 'non-spinnable',
694
- });
370
+ logger.error(
371
+ i18n(`${i18nKey}.devServer.cleanupError`, { message: e.message })
372
+ );
373
+ return false;
695
374
  }
696
375
  }
697
376
  }
698
377
 
699
- module.exports = { LocalDevManager, UPLOAD_PERMISSIONS };
378
+ module.exports = LocalDevManager;