@hubspot/cli 4.2.1-beta.0 → 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.
@@ -1,31 +1,41 @@
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
- setServer(key, serverInterfacePath) {
30
+ safeLoadServer() {
23
31
  try {
24
- this.devServers[key] = require(serverInterfacePath);
32
+ const { DevModeInterface } = require('@hubspot/ui-extensions-dev-server');
33
+ this.devServers[SERVER_KEYS.app] = {
34
+ componentType: COMPONENT_TYPES.app,
35
+ serverInterface: DevModeInterface,
36
+ };
25
37
  } catch (e) {
26
- logger.debug(
27
- `Failed to load dev server interface at ${serverInterfacePath}`
28
- );
38
+ logger.debug('Failed to load dev server interface: ', e);
29
39
  }
30
40
  }
31
41
 
@@ -34,8 +44,16 @@ class DevServerManager {
34
44
 
35
45
  for (let i = 0; i < serverKeys.length; i++) {
36
46
  const serverKey = serverKeys[i];
37
- const serverInterface = this.devServers[serverKey];
38
- 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
+ }
39
57
  }
40
58
  }
41
59
 
@@ -43,82 +61,104 @@ class DevServerManager {
43
61
  return this.path ? `${this.path}/${path}` : null;
44
62
  }
45
63
 
