@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.
- package/commands/accounts/info.js +4 -2
- package/commands/project/create.js +3 -3
- package/commands/project/deploy.js +22 -26
- package/commands/project/download.js +57 -63
- package/commands/project/listBuilds.js +4 -1
- package/commands/project/logs.js +16 -6
- package/commands/project/upload.js +42 -43
- package/commands/project/watch.js +31 -24
- package/commands/sandbox/create.js +8 -6
- package/commands/sandbox/delete.js +15 -0
- package/commands/sandbox/sync.js +32 -4
- package/commands/theme/generate-selectors.js +214 -0
- package/commands/theme.js +6 -5
- package/lang/en.lyaml +35 -19
- package/lib/LocalDevManager.js +16 -5
- package/lib/generate-selectors.js +161 -0
- package/lib/projects.js +12 -5
- package/lib/prompts/createProjectPrompt.js +7 -5
- package/lib/prompts/downloadProjectPrompt.js +17 -2
- package/lib/prompts/projectsLogsPrompt.js +29 -13
- package/lib/sandbox-create.js +15 -0
- package/lib/sandbox-sync.js +18 -0
- package/package.json +4 -4
|
@@ -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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
package/lib/LocalDevManager.js
CHANGED
|
@@ -29,7 +29,8 @@ const { uiAccountDescription, uiLink } = require('./ui');
|
|
|
29
29
|
|
|
30
30
|
const i18nKey = 'cli.lib.LocalDevManager';
|
|
31
31
|
|
|
32
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
280
|
+
logApiErrorInstance(err, new ApiErrorContext({ accountId, projectName }));
|
|
281
|
+
process.exit(EXIT_CODES.ERROR);
|
|
275
282
|
}
|
|
276
283
|
};
|
|
277
284
|
|