@hubspot/cli 4.0.1-beta.5 → 4.0.2-beta.1

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
@@ -35,6 +35,7 @@ const moduleCommand = require('../commands/module');
35
35
  const configCommand = require('../commands/config');
36
36
  const accountsCommand = require('../commands/accounts');
37
37
  const sandboxesCommand = require('../commands/sandbox');
38
+ const processCommand = require('../commands/process');
38
39
  const cmsCommand = require('../commands/cms');
39
40
  const { EXIT_CODES } = require('../lib/enums/exitCodes');
40
41
 
@@ -154,6 +155,7 @@ const argv = yargs
154
155
  .command(configCommand)
155
156
  .command(accountsCommand)
156
157
  .command(sandboxesCommand)
158
+ .command(processCommand, false)
157
159
  .help()
158
160
  .recommendCommands()
159
161
  .demandCommand(1, '')
@@ -0,0 +1,85 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { createIgnoreFilter } = require('@hubspot/cli-lib/ignoreRules');
4
+ const { isAllowedExtension } = require('@hubspot/cli-lib//path');
5
+ const { logger } = require('@hubspot/cli-lib/logger');
6
+ const { walk } = require('@hubspot/cli-lib/lib/walk');
7
+ const { getThemeJSONPath } = require('@hubspot/cli-lib/lib/files');
8
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
9
+ const {
10
+ FieldsJs,
11
+ isProcessableFieldsJs,
12
+ } = require('@hubspot/cli-lib/lib/handleFieldsJs');
13
+ const i18nKey = 'cli.commands.process';
14
+
15
+ exports.command = 'process';
16
+ exports.describe = false;
17
+
18
+ const invalidPath = src => {
19
+ logger.error(
20
+ i18n(`${i18nKey}.errors.invalidPath`, {
21
+ path: src,
22
+ })
23
+ );
24
+ };
25
+
26
+ exports.handler = async options => {
27
+ const src = options.src;
28
+ const projectRoot = path.dirname(getThemeJSONPath(src));
29
+ let stats;
30
+ try {
31
+ stats = fs.statSync(src);
32
+ if (!stats.isFile() && !stats.isDirectory()) {
33
+ invalidPath(src);
34
+ return;
35
+ }
36
+ } catch (e) {
37
+ invalidPath(src);
38
+ }
39
+
40
+ if (stats.isFile()) {
41
+ const fieldsJs = await new FieldsJs(
42
+ projectRoot,
43
+ src,
44
+ undefined,
45
+ options.fieldOptions
46
+ ).init();
47
+ if (fieldsJs.rejected) return;
48
+ fieldsJs.saveOutput();
49
+ } else if (stats.isDirectory()) {
50
+ const filePaths = await walk(src);
51
+ const allowedFilePaths = filePaths
52
+ .filter(file => {
53
+ if (!isAllowedExtension(file)) {
54
+ return false;
55
+ }
56
+ return true;
57
+ })
58
+ .filter(createIgnoreFilter());
59
+ for (const filePath of allowedFilePaths) {
60
+ if (isProcessableFieldsJs(projectRoot, filePath, true)) {
61
+ const fieldsJs = await new FieldsJs(
62
+ projectRoot,
63
+ filePath,
64
+ undefined,
65
+ options.fieldOptions
66
+ ).init();
67
+ if (fieldsJs.rejected) return;
68
+ fieldsJs.saveOutput();
69
+ }
70
+ }
71
+ }
72
+ };
73
+
74
+ exports.builder = yargs => {
75
+ yargs.positional('src', {
76
+ describe: i18n(`${i18nKey}.positionals.src.describe`),
77
+ type: 'string',
78
+ });
79
+ yargs.option('fieldOptions', {
80
+ describe: i18n(`${i18nKey}.options.options.describe`),
81
+ type: 'array',
82
+ default: [''],
83
+ });
84
+ return yargs;
85
+ };
@@ -120,10 +120,10 @@ exports.handler = async options => {
120
120
  succeedColor: 'white',
121
121
  });
122
122
 
123
- let namePrompt;
124
-
125
123
  trackCommandUsage('sandbox-create', {}, accountId);
126
124
 
125
+ let namePrompt;
126
+
127
127
  if (!name) {
128
128
  namePrompt = await createSandboxPrompt();
129
129
  }
