@hubspot/cli 4.1.8-beta.5 → 4.1.8-beta.7

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,214 @@
1
+ const fs = require('fs');
2
+ const { i18n } = require('../../lib/lang');
3
+ const { logger } = require('@hubspot/cli-lib/logger');
4
+ const {
5
+ findFieldsJsonPath,
6
+ combineThemeCss,
7
+ setPreviewSelectors,
8
+ generateInheritedSelectors,
9
+ generateSelectorsMap,
10
+ getMaxFieldsDepth,
11
+ } = require('../../lib/generate-selectors');
12
+ const { EXIT_CODES } = require('../../lib/enums/exitCodes');
13
+
14
+ const HUBL_EXPRESSION_REGEX = new RegExp(/{%\s*(.*)\s*%}/, 'g');
15
+ const HUBL_VARIABLE_NAME_REGEX = new RegExp(/{%\s*set\s*(\w*)/, 'i');
16
+ const HUBL_STATEMENT_REGEX = new RegExp(/{{\s*[\w.(,\d\-\s)|/~]*.*}}/, 'g');
17
+ const HUBL_STATEMENT_PLACEHOLDER_REGEX = new RegExp(/hubl_statement_\d*/, 'g');
18
+
19
+ const CSS_VARS_REGEX = new RegExp(/--([\w.(,\d\-)]*):(.*);/, 'g');
20
+ const CSS_VARS_NAME_REGEX = new RegExp(/(--[\w.(,\d\-)]*)/, 'g');
21
+ const CSS_SELECTORS_REGEX = new RegExp(/([\s\w:.,\0-[\]]*){/, 'i');
22
+ const CSS_EXPRESSION_REGEX = new RegExp(/(?!\s)([^}])*(?![.#\s,>])[^}]*}/, 'g');
23
+ const THEME_PATH_REGEX = new RegExp(/=\s*.*(theme\.(\w|\.)*)/, 'i');
24
+
25
+ const i18nKey = 'cli.commands.theme.subcommands.generateSelectors';
26
+
27
+ exports.command = 'generate-selectors <themePath>';
28
+ exports.describe = i18n(`${i18nKey}.describe`);
29
+
30
+ exports.handler = options => {
31
+ const { themePath } = options;
32
+
33
+ const fieldsJsonPath = findFieldsJsonPath(themePath);
34
+ if (!fieldsJsonPath) {
35
+ logger.error(i18n(`${i18nKey}.errors.fieldsNotFound`));
36
+ process.exit(EXIT_CODES.ERROR);
37
+ }
38
+
39
+ let fieldsJson = JSON.parse(fs.readFileSync(fieldsJsonPath));
40
+ let cssString = combineThemeCss(themePath);
41
+
42
+ /**
43
+ * Creates map of HubL variable names to theme field paths
44
+ */
45
+ const HubLExpressions = cssString.match(HUBL_EXPRESSION_REGEX) || [];
46
+ const hublVariableMap = HubLExpressions.reduce(
47
+ (_hublVariableMap, expression) => {
48
+ const variableName = expression.match(HUBL_VARIABLE_NAME_REGEX);
49
+ const themeFieldKey = expression.match(THEME_PATH_REGEX);
50
+
51
+ if (!themeFieldKey || !variableName) return _hublVariableMap;
52
+
53
+ _hublVariableMap[variableName[1]] = themeFieldKey[1];
54
+ return _hublVariableMap;
55
+ },
56
+ {}
57
+ );
58
+
59
+ /**
60
+ * Removes HubL variable expressions
61
+ */
62
+ cssString = cssString.replace(HUBL_EXPRESSION_REGEX, '');
63
+
64
+ /**
65
+ * Regex for HubL variable names
66
+ */
67
+ const HUBL_EXPRESSIONS = new RegExp(
68
+ `.*(${Object.keys(hublVariableMap).join('|')}).*`,
69
+ 'g'
70
+ );
71
+
72
+ /**
73
+ * Matches all HubL statements in the CSS and replaces them with a placeholder string
74
+ * This is to prevent the the css expression regex from capturing all the HubL as well
75
+ */
76
+ const hublStatements = cssString.match(HUBL_STATEMENT_REGEX) || [];
77
+ const hublStatementsMap = {};
78
+ hublStatements.forEach((statement, index) => {
79
+ const statementKey = `hubl_statement_${index}`;
80
+ hublStatementsMap[statementKey] = statement;
81
+ cssString = cssString.replace(statement, statementKey);
82
+ });
83
+
84
+ /**
85
+ * Matchs all css variables and determines if there are hubl within those vars.
86
+ */
87
+ const cssVars = cssString.match(CSS_VARS_REGEX) || [];
88
+ const cssVarsMap = cssVars.reduce((acc, expression) => {
89
+ const cssVarName = expression.match(CSS_VARS_NAME_REGEX);
90
+ const hublVariables = expression.match(HUBL_STATEMENT_PLACEHOLDER_REGEX);
91
+
92
+ if (!cssVarName || !hublVariables) return acc;
93
+
94
+ cssString = cssString.replace(expression, '');
95
+ return { ...acc, [cssVarName[0]]: hublVariables };
96
+ }, {});
97
+
98
+ // replace all css variable references with corresponding hubl placeholder
99
+ Object.keys(cssVarsMap).forEach(cssVarName => {
100
+ const hublPlaceholders = cssVarsMap[cssVarName];
101
+ cssString = cssString.replace(cssVarName, hublPlaceholders.join(' '));
102
+ });
103
+
104
+ /**
105
+ * Parses each css string for a HubL statement and tries to map theme field paths to CSS selectors
106
+ */
107
+ const cssExpressions = (
108
+ cssString.match(CSS_EXPRESSION_REGEX) || []
109
+ ).map(exp => exp.replace(/\r?\n/g, ' '));
110
+
111
+ const finalMap = cssExpressions.reduce(
112
+ (themeFieldsSelectorMap, cssExpression) => {
113
+ const hublStatementsPlaceholderKey =
114
+ cssExpression.match(HUBL_STATEMENT_PLACEHOLDER_REGEX) || [];
115
+
116
+ hublStatementsPlaceholderKey.forEach(placeholderKey => {
117
+ const hublStatement = hublStatementsMap[placeholderKey].match(
118
+ HUBL_EXPRESSIONS
119
+ );
120
+ const themeFieldPath = hublStatementsMap[placeholderKey].match(
121
+ /theme\.[\w|.]*/,
122
+ 'g'
123
+ );
124
+ const cssSelectors = cssExpression.match(CSS_SELECTORS_REGEX);
125
+
126
+ /**
127
+ * Try to match a HubL statement to any HubL Variables being used
128
+ */
129
+ if (cssSelectors && themeFieldPath) {
130
+ const cssSelector = cssSelectors[1].replace(/\n/g, ' ');
131
+ const hublThemePath = themeFieldPath[0];
132
+
133
+ if (!themeFieldsSelectorMap[hublThemePath]) {
134
+ themeFieldsSelectorMap[hublThemePath] = [];
135
+ }
136
+
137
+ if (!themeFieldsSelectorMap[hublThemePath].includes(cssSelector)) {
138
+ themeFieldsSelectorMap[hublThemePath] = themeFieldsSelectorMap[
139
+ hublThemePath
140
+ ].concat(cssSelector);
141
+ }
142
+ }
143
+
144
+ if (cssSelectors && hublStatement) {
145
+ const cssSelector = cssSelectors[1].replace(/\n/g, ' ');
146
+ const hublVariableName = Object.keys(hublVariableMap).find(_hubl => {
147
+ return hublStatement[0].includes(_hubl);
148
+ });
149
+
150
+ const themeFieldKey = hublVariableMap[hublVariableName];
151
+
152
+ /**
153
+ * If the theme path is referenced directly add selectors
154
+ */
155
+ if (themeFieldKey) {
156
+ if (!themeFieldsSelectorMap[themeFieldKey]) {
157
+ themeFieldsSelectorMap[themeFieldKey] = [];
158
+ }
159
+
160
+ if (!themeFieldsSelectorMap[themeFieldKey].includes(cssSelector)) {
161
+ themeFieldsSelectorMap[themeFieldKey] = themeFieldsSelectorMap[
162
+ themeFieldKey
163
+ ].concat(cssSelector);
164
+ }
165
+ }
166
+ }
167
+ });
168
+
169
+ return themeFieldsSelectorMap;
170
+ },
171
+ {}
172
+ );
173
+
174
+ if (!Object.keys(finalMap).length) {
175
+ logger.error(i18n(`${i18nKey}.errors.noSelectorsFound`));
176
+ process.exit(EXIT_CODES.ERROR);
177
+ }
178
+ Object.keys(finalMap).forEach(themeFieldKey => {
179
+ const fieldKey = themeFieldKey.split('.');
180
+ const selectors = finalMap[themeFieldKey];
181
+ fieldsJson = setPreviewSelectors(fieldsJson, fieldKey.splice(1), selectors);
182
+ });
183
+
184
+ // Because fields can have nested inheritance we generated inherited selectors
185
+ // multiple times to make sure all inherted selectors are bubbled up.
186
+ const maxFieldsDepth = getMaxFieldsDepth();
187
+ for (let i = 0; i < maxFieldsDepth; i += 1) {
188
+ fieldsJson = generateInheritedSelectors(fieldsJson, fieldsJson);
189
+ }
190
+
191
+ const selectorsMap = generateSelectorsMap(fieldsJson);
192
+ const selectorsPath = `${themePath}/editor-preview.json`;
193
+
194
+ fs.writeFileSync(
195
+ selectorsPath,
196
+ `${JSON.stringify({ selectors: selectorsMap }, null, 2)}\n`
197
+ );
198
+
199
+ logger.success(
200
+ i18n(`${i18nKey}.success`, {
201
+ themePath,
202
+ selectorsPath,
203
+ })
204
+ );
205
+ };
206
+
207
+ exports.builder = yargs => {
208
+ yargs.positional('themePath', {
209
+ describe: i18n(`${i18nKey}.positionals.themePath.describe`),
210
+ type: 'string',
211
+ });
212
+
213
+ return yargs;
214
+ };
package/commands/theme.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const marketplaceValidate = require('./theme/marketplace-validate');
2
- const { addConfigOptions, addAccountOptions } = require('../lib/commonOpts');
2
+ const generateSelectors = require('./theme/generate-selectors');
3
+
3
4
  const { i18n } = require('../lib/lang');
4
5
 
5
6
  const i18nKey = 'cli.commands.theme';
@@ -8,10 +9,10 @@ exports.command = 'theme';
8
9
  exports.describe = i18n(`${i18nKey}.describe`);
9
10
 
10
11
  exports.builder = yargs => {
11
- addConfigOptions(yargs, true);
12
- addAccountOptions(yargs, true);
13
-
14
- yargs.command(marketplaceValidate).demandCommand(1, '');
12
+ yargs
13
+ .command(marketplaceValidate)
14
+ .command(generateSelectors)
15
+ .demandCommand(1, '');
15
16
 
16
17
  return yargs;
17
18
  };
package/lang/en.lyaml CHANGED
@@ -8,10 +8,10 @@ en:
8
8
  describe: "Commands for working with accounts."
9
9
  subcommands:
10
10
  list:
11
- accounts: "Accounts:"
12
- defaultAccount: "Default account: {{ account }}"
11
+ accounts: "{{#bold}}Accounts{{/bold}}:"
12
+ defaultAccount: "{{#bold}}Default account{{/bold}}: {{ account }}"
13
13
  describe: "List names of accounts defined in config."
14
- configPath: "Config path: {{ configPath }}"
14
+ configPath: "{{#bold}}Config path{{/bold}}: {{ configPath }}"
15
15
  labels:
16
16
  accountId: "Account ID"
17
17
  authType: "Auth Type"
@@ -57,7 +57,7 @@ en:
57
57
  success:
58
58
  accountRemoved: "Account \"{{ accountName }}\" removed from the config"
59
59
  info:
60
- accountId: "Account ID: {{ accountId }}"
60
+ accountId: "{{#bold}}Account ID{{/bold}}: {{ accountId }}"
61
61
  describe: "Print information about the default account, or about the account specified with the \"--account\" option."
62
62
  errors:
63
63
  notUsingPersonalAccessKey: "This command currently only supports fetching scopes for the personal access key auth type."
@@ -65,8 +65,8 @@ en:
65
65
  default: "Print information for the default account"
66
66
  idBased: "Print information for the account with accountId equal to \"1234567\""
67
67
  nameBased: "Print information for the account in the config with name equal to \"MyAccount\""
68
- name: "Account name: {{ name }}"
69
- scopeGroups: "Scopes available:\n{{ scopeGroups }}"
68
+ name: "{{#bold}}Account name{{/bold}}: {{ name }}"
69
+ scopeGroups: "{{#bold}}Scopes available{{/bold}}:"
70
70
  auth:
71
71
  describe: "Configure authentication for a HubSpot account. Supported authentication protocols are {{ supportedProtocols }}."
72
72
  errors:
@@ -484,7 +484,7 @@ en:
484
484
  describe: "Project name (cannot be changed)"
485
485
  template:
486
486
  describe: "The starting template"
487
- repoPath:
487
+ templateSource:
488
488
  describe: "Path to custom GitHub repository from which to create project template"
489
489
  add:
490
490
  describe: "Create a new component within a project"
@@ -662,10 +662,11 @@ en:
662
662
  deleteDefault: "Sandbox \"{{ account }}\" with portalId \"{{ sandboxHubId }}\" was deleted successfully and removed as the default account."
663
663
  configFileUpdated: "Removed account {{ account }} from {{ configFilename }}."
664
664
  failure:
665
+ invalidUser: "Couldn't delete {{ accountName }} because your account has been removed from {{ parentAccountName }} or your permission set doesn't allow you to delete the sandbox. To update your permissions, contact a super admin in {{ parentAccountName }}."
665
666
  noAccount: "No account specified. Specify an account by using the --account flag."
666
667
  noSandboxAccounts: "There are no sandboxes connected to the CLI. To add a sandbox, run {{#bold}}hs auth{{/bold}}."
667
668
  noParentAccount: "This sandbox can't be deleted from the CLI because you haven't given the CLI access to its parent account. To do this, run {{#bold}}hs auth{{/bold}} and add the parent account."
668
- objectNotFound: "Sandbox {{#bold}}{{ account }}{{/bold}} may have been be deleted through the UI. The account has been removed from the config."
669
+ objectNotFound: "Sandbox {{#bold}}{{ account }}{{/bold}} may have been deleted through the UI. The account has been removed from the config."
669
670
  noParentPortalAvailable: "This sandbox can't be deleted from the CLI because you haven't given the CLI access to its parent account. To do this, run {{#bold}}{{ command }}{{/bold}}. You can also delete the sandbox from the HubSpot management tool: {{#bold}}{{ url }}{{/bold}}."
670
671
  invalidKey: "Your personal access key for account {{#bold}}{{ account }}{{/bold}} is inactive. To re-authenticate, please run {{#bold}}hs auth personalaccesskey{{/bold}}."
671
672
  options:
@@ -734,6 +735,16 @@ en:
734
735
  theme:
735
736
  describe: "Commands for working with themes, including marketplace validation with the marketplace-validate subcommand."
736
737
  subcommands:
738
+ generateSelectors:
739
+ describe: "Automatically generates an editor-preview.json file for the given theme. The selectors this command generates are not perfect, so please edit editor-preview.json after running."
740
+ errors:
741
+ invalidPath: "Could not find directory \"{{ themePath }}\""
742
+ fieldsNotFound: "Unable to find theme's fields.json."
743
+ noSelectorsFound: "No selectors found."
744
+ success: "Selectors generated for {{ themePath }}, please double check the selectors generated at {{ selectorsPath }} before uploading the theme."
745
+ positionals:
746
+ themePath:
747
+ describe: "The path of the theme you'd like to generate an editor-preview.json for."
737
748
  marketplaceValidate:
738
749
  describe: "Validate a theme for the marketplace"
739
750
  errors:
@@ -847,7 +858,7 @@ en:
847
858
  header:
848
859
  betaMessage: "{{#yellow}}{{#bold}}[beta]{{/bold}}{{/yellow}} HubSpot projects local development"
849
860
  learnMoreLink: "Learn more about the projects local dev server"
850
- running: "Running {{ projectName}} locally on {{ accountIdentifier }}, waiting for project file changes ..."
861
+ running: "Running {{ projectName}} locally on {{ accountIdentifier }}, waiting for changes ..."
851
862
  quitHelper: "Press {{#bold}}'q'{{/bold}} to stop the local dev server"
852
863
  viewInHubSpotLink: "View in HubSpot"
853
864
  status:
@@ -859,8 +870,8 @@ en:
859
870
  manualUpload: "{{#bold}}Status:{{/bold}} {{#green}}Manually uploading pending changes{{/green}}"
860
871
  upload:
861
872
  noUploadsAllowed: "The change to {{ filePath }} requires an upload, but the CLI cannot upload to a project that is using a github integration."
862
- manualUploadSkipped: "Manually upload and deploy project: {{#green}}(n){{/green}}"
863
- manualUploadConfirmed: "Manually upload and deploy project: {{#green}}(Y){{/green}}"
873
+ manualUploadSkipped: "Manually upload and deploy project to production account: {{#green}}(n){{/green}}"
874
+ manualUploadConfirmed: "Manually upload and deploy project to production account: {{#green}}(Y){{/green}}"
864
875
  manualUploadRequired: "Project file changes require a manual upload and deploy ..."
865
876
  manualUploadExplanation1: "{{#yellow}}> Dev server is running on a {{#bold}}non-sandbox account{{/bold}}.{{/yellow}}"
866
877
  manualUploadExplanation2: "{{#yellow}}> Uploading changes may overwrite production data.{{/yellow}}"
@@ -869,9 +880,9 @@ en:
869
880
  uploadedAddChange: "[INFO] Uploaded {{ filePath }}"
870
881
  uploadingRemoveChange: "[INFO] Removing {{ filePath }}"
871
882
  uploadedRemoveChange: "[INFO] Removed {{ filePath }}"
872
- uploadingChanges: "{{#bold}}Building and deploying recent changes on {{ accountIdentifier }}{{/bold}}"
873
- uploadedChangesSucceeded: "{{#bold}}Built and deployed recent changes on {{ accountIdentifier }}{{/bold}}"
874
- uploadedChangesFailed: "{{#bold}}Failed to build and deploy recent changes on {{ accountIdentifier }}{{/bold}}"
883
+ uploadingChanges: "{{#bold}}Building #{{ buildId }} and deploying recent changes on {{ accountIdentifier }}{{/bold}}"
884
+ uploadedChangesSucceeded: "{{#bold}}Built #{{ buildId }} and deployed recent changes on {{ accountIdentifier }}{{/bold}}"
885
+ uploadedChangesFailed: "{{#bold}}Failed to build #{{ buildId }} and deploy recent changes on {{ accountIdentifier }}{{/bold}}"
875
886
  projects:
876
887
  uploadProjectFiles:
877
888
  add: "Uploading {{#bold}}{{ projectName }}{{/bold}} project files to {{ accountIdentifier }}"
@@ -922,6 +933,9 @@ en:
922
933
  helpCommand:
923
934
  command: "hs help"
924
935
  message: "Run {{ command }} to see a list of available commands"
936
+ projectCreateCommand:
937
+ command: "hs project create"
938
+ message: "Run {{ command }} to create a new project"
925
939
  projectDeployCommand:
926
940
  command: "hs project deploy"
927
941
  message: "Ready to take your project live? Run {{ command }}"
@@ -933,13 +947,13 @@ en:
933
947
  message: "Run {{ command }} to upload your project to HubSpot and trigger builds"
934
948
  projectDevCommand:
935
949
  command: "hs project dev"
936
- message: "Run {{ command }} to start testing your project locally."
950
+ message: "Run {{ command }} to start testing your project locally"
937
951
  sandboxSyncDevelopmentCommand:
938
952
  command: "hs sandbox sync"
939
- message: "Run {{ command }} to to update CRM object definitions in your sandbox."
953
+ message: "Run {{ command }} to to update CRM object definitions in your sandbox"
940
954
  sandboxSyncStandardCommand:
941
955
  command: "hs sandbox sync"
942
- message: "Run {{ command }} to to update all supported assets to your sandbox from production."
956
+ message: "Run {{ command }} to to update all supported assets to your sandbox from production"
943
957
  commonOpts:
944
958
  options:
945
959
  portal:
@@ -959,7 +973,7 @@ en:
959
973
  describe: "Use environment variable config"
960
974
  prompts:
961
975
  projectDevTargetAccountPrompt:
962
- createNewSandboxOption: "<Test on a new dev sandbox>"
976
+ createNewSandboxOption: "<Test on a new development sandbox>"
963
977
  chooseDefaultAccountOption: "<{{#bold}}!{{/bold}} Test on this production account {{#bold}}!{{/bold}}>"
964
978
  promptMessage: "[--account] Choose a sandbox under {{ accountIdentifier }} to test with:"
965
979
  sandboxLimit: "You’ve reached the limit of {{ limit }} development sandboxes"
@@ -1115,6 +1129,7 @@ en:
1115
1129
  success:
1116
1130
  configFileUpdated: "{{ configFilename }} updated with {{ authMethod }} for account {{ account }}."
1117
1131
  failure:
1132
+ invalidUser: "Couldn't create {{#bold}}{{ accountName }}{{/bold}} because your account has been removed from {{#bold}}{{ parentAccountName }}{{/bold}} or your permission set doesn't allow you to create the sandbox. To update your permissions, contact a super admin in {{#bold}}{{ parentAccountName }}{{/bold}}."
1118
1133
  limit:
1119
1134
  developer:
1120
1135
  one: "{{#bold}}{{ accountName }}{{/bold}} reached the limit of {{ limit }} development sandbox.
@@ -1172,10 +1187,11 @@ en:
1172
1187
  fail: "Failed to fetch sync updates. View the sync status at: {{ url }}"
1173
1188
  succeed: "Sandbox sync complete."
1174
1189
  failure:
1190
+ invalidUser: "Couldn't sync {{ accountName }} because your account has been removed from {{ parentAccountName }} or your permission set doesn't allow you to sync the sandbox. To update your permissions, contact a super admin in {{ parentAccountName }}."
1175
1191
  missingScopes: "Couldn’t run the sync because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{#bold}}{{ accountName }}{{/bold}}, and generate a new one. Then run `hs auth` to update the CLI with the new key."
1176
1192
  syncInProgress: "Couldn’t run the sync because there’s another sync in progress. Wait for the current sync to finish and then try again. To check the sync status, visit the sync activity log: {{ url }}."
1177
1193
  notSuperAdmin: "Couldn't run the sync because you are not a super admin in {{ account }}. Ask the account owner for super admin access to the sandbox."
1178
- objectNotFound: "Couldn't sync the sandbox because {{#bold}}{{ account }}{{/bold}} may have been be deleted through the UI. Run {{#bold}}hs sandbox delete{{/bold}} to remove this account from the config. "
1194
+ objectNotFound: "Couldn't sync the sandbox because {{#bold}}{{ account }}{{/bold}} may have been deleted through the UI. Run {{#bold}}hs sandbox delete{{/bold}} to remove this account from the config. "
1179
1195
  types:
1180
1196
  parcels:
1181
1197
  label: "Account tools and features"
@@ -29,7 +29,8 @@ const { uiAccountDescription, uiLink } = require('./ui');
29
29
 
30
30
  const i18nKey = 'cli.lib.LocalDevManager';
31
31
 
32
- const BUILD_DEBOUNCE_TIME = 3500;
32
+ const BUILD_DEBOUNCE_TIME_LONG = 5000;
33
+ const BUILD_DEBOUNCE_TIME_SHORT = 3500;
33
34
 
34
35
  const WATCH_EVENTS = {
35
36
  add: 'add',
@@ -335,7 +336,7 @@ class LocalDevManager {
335
336
  await this.flushStandbyChanges();
336
337
 
337
338
  if (!this.uploadQueue.isPaused) {
338
- this.debounceQueueBuild();
339
+ this.debounceQueueBuild(changeInfo);
339
340
  }
340
341
 
341
342
  return this.uploadQueue.add(async () => {
@@ -463,7 +464,9 @@ class LocalDevManager {
463
464
  }
464
465
  }
465
466
 
466
- debounceQueueBuild() {
467
+ debounceQueueBuild(changeInfo) {
468
+ const { event } = changeInfo;
469
+
467
470
  if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
468
471
  this.updateDevModeStatus('uploadPending');
469
472
  }
@@ -472,9 +475,14 @@ class LocalDevManager {
472
475
  clearTimeout(this.debouncedBuild);
473
476
  }
474
477
 
478
+ const debounceWaitTime =
479
+ event === WATCH_EVENTS.add
480
+ ? BUILD_DEBOUNCE_TIME_LONG
481
+ : BUILD_DEBOUNCE_TIME_SHORT;
482
+
475
483
  this.debouncedBuild = setTimeout(
476
484
  this.queueBuild.bind(this),
477
- BUILD_DEBOUNCE_TIME
485
+ debounceWaitTime
478
486
  );
479
487
  }
480
488
 
@@ -482,6 +490,7 @@ class LocalDevManager {
482
490
  const spinniesKey = this.spinnies.add(null, {
483
491
  text: i18n(`${i18nKey}.upload.uploadingChanges`, {
484
492
  accountIdentifier: uiAccountDescription(this.targetAccountId),
493
+ buildId: this.currentStagedBuildId,
485
494
  }),
486
495
  noIndent: true,
487
496
  });
@@ -523,6 +532,7 @@ class LocalDevManager {
523
532
  this.spinnies.succeed(spinniesKey, {
524
533
  text: i18n(`${i18nKey}.upload.uploadedChangesSucceeded`, {
525
534
  accountIdentifier: uiAccountDescription(this.targetAccountId),
535
+ buildId: result.buildId,
526
536
  }),
527
537
  succeedColor: 'white',
528
538
  noIndent: true,
@@ -531,6 +541,7 @@ class LocalDevManager {
531
541
  this.spinnies.fail(spinniesKey, {
532
542
  text: i18n(`${i18nKey}.upload.uploadedChangesFailed`, {
533
543
  accountIdentifier: uiAccountDescription(this.targetAccountId),
544
+ buildId: result.buildId,
534
545
  }),
535
546
  failColor: 'white',
536
547
  noIndent: true,
@@ -561,7 +572,7 @@ class LocalDevManager {
561
572
  this.uploadPermission === UPLOAD_PERMISSIONS.always &&
562
573
  !this.uploadQueue.isPaused
563
574
  ) {
564
- this.debounceQueueBuild();
575
+ this.debounceQueueBuild(changeInfo);
565
576
  }
566
577
  await this.sendChanges(changeInfo);
567
578
  };
@@ -0,0 +1,161 @@
1
+ const fs = require('fs');
2
+ const { EXIT_CODES } = require('./enums/exitCodes');
3
+ const { logger } = require('@hubspot/cli-lib/logger');
4
+ const { i18n } = require('../lib/lang');
5
+
6
+ const CSS_COMMENTS_REGEX = new RegExp(/\/\*.*\*\//, 'g');
7
+ const CSS_PSEUDO_CLASS_REGEX = new RegExp(
8
+ /:active|:checked|:disabled|:empty|:enabled|:first-of-type|:focus|:hover|:in-range|:invalid|:link|:optional|:out-of-range|:read-only|:read-write|:required|:target|:valid|:visited/,
9
+ 'g'
10
+ );
11
+ const i18nKey = 'cli.commands.theme.subcommands.generateSelectors';
12
+
13
+ let maxFieldsDepth = 0;
14
+
15
+ function getMaxFieldsDepth() {
16
+ return maxFieldsDepth;
17
+ }
18
+
19
+ function findFieldsJsonPath(basePath) {
20
+ const _path = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
21
+ if (!fs.existsSync(_path)) {
22
+ logger.error(
23
+ i18n(`${i18nKey}.errors.invalidPath`, {
24
+ themePath: basePath,
25
+ })
26
+ );
27
+ process.exit(EXIT_CODES.ERROR);
28
+ }
29
+ const files = fs.readdirSync(_path);
30
+
31
+ if (files.includes('fields.json')) {
32
+ return `${_path}/fields.json`;
33
+ }
34
+
35
+ for (let i = 0; i < files.length; i++) {
36
+ const fileName = files[i];
37
+ const isDirectory = fs.lstatSync(`${_path}/${fileName}`).isDirectory();
38
+
39
+ if (isDirectory && !fileName.includes('.module')) {
40
+ const fieldsJsonPath = findFieldsJsonPath(`${_path}/${fileName}`);
41
+ if (fieldsJsonPath) return fieldsJsonPath;
42
+ }
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ function combineThemeCss(basePath, cssString = '') {
49
+ const _path = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
50
+ const isDirectory = fs.lstatSync(_path).isDirectory();
51
+
52
+ if (isDirectory) {
53
+ const filesList = fs.readdirSync(_path);
54
+ return filesList.reduce((css, fileName) => {
55
+ const newCss = combineThemeCss(`${_path}/${fileName}`);
56
+ return newCss ? `${css}\n${newCss}` : css;
57
+ }, cssString);
58
+ } else if (_path.includes('.css') && !_path.includes('.module')) {
59
+ return `${cssString}\n${fs.readFileSync(_path, 'utf8')}`;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function setPreviewSelectors(fields, fieldPath, selectors, depth = 0) {
66
+ fields.forEach((field, index) => {
67
+ if (field.name === fieldPath[0]) {
68
+ if (field.children && fieldPath.length > 0) {
69
+ fields[index].children = setPreviewSelectors(
70
+ fields[index].children,
71
+ fieldPath.splice(1),
72
+ selectors,
73
+ (depth += 1)
74
+ );
75
+ } else {
76
+ if (!field.selectors) field.selectors = [];
77
+
78
+ if (depth > maxFieldsDepth) {
79
+ maxFieldsDepth = depth;
80
+ }
81
+
82
+ selectors.forEach(selector => {
83
+ const fieldSelectors = field.selectors;
84
+ selector = selector.replace(CSS_COMMENTS_REGEX, '');
85
+ selector = selector.replace(CSS_PSEUDO_CLASS_REGEX, '').trim();
86
+
87
+ if (
88
+ !fieldSelectors.includes(selector) &&
89
+ !selector.includes('@media')
90
+ ) {
91
+ field.selectors = fieldSelectors.concat(selector);
92
+ }
93
+ });
94
+ }
95
+ }
96
+ });
97
+
98
+ return fields;
99
+ }
100
+
101
+ function generateInheritedSelectors(fields) {
102
+ let finalFieldsJson = [...fields];
103
+
104
+ const _generateInheritedSelectors = fieldsToCheck => {
105
+ fieldsToCheck.forEach(field => {
106
+ if (field.children) {
107
+ _generateInheritedSelectors(field.children);
108
+ }
109
+
110
+ const fieldInheritance =
111
+ field.inherited_value && field.inherited_value.property_value_paths;
112
+ const fieldSelectors = field.selectors;
113
+
114
+ if (fieldSelectors && fieldInheritance) {
115
+ Object.values(fieldInheritance).forEach(path => {
116
+ const fieldPath = path.split('.');
117
+ if (fieldPath[0] === 'theme') {
118
+ finalFieldsJson = setPreviewSelectors(
119
+ finalFieldsJson,
120
+ fieldPath.splice(1),
121
+ fieldSelectors
122
+ );
123
+ }
124
+ });
125
+ }
126
+ });
127
+ };
128
+
129
+ _generateInheritedSelectors(fields);
130
+
131
+ return finalFieldsJson;
132
+ }
133
+
134
+ function generateSelectorsMap(fields, fieldKey = []) {
135
+ let selectorsMap = {};
136
+
137
+ fields.forEach(field => {
138
+ const { children, name, selectors } = field;
139
+ const _fieldKey = [...fieldKey, name];
140
+
141
+ if (field.children) {
142
+ selectorsMap = {
143
+ ...selectorsMap,
144
+ ...generateSelectorsMap(children, _fieldKey),
145
+ };
146
+ } else {
147
+ selectorsMap[_fieldKey.join('.')] = selectors;
148
+ }
149
+ });
150
+
151
+ return selectorsMap;
152
+ }
153
+
154
+ module.exports = {
155
+ findFieldsJsonPath,
156
+ combineThemeCss,
157
+ setPreviewSelectors,
158
+ generateInheritedSelectors,
159
+ generateSelectorsMap,
160
+ getMaxFieldsDepth,
161
+ };
package/lib/projects.js CHANGED
@@ -92,7 +92,7 @@ const createProjectConfig = async (
92
92
  projectPath,
93
93
  projectName,
94
94
  template,
95
- repoPath
95
+ templateSource
96
96
  ) => {
97
97
  const { projectConfig, projectDir } = await getProjectConfig(projectPath);
98
98
 
@@ -137,7 +137,11 @@ const createProjectConfig = async (
137
137
  srcDir: 'src',
138
138
  });
139
139
  } else {
140
- await downloadGitHubRepoContents(repoPath, template.path, projectPath);
140
+ await downloadGitHubRepoContents(
141
+ templateSource,
142
+ template.path,
143
+ projectPath
144
+ );
141
145
  const _config = JSON.parse(fs.readFileSync(projectConfigPath));
142
146
  writeProjectConfig(projectConfigPath, {
143
147
  ..._config,
@@ -256,7 +260,10 @@ const ensureProjectExists = async (
256
260
  );
257
261
  return true;
258
262
  } catch (err) {
259
- return logApiErrorInstance(err, new ApiErrorContext({ accountId }));
263
+ return logApiErrorInstance(
264
+ err,
265
+ new ApiErrorContext({ accountId, projectName })
266
+ );
260
267
  }
261
268
  } else {
262
269
  if (!noLogs) {
@@ -270,8 +277,8 @@ const ensureProjectExists = async (
270
277
  return false;
271
278
  }
272
279
  }
273
- logApiErrorInstance(err, new ApiErrorContext({ accountId }));
274
- return false;
280
+ logApiErrorInstance(err, new ApiErrorContext({ accountId, projectName }));
281
+ process.exit(EXIT_CODES.ERROR);
275
282
  }
276
283
  };
277
284