46
- makeLogger(spinniesLogger, serverKey) {
47
- return {
48
- debug: (...args) => spinniesLogger(serverKey, '[DEBUG] ', ...args),
49
- error: (...args) => spinniesLogger(serverKey, '[ERROR] ', ...args),
50
- info: (...args) => spinniesLogger(serverKey, '[INFO] ', ...args),
51
- log: (...args) => spinniesLogger(serverKey, '[INFO] ', ...args),
52
- warn: (...args) => spinniesLogger(serverKey, '[WARN] ', ...args),
53
- };
64
+ arrangeComponentsByType(components) {
65
+ return components.reduce((acc, component) => {
66
+ if (!acc[component.type]) {
67
+ acc[component.type] = {};
68
+ }
69
+
70
+ acc[component.type][component.config.name] = component;
71
+
72
+ return acc;
73
+ }, {});
54
74
  }
55
75
 
56
- async start({
57
- accountId,
58
- debug,
59
- extension,
60
- projectConfig,
61
- projectSourceDir,
62
- spinniesLogger,
63
- }) {
64
- const app = express();
65
-
66
- // Install Middleware
67
- app.use(bodyParser.json({ limit: '50mb' }));
68
- app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
69
- app.use(cors());
70
-
71
- // Configure
72
- app.set('trust proxy', true);
73
-
74
- // Initialize a base route
75
- app.get('/', (req, res) => {
76
- res.send('HubSpot local dev server');
77
- });
78
-
79
- // Initialize URL redirects
80
- app.get('/hs/project', (req, res) => {
81
- res.redirect(getProjectDetailUrl(projectConfig.name, accountId));
82
- });
83
-
84
- // Start server
85
- this.server = await app.listen(DEFAULT_PORT).on('error', err => {
86
- if (err.code === 'EADDRINUSE') {
87
- logger.error(i18n(`${i18nKey}.portConflict`, { port: DEFAULT_PORT }));
88
- logger.log();
89
- process.exit(EXIT_CODES.ERROR);
90
- }
91
- });
92
-
93
- const projectFiles = await walk(projectSourceDir);
94
-
95
- // Initialize component servers
96
- await this.iterateDevServers(async (serverInterface, serverKey) => {
97
- if (serverInterface.start) {
98
- await serverInterface.start({
99
- debug,
100
- extension,
101
- logger: this.makeLogger(spinniesLogger, serverKey),
102
- projectConfig,
103
- projectFiles,
104
- });
105
- }
106
- });
76
+ async setup({ alpha, components, debug, onUploadRequired }) {
77
+ this.debug = debug;
78
+
79
+ this.componentsByType = this.arrangeComponentsByType(components);
80
+
81
+ this.safeLoadServer();
107
82
 
108
- this.path = this.server.address()
109
- ? `http://localhost:${this.server.address().port}`
110
- : null;
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
+ );
111
96
 
112
97
  this.initialized = true;
113
98
  }
114
99
 
115
- async cleanup() {
100
+ async start({ alpha, accountId, projectConfig }) {
116
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) {
117
156
  await this.iterateDevServers(async serverInterface => {
118
157
  if (serverInterface.cleanup) {
119
158
  await serverInterface.cleanup();
120
159
  }
121
160
  });
161
+
122
162
  if (this.server) {
123
163
  await this.server.close();
124
164
  }
@@ -54,8 +54,6 @@ class LocalDevManager {
54
54
  this.targetAccountId = options.targetAccountId;
55
55
  this.projectConfig = options.projectConfig;
56
56
  this.projectDir = options.projectDir;
57
- this.extension = options.extension;
58
- this.devServerPath = options.devServerPath;
59
57
  this.uploadPermission =
60
58
  options.uploadPermission || UPLOAD_PERMISSIONS.always;
61
59
  this.debug = options.debug || false;
@@ -102,12 +100,8 @@ class LocalDevManager {
102
100
 
103
101
  await this.devServerStart();
104
102
 
105
- if (!this.devServerPath) {
106
- this.uploadQueue.start();
107
- await this.startWatching();
108
- } else {
109
- this.uploadPermission = UPLOAD_PERMISSIONS.never;
110
- }
103
+ this.uploadQueue.start();
104
+ await this.startWatching();
111
105
 
112
106
  this.updateKeypressListeners();
113
107
 
@@ -309,7 +303,8 @@ class LocalDevManager {
309
303
  try {
310
304
  const { buildId } = await provisionBuild(
311
305
  this.targetAccountId,
312
- this.projectConfig.name
306
+ this.projectConfig.name,
307
+ this.projectConfig.platformVersion
313
308
  );
314
309
  this.currentStagedBuildId = buildId;
315
310
  } catch (err) {
@@ -485,10 +480,12 @@ class LocalDevManager {
485
480
  }),
486
481
  status: 'non-spinnable',
487
482
  });
483
+ const path =
484
+ event === WATCH_EVENTS.unlinkDir ? `${remotePath}/` : remotePath;
488
485
  await deleteFileFromBuild(
489
486
  this.targetAccountId,
490
487
  this.projectConfig.name,
491
- remotePath
488
+ path
492
489
  );
493
490
  SpinniesManager.update(spinnerName, {
494
491
  text: i18n(`${i18nKey}.upload.uploadedRemoveChange`, {
@@ -540,7 +537,11 @@ class LocalDevManager {
540
537
  let queueBuildError;
541
538
 
542
539
  try {
543
- await queueBuild(this.targetAccountId, this.projectConfig.name);
540
+ await queueBuild(
541
+ this.targetAccountId,
542
+ this.projectConfig.name,
543
+ this.projectConfig.platformVersion
544
+ );
544
545
  } catch (err) {
545
546
  queueBuildError = err;
546
547
  }
@@ -667,13 +668,12 @@ class LocalDevManager {
667
668
 
668
669
  async devServerStart() {
669
670
  try {
670
- if (this.devServerPath) {
671
- DevServerManager.setServer('uie', this.devServerPath);
672
- }
671
+ // Set this to true manually for now
672
+ DevServerManager.initialized = true;
673
+
673
674
  await DevServerManager.start({
674
675
  accountId: this.targetAccountId,
675
676
  debug: this.debug,
676
- extension: this.extension,
677
677
  spinniesLogger: this.handleServerLog,
678
678
  projectConfig: this.projectConfig,
679
679
  projectSourceDir: this.projectSourceDir,
@@ -0,0 +1,239 @@
1
+ const path = require('path');
2
+ const chalk = require('chalk');
3
+ const { i18n } = require('./lang');
4
+ const { logger } = require('@hubspot/cli-lib/logger');
5
+ const { handleKeypress } = require('@hubspot/cli-lib/lib/process');
6
+ const {
7
+ getAccountId,
8
+ getConfigDefaultAccount,
9
+ } = require('@hubspot/cli-lib/lib/config');
10
+ const SpinniesManager = require('./SpinniesManager');
11
+ const DevServerManager = require('./DevServerManager');
12
+ const { EXIT_CODES } = require('./enums/exitCodes');
13
+ const { getProjectDetailUrl } = require('./projects');
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');
26
+
27
+ const i18nKey = 'cli.lib.LocalDevManagerV2';
28
+
29
+ class LocalDevManagerV2 {
30
+ constructor(options) {
31
+ this.targetAccountId = options.targetAccountId;
32
+ this.projectConfig = options.projectConfig;
33
+ this.projectDir = options.projectDir;
34
+ this.debug = options.debug || false;
35
+ this.alpha = options.alpha;
36
+ this.deployedBuild = options.deployedBuild;
37
+
38
+ this.projectSourceDir = path.join(
39
+ this.projectDir,
40
+ this.projectConfig.srcDir
41
+ );
42
+
43
+ if (!this.targetAccountId || !this.projectConfig || !this.projectDir) {
44
+ logger.log(i18n(`${i18nKey}.failedToInitialize`));
45
+ process.exit(EXIT_CODES.ERROR);
46
+ }
47
+ }
48
+
49
+ async start() {
50
+ SpinniesManager.removeAll();
51
+ SpinniesManager.init();
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
+
78
+ uiBetaMessage(i18n(`${i18nKey}.betaMessage`));
79
+ logger.log();
80
+ logger.log(
81
+ chalk.hex(UI_COLORS.orange)(
82
+ i18n(`${i18nKey}.running`, {
83
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
84
+ projectName: this.projectConfig.name,
85
+ })
86
+ )
87
+ );
88
+ logger.log(
89
+ uiLink(
90
+ i18n(`${i18nKey}.viewInHubSpotLink`),
91
+ getProjectDetailUrl(this.projectConfig.name, this.targetAccountId)
92
+ )
93
+ );
94
+ logger.log();
95
+ logger.log(i18n(`${i18nKey}.quitHelper`));
96
+ uiLine();
97
+ logger.log();
98
+
99
+ await this.devServerStart();
100
+
101
+ this.updateKeypressListeners();
102
+
103
+ this.compareLocalProjectToDeployed(runnableComponents);
104
+ }
105
+
106
+ async stop() {
107
+ SpinniesManager.add('cleanupMessage', {
108
+ text: i18n(`${i18nKey}.exitingStart`),
109
+ });
110
+
111
+ const cleanupSucceeded = await this.devServerCleanup();
112
+
113
+ if (!cleanupSucceeded) {
114
+ SpinniesManager.fail('cleanupMessage', {
115
+ text: i18n(`${i18nKey}.exitingFail`),
116
+ });
117
+ process.exit(EXIT_CODES.ERROR);
118
+ }
119
+
120
+ SpinniesManager.succeed('cleanupMessage', {
121
+ text: i18n(`${i18nKey}.exitingSucceed`),
122
+ });
123
+ process.exit(EXIT_CODES.SUCCESS);
124
+ }
125
+
126
+ updateKeypressListeners() {
127
+ handleKeypress(async key => {
128
+ if ((key.ctrl && key.name === 'c') || key.name === 'q') {
129
+ this.stop();
130
+ }
131
+ });
132
+ }
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
+
205
+ async devServerStart() {
206
+ try {
207
+ await DevServerManager.start({
208
+ alpha: this.alpha,
209
+ accountId: this.targetAccountId,
210
+ projectConfig: this.projectConfig,
211
+ });
212
+ } catch (e) {
213
+ if (this.debug) {
214
+ logger.error(e);
215
+ }
216
+ logger.error(
217
+ i18n(`${i18nKey}.devServer.startError`, { message: e.message })
218
+ );
219
+ process.exit(EXIT_CODES.ERROR);
220
+ }
221
+ }
222
+
223
+ async devServerCleanup() {
224
+ try {
225
+ await DevServerManager.cleanup();
226
+ return true;
227
+ } catch (e) {
228
+ if (this.debug) {
229
+ logger.error(e);
230
+ }
231
+ logger.error(
232
+ i18n(`${i18nKey}.devServer.cleanupError`, { message: e.message })
233
+ );
234
+ return false;
235
+ }
236
+ }
237
+ }
238
+
239
+ module.exports = LocalDevManagerV2;
@@ -53,6 +53,9 @@ class SpinniesManager {
53
53
  // Default Spinnies fields
54
54
  this.spinners = {};
55
55
  this.isCursorHidden = false;
56
+ if (this.currentInterval) {
57
+ clearInterval(this.currentInterval);
58
+ }
56
59
  this.currentInterval = null;
57
60
  this.stream = process.stderr;
58
61
  this.lineCount = 0;
@@ -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
+ };