@@ -151,6 +151,8 @@ exports.handler = async options => {
151
151
  } catch (err) {
152
152
  debugErrorAndContext(err);
153
153
 
154
+ trackCommandUsage('sandbox-create', { success: false }, accountId);
155
+
154
156
  spinnies.fail('sandboxCreate', {
155
157
  text: i18n(`${i18nKey}.loading.fail`, {
156
158
  sandboxName,
@@ -16,7 +16,9 @@ const { deleteSandbox } = require('@hubspot/cli-lib/sandboxes');
16
16
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
17
17
  const { getConfig, getEnv } = require('@hubspot/cli-lib');
18
18
  const { deleteSandboxPrompt } = require('../../lib/prompts/sandboxesPrompt');
19
- const { removeAccountFromConfig } = require('@hubspot/cli-lib/lib/config');
19
+ const {
20
+ removeSandboxAccountFromConfig,
21
+ } = require('@hubspot/cli-lib/lib/config');
20
22
  const {
21
23
  selectAndSetAsDefaultAccountPrompt,
22
24
  } = require('../../lib/prompts/accountsPrompt');
@@ -114,7 +116,9 @@ exports.handler = async options => {
114
116
  );
115
117
  logger.log('');
116
118
 
117
- const promptDefaultAccount = removeAccountFromConfig(sandboxAccountId);
119
+ const promptDefaultAccount = removeSandboxAccountFromConfig(
120
+ sandboxAccountId
121
+ );
118
122
  if (promptDefaultAccount) {
119
123
  await selectAndSetAsDefaultAccountPrompt(config);
120
124
  }
@@ -122,6 +126,8 @@ exports.handler = async options => {
122
126
  } catch (err) {
123
127
  debugErrorAndContext(err);
124
128
 
129
+ trackCommandUsage('sandbox-delete', { success: false }, sandboxAccountId);
130
+
125
131
  if (
126
132
  err.error &&
127
133
  err.error.category === OBJECT_NOT_FOUND &&
@@ -135,7 +141,9 @@ exports.handler = async options => {
135
141
  );
136
142
  logger.log('');
137
143
 
138
- const promptDefaultAccount = removeAccountFromConfig(sandboxAccountId);
144
+ const promptDefaultAccount = removeSandboxAccountFromConfig(
145
+ sandboxAccountId
146
+ );
139
147
  if (promptDefaultAccount) {
140
148
  await selectAndSetAsDefaultAccountPrompt(config);
141
149
  }
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
-
3
+ const { walk } = require('@hubspot/cli-lib/lib/walk');
4
+ const { createIgnoreFilter } = require('@hubspot/cli-lib/ignoreRules');
4
5
  const { uploadFolder, hasUploadErrors } = require('@hubspot/cli-lib');
5
6
  const { getFileMapperQueryValues } = require('@hubspot/cli-lib/fileMapper');
6
7
  const { upload } = require('@hubspot/cli-lib/api/fileMapper');
@@ -27,13 +28,21 @@ const {
27
28
  getMode,
28
29
  } = require('../lib/commonOpts');
29
30
  const { uploadPrompt } = require('../lib/prompts/uploadPrompt');
31
+ const { fieldsJsPrompt } = require('../lib/prompts/cmsFieldPrompt');
30
32
  const { validateMode, loadAndValidateOptions } = require('../lib/validation');
31
33
  const { trackCommandUsage } = require('../lib/usageTracking');
32
- const { getThemePreviewUrl } = require('@hubspot/cli-lib/lib/files');
34
+ const {
35
+ getThemePreviewUrl,
36
+ getThemeJSONPath,
37
+ } = require('@hubspot/cli-lib/lib/files');
33
38
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
34
-
35
39
  const i18nKey = 'cli.commands.upload';
36
40
  const { EXIT_CODES } = require('../lib/enums/exitCodes');
41
+ const {
42
+ FieldsJs,
43
+ isProcessableFieldsJs,
44
+ cleanupTmpDirSync,
45
+ } = require('@hubspot/cli-lib/lib/handleFieldsJs');
37
46
 
38
47
  exports.command = 'upload [--src] [--dest]';
39
48
  exports.describe = i18n(`${i18nKey}.describe`);
@@ -61,11 +70,34 @@ exports.handler = async options => {
61
70
  const mode = getMode(options);
62
71
 
63
72
  const uploadPromptAnswers = await uploadPrompt(options);
64
-
65
73
  const src = options.src || uploadPromptAnswers.src;
66
- const dest = options.dest || uploadPromptAnswers.dest;
67
-
68
- const absoluteSrcPath = path.resolve(getCwd(), src);
74
+ const saveOutput = options.saveOutput;
75
+ let dest = options.dest || uploadPromptAnswers.dest;
76
+ let absoluteSrcPath = path.resolve(getCwd(), src);
77
+ if (!dest) {
78
+ logger.error(i18n(`${i18nKey}.errors.destinationRequired`));
79
+ return;
80
+ }
81
+ // The theme.json file must always be at the root of the project - so we look for that and determine the root path based on it.
82
+ const projectRoot = path.dirname(getThemeJSONPath(absoluteSrcPath));
83
+ const processFieldsJs = isProcessableFieldsJs(
84
+ projectRoot,
85
+ absoluteSrcPath,
86
+ options.processFieldsJs
87
+ );
88
+ let fieldsJs;
89
+ if (processFieldsJs) {
90
+ fieldsJs = await new FieldsJs(
91
+ projectRoot,
92
+ absoluteSrcPath,
93
+ undefined,
94
+ options.fieldOptions
95
+ ).init();
96
+ if (fieldsJs.rejected) return;
97
+ // Ensures that the dest path is a .json. The user might pass '.js' accidentally - this ensures it just works.
98
+ absoluteSrcPath = fieldsJs.outputPath;
99
+ dest = path.join(path.dirname(dest), 'fields.json');
100
+ }
69
101
  let stats;
70
102
  try {
71
103
  stats = fs.statSync(absoluteSrcPath);
@@ -86,10 +118,6 @@ exports.handler = async options => {
86
118
  return;
87
119
  }
88
120
 
89
- if (!dest) {
90
- logger.error(i18n(`${i18nKey}.errors.destinationRequired`));
91
- return;
92
- }
93
121
  const normalizedDest = convertToUnixPath(dest);
94
122
  trackCommandUsage(
95
123
  'upload',
@@ -106,7 +134,7 @@ exports.handler = async options => {
106
134
  process.exit(EXIT_CODES.WARNING);
107
135
  }
108
136
  if (stats.isFile()) {
109
- if (!isAllowedExtension(src)) {
137
+ if (!isAllowedExtension(src) && !processFieldsJs) {
110
138
  logger.error(
111
139
  i18n(`${i18nKey}.errors.invalidPath`, {
112
140
  path: src,
@@ -123,7 +151,6 @@ exports.handler = async options => {
123
151
  );
124
152
  return;
125
153
  }
126
-
127
154
  upload(
128
155
  accountId,
129
156
  absoluteSrcPath,
@@ -156,6 +183,13 @@ exports.handler = async options => {
156
183
  })
157
184
  );
158
185
  process.exit(EXIT_CODES.WARNING);
186
+ })
187
+ .finally(() => {
188
+ if (!processFieldsJs) return;
189
+ if (saveOutput) {
190
+ fieldsJs.saveOutput();
191
+ }
192
+ cleanupTmpDirSync(fieldsJs.rootWriteDir);
159
193
  });
160
194
  } else {
161
195
  logger.log(
@@ -165,9 +199,22 @@ exports.handler = async options => {
165
199
  src,
166
200
  })
167
201
  );
168
- uploadFolder(accountId, absoluteSrcPath, dest, {
169
- mode,
170
- })
202
+
203
+ // Generate the first-pass file list in here, and pass to uploadFolder.
204
+ const filePaths = await getUploadableFileList(
205
+ absoluteSrcPath,
206
+ options.processFieldsJs
207
+ );
208
+ uploadFolder(
209
+ accountId,
210
+ absoluteSrcPath,
211
+ dest,
212
+ {
213
+ mode,
214
+ },
215
+ options,
216
+ filePaths
217
+ )
171
218
  .then(results => {
172
219
  if (!hasUploadErrors(results)) {
173
220
  logger.success(
@@ -200,6 +247,46 @@ exports.handler = async options => {
200
247
  }
201
248
  };
202
249
 
250
+ /*
251
+ * Walks the src folder for files, filters them based on ignore filter.
252
+ * If processFieldsJs is true then will check for any JS fields conflicts (i.e., JS fields file and fields.json file) and prompt to resolve
253
+ */
254
+ const getUploadableFileList = async (src, processFieldsJs) => {
255
+ const filePaths = await walk(src);
256
+ const allowedFiles = filePaths
257
+ .filter(file => {
258
+ if (!isAllowedExtension(file)) {
259
+ return false;
260
+ }
261
+ return true;
262
+ })
263
+ .filter(createIgnoreFilter());
264
+ if (!processFieldsJs) {
265
+ return allowedFiles;
266
+ }
267
+
268
+ let uploadableFiles = [];
269
+ let skipFiles = [];
270
+ for (const filePath of allowedFiles) {
271
+ const fileName = path.basename(filePath);
272
+ if (skipFiles.includes(filePath)) continue;
273
+ const isProcessable = isProcessableFieldsJs(src, filePath, processFieldsJs);
274
+ if (isProcessable || fileName == 'fields.json') {
275
+ // This prompt checks if there are multiple field files in the folder and gets user to choose.
276
+ const [choice, updatedSkipFiles] = await fieldsJsPrompt(
277
+ filePath,
278
+ src,
279
+ skipFiles
280
+ );
281
+ skipFiles = updatedSkipFiles;
282
+ // If they chose something other than the current file, move on.
283
+ if (choice !== filePath) continue;
284
+ }
285
+ uploadableFiles.push(filePath);
286
+ }
287
+ return uploadableFiles;
288
+ };
289
+
203
290
  exports.builder = yargs => {
204
291
  addConfigOptions(yargs, true);
205
292
  addAccountOptions(yargs, true);
@@ -214,5 +301,24 @@ exports.builder = yargs => {
214
301
  describe: i18n(`${i18nKey}.positionals.dest.describe`),
215
302
  type: 'string',
216
303
  });
304
+ yargs.option('fieldOptions', {
305
+ describe: i18n(`${i18nKey}.options.options.describe`),
306
+ type: 'array',
307
+ default: [''],
308
+ hidden: true,
309
+ });
310
+ yargs.option('saveOutput', {
311
+ describe: i18n(`${i18nKey}.options.saveOutput.describe`),
312
+ type: 'boolean',
313
+ default: false,
314
+ hidden: true,
315
+ });
316
+ yargs.option('processFieldsJs', {
317
+ describe: i18n(`${i18nKey}.options.processFields.describe`),
318
+ alias: ['processFields'],
319
+ type: 'boolean',
320
+ default: false,
321
+ hidden: true,
322
+ });
217
323
  return yargs;
218
324
  };
package/commands/watch.js CHANGED
@@ -82,6 +82,7 @@ exports.handler = async options => {
82
82
  remove,
83
83
  disableInitial: initialUpload ? false : true,
84
84
  notify,
85
+ commandOptions: options,
85
86
  });
86
87
  };
87
88
 
@@ -99,6 +100,12 @@ exports.builder = yargs => {
99
100
  describe: i18n(`${i18nKey}.positionals.dest.describe`),
100
101
  type: 'string',
101
102
  });
103
+ yargs.option('fieldOptions', {
104
+ describe: i18n(`${i18nKey}.options.options.describe`),
105
+ type: 'array',
106
+ default: [''],
107
+ hidden: true,
108
+ });
102
109
  yargs.option('remove', {
103
110
  alias: 'r',
104
111
  describe: i18n(`${i18nKey}.options.remove.describe`),
@@ -121,6 +128,18 @@ exports.builder = yargs => {
121
128
  type: 'string',
122
129
  requiresArg: true,
123
130
  });
124
-
131
+ yargs.option('processFieldsJs', {
132
+ describe: i18n(`${i18nKey}.options.processFields.describe`),
133
+ alias: ['processFields'],
134
+ type: 'boolean',
135
+ default: false,
136
+ hidden: true,
137
+ });
138
+ yargs.option('saveOutput', {
139
+ describe: i18n(`${i18nKey}.options.saveOutput.describe`),
140
+ type: 'boolean',
141
+ default: false,
142
+ hidden: true,
143
+ });
125
144
  return yargs;
126
145
  };
package/lib/projects.js CHANGED
@@ -224,6 +224,13 @@ const getProjectBuildDetailUrl = (projectName, buildId, accountId) => {
224
224
  return `${getProjectDetailUrl(projectName, accountId)}/build/${buildId}`;
225
225
  };
226
226
 
227
+ const getProjectDeployDetailUrl = (projectName, deployId, accountId) => {
228
+ if (!projectName || !deployId || !accountId) return;
229
+ return `${getProjectDetailUrl(
230
+ projectName,
231
+ accountId
232
+ )}/activity/deploy/${deployId}`;
233
+ };
227
234
  const uploadProjectFiles = async (accountId, projectName, filePath) => {
228
235
  const i18nKey = 'cli.commands.project.subcommands.upload';
229
236
  const spinnies = new Spinnies({
@@ -347,7 +354,9 @@ const makePollTaskStatusFunc = ({
347
354
  const displayId = deployedBuildId || taskId;
348
355
 
349
356
  if (linkToHubSpot) {
350
- logger.log(`\n${linkToHubSpot(taskName, taskId, accountId)}\n`);
357
+ logger.log(
358
+ `\n${linkToHubSpot(accountId, taskName, taskId, deployedBuildId)}\n`
359
+ );
351
360
  }
352
361
 
353
362
  const spinnies = new Spinnies({
@@ -471,10 +480,10 @@ const makePollTaskStatusFunc = ({
471
480
  };
472
481
 
473
482
  const pollBuildStatus = makePollTaskStatusFunc({
474
- linkToHubSpot: (projectName, buildId, accountId) =>
483
+ linkToHubSpot: (accountId, taskName, taskId) =>
475
484
  uiLink(
476
- `View build #${buildId} in HubSpot`,
477
- getProjectBuildDetailUrl(projectName, buildId, accountId)
485
+ `View build #${taskId} in HubSpot`,
486
+ getProjectBuildDetailUrl(taskName, taskId, accountId)
478
487
  ),
479
488
  statusFn: getBuildStatus,
480
489
  statusText: PROJECT_BUILD_TEXT,
@@ -490,6 +499,11 @@ const pollBuildStatus = makePollTaskStatusFunc({
490
499
  });
491
500
 
492
501
  const pollDeployStatus = makePollTaskStatusFunc({
502
+ linkToHubSpot: (accountId, taskName, taskId, deployedBuildId) =>
503
+ uiLink(
504
+ `View deploy of build #${deployedBuildId} in HubSpot`,
505
+ getProjectDeployDetailUrl(taskName, taskId, accountId)
506
+ ),
493
507
  statusFn: getDeployStatus,
494
508
  statusText: PROJECT_DEPLOY_TEXT,
495
509
  statusStrings: {
@@ -0,0 +1,50 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { promptUser } = require('./promptUtils');
4
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
5
+ const escapeRegExp = require('@hubspot/cli-lib/lib/escapeRegExp');
6
+ const i18nKey = 'cli.lib.prompts.uploadPrompt';
7
+ const FIELDS_FILES = ['fields.json', 'fields.js', 'fields.cjs', 'fields.mjs'];
8
+
9
+ const fieldsJsPrompt = async (filePath, projectDir, skipFiles = []) => {
10
+ const dirName = path.dirname(filePath);
11
+
12
+ // Get a list of all field files in the directory, resolve their absolute path, and remove the ones that we already skipped.
13
+ const fileChoices = fs
14
+ .readdirSync(dirName)
15
+ .filter(file => FIELDS_FILES.includes(file))
16
+ .map(file => path.resolve(dirName, file))
17
+ .filter(file => !skipFiles.includes(file));
18
+
19
+ if (!fileChoices.length) return [filePath, []];
20
+ if (fileChoices.length == 1) return [fileChoices[0], []];
21
+
22
+ // We get the directory above the project one so that relative paths are printed with the root of the project dir attached.
23
+ projectDir = projectDir.substring(0, projectDir.lastIndexOf('/'));
24
+ const projectDirRegex = new RegExp(`^${escapeRegExp(projectDir)}`);
25
+ const fileDir = path.dirname(fileChoices[0]).replace(projectDirRegex, '');
26
+
27
+ const selection = [];
28
+ fileChoices.forEach(fileChoice => {
29
+ selection.push({
30
+ name: fileChoice.replace(projectDirRegex, ''),
31
+ value: fileChoice,
32
+ });
33
+ });
34
+ const promptVal = await promptUser([
35
+ {
36
+ message: i18n(`${i18nKey}.fieldsPrompt`, { dir: fileDir }),
37
+ type: 'list',
38
+ name: 'filePathChoice',
39
+ choices: selection,
40
+ },
41
+ ]);
42
+ const choice = promptVal.filePathChoice;
43
+
44
+ // Add the ones that were not picked to skip files array.
45
+ const notPicked = fileChoices.filter(item => item !== choice);
46
+ skipFiles.push(...notPicked);
47
+ return [choice, skipFiles];
48
+ };
49
+
50
+ module.exports = { fieldsJsPrompt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "4.0.1-beta.5",
3
+ "version": "4.0.2-beta.1",
4
4
  "description": "CLI for working with HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,8 +8,8 @@
8
8
  "url": "https://github.com/HubSpot/hubspot-cms-tools"
9
9
  },
10
10
  "dependencies": {
11
- "@hubspot/cli-lib": "4.0.1-beta.5",
12
- "@hubspot/serverless-dev-runtime": "4.0.1-beta.5",
11
+ "@hubspot/cli-lib": "4.0.2-beta.1",
12
+ "@hubspot/serverless-dev-runtime": "4.0.2-beta.1",
13
13
  "archiver": "^5.3.0",
14
14
  "chalk": "^4.1.2",
15
15
  "express": "^4.17.1",
@@ -37,5 +37,5 @@
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
- "gitHead": "af927b5e071f91db0dd415fca154a446f259fe3b"
40
+ "gitHead": "75ff004ab82bfdf35107d602530237b6dd37ebee"
41
41
  }