@hubspot/cli 5.2.1-beta.0 → 5.2.1-beta.2

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,444 @@
1
+ const { logger } = require('@hubspot/local-dev-lib/logger');
2
+ const {
3
+ HUBSPOT_ACCOUNT_TYPES,
4
+ HUBSPOT_ACCOUNT_TYPE_STRINGS,
5
+ } = require('@hubspot/local-dev-lib/constants/config');
6
+ const {
7
+ isMissingScopeError,
8
+ isSpecifiedError,
9
+ } = require('@hubspot/local-dev-lib/errors/apiErrors');
10
+ const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls');
11
+ const { getAccountConfig } = require('@hubspot/local-dev-lib/config');
12
+ const { createProject } = require('@hubspot/local-dev-lib/api/projects');
13
+
14
+ const {
15
+ confirmDefaultAccountPrompt,
16
+ selectSandboxTargetAccountPrompt,
17
+ selectDeveloperTestTargetAccountPrompt,
18
+ confirmUseExistingDeveloperTestAccountPrompt,
19
+ } = require('./prompts/projectDevTargetAccountPrompt');
20
+ const { sandboxNamePrompt } = require('./prompts/sandboxesPrompt');
21
+ const {
22
+ developerTestAccountNamePrompt,
23
+ } = require('./prompts/developerTestAccountNamePrompt');
24
+ const { confirmPrompt } = require('./prompts/promptUtils');
25
+ const {
26
+ validateSandboxUsageLimits,
27
+ getSandboxTypeAsString,
28
+ getAvailableSyncTypes,
29
+ } = require('./sandboxes');
30
+ const { buildSandbox } = require('./sandboxCreate');
31
+ const { syncSandbox } = require('./sandboxSync');
32
+ const {
33
+ validateDevTestAccountUsageLimits,
34
+ } = require('./developerTestAccounts');
35
+ const {
36
+ buildDeveloperTestAccount,
37
+ saveDevTestAccountToConfig,
38
+ } = require('./developerTestAccountCreate');
39
+ const { logErrorInstance } = require('./errorHandlers/standardErrors');
40
+ const { uiCommandReference, uiLine, uiAccountDescription } = require('./ui');
41
+ const SpinniesManager = require('./ui/SpinniesManager');
42
+ const { i18n } = require('./lang');
43
+ const { EXIT_CODES } = require('./enums/exitCodes');
44
+ const { trackCommandMetadataUsage } = require('./usageTracking');
45
+ const {
46
+ isAppDeveloperAccount,
47
+ isDeveloperTestAccount,
48
+ } = require('./accountTypes');
49
+ const {
50
+ handleProjectUpload,
51
+ pollProjectBuildAndDeploy,
52
+ } = require('./projects');
53
+ const {
54
+ PROJECT_ERROR_TYPES,
55
+ PROJECT_BUILD_TEXT,
56
+ PROJECT_DEPLOY_TEXT,
57
+ } = require('./constants');
58
+ const {
59
+ logApiErrorInstance,
60
+ ApiErrorContext,
61
+ } = require('./errorHandlers/apiErrors');
62
+ const {
63
+ PERSONAL_ACCESS_KEY_AUTH_METHOD,
64
+ } = require('@hubspot/local-dev-lib/constants/auth');
65
+
66
+ const i18nKey = 'cli.lib.localDev';
67
+
68
+ // If the user passed in the --account flag, confirm they want to use that account as
69
+ // their target account, otherwise exit
70
+ const confirmDefaultAccountIsTarget = async accountConfig => {
71
+ logger.log();
72
+ const useDefaultAccount = await confirmDefaultAccountPrompt(
73
+ accountConfig.name,
74
+ isDeveloperTestAccount(accountConfig)
75
+ ? HUBSPOT_ACCOUNT_TYPE_STRINGS[HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST]
76
+ : `${getSandboxTypeAsString(accountConfig.accountType)} sandbox`
77
+ );
78
+
79
+ if (!useDefaultAccount) {
80
+ logger.log(
81
+ i18n(
82
+ `${i18nKey}.confirmDefaultAccountIsTarget.declineDefaultAccountExplanation`,
83
+ {
84
+ useCommand: uiCommandReference('hs accounts use'),
85
+ devCommand: uiCommandReference('hs project dev'),
86
+ }
87
+ )
88
+ );
89
+ process.exit(EXIT_CODES.SUCCESS);
90
+ }
91
+ };
92
+
93
+ // Confirm the default account is a developer account if developing public apps
94
+ const checkIfAppDeveloperAccount = accountConfig => {
95
+ if (!isAppDeveloperAccount(accountConfig)) {
96
+ logger.error(i18n(`${i18nKey}.checkIfAppDevloperAccount`));
97
+ process.exit(EXIT_CODES.SUCCESS);
98
+ }
99
+ };
100
+
101
+ // Confirm the default account is a developer account if developing public apps
102
+ const checkIfDeveloperTestAccount = accountConfig => {
103
+ if (!isDeveloperTestAccount(accountConfig)) {
104
+ logger.error(i18n(`${i18nKey}.checkIfDeveloperTestAccount`));
105
+ process.exit(EXIT_CODES.SUCCESS);
106
+ }
107
+ };
108
+
109
+ // If the user isn't using the recommended account type, prompt them to use or create one
110
+ const suggestRecommendedNestedAccount = async (
111
+ accounts,
112
+ accountConfig,
113
+ hasPublicApps
114
+ ) => {
115
+ logger.log();
116
+ uiLine();
117
+ if (hasPublicApps) {
118
+ logger.warn(
119
+ i18n(
120
+ `${i18nKey}.suggestRecommendedNestedAccount.publicAppNonDeveloperTestAccountWarning`
121
+ )
122
+ );
123
+ } else if (isAppDeveloperAccount(accountConfig)) {
124
+ logger.warn(
125
+ i18n(
126
+ `${i18nKey}.suggestRecommendedNestedAccount.publicAppNonDeveloperTestAccountWarning`
127
+ )
128
+ );
129
+ } else {
130
+ logger.warn(
131
+ i18n(`${i18nKey}.suggestRecommendedNestedAccount.nonSandboxWarning`)
132
+ );
133
+ }
134
+ uiLine();
135
+ logger.log();
136
+
137
+ const targetAccountPrompt = isAppDeveloperAccount(accountConfig)
138
+ ? selectDeveloperTestTargetAccountPrompt
139
+ : selectSandboxTargetAccountPrompt;
140
+
141
+ return targetAccountPrompt(accounts, accountConfig, hasPublicApps);
142
+ };
143
+
144
+ // Create a new sandbox and return its accountId
145
+ const createSandboxForLocalDev = async (accountId, accountConfig, env) => {
146
+ try {
147
+ await validateSandboxUsageLimits(
148
+ accountConfig,
149
+ HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
150
+ env
151
+ );
152
+ } catch (err) {
153
+ if (isMissingScopeError(err)) {
154
+ logger.error(
155
+ i18n('cli.lib.sandbox.create.failure.scopes.message', {
156
+ accountName: accountConfig.name || accountId,
157
+ })
158
+ );
159
+ const websiteOrigin = getHubSpotWebsiteOrigin(env);
160
+ const url = `${websiteOrigin}/personal-access-key/${accountId}`;
161
+ logger.info(
162
+ i18n('cli.lib.sandbox.create.failure.scopes.instructions', {
163
+ accountName: accountConfig.name || accountId,
164
+ url,
165
+ })
166
+ );
167
+ } else {
168
+ logErrorInstance(err);
169
+ }
170
+ process.exit(EXIT_CODES.ERROR);
171
+ }
172
+ try {
173
+ const { name } = await sandboxNamePrompt(
174
+ HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX
175
+ );
176
+
177
+ trackCommandMetadataUsage(
178
+ 'sandbox-create',
179
+ { step: 'project-dev' },
180
+ accountId
181
+ );
182
+
183
+ const { result } = await buildSandbox({
184
+ name,
185
+ type: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
186
+ accountConfig,
187
+ env,
188
+ });
189
+
190
+ const targetAccountId = result.sandbox.sandboxHubId;
191
+
192
+ const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId);
193
+ const syncTasks = await getAvailableSyncTypes(
194
+ accountConfig,
195
+ sandboxAccountConfig
196
+ );
197
+ await syncSandbox({
198
+ accountConfig: sandboxAccountConfig,
199
+ parentAccountConfig: accountConfig,
200
+ env,
201
+ syncTasks,
202
+ allowEarlyTermination: false, // Don't let user terminate early in this flow
203
+ skipPolling: true, // Skip polling, sync will run and complete in the background
204
+ });
205
+ return targetAccountId;
206
+ } catch (err) {
207
+ logErrorInstance(err);
208
+ process.exit(EXIT_CODES.ERROR);
209
+ }
210
+ };
211
+
212
+ // Create a developer test account and return its accountId
213
+ const createDeveloperTestAccountForLocalDev = async (
214
+ accountId,
215
+ accountConfig,
216
+ env
217
+ ) => {
218
+ let currentPortalCount = 0;
219
+ let maxTestPortals = 10;
220
+ try {
221
+ const validateResult = await validateDevTestAccountUsageLimits(
222
+ accountConfig
223
+ );
224
+ if (validateResult) {
225
+ currentPortalCount = validateResult.results
226
+ ? validateResult.results.length
227
+ : 0;
228
+ maxTestPortals = validateResult.maxTestPortals;
229
+ }
230
+ } catch (err) {
231
+ if (isMissingScopeError(err)) {
232
+ logger.error(
233
+ i18n('cli.lib.developerTestAccount.create.failure.scopes.message', {
234
+ accountName: accountConfig.name || accountId,
235
+ })
236
+ );
237
+ const websiteOrigin = getHubSpotWebsiteOrigin(env);
238
+ const url = `${websiteOrigin}/personal-access-key/${accountId}`;
239
+ logger.info(
240
+ i18n(
241
+ 'cli.lib.developerTestAccount.create.failure.scopes.instructions',
242
+ {
243
+ accountName: accountConfig.name || accountId,
244
+ url,
245
+ }
246
+ )
247
+ );
248
+ } else {
249
+ logErrorInstance(err);
250
+ }
251
+ process.exit(EXIT_CODES.ERROR);
252
+ }
253
+
254
+ try {
255
+ const { name } = await developerTestAccountNamePrompt(currentPortalCount);
256
+ trackCommandMetadataUsage(
257
+ 'developer-test-account-create',
258
+ { step: 'project-dev' },
259
+ accountId
260
+ );
261
+
262
+ const { result } = await buildDeveloperTestAccount({
263
+ name,
264
+ accountConfig,
265
+ env,
266
+ maxTestPortals,
267
+ });
268
+
269
+ return result.id;
270
+ } catch (err) {
271
+ logErrorInstance(err);
272
+ process.exit(EXIT_CODES.ERROR);
273
+ }
274
+ };
275
+
276
+ // Prompt user to confirm usage of an existing developer test account that is not currently in the config
277
+ const useExistingDevTestAccount = async (env, account) => {
278
+ const useExistingDevTestAcct = await confirmUseExistingDeveloperTestAccountPrompt(
279
+ account
280
+ );
281
+ if (!useExistingDevTestAcct) {
282
+ logger.log('');
283
+ logger.log(
284
+ i18n(
285
+ `${i18nKey}.confirmDefaultAccountIsTarget.declineDefaultAccountExplanation`,
286
+ {
287
+ useCommand: uiCommandReference('hs accounts use'),
288
+ devCommand: uiCommandReference('hs project dev'),
289
+ }
290
+ )
291
+ );
292
+ logger.log('');
293
+ process.exit(EXIT_CODES.SUCCESS);
294
+ }
295
+ const devTestAcctConfigName = await saveDevTestAccountToConfig(env, account);
296
+ logger.success(
297
+ i18n(`cli.lib.developerTestAccount.create.success.configFileUpdated`, {
298
+ accountName: devTestAcctConfigName,
299
+ authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.name,
300
+ })
301
+ );
302
+ };
303
+
304
+ // Prompt the user to create a new project if one doesn't exist on their target account
305
+ const createNewProjectForLocalDev = async (
306
+ projectConfig,
307
+ targetAccountId,
308
+ shouldCreateWithoutConfirmation,
309
+ hasPublicApps
310
+ ) => {
311
+ // Create the project without prompting if this is a newly created sandbox
312
+ let shouldCreateProject = shouldCreateWithoutConfirmation;
313
+
314
+ if (!shouldCreateProject) {
315
+ const explanationString = i18n(
316
+ hasPublicApps
317
+ ? `${i18nKey}.createNewProjectForLocalDev.publicAppProjectMustExistExplanation`
318
+ : `${i18nKey}.createNewProjectForLocalDev.projectMustExistExplanation`,
319
+ {
320
+ accountIdentifier: uiAccountDescription(targetAccountId),
321
+ projectName: projectConfig.name,
322
+ }
323
+ );
324
+ logger.log();
325
+ uiLine();
326
+ logger.warn(explanationString);
327
+ uiLine();
328
+
329
+ shouldCreateProject = await confirmPrompt(
330
+ i18n(`${i18nKey}.createNewProjectForLocalDev.createProject`, {
331
+ accountIdentifier: uiAccountDescription(targetAccountId),
332
+ projectName: projectConfig.name,
333
+ })
334
+ );
335
+ }
336
+
337
+ if (shouldCreateProject) {
338
+ SpinniesManager.add('createProject', {
339
+ text: i18n(`${i18nKey}.createNewProjectForLocalDev.creatingProject`, {
340
+ accountIdentifier: uiAccountDescription(targetAccountId),
341
+ projectName: projectConfig.name,
342
+ }),
343
+ });
344
+
345
+ try {
346
+ await createProject(targetAccountId, projectConfig.name);
347
+ SpinniesManager.succeed('createProject', {
348
+ text: i18n(`${i18nKey}.createNewProjectForLocalDev.createdProject`, {
349
+ accountIdentifier: uiAccountDescription(targetAccountId),
350
+ projectName: projectConfig.name,
351
+ }),
352
+ succeedColor: 'white',
353
+ });
354
+ } catch (err) {
355
+ SpinniesManager.fail('createProject');
356
+ logger.log(
357
+ i18n(`${i18nKey}.createNewProjectForLocalDev.failedToCreateProject`)
358
+ );
359
+ process.exit(EXIT_CODES.ERROR);
360
+ }
361
+ } else {
362
+ // We cannot continue if the project does not exist in the target account
363
+ logger.log();
364
+ logger.log(
365
+ i18n(`${i18nKey}.createNewProjectForLocalDev.choseNotToCreateProject`)
366
+ );
367
+ process.exit(EXIT_CODES.SUCCESS);
368
+ }
369
+ };
370
+
371
+ // Create an initial build if the project was newly created in the account
372
+ // Return the newly deployed build
373
+ const createInitialBuildForNewProject = async (
374
+ projectConfig,
375
+ projectDir,
376
+ targetAccountId
377
+ ) => {
378
+ const initialUploadResult = await handleProjectUpload(
379
+ targetAccountId,
380
+ projectConfig,
381
+ projectDir,
382
+ (...args) => pollProjectBuildAndDeploy(...args, true),
383
+ i18n(`${i18nKey}.createInitialBuildForNewProject.initialUploadMessage`)
384
+ );
385
+
386
+ if (initialUploadResult.uploadError) {
387
+ if (
388
+ isSpecifiedError(initialUploadResult.uploadError, {
389
+ subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED,
390
+ })
391
+ ) {
392
+ logger.log();
393
+ logger.error(
394
+ i18n(`${i18nKey}.createInitialBuildForNewProject.projectLockedError`)
395
+ );
396
+ logger.log();
397
+ } else {
398
+ logApiErrorInstance(
399
+ initialUploadResult.uploadError,
400
+ new ApiErrorContext({
401
+ accountId: targetAccountId,
402
+ projectName: projectConfig.name,
403
+ })
404
+ );
405
+ }
406
+ process.exit(EXIT_CODES.ERROR);
407
+ }
408
+
409
+ if (!initialUploadResult.succeeded) {
410
+ let subTasks = [];
411
+
412
+ if (initialUploadResult.buildResult.status === 'FAILURE') {
413
+ subTasks =
414
+ initialUploadResult.buildResult[PROJECT_BUILD_TEXT.SUBTASK_KEY];
415
+ } else if (initialUploadResult.deployResult.status === 'FAILURE') {
416
+ subTasks =
417
+ initialUploadResult.deployResult[PROJECT_DEPLOY_TEXT.SUBTASK_KEY];
418
+ }
419
+
420
+ const failedSubTasks = subTasks.filter(task => task.status === 'FAILURE');
421
+
422
+ logger.log();
423
+ failedSubTasks.forEach(failedSubTask => {
424
+ console.error(failedSubTask.errorMessage);
425
+ });
426
+ logger.log();
427
+
428
+ process.exit(EXIT_CODES.ERROR);
429
+ }
430
+
431
+ return initialUploadResult.buildResult;
432
+ };
433
+
434
+ module.exports = {
435
+ confirmDefaultAccountIsTarget,
436
+ checkIfAppDeveloperAccount,
437
+ checkIfDeveloperTestAccount,
438
+ suggestRecommendedNestedAccount,
439
+ createSandboxForLocalDev,
440
+ createDeveloperTestAccountForLocalDev,
441
+ useExistingDevTestAccount,
442
+ createNewProjectForLocalDev,
443
+ createInitialBuildForNewProject,
444
+ };
@@ -7,11 +7,13 @@ const { logErrorInstance } = require('./errorHandlers/standardErrors');
7
7
  const COMPONENT_TYPES = Object.freeze({
8
8
  privateApp: 'private-app',
9
9
  publicApp: 'public-app',
10
+ hublTheme: 'hubl-theme',
10
11
  });
