@hubspot/cli 4.2.1-beta.1 → 4.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.
package/bin/cli.js CHANGED
@@ -115,7 +115,7 @@ const argv = yargs
115
115
  .option('debug', {
116
116
  alias: 'd',
117
117
  default: false,
118
- describe: 'set log level to debug',
118
+ describe: 'Set log level to debug',
119
119
  type: 'boolean',
120
120
  })
121
121
  .option('noHyperlinks', {
@@ -71,7 +71,7 @@ const getPortalData = mappedPortalData => {
71
71
  };
72
72
 
73
73
  exports.handler = async options => {
74
- await loadAndValidateOptions(options);
74
+ await loadAndValidateOptions(options, false);
75
75
 
76
76
  const accountId = getAccountId(options);
77
77
 
@@ -11,7 +11,7 @@ const { loadAndValidateOptions } = require('../../lib/validation');
11
11
  const i18nKey = 'cli.commands.project.subcommands.add';
12
12
 
13
13
  exports.command = 'add';
14
- exports.describe = null; //i18n(`${i18nKey}.describe`);
14
+ exports.describe = i18n(`${i18nKey}.describe`);
15
15
 
16
16
  exports.handler = async options => {
17
17
  await loadAndValidateOptions(options);
@@ -5,7 +5,10 @@ const {
5
5
  addUseEnvironmentOptions,
6
6
  addTestingOptions,
7
7
  } = require('../../lib/commonOpts');
8
- const { trackCommandUsage } = require('../../lib/usageTracking');
8
+ const {
9
+ trackCommandUsage,
10
+ trackCommandMetadataUsage,
11
+ } = require('../../lib/usageTracking');
9
12
  const { loadAndValidateOptions } = require('../../lib/validation');
10
13
  const { i18n } = require('../../lib/lang');
11
14
  const { logger } = require('@hubspot/cli-lib/logger');
@@ -17,6 +20,7 @@ const {
17
20
  ensureProjectExists,
18
21
  handleProjectUpload,
19
22
  pollProjectBuildAndDeploy,
23
+ showPlatformVersionWarning,
20
24
  } = require('../../lib/projects');
21
25
  const { EXIT_CODES } = require('../../lib/enums/exitCodes');
22
26
  const { uiAccountDescription, uiBetaMessage, uiLine } = require('../../lib/ui');
@@ -60,7 +64,7 @@ const {
60
64
  const i18nKey = 'cli.commands.project.subcommands.dev';
61
65
 
62
66
  exports.command = 'dev [--account]';
63
- exports.describe = null; //i18n(`${i18nKey}.describe`);
67
+ exports.describe = i18n(`${i18nKey}.describe`);
64
68
 
65
69
  exports.handler = async options => {
66
70
  await loadAndValidateOptions(options);
@@ -79,6 +83,8 @@ exports.handler = async options => {
79
83
  process.exit(EXIT_CODES.ERROR);
80
84
  }
81
85
 
86
+ await showPlatformVersionWarning(accountId, projectConfig);
87
+
82
88
  const accounts = getConfigAccounts();
83
89
  let targetAccountId = options.account ? accountId : null;
84
90
  let createNewSandbox = false;
@@ -129,6 +135,13 @@ exports.handler = async options => {
129
135
  }
130
136
  try {
131
137
  const { name } = await sandboxNamePrompt(DEVELOPER_SANDBOX);
138
+
139
+ trackCommandMetadataUsage(
140
+ 'sandbox-create',
141
+ { step: 'project-dev' },
142
+ accountId
143
+ );
144
+
132
145
  const { result } = await buildSandbox({
133
146
  name,
134
147
  type: DEVELOPER_SANDBOX,
@@ -176,12 +189,13 @@ exports.handler = async options => {
176
189
  ? UPLOAD_PERMISSIONS.manual
177
190
  : UPLOAD_PERMISSIONS.always;
178
191
 
192
+ let deployedBuild;
193
+
179
194
  if (projectExists) {
180
- const { sourceIntegration } = await fetchProject(
181
- targetAccountId,
182
- projectConfig.name
183
- );
184
- if (options.extension || sourceIntegration) {
195
+ const project = await fetchProject(targetAccountId, projectConfig.name);
196
+ deployedBuild = project.deployedBuild;
197
+
198
+ if (options.local || options.localAll || project.sourceIntegration) {
185
199
  uploadPermission = UPLOAD_PERMISSIONS.never;
186
200
  }
187
201
  }
@@ -285,7 +299,7 @@ exports.handler = async options => {
285
299
  // Do this before starting the dev server for v2 behavior because we cannot
286
300
  // run a server on a broken project
287
301
  if (
288
- options.extension &&
302
+ (options.local || options.localAll) &&
289
303
  initialUploadResult &&
290
304
  !initialUploadResult.succeeded
291
305
  ) {
@@ -314,27 +328,30 @@ exports.handler = async options => {
314
328
 
315
329
  SpinniesManager.remove('devModeSetup');
316
330
 
317
- const LocalDev = options.extension
318
- ? new LocalDevManagerV2({
319
- debug: options.debug,
320
- extension: options.extension,
321
- projectConfig,
322
- projectDir,
323
- targetAccountId,
324
- })
325
- : new LocalDevManager({
326
- debug: options.debug,
327
- projectConfig,
328
- projectDir,
329
- targetAccountId,
330
- uploadPermission,
331
- });
331
+ const LocalDev =
332
+ options.local || options.localAll
333
+ ? new LocalDevManagerV2({
334
+ alpha: options.localAll,
335
+ debug: options.debug,
336
+ deployedBuild,
337
+ projectConfig,
338
+ projectDir,
339
+ targetAccountId,
340
+ })
341
+ : new LocalDevManager({
342
+ debug: options.debug,
343
+ projectConfig,
344
+ projectDir,
345
+ targetAccountId,
346
+ uploadPermission,
347
+ });
332
348
 
333
349
  await LocalDev.start();
334
350
 
335
351
  // Let the user know when the initial build or deploy fails
336
352
  if (
337
- !options.extension &&
353
+ !options.local &&
354
+ !options.localAll &&
338
355
  initialUploadResult &&
339
356
  !initialUploadResult.succeeded
340
357
  ) {
@@ -354,9 +371,15 @@ exports.builder = yargs => {
354
371
  addUseEnvironmentOptions(yargs, true);
355
372
  addTestingOptions(yargs, true);
356
373
 
357
- yargs.option('extension', {
358
- describe: i18n(`${i18nKey}.options.extension.describe`),
359
- type: 'string',
374
+ yargs.option('local', {
375
+ describe: i18n(`${i18nKey}.options.local.describe`),
376
+ type: 'boolean',
377
+ hidden: true,
378
+ });
379
+
380
+ yargs.option('local-all', {
381
+ describe: i18n(`${i18nKey}.options.localAll.describe`),
382
+ type: 'boolean',
360
383
  hidden: true,
361
384
  });
362
385
 
@@ -7,6 +7,7 @@ const {
7
7
  addUseEnvironmentOptions,
8
8
  } = require('../../lib/commonOpts');
9
9
  const { trackCommandUsage } = require('../../lib/usageTracking');
10
+ const { i18n } = require('../../lib/lang');
10
11
  const {
11
12
  logApiErrorInstance,
12
13
  ApiErrorContext,
@@ -31,8 +32,10 @@ const {
31
32
  const moment = require('moment');
32
33
  const { promptUser } = require('../../lib/prompts/promptUtils');
33
34
 
35
+ const i18nKey = 'cli.commands.project.subcommands.listBuilds';
36
+
34
37
  exports.command = 'list-builds [path]';
35
- exports.describe = false;
38
+ exports.describe = i18n(`${i18nKey}.describe`);
36
39
 
37
40
  exports.handler = async options => {
38
41
  await loadAndValidateOptions(options);
@@ -16,6 +16,7 @@ const {
16
16
  logFeedbackMessage,
17
17
  validateProjectConfig,
18
18
  pollProjectBuildAndDeploy,
19
+ showPlatformVersionWarning,
19
20
  } = require('../../lib/projects');
20
21
  const { i18n } = require('../../lib/lang');
21
22
  const { getAccountConfig } = require('@hubspot/cli-lib');
@@ -48,6 +49,8 @@ exports.handler = async options => {
48
49
 
49
50
  validateProjectConfig(projectConfig, projectDir);
50
51
 
52
+ await showPlatformVersionWarning(accountId, projectConfig);
53
+
51
54
  await ensureProjectExists(accountId, projectConfig.name, { forceCreate });
52
55
 
53
56
  try {
@@ -21,6 +21,7 @@ const {
21
21
  pollDeployStatus,
22
22
  validateProjectConfig,
23
23
  logFeedbackMessage,
24
+ showPlatformVersionWarning,
24
25
  } = require('../../lib/projects');
25
26
  const {
26
27
  cancelStagedBuild,
@@ -97,6 +98,8 @@ exports.handler = async options => {
97
98
 
98
99
  validateProjectConfig(projectConfig, projectDir);
99
100
 
101
+ await showPlatformVersionWarning(accountId, projectConfig);
102
+
100
103
  await ensureProjectExists(accountId, projectConfig.name);
101
104
 
102
105
  try {
@@ -1,4 +1,5 @@
1
1
  const { addConfigOptions, addAccountOptions } = require('../lib/commonOpts');
2
+ const { i18n } = require('../lib/lang');
2
3
  const deploy = require('./project/deploy');
3
4
  const create = require('./project/create');
4
5
  const upload = require('./project/upload');
@@ -10,24 +11,26 @@ const open = require('./project/open');
10
11
  const dev = require('./project/dev');
11
12
  const add = require('./project/add');
12
13
 
14
+ const i18nKey = 'cli.commands.project';
15
+
13
16
  exports.command = 'project';
14
- exports.describe = false; //'Commands for working with projects';
17
+ exports.describe = i18n(`${i18nKey}.describe`);
15
18
 
16
19
  exports.builder = yargs => {
17
20
  addConfigOptions(yargs, true);
18
21
  addAccountOptions(yargs, true);
19
22
 
20
23
  // TODO: deploy must be updated
21
- yargs.command(deploy).demandCommand(1, '');
22
24
  yargs.command(create).demandCommand(0, '');
23
- yargs.command(upload).demandCommand(0, '');
25
+ yargs.command(add).demandCommand(0, '');
24
26
  yargs.command(watch).demandCommand(0, '');
25
- yargs.command(listBuilds).demandCommand(0, '');
27
+ yargs.command(dev).demandCommand(0, '');
28
+ yargs.command(upload).demandCommand(0, '');
29
+ yargs.command(deploy).demandCommand(1, '');
26
30
  yargs.command(logs).demandCommand(1, '');
31
+ yargs.command(listBuilds).demandCommand(0, '');
27
32
  yargs.command(download).demandCommand(0, '');
28
33
  yargs.command(open).demandCommand(0, '');
29
- yargs.command(dev).demandCommand(0, '');
30
- yargs.command(add).demandCommand(0, '');
31
34
 
32
35
  return yargs;
33
36
  };
@@ -22,7 +22,10 @@ const {
22
22
  } = require('../../lib/sandboxes');
23
23
  const { getValidEnv } = require('@hubspot/cli-lib/lib/environment');
24
24
  const { logger } = require('@hubspot/cli-lib/logger');
25
- const { trackCommandUsage } = require('../../lib/usageTracking');
25
+ const {
26
+ trackCommandUsage,
27
+ trackCommandMetadataUsage,
28
+ } = require('../../lib/usageTracking');
26
29
  const {
27
30
  sandboxTypePrompt,
28
31
  sandboxNamePrompt,
@@ -153,6 +156,12 @@ exports.handler = async options => {
153
156
  // Prompt user to sync assets after sandbox creation
154
157
  const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId);
155
158
  const handleSyncSandbox = async syncTasks => {
159
+ // Send tracking event for secondary action, in this case a sandbox sync within the sandbox create flow
160
+ trackCommandMetadataUsage(
161
+ 'sandbox-sync',
162
+ { step: 'sandbox-create' },
163
+ result.sandbox.sandboxHubId
164
+ );
156
165
  await syncSandbox({
157
166
  accountConfig: sandboxAccountConfig,
158
167
  parentAccountConfig: accountConfig,
@@ -168,19 +168,15 @@ exports.handler = async options => {
168
168
  } catch (err) {
169
169
  debugErrorAndContext(err);
170
170
 
171
- if (err instanceof HubSpotAuthError) {
171
+ if (err instanceof HubSpotAuthError && err.statusCode === 401) {
172
172
  // Intercept invalid key error
173
173
  // This command uses the parent portal PAK to delete a sandbox, so we must specify which account needs a new key
174
- const regex = /\bYour personal access key is invalid\b/;
175
- const match = err.message.match(regex);
176
- if (match && match[0]) {
177
- logger.log('');
178
- logger.error(
179
- i18n(`${i18nKey}.failure.invalidKey`, {
180
- account: getAccountName(parentAccount),
181
- })
182
- );
183
- }
174
+ logger.log('');
175
+ logger.error(
176
+ i18n(`${i18nKey}.failure.invalidKey`, {
177
+ account: getAccountName(parentAccount),
178
+ })
179
+ );
184
180
  } else if (
185
181
  isSpecifiedError(err, {
186
182
  statusCode: 403,
package/lang/en.lyaml CHANGED
@@ -444,9 +444,10 @@ en:
444
444
  describe: "Shortcut of the link you'd like to open"
445
445
  selectLink: "Select a link to open"
446
446
  project:
447
+ describe: "{{#bold}}[beta]{{/bold}} Commands for working with projects. For more information, visit our documentation: https://developers.hubspot.com/docs/platform/build-and-deploy-using-hubspot-projects"
447
448
  subcommands:
448
449
  dev:
449
- describe: "Start local dev for the current project"
450
+ describe: "{{#bold}}[beta]{{/bold}} Start local dev for the current project"
450
451
  logs:
451
452
  betaMessage: "HubSpot projects local development"
452
453
  nonSandboxWarning: "Testing in a sandbox is strongly recommended. To switch the target account, select an option below or run {{#bold}}`hs accounts use`{{/bold}} before running the command again."
@@ -461,17 +462,18 @@ en:
461
462
  startupMessage: "Starting local dev server for {{#bold}}{{ projectName }}{{/bold}} ..."
462
463
  prompt:
463
464
  createProject: "Create new project {{ projectName}} in {{#bold}}[{{ accountIdentifier }}]{{/bold}}?"
464
- targetNonSandbox: "Continue testing in a non-sandbox account?"
465
465
  options:
466
- extension:
467
- describe: "The extension that you would like to run locally"
466
+ local:
467
+ describe: "Run the alpha version of this command with some local dev server functionality"
468
+ localAll:
469
+ describe: "Run the alpha version of this command with all local dev server functionality"
468
470
  errors:
469
471
  noProjectConfig: "No project detected. Please run this command again from a project directory."
470
472
  projectLockedError: "Your project is locked. This may mean that another user is running the {{#bold}}`hs project dev`{{/bold}} command for this project. If this is you, unlock the project in Projects UI."
471
473
  examples:
472
474
  default: "Start local dev for the current project"
473
475
  create:
474
- describe: "Create a new project"
476
+ describe: "{{#bold}}[beta]{{/bold}} Create a new project"
475
477
  logs:
476
478
  welcomeMessage: "Welcome to HubSpot Developer Projects!"
477
479
  examples:
@@ -486,7 +488,7 @@ en:
486
488
  templateSource:
487
489
  describe: "Path to custom GitHub repository from which to create project template"
488
490
  add:
489
- describe: "Create a new component within a project"
491
+ describe: "{{#bold}}[beta]{{/bold}} Create a new component within a project"
490
492
  options:
491
493
  name:
492
494
  describe: "Component name"
@@ -501,7 +503,7 @@ en:
501
503
  examples:
502
504
  default: "Create a component within your project"
503
505
  deploy:
504
- describe: "Deploy a project build"
506
+ describe: "{{#bold}}[beta]{{/bold}} Deploy a project build"
505
507
  debug:
506
508
  deploying: "Deploying project at path: {{ path }}"
507
509
  errors:
@@ -516,8 +518,10 @@ en:
516
518
  describe: "Project build ID to be deployed"
517
519
  project:
518
520
  describe: "Project name"
521
+ listBuilds:
522
+ describe: "{{#bold}}[beta]{{/bold}} List the project's builds"
519
523
  logs:
520
- describe: "Get execution logs for a serverless function within a project"
524
+ describe: "{{#bold}}[beta]{{/bold}} Get execution logs for a serverless function within a project"
521
525
  errors:
522
526
  invalidAppName: "Could not find app with name \"{{ appName }}\" in project \"{{ projectName }}\""
523
527
  logs:
@@ -552,7 +556,7 @@ en:
552
556
  endpoint:
553
557
  describe: "Public endpoint path"
554
558
  upload:
555
- describe: "Upload your project files and create a new build"
559
+ describe: "{{#bold}}[beta]{{/bold}} Upload your project files and create a new build"
556
560
  examples:
557
561
  default: "Upload a project"
558
562
  logs:
@@ -570,7 +574,7 @@ en:
570
574
  path:
571
575
  describe: "Path to a project folder"
572
576
  watch:
573
- describe: "Watch your local project for changes and automatically upload changed files to a new build in HubSpot"
577
+ describe: "{{#bold}}[beta]{{/bold}} Watch your local project for changes and automatically upload changed files to a new build in HubSpot"
574
578
  examples:
575
579
  default: "Watch a project within the myProjectFolder folder"
576
580
  logs:
@@ -582,7 +586,7 @@ en:
582
586
  initialUpload:
583
587
  describe: "Upload directory before watching for updates"
584
588
  download:
585
- describe: "Download your project files from HubSpot and write to a path on your computer"
589
+ describe: "{{#bold}}[beta]{{/bold}} Download your project files from HubSpot and write to a path on your computer"
586
590
  examples:
587
591
  default: "Download the project myProject into myProjectFolder folder"
588
592
  logs:
@@ -599,7 +603,7 @@ en:
599
603
  dest:
600
604
  describe: "Destination folder for the project"
601
605
  open:
602
- describe: "Open available projects for the specified account"
606
+ describe: "{{#bold}}[beta]{{/bold}} Open the specified project's details page in the browser"
603
607
  options:
604
608
  project:
605
609
  describe: "Name of project to open"
@@ -848,8 +852,12 @@ en:
848
852
  lib:
849
853
  DevServerManager:
850
854
  portConflict: "The port {{ port }} is already in use."
855
+ notInitialized: "The Dev Server Manager must be initialized before it is started."
856
+ noCompatibleComponents: "Skipping call to {{ serverKey }} because there are no compatible components in the project."
851
857
  LocalDevManagerV2:
852
858
  failedToInitialize: "Missing required arguments to initialize Local Dev"
859
+ noComponents: "There are no components in this project."
860
+ noRunnableComponents: "There are no components in this project that support local development."
853
861
  betaMessage: "HubSpot projects local development"
854
862
  running: "Running {{#bold}}{{ projectName }}{{/bold}} locally on {{ accountIdentifier }}, waiting for changes ..."
855
863
  quitHelper: "Press {{#bold}}'q'{{/bold}} to stop the local dev server"
@@ -857,8 +865,16 @@ en:
857
865
  exitingStart: "Stopping local dev server ..."
858
866
  exitingSucceed: "Successfully exited"
859
867
  exitingFail: "Failed to cleanup before exiting"
868
+ uploadWarning:
869
+ missingComponents: "Your deployed project does not contain the components {{#bold}}'{{ missingComponents }}'{{/bold}}. This may cause issues in local development."
870
+ header: "{{ reason }} To reflect these changes:"
871
+ stopDev: " * Stop {{#bold}}`hs project dev`{{/bold}}"
872
+ runUpload: " * Run {{#bold}}`hs project upload`{{/bold}}"
873
+ runUploadWithAccount: " * Run {{#bold}}`hs project upload --account={{ accountId }}`{{/bold}}"
874
+ restartDev: " * Re-run {{#bold}}`hs project dev`{{/bold}}"
860
875
  devServer:
861
876
  cleanupError: "Failed to cleanup local dev server: {{ message }}"
877
+ setupError: "Failed to setup local dev server: {{ message }}"
862
878
  startError: "Failed to start local dev server: {{ message }}"
863
879
  LocalDevManager:
864
880
  failedToInitialize: "Missing required arguments to initialize Local Dev Manager"
@@ -925,6 +941,9 @@ en:
925
941
  logFeedbackMessage:
926
942
  feedbackHeader: "We'd love to hear your feedback!"
927
943
  feedbackMessage: "How are you liking the new projects and developer tools? \n > Run `{{#yellow}}hs feedback{{/yellow}}` to let us know what you think!\n"
944
+ showPlatformVersionWarning:
945
+ noPlatformVersion: "No platformVersion found in hsproject.json. Falling back to version \"{{ defaultVersion }}\"."
946
+ noPlatformVersionAlt: "No platformVersion found in hsproject.json. Falling back to default version."
928
947
  ui:
929
948
  betaTag: "{{#bold}}[beta]{{/bold}}"
930
949
  betaWarning:
@@ -1,28 +1,39 @@
1
1
  const express = require('express');
2
2
  const bodyParser = require('body-parser');
3
3
  const cors = require('cors');
4
- const { walk } = require('@hubspot/cli-lib/lib/walk');
4
+ const httpClient = require('@hubspot/cli-lib/http');
5
+ const { logger } = require('@hubspot/cli-lib/logger');
5
6
  const { getProjectDetailUrl } = require('./projects');
7
+ const { COMPONENT_TYPES } = require('./projectStructure');
6
8
  const { i18n } = require('./lang');
7
9
  const { EXIT_CODES } = require('./enums/exitCodes');
8
- const { logger } = require('@hubspot/cli-lib/logger');
10
+ const { promptUser } = require('./prompts/promptUtils');
9
11
 
10
12
  const i18nKey = 'cli.lib.DevServerManager';
11
13
 
12
14
  const DEFAULT_PORT = 8080;
15
+ const SERVER_KEYS = {
16
+ app: 'app',
17
+ };
13
18
 
14
19
  class DevServerManager {
15
20
  constructor() {
16
21
  this.initialized = false;
22
+ this.started = false;
23
+ this.componentsByType = {};
17
24
  this.server = null;
18
25
  this.path = null;
19
26
  this.devServers = {};
27
+ this.debug = false;
20
28
  }
21
29
 
22
30
  safeLoadServer() {
23
31
  try {
24
32
  const { DevModeInterface } = require('@hubspot/ui-extensions-dev-server');
25
- this.devServers['uie'] = DevModeInterface;
33
+ this.devServers[SERVER_KEYS.app] = {
34
+ componentType: COMPONENT_TYPES.app,
35
+ serverInterface: DevModeInterface,
36
+ };
26
37
  } catch (e) {
27
38
  logger.debug('Failed to load dev server interface: ', e);
28
39
  }
@@ -33,8 +44,16 @@ class DevServerManager {
33
44
 
34
45
  for (let i = 0; i < serverKeys.length; i++) {
35
46
  const serverKey = serverKeys[i];
36
- const serverInterface = this.devServers[serverKey];
37
- await callback(serverInterface, serverKey);
47
+ const devServer = this.devServers[serverKey];
48
+
49
+ const compatibleComponents =
50
+ this.componentsByType[devServer.componentType] || {};
51
+
52
+ if (Object.keys(compatibleComponents).length) {
53
+ await callback(devServer.serverInterface, compatibleComponents);
54
+ } else {
55
+ logger.debug(i18n(`${i18nKey}.noCompatibleComponents`, { serverKey }));
56
+ }
38
57
  }
39
58
  }
40
59
 
@@ -42,66 +61,98 @@ class DevServerManager {
42
61
  return this.path ? `${this.path}/${path}` : null;
43
62
  }
44
63
 
45
- async start({
46
- accountId,
47
- debug,
48
- extension,
49
- projectConfig,
50
- projectSourceDir,
51
- }) {
52
- const app = express();
53
-
54
- // Install Middleware
55
- app.use(bodyParser.json({ limit: '50mb' }));
56
- app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
57
- app.use(cors());
58
-
59
- // Configure
60
- app.set('trust proxy', true);
61
-
62
- // Initialize a base route
63
- app.get('/', (req, res) => {
64
- res.send('HubSpot local dev server');
65
- });
66
-
67
- // Initialize URL redirects
68
- app.get('/hs/project', (req, res) => {
69
- res.redirect(getProjectDetailUrl(projectConfig.name, accountId));
70
- });
71
-
72
- // Start server
73
- this.server = await app.listen(DEFAULT_PORT).on('error', err => {
74
- if (err.code === 'EADDRINUSE') {
75
- logger.error(i18n(`${i18nKey}.portConflict`, { port: DEFAULT_PORT }));
76
- logger.log();
77
- process.exit(EXIT_CODES.ERROR);
64
+ arrangeComponentsByType(components) {
65
+ return components.reduce((acc, component) => {
66
+ if (!acc[component.type]) {
67
+ acc[component.type] = {};
78
68
  }
79
- });
80
-
81
- const projectFiles = await walk(projectSourceDir);
82
-
83
- // Initialize component servers
84
- await this.iterateDevServers(async serverInterface => {
85
- if (serverInterface.start) {
86
- await serverInterface.start({
87
- accountId,
88
- debug,
89
- extension,
90
- projectConfig,
91
- projectFiles,
92
- });
93
- }
94
- });
95
69
 
96
- this.path = this.server.address()
97
- ? `http://localhost:${this.server.address().port}`
98
- : null;
70
+ acc[component.type][component.config.name] = component;
71
+
72
+ return acc;
73
+ }, {});
74
+ }
75
+
76
+ async setup({ alpha, components, debug, onUploadRequired }) {
77
+ this.debug = debug;
78
+
79
+ this.componentsByType = this.arrangeComponentsByType(components);
80
+
81
+ this.safeLoadServer();
82
+
83
+ await this.iterateDevServers(
84
+ async (serverInterface, compatibleComponents) => {
85
+ if (serverInterface.setup) {
86
+ await serverInterface.setup({
87
+ alpha,
88
+ components: compatibleComponents,
89
+ debug,
90
+ onUploadRequired,
91
+ promptUser,
92
+ });
93
+ }
94
+ }
95
+ );
99
96
 
100
97
  this.initialized = true;
101
98
  }
102
99
 
103
- async cleanup() {
100
+ async start({ alpha, accountId, projectConfig }) {
104
101
  if (this.initialized) {
102
+ const app = express();
103
+
104
+ // Install Middleware
105
+ app.use(bodyParser.json({ limit: '50mb' }));
106
+ app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
107
+ app.use(cors());
108
+
109
+ // Configure
110
+ app.set('trust proxy', true);
111
+
112
+ // Initialize a base route
113
+ app.get('/', (req, res) => {
114
+ res.send('HubSpot local dev server');
115
+ });
116
+
117
+ // Initialize URL redirects
118
+ app.get('/hs/project', (req, res) => {
119
+ res.redirect(getProjectDetailUrl(projectConfig.name, accountId));
120
+ });
121
+
122
+ // Start server
123
+ this.server = await app.listen(DEFAULT_PORT).on('error', err => {
124
+ if (err.code === 'EADDRINUSE') {
125
+ logger.error(i18n(`${i18nKey}.portConflict`, { port: DEFAULT_PORT }));
126
+ logger.log();
127
+ process.exit(EXIT_CODES.ERROR);
128
+ }
129
+ });
130
+
131
+ // Initialize component servers
132
+ await this.iterateDevServers(async serverInterface => {
133
+ if (serverInterface.start) {
134
+ await serverInterface.start({
135
+ alpha,
136
+ accountId,
137
+ debug: this.debug,
138
+ httpClient,
139
+ projectConfig,
140
+ });
141
+ }
142
+ });
143
+
144
+ this.path = this.server.address()
145
+ ? `http://localhost:${this.server.address().port}`
146
+ : null;
147
+ } else {
148
+ throw new Error(i18n(`${i18nKey}.notInitialized`));
149
+ }
150
+
151
+ this.started = true;
152
+ }
153
+
154
+ async cleanup() {
155
+ if (this.started) {
105
156
  await this.iterateDevServers(async serverInterface => {
106
157
  if (serverInterface.cleanup) {
107
158
  await serverInterface.cleanup();
@@ -303,7 +303,8 @@ class LocalDevManager {
303
303
  try {
304
304
  const { buildId } = await provisionBuild(
305
305
  this.targetAccountId,
306
- this.projectConfig.name
306
+ this.projectConfig.name,
307
+ this.projectConfig.platformVersion
307
308
  );
308
309
  this.currentStagedBuildId = buildId;
309
310
  } catch (err) {
@@ -536,7 +537,11 @@ class LocalDevManager {
536
537
  let queueBuildError;
537
538
 
538
539
  try {
539
- await queueBuild(this.targetAccountId, this.projectConfig.name);
540
+ await queueBuild(
541
+ this.targetAccountId,
542
+ this.projectConfig.name,
543
+ this.projectConfig.platformVersion
544
+ );
540
545
  } catch (err) {
541
546
  queueBuildError = err;
542
547
  }
@@ -663,6 +668,9 @@ class LocalDevManager {
663
668
 
664
669
  async devServerStart() {
665
670
  try {
671
+ // Set this to true manually for now
672
+ DevServerManager.initialized = true;
673
+
666
674
  await DevServerManager.start({
667
675
  accountId: this.targetAccountId,
668
676
  debug: this.debug,
@@ -3,11 +3,26 @@ const chalk = require('chalk');
3
3
  const { i18n } = require('./lang');
4
4
  const { logger } = require('@hubspot/cli-lib/logger');
5
5
  const { handleKeypress } = require('@hubspot/cli-lib/lib/process');
6
+ const {
7
+ getAccountId,
8
+ getConfigDefaultAccount,
9
+ } = require('@hubspot/cli-lib/lib/config');
6
10
  const SpinniesManager = require('./SpinniesManager');
7
11
  const DevServerManager = require('./DevServerManager');
8
12
  const { EXIT_CODES } = require('./enums/exitCodes');
9
13
  const { getProjectDetailUrl } = require('./projects');
10
- const { uiAccountDescription, uiBetaMessage, uiLink, uiLine } = require('./ui');
14
+ const {
15
+ COMPONENT_TYPES,
16
+ findProjectComponents,
17
+ getAppCardConfigs,
18
+ } = require('./projectStructure');
19
+ const {
20
+ UI_COLORS,
21
+ uiAccountDescription,
22
+ uiBetaMessage,
23
+ uiLink,
24
+ uiLine,
25
+ } = require('./ui');
11
26
 
12
27
  const i18nKey = 'cli.lib.LocalDevManagerV2';
13
28
 
@@ -16,8 +31,9 @@ class LocalDevManagerV2 {
16
31
  this.targetAccountId = options.targetAccountId;
17
32
  this.projectConfig = options.projectConfig;
18
33
  this.projectDir = options.projectDir;
19
- this.extension = options.extension;
20
34
  this.debug = options.debug || false;
35
+ this.alpha = options.alpha;
36
+ this.deployedBuild = options.deployedBuild;
21
37
 
22
38
  this.projectSourceDir = path.join(
23
39
  this.projectDir,
@@ -31,14 +47,38 @@ class LocalDevManagerV2 {
31
47
  }
32
48
 
33
49
  async start() {
34
- console.clear();
35
50
  SpinniesManager.removeAll();
36
51
  SpinniesManager.init();
37
52
 
53
+ const components = await findProjectComponents(this.projectSourceDir);
54
+
55
+ if (!components.length) {
56
+ logger.log();
57
+ logger.error(i18n(`${i18nKey}.noComponents`));
58
+ process.exit(EXIT_CODES.SUCCESS);
59
+ }
60
+
61
+ const runnableComponents = components.filter(
62
+ component => component.runnable
63
+ );
64
+
65
+ if (!runnableComponents.length) {
66
+ logger.log();
67
+ logger.error(i18n(`${i18nKey}.noRunnableComponents`));
68
+ process.exit(EXIT_CODES.SUCCESS);
69
+ }
70
+
71
+ logger.log();
72
+ const setupSucceeded = await this.devServerSetup(runnableComponents);
73
+
74
+ if (setupSucceeded || !this.debug) {
75
+ console.clear();
76
+ }
77
+
38
78
  uiBetaMessage(i18n(`${i18nKey}.betaMessage`));
39
79
  logger.log();
40
80
  logger.log(
41
- chalk.hex('#FF8F59')(
81
+ chalk.hex(UI_COLORS.orange)(
42
82
  i18n(`${i18nKey}.running`, {
43
83
  accountIdentifier: uiAccountDescription(this.targetAccountId),
44
84
  projectName: this.projectConfig.name,
@@ -59,6 +99,8 @@ class LocalDevManagerV2 {
59
99
  await this.devServerStart();
60
100
 
61
101
  this.updateKeypressListeners();
102
+
103
+ this.compareLocalProjectToDeployed(runnableComponents);
62
104
  }
63
105
 
64
106
  async stop() {
@@ -89,15 +131,83 @@ class LocalDevManagerV2 {
89
131
  });
90
132
  }
91
133
 
134
+ logUploadWarning(reason) {
135
+ const currentDefaultAccount = getConfigDefaultAccount();
136
+ const defaultAccountId = getAccountId(currentDefaultAccount);
137
+
138
+ logger.log();
139
+ logger.warn(i18n(`${i18nKey}.uploadWarning.header`, { reason }));
140
+ logger.log(i18n(`${i18nKey}.uploadWarning.stopDev`));
141
+ if (this.targetAccountId !== defaultAccountId) {
142
+ logger.log(
143
+ i18n(`${i18nKey}.uploadWarning.runUploadWithAccount`, {
144
+ accountId: this.targetAccountId,
145
+ })
146
+ );
147
+ } else {
148
+ logger.log(i18n(`${i18nKey}.uploadWarning.runUpload`));
149
+ }
150
+ logger.log(i18n(`${i18nKey}.uploadWarning.restartDev`));
151
+ }
152
+
153
+ compareLocalProjectToDeployed(runnableComponents) {
154
+ const deployedComponentNames = this.deployedBuild.subbuildStatuses.map(
155
+ subbuildStatus => subbuildStatus.buildName
156
+ );
157
+
158
+ let missingComponents = [];
159
+
160
+ runnableComponents.forEach(({ type, config, path }) => {
161
+ if (type === COMPONENT_TYPES.app) {
162
+ const cardConfigs = getAppCardConfigs(config, path);
163
+
164
+ cardConfigs.forEach(cardConfig => {
165
+ if (
166
+ cardConfig.data &&
167
+ cardConfig.data.title &&
168
+ !deployedComponentNames.includes(cardConfig.data.title)
169
+ ) {
170
+ missingComponents.push(cardConfig.data.title);
171
+ }
172
+ });
173
+ }
174
+ });
175
+
176
+ if (missingComponents.length) {
177
+ this.logUploadWarning(
178
+ i18n(`${i18nKey}.uploadWarning.missingComponents`, {
179
+ missingComponents: missingComponents.join(','),
180
+ })
181
+ );
182
+ }
183
+ }
184
+
185
+ async devServerSetup(components) {
186
+ try {
187
+ await DevServerManager.setup({
188
+ alpha: this.alpha,
189
+ components,
190
+ debug: this.debug,
191
+ onUploadRequired: this.logUploadWarning.bind(this),
192
+ });
193
+ return true;
194
+ } catch (e) {
195
+ if (this.debug) {
196
+ logger.error(e);
197
+ }
198
+ logger.error(
199
+ i18n(`${i18nKey}.devServer.setupError`, { message: e.message })
200
+ );
201
+ return false;
202
+ }
203
+ }
204
+
92
205
  async devServerStart() {
93
206
  try {
94
- DevServerManager.safeLoadServer();
95
207
  await DevServerManager.start({
208
+ alpha: this.alpha,
96
209
  accountId: this.targetAccountId,
97
- debug: this.debug,
98
- extension: this.extension,
99
210
  projectConfig: this.projectConfig,
100
- projectSourceDir: this.projectSourceDir,
101
211
  });
102
212
  } catch (e) {
103
213
  if (this.debug) {
@@ -0,0 +1,106 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { walk } = require('@hubspot/cli-lib/lib/walk');
4
+ const { logger } = require('@hubspot/cli-lib/logger');
5
+
6
+ const COMPONENT_TYPES = Object.freeze({
7
+ app: 'app',
8
+ });
9
+
10
+ const APP_COMPONENT_CONFIG = 'app.json';
11
+
12
+ function loadConfigFile(configPath) {
13
+ if (configPath) {
14
+ try {
15
+ const source = fs.readFileSync(configPath);
16
+ const parsedConfig = JSON.parse(source);
17
+ return parsedConfig;
18
+ } catch (e) {
19
+ logger.debug(e);
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function getAppCardConfigs(appConfig, appPath) {
26
+ let cardConfigs = [];
27
+ let cards;
28
+
29
+ if (appConfig && appConfig.extensions && appConfig.extensions.crm) {
30
+ cards = appConfig.extensions.crm.cards;
31
+ }
32
+
33
+ if (cards) {
34
+ cards.forEach(({ file }) => {
35
+ const cardConfigPath = path.join(appPath, file);
36
+ const cardConfig = loadConfigFile(cardConfigPath);
37
+
38
+ if (cardConfig) {
39
+ cardConfigs.push(cardConfig);
40
+ }
41
+ });
42
+ }
43
+
44
+ return cardConfigs;
45
+ }
46
+
47
+ function getIsLegacyApp(appConfig, appPath) {
48
+ const cardConfigs = getAppCardConfigs(appConfig, appPath);
49
+
50
+ if (!cardConfigs.length) {
51
+ // Assume any app that does not have any cards is not legacy
52
+ return false;
53
+ }
54
+
55
+ let hasAnyReactExtensions = false;
56
+
57
+ cardConfigs.forEach(cardConfig => {
58
+ if (!hasAnyReactExtensions) {
59
+ const isReactExtension =
60
+ cardConfig &&
61
+ !!cardConfig.data &&
62
+ !!cardConfig.data.module &&
63
+ !!cardConfig.data.module.file;
64
+
65
+ hasAnyReactExtensions = isReactExtension;
66
+ }
67
+ });
68
+
69
+ return !hasAnyReactExtensions;
70
+ }
71
+
72
+ async function findProjectComponents(projectSourceDir) {
73
+ const components = [];
74
+
75
+ const projectFiles = await walk(projectSourceDir);
76
+
77
+ projectFiles.forEach(projectFile => {
78
+ // Find app components
79
+ if (projectFile.endsWith(APP_COMPONENT_CONFIG)) {
80
+ const parsedAppConfig = loadConfigFile(projectFile);
81
+
82
+ if (parsedAppConfig && parsedAppConfig.name) {
83
+ const appPath = projectFile.substring(
84
+ 0,
85
+ projectFile.indexOf(APP_COMPONENT_CONFIG)
86
+ );
87
+ const isLegacy = getIsLegacyApp(parsedAppConfig, appPath);
88
+
89
+ components.push({
90
+ type: COMPONENT_TYPES.app,
91
+ config: parsedAppConfig,
92
+ runnable: !isLegacy,
93
+ path: appPath,
94
+ });
95
+ }
96
+ }
97
+ });
98
+
99
+ return components;
100
+ }
101
+
102
+ module.exports = {
103
+ COMPONENT_TYPES,
104
+ findProjectComponents,
105
+ getAppCardConfigs,
106
+ };
package/lib/projects.js CHANGED
@@ -17,6 +17,9 @@ const {
17
17
  PROJECT_TASK_TYPES,
18
18
  SPINNER_STATUS,
19
19
  } = require('@hubspot/cli-lib/lib/constants');
20
+ const {
21
+ fetchDefaultVersion,
22
+ } = require('@hubspot/cli-lib/lib/projectPlatformVersion');
20
23
  const {
21
24
  createProject,
22
25
  getBuildStatus,
@@ -312,7 +315,8 @@ const uploadProjectFiles = async (
312
315
  accountId,
313
316
  projectName,
314
317
  filePath,
315
- uploadMessage
318
+ uploadMessage,
319
+ platformVersion
316
320
  ) => {
317
321
  SpinniesManager.init({});
318
322
  const accountIdentifier = uiAccountDescription(accountId);
@@ -333,7 +337,8 @@ const uploadProjectFiles = async (
333
337
  accountId,
334
338
  projectName,
335
339
  filePath,
336
- uploadMessage
340
+ uploadMessage,
341
+ platformVersion
337
342
  );
338
343
 
339
344
  buildId = upload.buildId;
@@ -490,7 +495,8 @@ const handleProjectUpload = async (
490
495
  accountId,
491
496
  projectConfig.name,
492
497
  tempFile.name,
493
- uploadMessage
498
+ uploadMessage,
499
+ projectConfig.platformVersion
494
500
  );
495
501
 
496
502
  if (error) {
@@ -807,6 +813,28 @@ const createProjectComponent = async (component, name) => {
807
813
  );
808
814
  };
809
815
 
816
+ const showPlatformVersionWarning = async (accountId, projectConfig) => {
817
+ const platformVersion = projectConfig.platformVersion;
818
+
819
+ if (!platformVersion) {
820
+ try {
821
+ const defaultVersion = await fetchDefaultVersion(accountId);
822
+ logger.log('');
823
+ logger.warn(
824
+ i18n(`${i18nKey}.showPlatformVersionWarning.noPlatformVersion`, {
825
+ defaultVersion,
826
+ })
827
+ );
828
+ } catch (e) {
829
+ logger.log('');
830
+ logger.warn(
831
+ i18n(`${i18nKey}.showPlatformVersionWarning.noPlatformVersionAlt`)
832
+ );
833
+ logger.debug(e.error);
834
+ }
835
+ }
836
+ };
837
+
810
838
  module.exports = {
811
839
  writeProjectConfig,
812
840
  getProjectConfig,
@@ -823,4 +851,5 @@ module.exports = {
823
851
  ensureProjectExists,
824
852
  logFeedbackMessage,
825
853
  createProjectComponent,
854
+ showPlatformVersionWarning,
826
855
  };
package/lib/ui.js CHANGED
@@ -6,6 +6,10 @@ const { getAccountConfig } = require('@hubspot/cli-lib/lib/config');
6
6
  const { i18n } = require('./lang');
7
7
  const { logger } = require('@hubspot/cli-lib/logger');
8
8
 
9
+ const UI_COLORS = {
10
+ orange: '#FF8F59',
11
+ };
12
+
9
13
  /**
10
14
  * Outputs horizontal line
11
15
  *
@@ -108,7 +112,7 @@ const uiFeatureHighlight = (commands, title) => {
108
112
  const uiBetaMessage = message => {
109
113
  const i18nKey = 'cli.lib.ui';
110
114
 
111
- logger.log(chalk.hex('#bda9ea')(i18n(`${i18nKey}.betaTag`)), message);
115
+ logger.log(chalk.hex(UI_COLORS.orange)(i18n(`${i18nKey}.betaTag`)), message);
112
116
  };
113
117
 
114
118
  const uiBetaWarning = logMessage => {
@@ -120,6 +124,7 @@ const uiBetaWarning = logMessage => {
120
124
  };
121
125
 
122
126
  module.exports = {
127
+ UI_COLORS,
123
128
  uiAccountDescription,
124
129
  uiBetaMessage,
125
130
  uiBetaWarning,
@@ -7,6 +7,25 @@ const { version } = require('../package.json');
7
7
  const { getPlatform } = require('./environment');
8
8
  const { setLogLevel } = require('./commonOpts');
9
9
 
10
+ /* **
11
+ Available tracking meta properties
12
+
13
+ - action: "The specific action taken in the CLI"
14
+ - os: "The user's OS"
15
+ - nodeVersion: "The user's version of node.js"
16
+ - nodeMajorVersion: "The user's major version of node.js"
17
+ - version: "The user's version of the CLI"
18
+ - command: "The specific command that the user ran in this interaction"
19
+ - authType: "The configured auth type the user has for the CLI"
20
+ - step: "The specific step in the auth process"
21
+ - assetType: "The CMS asset type"
22
+ - mode: "The CLI mode (draft or publish"
23
+ - type: "The upload type (file or folder)"
24
+ - file: "Whether or not the 'file' flag was used"
25
+ - successful: "Whether or not the CLI interaction was successful"
26
+
27
+ */
28
+
10
29
  const EventClass = {
11
30
  USAGE: 'USAGE',
12
31
  INTERACTION: 'INTERACTION',
@@ -132,10 +151,48 @@ const trackAuthAction = async (command, authType, step, accountId) => {
132
151
  }
133
152
  };
134
153
 
154
+ function trackCommandMetadataUsage(command, meta = {}, accountId) {
155
+ if (!isTrackingAllowed()) {
156
+ return;
157
+ }
158
+ logger.debug('Attempting to track metadata usage of "%s" command', command);
159
+ let authType = 'unknown';
160
+ if (accountId) {
161
+ const accountConfig = getAccountConfig(accountId);
162
+ authType =
163
+ accountConfig && accountConfig.authType
164
+ ? accountConfig.authType
165
+ : API_KEY_AUTH_METHOD.value;
166
+ }
167
+ setImmediate(async () => {
168
+ const usageTrackingEvent = {
169
+ action: 'cli-command-metadata',
170
+ os: getPlatform(),
171
+ ...getNodeVersionData(),
172
+ version,
173
+ command,
174
+ authType,
175
+ ...meta,
176
+ };
177
+ try {
178
+ await trackUsage(
179
+ 'cli-interaction',
180
+ EventClass.INTERACTION,
181
+ usageTrackingEvent,
182
+ accountId
183
+ );
184
+ logger.debug('Sent usage tracking command event: %o', usageTrackingEvent);
185
+ } catch (e) {
186
+ logger.debug('Metadata usage tracking failed: %s', e.message);
187
+ }
188
+ });
189
+ }
190
+
135
191
  module.exports = {
136
192
  trackCommandUsage,
137
193
  trackHelpUsage,
138
194
  addHelpUsageTracking,
139
195
  trackConvertFieldsUsage,
140
196
  trackAuthAction,
197
+ trackCommandMetadataUsage,
141
198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "4.2.1-beta.1",
3
+ "version": "4.2.1-beta.2",
4
4
  "description": "CLI for working with HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,9 +8,9 @@
8
8
  "url": "https://github.com/HubSpot/hubspot-cms-tools"
9
9
  },
10
10
  "dependencies": {
11
- "@hubspot/cli-lib": "^4.1.14",
12
- "@hubspot/serverless-dev-runtime": "4.1.8-beta.6",
13
- "@hubspot/ui-extensions-dev-server": "^0.4.0",
11
+ "@hubspot/cli-lib": "^4.2.1",
12
+ "@hubspot/serverless-dev-runtime": "4.2.1-beta.2",
13
+ "@hubspot/ui-extensions-dev-server": "^0.5.0",
14
14
  "archiver": "^5.3.0",
15
15
  "body-parser": "^1.19.0",
16
16
  "chalk": "^4.1.2",
@@ -45,5 +45,5 @@
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
- "gitHead": "a79d4a4d6c61971986a7df3aaa8374cbe2405a0b"
48
+ "gitHead": "9a111f2df1b19153efe867f3501599bfa4aedabe"
49
49
  }