11
12
 
12
13
  const CONFIG_FILES = {
13
14
  [COMPONENT_TYPES.privateApp]: 'app.json',
14
15
  [COMPONENT_TYPES.publicApp]: 'public-app.json',
16
+ [COMPONENT_TYPES.hublTheme]: 'theme.json',
15
17
  };
16
18
 
17
19
  function getTypeFromConfigFile(configFile) {
@@ -102,13 +104,14 @@ async function findProjectComponents(projectSourceDir) {
102
104
  if (Object.values(CONFIG_FILES).includes(base)) {
103
105
  const parsedAppConfig = loadConfigFile(projectFile);
104
106
 
105
- if (parsedAppConfig && parsedAppConfig.name) {
107
+ if (parsedAppConfig) {
106
108
  const isLegacy = getIsLegacyApp(parsedAppConfig, dir);
109
+ const isHublTheme = base === CONFIG_FILES[COMPONENT_TYPES.hublTheme];
107
110
 
108
111
  components.push({
109
112
  type: getTypeFromConfigFile(base),
110
113
  config: parsedAppConfig,
111
- runnable: !isLegacy,
114
+ runnable: !isLegacy && !isHublTheme,
112
115
  path: dir,
113
116
  });
114
117
  }
@@ -118,9 +121,16 @@ async function findProjectComponents(projectSourceDir) {
118
121
  return components;
119
122
  }
120
123
 
124
+ function getProjectComponentTypes(components) {
125
+ const projectContents = {};
126
+ components.forEach(({ type }) => (projectContents[type] = true));
127
+ return projectContents;
128
+ }
129
+
121
130
  module.exports = {
122
131
  CONFIG_FILES,
123
132
  COMPONENT_TYPES,
124
133
  findProjectComponents,
125
134
  getAppCardConfigs,
135
+ getProjectComponentTypes,
126
136
  };
package/lib/projects.js CHANGED
@@ -26,6 +26,8 @@ const {
26
26
  getDeployStructure,
27
27
  fetchProject,
28
28
  uploadProject,
29
+ fetchBuildWarnLogs,
30
+ fetchDeployWarnLogs,
29
31
  } = require('@hubspot/local-dev-lib/api/projects');
30
32
  const {
31
33
  isSpecifiedError,
@@ -445,7 +447,10 @@ const pollProjectBuildAndDeploy = async (
445
447
  }
446
448
  )
447
449
  );
450
+
451
+ displayWarnLogs(accountId, projectConfig.name, buildId);
448
452
  }
453
+
449
454
  const deployStatus = await pollDeployStatus(
450
455
  accountId,
451
456
  projectConfig.name,
@@ -473,6 +478,14 @@ const pollProjectBuildAndDeploy = async (
473
478
  logger.error(e);
474
479
  }
475
480
 
481
+ if (result && result.deployResult) {
482
+ displayWarnLogs(
483
+ accountId,
484
+ projectConfig.name,
485
+ result.deployResult.deployId,
486
+ true
487
+ );
488
+ }
476
489
  return result;
477
490
  };
478
491
 
@@ -871,6 +884,28 @@ const createProjectComponent = async (
871
884
  );
872
885
  };
873
886
 
887
+ const displayWarnLogs = async (
888
+ accountId,
889
+ projectName,
890
+ taskId,
891
+ isDeploy = false
892
+ ) => {
893
+ let result;
894
+
895
+ if (isDeploy) {
896
+ result = await fetchDeployWarnLogs(accountId, projectName, taskId);
897
+ } else {
898
+ result = await fetchBuildWarnLogs(accountId, projectName, taskId);
899
+ }
900
+
901
+ if (result && result.logs.length) {
902
+ result.logs.forEach(log => {
903
+ logger.warn(log.message);
904
+ logger.log('');
905
+ });
906
+ }
907
+ };
908
+
874
909
  module.exports = {
875
910
  writeProjectConfig,
876
911
  getProjectConfig,
@@ -887,4 +922,5 @@ module.exports = {
887
922
  ensureProjectExists,
888
923
  logFeedbackMessage,
889
924
  createProjectComponent,
925
+ displayWarnLogs,
890
926
  };
@@ -0,0 +1,29 @@
1
+ const { promptUser } = require('./promptUtils');
2
+ const { i18n } = require('../lang');
3
+ const { accountNameExistsInConfig } = require('@hubspot/local-dev-lib/config');
4
+
5
+ const i18nKey = 'cli.lib.prompts.developerTestAccountPrompt';
6
+
7
+ const developerTestAccountNamePrompt = currentPortalCount => {
8
+ return promptUser([
9
+ {
10
+ name: 'name',
11
+ message: i18n(`${i18nKey}.name.message`),
12
+ validate(val) {
13
+ if (typeof val !== 'string') {
14
+ return i18n(`${i18nKey}.name.errors.invalidName`);
15
+ } else if (!val.length) {
16
+ return i18n(`${i18nKey}.name.errors.nameRequired`);
17
+ }
18
+ return accountNameExistsInConfig(val)
19
+ ? i18n(`${i18nKey}.name.errors.accountNameExists`, { name: val })
20
+ : true;
21
+ },
22
+ default: `Developer test account ${currentPortalCount + 1}`,
23
+ },
24
+ ]);
25
+ };
26
+
27
+ module.exports = {
28
+ developerTestAccountNamePrompt,
29
+ };
@@ -34,6 +34,24 @@ const previewPrompt = (promptOptions = {}) => {
34
34
  ]);
35
35
  };
36
36
 
37
+ const previewProjectPrompt = async themeComponents => {
38
+ return promptUser([
39
+ {
40
+ name: 'themeComponentPath',
41
+ message: i18n(`${i18nKey}.themeProjectSelect`),
42
+ type: 'list',
43
+ choices: themeComponents.map(t => {
44
+ const themeName = path.basename(t.path);
45
+ return {
46
+ name: themeName,
47
+ value: t.path,
48
+ };
49
+ }),
50
+ },
51
+ ]);
52
+ };
53
+
37
54
  module.exports = {
38
55
  previewPrompt,
56
+ previewProjectPrompt,
39
57
  };