@hubspot/cli 4.0.2-beta.7 → 4.1.0
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/theme/marketplace-validate.js +97 -59
- package/lib/validators/__tests__/validatorTestUtils.js +0 -28
- package/lib/validators/applyValidators.js +1 -17
- package/lib/validators/constants.js +0 -5
- package/lib/validators/index.js +0 -13
- package/package.json +4 -4
- package/lib/validators/__tests__/AbsoluteValidator.js +0 -28
- package/lib/validators/__tests__/TemplateValidator.js +0 -101
- package/lib/validators/__tests__/ThemeConfigValidator.js +0 -70
- package/lib/validators/__tests__/ThemeDependencyValidator.js +0 -89
- package/lib/validators/__tests__/ThemeModuleValidator.js +0 -84
- package/lib/validators/marketplaceValidators/AbsoluteValidator.js +0 -50
- package/lib/validators/marketplaceValidators/theme/SectionValidator.js +0 -96
- package/lib/validators/marketplaceValidators/theme/TemplateValidator.js +0 -175
- package/lib/validators/marketplaceValidators/theme/ThemeConfigValidator.js +0 -106
- package/lib/validators/marketplaceValidators/theme/ThemeDependencyValidator.js +0 -126
- package/lib/validators/marketplaceValidators/theme/ThemeModuleValidator.js +0 -117
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const Spinnies = require('spinnies');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
3
|
|
|
4
|
-
const { getCwd } = require('@hubspot/cli-lib/path');
|
|
5
4
|
const { logger } = require('@hubspot/cli-lib/logger');
|
|
6
|
-
const { walk } = require('@hubspot/cli-lib');
|
|
7
5
|
|
|
8
6
|
const {
|
|
9
7
|
addConfigOptions,
|
|
@@ -14,17 +12,16 @@ const {
|
|
|
14
12
|
const { loadAndValidateOptions } = require('../../lib/validation');
|
|
15
13
|
const { trackCommandUsage } = require('../../lib/usageTracking');
|
|
16
14
|
const {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const MARKETPLACE_VALIDATORS = require('../../lib/validators');
|
|
23
|
-
const { VALIDATION_RESULT } = require('../../lib/validators/constants');
|
|
15
|
+
requestValidation,
|
|
16
|
+
getValidationStatus,
|
|
17
|
+
getValidationResults,
|
|
18
|
+
} = require('@hubspot/cli-lib/api/marketplaceValidation');
|
|
19
|
+
|
|
24
20
|
const { i18n } = require('@hubspot/cli-lib/lib/lang');
|
|
25
21
|
|
|
26
22
|
const i18nKey = 'cli.commands.theme.subcommands.marketplaceValidate';
|
|
27
23
|
const { EXIT_CODES } = require('../../lib/enums/exitCodes');
|
|
24
|
+
const SLEEP_TIME = 2000;
|
|
28
25
|
|
|
29
26
|
exports.command = 'marketplace-validate <src>';
|
|
30
27
|
exports.describe = i18n(`${i18nKey}.describe`);
|
|
@@ -35,54 +32,101 @@ exports.handler = async options => {
|
|
|
35
32
|
await loadAndValidateOptions(options);
|
|
36
33
|
|
|
37
34
|
const accountId = getAccountId(options);
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
|
|
36
|
+
trackCommandUsage('validate', null, accountId);
|
|
37
|
+
|
|
38
|
+
const spinnies = new Spinnies();
|
|
39
|
+
|
|
40
|
+
spinnies.add('marketplaceValidation', {
|
|
41
|
+
text: i18n(`${i18nKey}.logs.validatingTheme`, {
|
|
42
|
+
path: src,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Kick off validation
|
|
47
|
+
let requestResult;
|
|
48
|
+
const assetType = 'THEME';
|
|
49
|
+
const requestGroup = 'EXTERNAL_DEVELOPER';
|
|
40
50
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
requestResult = await requestValidation(accountId, {
|
|
52
|
+
path: src,
|
|
53
|
+
assetType,
|
|
54
|
+
requestGroup,
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.debug(err);
|
|
58
|
+
process.exit(EXIT_CODES.ERROR);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Poll till validation is finished
|
|
62
|
+
try {
|
|
63
|
+
const checkValidationStatus = async () => {
|
|
64
|
+
const validationStatus = await getValidationStatus(accountId, {
|
|
65
|
+
validationId: requestResult,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (validationStatus === 'REQUESTED') {
|
|
69
|
+
await new Promise(resolve => setTimeout(resolve, SLEEP_TIME));
|
|
70
|
+
await checkValidationStatus();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await checkValidationStatus();
|
|
75
|
+
|
|
76
|
+
spinnies.remove('marketplaceValidation');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger.debug(err);
|
|
79
|
+
process.exit(EXIT_CODES.ERROR);
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
82
|
+
// Fetch the validation results
|
|
83
|
+
let validationResults;
|
|
84
|
+
try {
|
|
85
|
+
validationResults = await getValidationResults(accountId, {
|
|
86
|
+
validationId: requestResult,
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logger.debug(err);
|
|
90
|
+
process.exit(EXIT_CODES.ERROR);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (validationResults.errors.length) {
|
|
94
|
+
const { errors } = validationResults;
|
|
95
|
+
|
|
96
|
+
errors.forEach(err => {
|
|
97
|
+
logger.error(`${err.context}`);
|
|
98
|
+
});
|
|
99
|
+
process.exit(EXIT_CODES.ERROR);
|
|
65
100
|
}
|
|
66
|
-
trackCommandUsage('validate', null, accountId);
|
|
67
101
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
const displayResults = checks => {
|
|
103
|
+
if (checks) {
|
|
104
|
+
const { status, results } = checks;
|
|
105
|
+
|
|
106
|
+
if (status === 'FAIL') {
|
|
107
|
+
const failedValidations = results.filter(
|
|
108
|
+
test => test.status === 'FAIL'
|
|
109
|
+
);
|
|
110
|
+
failedValidations.forEach(val => {
|
|
111
|
+
logger.error(`${val.message}`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (status === 'PASS') {
|
|
116
|
+
logger.success(i18n(`${i18nKey}.results.noErrors`));
|
|
117
|
+
}
|
|
84
118
|
}
|
|
85
|
-
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
logger.log(chalk.bold(i18n(`${i18nKey}.results.required`)));
|
|
123
|
+
displayResults(validationResults.results['REQUIRED']);
|
|
124
|
+
logger.log();
|
|
125
|
+
logger.log(chalk.bold(i18n(`${i18nKey}.results.recommended`)));
|
|
126
|
+
displayResults(validationResults.results['RECOMMENDED']);
|
|
127
|
+
logger.log();
|
|
128
|
+
|
|
129
|
+
process.exit();
|
|
86
130
|
};
|
|
87
131
|
|
|
88
132
|
exports.builder = yargs => {
|
|
@@ -90,12 +134,6 @@ exports.builder = yargs => {
|
|
|
90
134
|
addAccountOptions(yargs, true);
|
|
91
135
|
addUseEnvironmentOptions(yargs, true);
|
|
92
136
|
|
|
93
|
-
yargs.options({
|
|
94
|
-
json: {
|
|
95
|
-
describe: i18n(`${i18nKey}.options.json.describe`),
|
|
96
|
-
type: 'boolean',
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
137
|
yargs.positional('src', {
|
|
100
138
|
describe: i18n(`${i18nKey}.positionals.src.describe`),
|
|
101
139
|
type: 'string',
|
|
@@ -1,36 +1,8 @@
|
|
|
1
1
|
//HACK so we can keep this util file next to the tests that use it
|
|
2
2
|
test.skip('skip', () => null);
|
|
3
3
|
|
|
4
|
-
const THEME_PATH = '/path/to/a/theme';
|
|
5
4
|
const MODULE_PATH = 'module/path';
|
|
6
5
|
|
|
7
|
-
const makeFindError = baseKey => (errors, errorKey) =>
|
|
8
|
-
errors.find(error => error.key === `${baseKey}.${errorKey}`);
|
|
9
|
-
|
|
10
|
-
const generateModulesList = numFiles => {
|
|
11
|
-
const files = [];
|
|
12
|
-
for (let i = 0; i < numFiles; i++) {
|
|
13
|
-
const base = `module-${i}.module`;
|
|
14
|
-
files.push(`${base}/meta.json`);
|
|
15
|
-
files.push(`${base}/fields.json`);
|
|
16
|
-
files.push(`${base}/module.html`);
|
|
17
|
-
files.push(`${base}/module.js`);
|
|
18
|
-
}
|
|
19
|
-
return files;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const generateTemplatesList = numFiles => {
|
|
23
|
-
const files = [];
|
|
24
|
-
for (let i = 0; i < numFiles; i++) {
|
|
25
|
-
files.push(`template-${i}.html`);
|
|
26
|
-
}
|
|
27
|
-
return files;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
6
|
module.exports = {
|
|
31
|
-
generateModulesList,
|
|
32
|
-
generateTemplatesList,
|
|
33
|
-
makeFindError,
|
|
34
|
-
THEME_PATH,
|
|
35
7
|
MODULE_PATH,
|
|
36
8
|
};
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
async function applyAbsoluteValidators(validators, absolutePath, ...args) {
|
|
2
|
-
return Promise.all(
|
|
3
|
-
validators.map(async Validator => {
|
|
4
|
-
Validator.setAbsolutePath(absolutePath);
|
|
5
|
-
const validationResult = await Validator.validate(...args);
|
|
6
|
-
Validator.clearAbsolutePath();
|
|
7
|
-
|
|
8
|
-
if (!validationResult.length) {
|
|
9
|
-
// Return a success obj so we can log the successes
|
|
10
|
-
return [Validator.getSuccess()];
|
|
11
|
-
}
|
|
12
|
-
return validationResult;
|
|
13
|
-
})
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
1
|
async function applyRelativeValidators(validators, relativePath, ...args) {
|
|
18
2
|
return Promise.all(
|
|
19
3
|
validators.map(async Validator => {
|
|
@@ -30,4 +14,4 @@ async function applyRelativeValidators(validators, relativePath, ...args) {
|
|
|
30
14
|
);
|
|
31
15
|
}
|
|
32
16
|
|
|
33
|
-
module.exports = {
|
|
17
|
+
module.exports = { applyRelativeValidators };
|
|
@@ -4,11 +4,6 @@ const SUCCESS = 'SUCCESS';
|
|
|
4
4
|
|
|
5
5
|
const VALIDATION_RESULT = { WARNING, FATAL, SUCCESS };
|
|
6
6
|
const VALIDATOR_KEYS = {
|
|
7
|
-
themeDependency: 'themeDependency',
|
|
8
|
-
themeModule: 'themeModule',
|
|
9
|
-
section: 'section',
|
|
10
|
-
template: 'template',
|
|
11
|
-
themeConfig: 'themeConfig',
|
|
12
7
|
module: 'module',
|
|
13
8
|
moduleDependency: 'moduleDependency',
|
|
14
9
|
};
|
package/lib/validators/index.js
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
const ThemeConfigValidator = require('./marketplaceValidators/theme/ThemeConfigValidator');
|
|
2
|
-
const SectionValidator = require('./marketplaceValidators/theme/SectionValidator');
|
|
3
|
-
const TemplateValidator = require('./marketplaceValidators/theme/TemplateValidator');
|
|
4
|
-
const ThemeModuleValidator = require('./marketplaceValidators/theme/ThemeModuleValidator');
|
|
5
|
-
const ThemeDependencyValidator = require('./marketplaceValidators/theme/ThemeDependencyValidator');
|
|
6
|
-
|
|
7
1
|
const ModuleValidator = require('./marketplaceValidators/module/ModuleValidator');
|
|
8
2
|
const ModuleDependencyValidator = require('./marketplaceValidators/module/ModuleDependencyValidator');
|
|
9
3
|
|
|
10
4
|
const MARKETPLACE_VALIDATORS = {
|
|
11
|
-
theme: [
|
|
12
|
-
ThemeConfigValidator,
|
|
13
|
-
SectionValidator,
|
|
14
|
-
TemplateValidator,
|
|
15
|
-
ThemeModuleValidator,
|
|
16
|
-
ThemeDependencyValidator,
|
|
17
|
-
],
|
|
18
5
|
module: [ModuleValidator, ModuleDependencyValidator],
|
|
19
6
|
};
|
|
20
7
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
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
|
|
12
|
-
"@hubspot/serverless-dev-runtime": "4.0
|
|
11
|
+
"@hubspot/cli-lib": "4.1.0",
|
|
12
|
+
"@hubspot/serverless-dev-runtime": "4.1.0",
|
|
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": "
|
|
40
|
+
"gitHead": "642e8486f6dad6d91b1e5854fc427239ca4d11bb"
|
|
41
41
|
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const AbsoluteValidator = require('../marketplaceValidators/AbsoluteValidator');
|
|
2
|
-
const { VALIDATION_RESULT } = require('../constants');
|
|
3
|
-
|
|
4
|
-
const Validator = new AbsoluteValidator({
|
|
5
|
-
name: 'Test validator',
|
|
6
|
-
key: 'validatorKey',
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
describe('validators/marketplaceValidators/AbsoluteValidator', () => {
|
|
10
|
-
it('getSuccess returns expected object', async () => {
|
|
11
|
-
const success = Validator.getSuccess();
|
|
12
|
-
|
|
13
|
-
expect(success.validatorKey).toBe('validatorKey');
|
|
14
|
-
expect(success.validatorName).toBe('Test validator');
|
|
15
|
-
expect(success.result).toBe(VALIDATION_RESULT.SUCCESS);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('getError returns expected object', async () => {
|
|
19
|
-
const errorObj = { key: 'errorkey', getCopy: () => 'Some error copy' };
|
|
20
|
-
const success = Validator.getError(errorObj);
|
|
21
|
-
|
|
22
|
-
expect(success.validatorKey).toBe('validatorKey');
|
|
23
|
-
expect(success.validatorName).toBe('Test validator');
|
|
24
|
-
expect(success.error).toBe('Some error copy');
|
|
25
|
-
expect(success.result).toBe(VALIDATION_RESULT.FATAL);
|
|
26
|
-
expect(success.key).toBe('validatorKey.errorkey');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const templates = require('@hubspot/cli-lib/templates');
|
|
3
|
-
|
|
4
|
-
const TemplateValidator = require('../marketplaceValidators/theme/TemplateValidator');
|
|
5
|
-
const { VALIDATION_RESULT } = require('../constants');
|
|
6
|
-
const {
|
|
7
|
-
generateTemplatesList,
|
|
8
|
-
makeFindError,
|
|
9
|
-
THEME_PATH,
|
|
10
|
-
} = require('./validatorTestUtils');
|
|
11
|
-
|
|
12
|
-
jest.mock('fs');
|
|
13
|
-
jest.mock('@hubspot/cli-lib/templates');
|
|
14
|
-
|
|
15
|
-
const TEMPLATE_LIMIT = 50;
|
|
16
|
-
|
|
17
|
-
const mockGetAnnotationValue = (templateType, rest) => {
|
|
18
|
-
templates.buildAnnotationValueGetter.mockImplementation(() => {
|
|
19
|
-
return key => {
|
|
20
|
-
if (key === 'templateType') {
|
|
21
|
-
return templateType;
|
|
22
|
-
}
|
|
23
|
-
return rest;
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const findError = makeFindError('template');
|
|
29
|
-
|
|
30
|
-
describe('validators/marketplaceValidators/theme/TemplateValidator', () => {
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
TemplateValidator.setAbsolutePath(THEME_PATH);
|
|
33
|
-
templates.isCodedFile.mockReturnValue(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns error if template limit is exceeded', async () => {
|
|
37
|
-
mockGetAnnotationValue('page');
|
|
38
|
-
|
|
39
|
-
const validationErrors = TemplateValidator.validate(
|
|
40
|
-
generateTemplatesList(TEMPLATE_LIMIT + 1)
|
|
41
|
-
);
|
|
42
|
-
const limitError = findError(validationErrors, 'limitExceeded');
|
|
43
|
-
expect(limitError).toBeDefined();
|
|
44
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('returns no errors if template limit is not exceeded', async () => {
|
|
48
|
-
mockGetAnnotationValue('page');
|
|
49
|
-
|
|
50
|
-
const validationErrors = TemplateValidator.validate(
|
|
51
|
-
generateTemplatesList(TEMPLATE_LIMIT)
|
|
52
|
-
);
|
|
53
|
-
const limitError = findError(validationErrors, 'limitExceeded');
|
|
54
|
-
expect(limitError).not.toBeDefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('returns error if template annotation is missing label and screenshotPath', async () => {
|
|
58
|
-
fs.readFileSync.mockReturnValue('mock');
|
|
59
|
-
mockGetAnnotationValue('page');
|
|
60
|
-
|
|
61
|
-
const validationErrors = TemplateValidator.validate(['template.html']);
|
|
62
|
-
expect(validationErrors.length).toBe(2);
|
|
63
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns error if template type is not allowed', async () => {
|
|
67
|
-
fs.readFileSync.mockReturnValue('mock');
|
|
68
|
-
mockGetAnnotationValue('starter_landing_pages', 'value');
|
|
69
|
-
|
|
70
|
-
const validationErrors = TemplateValidator.validate(['template.html']);
|
|
71
|
-
|
|
72
|
-
expect(validationErrors.length).toBe(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('returns error if template type is unknown', async () => {
|
|
76
|
-
fs.readFileSync.mockReturnValue('mock');
|
|
77
|
-
mockGetAnnotationValue('unknown-type', 'value');
|
|
78
|
-
|
|
79
|
-
const validationErrors = TemplateValidator.validate(['template.html']);
|
|
80
|
-
|
|
81
|
-
expect(validationErrors.length).toBe(1);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('returns error if template type is not found', async () => {
|
|
85
|
-
fs.readFileSync.mockReturnValue('mock');
|
|
86
|
-
mockGetAnnotationValue(null, 'value');
|
|
87
|
-
|
|
88
|
-
const validationErrors = TemplateValidator.validate(['template.html']);
|
|
89
|
-
|
|
90
|
-
expect(validationErrors.length).toBe(1);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('returns no error if template annotation has label and screenshotPath', async () => {
|
|
94
|
-
fs.readFileSync.mockReturnValue('mock');
|
|
95
|
-
mockGetAnnotationValue('page', 'value');
|
|
96
|
-
|
|
97
|
-
const validationErrors = TemplateValidator.validate(['template.html']);
|
|
98
|
-
|
|
99
|
-
expect(validationErrors.length).toBe(0);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const ThemeConfigValidator = require('../../validators/marketplaceValidators/theme/ThemeConfigValidator');
|
|
5
|
-
const { VALIDATION_RESULT } = require('../../validators/constants');
|
|
6
|
-
const { THEME_PATH } = require('./validatorTestUtils');
|
|
7
|
-
|
|
8
|
-
jest.mock('fs');
|
|
9
|
-
jest.mock('path');
|
|
10
|
-
|
|
11
|
-
describe('validators/marketplaceValidators/theme/ThemeConfigValidator', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
ThemeConfigValidator.setAbsolutePath(THEME_PATH);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('returns error if no theme.json file exists', async () => {
|
|
17
|
-
const validationErrors = ThemeConfigValidator.validate(['someFile.html']);
|
|
18
|
-
expect(validationErrors.length).toBe(1);
|
|
19
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('returns error if theme.json file has invalid json', async () => {
|
|
23
|
-
fs.readFileSync.mockReturnValue('{} bad json }');
|
|
24
|
-
|
|
25
|
-
const validationErrors = ThemeConfigValidator.validate(['theme.json']);
|
|
26
|
-
expect(validationErrors.length).toBe(1);
|
|
27
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('returns error if theme.json file is missing a label field', async () => {
|
|
31
|
-
fs.readFileSync.mockReturnValue('{ "screenshot_path": "./relative/path" }');
|
|
32
|
-
|
|
33
|
-
const validationErrors = ThemeConfigValidator.validate(['theme.json']);
|
|
34
|
-
expect(validationErrors.length).toBe(1);
|
|
35
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns error if theme.json has screenshot path that is non-relative', async () => {
|
|
39
|
-
fs.readFileSync.mockReturnValue(
|
|
40
|
-
'{ "label": "yay", "screenshot_path": "/absolute/path" }'
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const validationErrors = ThemeConfigValidator.validate(['theme.json']);
|
|
44
|
-
expect(validationErrors.length).toBe(1);
|
|
45
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('returns error if theme.json has screenshot path that does not resolve', async () => {
|
|
49
|
-
fs.readFileSync.mockReturnValue(
|
|
50
|
-
'{ "label": "yay", "screenshot_path": "/absolute/path" }'
|
|
51
|
-
);
|
|
52
|
-
path.relative.mockReturnValue('theme.json');
|
|
53
|
-
fs.existsSync.mockReturnValue(false);
|
|
54
|
-
|
|
55
|
-
const validationErrors = ThemeConfigValidator.validate(['theme.json']);
|
|
56
|
-
expect(validationErrors.length).toBe(1);
|
|
57
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('returns no error if theme.json file exists and has all required fields', async () => {
|
|
61
|
-
fs.readFileSync.mockReturnValue(
|
|
62
|
-
'{ "label": "yay", "screenshot_path": "./relative/path" }'
|
|
63
|
-
);
|
|
64
|
-
path.relative.mockReturnValue('theme.json');
|
|
65
|
-
fs.existsSync.mockReturnValue(true);
|
|
66
|
-
|
|
67
|
-
const validationErrors = ThemeConfigValidator.validate(['theme.json']);
|
|
68
|
-
expect(validationErrors.length).toBe(0);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const marketplace = require('@hubspot/cli-lib/api/marketplace');
|
|
3
|
-
|
|
4
|
-
const ThemeDependencyValidator = require('../marketplaceValidators/theme/ThemeDependencyValidator');
|
|
5
|
-
const { VALIDATION_RESULT } = require('../constants');
|
|
6
|
-
const { THEME_PATH } = require('./validatorTestUtils');
|
|
7
|
-
|
|
8
|
-
jest.mock('fs-extra');
|
|
9
|
-
jest.mock('@hubspot/cli-lib/api/marketplace');
|
|
10
|
-
|
|
11
|
-
const getMockDependencyResult = (customPaths = []) => {
|
|
12
|
-
const result = {
|
|
13
|
-
dependencies: ['./relative/file-2.js', ...customPaths],
|
|
14
|
-
};
|
|
15
|
-
return Promise.resolve(result);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
describe('validators/marketplaceValidators/theme/ThemeDependencyValidator', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
ThemeDependencyValidator.setAbsolutePath(THEME_PATH);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('isExternalDep', () => {
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
ThemeDependencyValidator.setAbsolutePath(THEME_PATH);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('returns true if dep is external to the provided absolute path', () => {
|
|
29
|
-
const absoluteFilePath = `${THEME_PATH}/file.js`;
|
|
30
|
-
const relativeDepPath = '../external/dep/path/file2.js';
|
|
31
|
-
const isExternal = ThemeDependencyValidator.isExternalDep(
|
|
32
|
-
absoluteFilePath,
|
|
33
|
-
relativeDepPath
|
|
34
|
-
);
|
|
35
|
-
expect(isExternal).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns false if dep is not external to the provided absolute path', () => {
|
|
39
|
-
const absoluteFilePath = `${THEME_PATH}/file.js`;
|
|
40
|
-
const relativeDepPath = './internal/dep/path/file2.js';
|
|
41
|
-
const isExternal = ThemeDependencyValidator.isExternalDep(
|
|
42
|
-
absoluteFilePath,
|
|
43
|
-
relativeDepPath
|
|
44
|
-
);
|
|
45
|
-
expect(isExternal).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('validate', () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
fs.readFile.mockImplementation(() => Promise.resolve('source'));
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('returns error if any referenced path is absolute', async () => {
|
|
55
|
-
marketplace.fetchTemplateDependencies.mockReturnValue(
|
|
56
|
-
getMockDependencyResult(['/absolute/file-3.js'])
|
|
57
|
-
);
|
|
58
|
-
const validationErrors = await ThemeDependencyValidator.validate([
|
|
59
|
-
`${THEME_PATH}/template.html`,
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
|
-
expect(validationErrors.length).toBe(1);
|
|
63
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns error if any referenced path is external to the theme', async () => {
|
|
67
|
-
marketplace.fetchTemplateDependencies.mockReturnValue(
|
|
68
|
-
getMockDependencyResult(['../../external/file-3.js'])
|
|
69
|
-
);
|
|
70
|
-
const validationErrors = await ThemeDependencyValidator.validate([
|
|
71
|
-
`${THEME_PATH}/template.html`,
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
expect(validationErrors.length).toBe(1);
|
|
75
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('returns no errors if paths are relative and internal', async () => {
|
|
79
|
-
marketplace.fetchTemplateDependencies.mockReturnValue(
|
|
80
|
-
getMockDependencyResult()
|
|
81
|
-
);
|
|
82
|
-
const validationErrors = await ThemeDependencyValidator.validate([
|
|
83
|
-
`${THEME_PATH}/template.html`,
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
expect(validationErrors.length).toBe(0);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const ThemeModuleValidator = require('../marketplaceValidators/theme/ThemeModuleValidator');
|
|
3
|
-
const { VALIDATION_RESULT } = require('../constants');
|
|
4
|
-
const {
|
|
5
|
-
generateModulesList,
|
|
6
|
-
makeFindError,
|
|
7
|
-
THEME_PATH,
|
|
8
|
-
} = require('./validatorTestUtils');
|
|
9
|
-
|
|
10
|
-
jest.mock('fs');
|
|
11
|
-
|
|
12
|
-
const MODULE_LIMIT = 50;
|
|
13
|
-
|
|
14
|
-
const findError = makeFindError('themeModule');
|
|
15
|
-
|
|
16
|
-
describe('validators/marketplaceValidators/theme/ThemeModuleValidator', () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
ThemeModuleValidator.setAbsolutePath(THEME_PATH);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('returns error if module limit is exceeded', async () => {
|
|
22
|
-
const validationErrors = ThemeModuleValidator.validate(
|
|
23
|
-
generateModulesList(MODULE_LIMIT + 1)
|
|
24
|
-
);
|
|
25
|
-
const limitError = findError(validationErrors, 'limitExceeded');
|
|
26
|
-
expect(limitError).toBeDefined();
|
|
27
|
-
expect(limitError.result).toBe(VALIDATION_RESULT.FATAL);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('returns no limit error if module limit is not exceeded', async () => {
|
|
31
|
-
const validationErrors = ThemeModuleValidator.validate(
|
|
32
|
-
generateModulesList(MODULE_LIMIT)
|
|
33
|
-
);
|
|
34
|
-
const limitError = findError(validationErrors, 'limitExceeded');
|
|
35
|
-
expect(limitError).not.toBeDefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns error if no module meta.json file exists', async () => {
|
|
39
|
-
const validationErrors = ThemeModuleValidator.validate([
|
|
40
|
-
'module.module/module.html',
|
|
41
|
-
]);
|
|
42
|
-
expect(validationErrors.length).toBe(1);
|
|
43
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('returns error if module meta.json file has invalid json', async () => {
|
|
47
|
-
fs.readFileSync.mockReturnValue('{} bad json }');
|
|
48
|
-
|
|
49
|
-
const validationErrors = ThemeModuleValidator.validate([
|
|
50
|
-
'module.module/meta.json',
|
|
51
|
-
]);
|
|
52
|
-
expect(validationErrors.length).toBe(1);
|
|
53
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('returns error if module meta.json file is missing a label field', async () => {
|
|
57
|
-
fs.readFileSync.mockReturnValue('{ "icon": "woo" }');
|
|
58
|
-
|
|
59
|
-
const validationErrors = ThemeModuleValidator.validate([
|
|
60
|
-
'module.module/meta.json',
|
|
61
|
-
]);
|
|
62
|
-
expect(validationErrors.length).toBe(1);
|
|
63
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns error if module meta.json file is missing an icon field', async () => {
|
|
67
|
-
fs.readFileSync.mockReturnValue('{ "label": "yay" }');
|
|
68
|
-
|
|
69
|
-
const validationErrors = ThemeModuleValidator.validate([
|
|
70
|
-
'module.module/meta.json',
|
|
71
|
-
]);
|
|
72
|
-
expect(validationErrors.length).toBe(1);
|
|
73
|
-
expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('returns no error if module meta.json file exists and has all required fields', async () => {
|
|
77
|
-
fs.readFileSync.mockReturnValue('{ "label": "yay", "icon": "woo" }');
|
|
78
|
-
|
|
79
|
-
const validationErrors = ThemeModuleValidator.validate([
|
|
80
|
-
'module.module/meta.json',
|
|
81
|
-
]);
|
|
82
|
-
expect(validationErrors.length).toBe(0);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
|
-
const { VALIDATION_RESULT } = require('../constants');
|
|
4
|
-
|
|
5
|
-
class AbsoluteValidator {
|
|
6
|
-
constructor({ name, key }) {
|
|
7
|
-
this.name = name;
|
|
8
|
-
this.key = key;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
clearAbsolutePath() {
|
|
12
|
-
this._absolutePath = null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
setAbsolutePath(path) {
|
|
16
|
-
this._absolutePath = path;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
getRelativePath(filePath) {
|
|
20
|
-
return this._absolutePath
|
|
21
|
-
? path.relative(this._absolutePath, filePath)
|
|
22
|
-
: filePath;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
getSuccess() {
|
|
26
|
-
return {
|
|
27
|
-
validatorKey: this.key,
|
|
28
|
-
validatorName: this.name,
|
|
29
|
-
result: VALIDATION_RESULT.SUCCESS,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
getError(errorObj, file, extraContext = {}) {
|
|
34
|
-
const relativeFilePath = file ? this.getRelativePath(file) : null;
|
|
35
|
-
const context = {
|
|
36
|
-
filePath: relativeFilePath,
|
|
37
|
-
...extraContext,
|
|
38
|
-
};
|
|
39
|
-
return {
|
|
40
|
-
validatorKey: this.key,
|
|
41
|
-
validatorName: this.name,
|
|
42
|
-
error: errorObj.getCopy(context),
|
|
43
|
-
result: errorObj.severity || VALIDATION_RESULT.FATAL,
|
|
44
|
-
key: `${this.key}.${errorObj.key}`,
|
|
45
|
-
context,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = AbsoluteValidator;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
ANNOTATION_KEYS,
|
|
3
|
-
buildAnnotationValueGetter,
|
|
4
|
-
} = require('@hubspot/cli-lib/templates');
|
|
5
|
-
const AbsoluteValidator = require('../AbsoluteValidator');
|
|
6
|
-
const { VALIDATOR_KEYS } = require('../../constants');
|
|
7
|
-
|
|
8
|
-
const SECTION_LIMIT = 50;
|
|
9
|
-
|
|
10
|
-
class SectionValidator extends AbsoluteValidator {
|
|
11
|
-
constructor(options) {
|
|
12
|
-
super(options);
|
|
13
|
-
|
|
14
|
-
this.errors = {
|
|
15
|
-
LIMIT_EXCEEDED: {
|
|
16
|
-
key: 'limitExceeded',
|
|
17
|
-
getCopy: ({ limit, total }) =>
|
|
18
|
-
`Section limit exceeded. Themes can only have ${limit} sections, but this theme has ${total}`,
|
|
19
|
-
},
|
|
20
|
-
MISSING_LABEL: {
|
|
21
|
-
key: 'missingLabel',
|
|
22
|
-
getCopy: ({ filePath }) =>
|
|
23
|
-
`Missing required property for ${filePath}. The section is missing the "label" property`,
|
|
24
|
-
},
|
|
25
|
-
MISSING_SCREENSHOT_PATH: {
|
|
26
|
-
key: 'missingScreenshotPath',
|
|
27
|
-
getCopy: ({ filePath }) =>
|
|
28
|
-
`Missing required property for ${filePath}. The section is missing the "screenshotPath" property`,
|
|
29
|
-
},
|
|
30
|
-
MISSING_DESCRIPTION: {
|
|
31
|
-
key: 'missingDescription',
|
|
32
|
-
getCopy: ({ filePath }) =>
|
|
33
|
-
`Missing required property for ${filePath}. The section is missing the "description" property`,
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Validates:
|
|
39
|
-
// - All sections have a "description" annotation
|
|
40
|
-
// - All sections have a "label" annotation
|
|
41
|
-
// - All sections have a "screenshotPath" annotation
|
|
42
|
-
// - Theme does not have more than SECTION_LIMIT sections
|
|
43
|
-
|
|
44
|
-
validate(files) {
|
|
45
|
-
let validationErrors = [];
|
|
46
|
-
let sectionCount = 0;
|
|
47
|
-
|
|
48
|
-
files.forEach(file => {
|
|
49
|
-
if (file) {
|
|
50
|
-
const getAnnotationValue = buildAnnotationValueGetter(file);
|
|
51
|
-
const templateType = getAnnotationValue(ANNOTATION_KEYS.templateType);
|
|
52
|
-
|
|
53
|
-
if (templateType !== 'section') {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
sectionCount++;
|
|
57
|
-
|
|
58
|
-
const description = getAnnotationValue(ANNOTATION_KEYS.description);
|
|
59
|
-
const label = getAnnotationValue(ANNOTATION_KEYS.label);
|
|
60
|
-
const screenshotPath = getAnnotationValue(
|
|
61
|
-
ANNOTATION_KEYS.screenshotPath
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
if (!description) {
|
|
65
|
-
validationErrors.push(
|
|
66
|
-
this.getError(this.errors.MISSING_DESCRIPTION, file)
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
if (!label) {
|
|
70
|
-
validationErrors.push(this.getError(this.errors.MISSING_LABEL, file));
|
|
71
|
-
}
|
|
72
|
-
if (!screenshotPath) {
|
|
73
|
-
validationErrors.push(
|
|
74
|
-
this.getError(this.errors.MISSING_SCREENSHOT_PATH, file)
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (sectionCount > SECTION_LIMIT) {
|
|
81
|
-
validationErrors.push(
|
|
82
|
-
this.getError(this.errors.LIMIT_EXCEEDED, null, {
|
|
83
|
-
limit: SECTION_LIMIT,
|
|
84
|
-
total: sectionCount,
|
|
85
|
-
})
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return validationErrors;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = new SectionValidator({
|
|
94
|
-
name: 'Section',
|
|
95
|
-
key: VALIDATOR_KEYS.section,
|
|
96
|
-
});
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
ANNOTATION_KEYS,
|
|
3
|
-
buildAnnotationValueGetter,
|
|
4
|
-
isCodedFile,
|
|
5
|
-
} = require('@hubspot/cli-lib/templates');
|
|
6
|
-
const AbsoluteValidator = require('../AbsoluteValidator');
|
|
7
|
-
const { VALIDATOR_KEYS } = require('../../constants');
|
|
8
|
-
|
|
9
|
-
const TEMPLATE_LIMIT = 50;
|
|
10
|
-
const TEMPLATE_IGNORE_LIST = ['section'];
|
|
11
|
-
const TEMPLATE_COUNT_IGNORE_LIST = ['global_partial', 'section', 'none'];
|
|
12
|
-
const VALIDATIONS_BY_TYPE = {
|
|
13
|
-
page: { allowed: true, label: true, screenshot: true },
|
|
14
|
-
starter_landing_pages: { allowed: false },
|
|
15
|
-
email: { allowed: false },
|
|
16
|
-
blog: { allowed: false },
|
|
17
|
-
none: { allowed: true, label: false, screenshot: false },
|
|
18
|
-
error_page: { allowed: true, label: true, screenshot: false },
|
|
19
|
-
password_prompt_page: { allowed: true, label: true, screenshot: false },
|
|
20
|
-
email_subscription_preferences_page: {
|
|
21
|
-
allowed: true,
|
|
22
|
-
label: true,
|
|
23
|
-
screenshot: false,
|
|
24
|
-
},
|
|
25
|
-
email_backup_unsubscribe_page: {
|
|
26
|
-
allowed: true,
|
|
27
|
-
label: true,
|
|
28
|
-
screenshot: false,
|
|
29
|
-
},
|
|
30
|
-
email_subscriptions_confirmation_page: {
|
|
31
|
-
allowed: true,
|
|
32
|
-
label: true,
|
|
33
|
-
screenshot: false,
|
|
34
|
-
},
|
|
35
|
-
search_results_page: { allowed: true, label: true, screenshot: false },
|
|
36
|
-
membership_login_page: { allowed: true, label: true, screenshot: false },
|
|
37
|
-
membership_register_page: { allowed: true, label: true, screenshot: false },
|
|
38
|
-
membership_reset_page: { allowed: true, label: true, screenshot: false },
|
|
39
|
-
membership_reset_request_page: {
|
|
40
|
-
allowed: true,
|
|
41
|
-
label: true,
|
|
42
|
-
screenshot: false,
|
|
43
|
-
},
|
|
44
|
-
membership_email_page: { allowed: true, label: true, screenshot: false },
|
|
45
|
-
global_partial: { allowed: true, label: true, screenshot: false },
|
|
46
|
-
knowledge_article: { allowed: false },
|
|
47
|
-
drag_drop_email: { allowed: false },
|
|
48
|
-
proposal: { allowed: false },
|
|
49
|
-
blog_listing: { allowed: true, label: true, screenshot: true },
|
|
50
|
-
blog_post: { allowed: true, label: true, screenshot: true },
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
class TemplateValidator extends AbsoluteValidator {
|
|
54
|
-
constructor(options) {
|
|
55
|
-
super(options);
|
|
56
|
-
|
|
57
|
-
this.errors = {
|
|
58
|
-
LIMIT_EXCEEDED: {
|
|
59
|
-
key: 'limitExceeded',
|
|
60
|
-
getCopy: ({ limit, total }) =>
|
|
61
|
-
`Template limit exceeded. Themes can only have ${limit} templates, but this theme has ${total}`,
|
|
62
|
-
},
|
|
63
|
-
MISSING_TEMPLATE_TYPE: {
|
|
64
|
-
key: 'missingTemplateType',
|
|
65
|
-
getCopy: ({ filePath }) =>
|
|
66
|
-
`Missing required property for ${filePath}. The template is missing the "templateType" property`,
|
|
67
|
-
},
|
|
68
|
-
UNKNOWN_TEMPLATE_TYPE: {
|
|
69
|
-
key: 'unknownTemplateType',
|
|
70
|
-
getCopy: ({ filePath, templateType }) =>
|
|
71
|
-
`Template ${filePath} has an unknown template type of ${templateType}`,
|
|
72
|
-
},
|
|
73
|
-
RESTRICTED_TEMPLATE_TYPE: {
|
|
74
|
-
key: 'restrictedTemplateType',
|
|
75
|
-
getCopy: ({ filePath, templateType }) =>
|
|
76
|
-
`Template ${filePath} has a restricted template type of ${templateType}`,
|
|
77
|
-
},
|
|
78
|
-
MISSING_LABEL: {
|
|
79
|
-
key: 'missingLabel',
|
|
80
|
-
getCopy: ({ filePath }) =>
|
|
81
|
-
`Missing required property for ${filePath}. The template is missing the "label" property`,
|
|
82
|
-
},
|
|
83
|
-
MISSING_SCREENSHOT_PATH: {
|
|
84
|
-
key: 'missingScreenshotPath',
|
|
85
|
-
getCopy: ({ filePath }) =>
|
|
86
|
-
`Missing required property for ${filePath}. The template is missing the "screenshotPath" property`,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Validates:
|
|
92
|
-
// - Theme does not contain more than TEMPLATE_LIMIT templates
|
|
93
|
-
// - All templates have valid template types
|
|
94
|
-
// - All templates that require a label have a "label" annotation
|
|
95
|
-
// - All templates that require a screenshot have a "screenshotPath" annotation
|
|
96
|
-
validate(files) {
|
|
97
|
-
let validationErrors = [];
|
|
98
|
-
let templateCount = 0;
|
|
99
|
-
|
|
100
|
-
files.forEach(file => {
|
|
101
|
-
if (isCodedFile(file)) {
|
|
102
|
-
const getAnnotationValue = buildAnnotationValueGetter(file);
|
|
103
|
-
const isAvailableForNewContent = getAnnotationValue(
|
|
104
|
-
ANNOTATION_KEYS.isAvailableForNewContent
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
if (isAvailableForNewContent !== 'false') {
|
|
108
|
-
const templateType = getAnnotationValue(ANNOTATION_KEYS.templateType);
|
|
109
|
-
|
|
110
|
-
if (TEMPLATE_IGNORE_LIST.includes(templateType)) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (templateType) {
|
|
115
|
-
const label = getAnnotationValue(ANNOTATION_KEYS.label);
|
|
116
|
-
const screenshotPath = getAnnotationValue(
|
|
117
|
-
ANNOTATION_KEYS.screenshotPath
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// Ignore global partials, sections, and templates with type of none in count
|
|
121
|
-
if (!TEMPLATE_COUNT_IGNORE_LIST.includes(templateType)) {
|
|
122
|
-
templateCount++;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const validations = VALIDATIONS_BY_TYPE[templateType];
|
|
126
|
-
|
|
127
|
-
if (validations) {
|
|
128
|
-
if (!validations.allowed) {
|
|
129
|
-
validationErrors.push(
|
|
130
|
-
this.getError(this.errors.RESTRICTED_TEMPLATE_TYPE, file, {
|
|
131
|
-
templateType,
|
|
132
|
-
})
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
if (validations.label && !label) {
|
|
136
|
-
validationErrors.push(
|
|
137
|
-
this.getError(this.errors.MISSING_LABEL, file)
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
if (validations.screenshot && !screenshotPath) {
|
|
141
|
-
validationErrors.push(
|
|
142
|
-
this.getError(this.errors.MISSING_SCREENSHOT_PATH, file)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
validationErrors.push(
|
|
147
|
-
this.getError(this.errors.UNKNOWN_TEMPLATE_TYPE, file)
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
validationErrors.push(
|
|
152
|
-
this.getError(this.errors.MISSING_TEMPLATE_TYPE, file)
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (templateCount > TEMPLATE_LIMIT) {
|
|
160
|
-
validationErrors.push(
|
|
161
|
-
this.getError(this.errors.LIMIT_EXCEEDED, null, {
|
|
162
|
-
limit: TEMPLATE_LIMIT,
|
|
163
|
-
total: templateCount,
|
|
164
|
-
})
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return validationErrors;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
module.exports = new TemplateValidator({
|
|
173
|
-
name: 'Template',
|
|
174
|
-
key: VALIDATOR_KEYS.template,
|
|
175
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const { isRelativePath } = require('@hubspot/cli-lib/path');
|
|
5
|
-
const AbsoluteValidator = require('../AbsoluteValidator');
|
|
6
|
-
const { VALIDATOR_KEYS } = require('../../constants');
|
|
7
|
-
|
|
8
|
-
class ThemeValidator extends AbsoluteValidator {
|
|
9
|
-
constructor(options) {
|
|
10
|
-
super(options);
|
|
11
|
-
|
|
12
|
-
this.errors = {
|
|
13
|
-
MISSING_THEME_JSON: {
|
|
14
|
-
key: 'missingThemeJSON',
|
|
15
|
-
getCopy: () =>
|
|
16
|
-
'Missing the theme.json file. This file is required in all themes',
|
|
17
|
-
},
|
|
18
|
-
INVALID_THEME_JSON: {
|
|
19
|
-
key: 'invalidThemeJSON',
|
|
20
|
-
getCopy: ({ filePath }) => `Invalid json in the ${filePath} file`,
|
|
21
|
-
},
|
|
22
|
-
MISSING_LABEL: {
|
|
23
|
-
key: 'missingLabel',
|
|
24
|
-
getCopy: ({ filePath }) =>
|
|
25
|
-
`Missing required field in ${filePath}. The "label" field is required`,
|
|
26
|
-
},
|
|
27
|
-
MISSING_SCREENSHOT_PATH: {
|
|
28
|
-
key: 'missingScreenshotPath',
|
|
29
|
-
getCopy: ({ filePath }) =>
|
|
30
|
-
`Missing required field in ${filePath}. The "screenshot_path" field is required`,
|
|
31
|
-
},
|
|
32
|
-
ABSOLUTE_SCREENSHOT_PATH: {
|
|
33
|
-
key: 'absoluteScreenshotPath',
|
|
34
|
-
getCopy: ({ fieldPath }) =>
|
|
35
|
-
`Relative path required. The path for "screenshot_path" in ${fieldPath} must be relative`,
|
|
36
|
-
},
|
|
37
|
-
MISSING_SCREENSHOT: {
|
|
38
|
-
key: 'missingScreenshot',
|
|
39
|
-
getCopy: ({ fieldPath }) =>
|
|
40
|
-
`File not found. No file exists for the provided "screenshot_path" in ${fieldPath}`,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Validates:
|
|
46
|
-
// - Theme contains a theme.json file at the theme root dir
|
|
47
|
-
// - theme.json file contains valid json
|
|
48
|
-
// - theme.json file has a "label" field
|
|
49
|
-
// - theme.json file has a relative path for "screenshot" field that resolves
|
|
50
|
-
validate(files) {
|
|
51
|
-
let validationErrors = [];
|
|
52
|
-
const themeJSONFile = files.find(filePath => {
|
|
53
|
-
// Check for theme.json at the theme root
|
|
54
|
-
const fileName = this.getRelativePath(filePath);
|
|
55
|
-
return fileName === 'theme.json';
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (!themeJSONFile) {
|
|
59
|
-
validationErrors.push(this.getError(this.errors.MISSING_THEME_JSON));
|
|
60
|
-
} else {
|
|
61
|
-
let themeJSON;
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
themeJSON = JSON.parse(fs.readFileSync(themeJSONFile));
|
|
65
|
-
} catch (err) {
|
|
66
|
-
validationErrors.push(
|
|
67
|
-
this.getError(this.errors.INVALID_THEME_JSON, themeJSONFile)
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (themeJSON) {
|
|
72
|
-
if (!themeJSON.label) {
|
|
73
|
-
validationErrors.push(
|
|
74
|
-
this.getError(this.errors.MISSING_LABEL, themeJSONFile)
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
if (!themeJSON.screenshot_path) {
|
|
78
|
-
validationErrors.push(
|
|
79
|
-
this.getError(this.errors.MISSING_SCREENSHOT_PATH, themeJSONFile)
|
|
80
|
-
);
|
|
81
|
-
} else if (!isRelativePath(themeJSON.screenshot_path)) {
|
|
82
|
-
validationErrors.push(
|
|
83
|
-
this.getError(this.errors.ABSOLUTE_SCREENSHOT_PATH, themeJSONFile)
|
|
84
|
-
);
|
|
85
|
-
} else {
|
|
86
|
-
const absoluteScreenshotPath = path.resolve(
|
|
87
|
-
this._absolutePath,
|
|
88
|
-
themeJSON.screenshot_path
|
|
89
|
-
);
|
|
90
|
-
if (!fs.existsSync(absoluteScreenshotPath)) {
|
|
91
|
-
validationErrors.push(
|
|
92
|
-
this.getError(this.errors.MISSING_SCREENSHOT, themeJSONFile)
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return validationErrors;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
module.exports = new ThemeValidator({
|
|
104
|
-
name: 'Theme config',
|
|
105
|
-
key: VALIDATOR_KEYS.themeConfig,
|
|
106
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const { logger } = require('@hubspot/cli-lib/logger');
|
|
5
|
-
const {
|
|
6
|
-
HUBL_EXTENSIONS,
|
|
7
|
-
HUBSPOT_FOLDER,
|
|
8
|
-
} = require('@hubspot/cli-lib/lib/constants');
|
|
9
|
-
const {
|
|
10
|
-
fetchTemplateDependencies,
|
|
11
|
-
} = require('@hubspot/cli-lib/api/marketplace');
|
|
12
|
-
const { getExt, isRelativePath } = require('@hubspot/cli-lib/path');
|
|
13
|
-
|
|
14
|
-
const AbsoluteValidator = require('../AbsoluteValidator');
|
|
15
|
-
const { VALIDATOR_KEYS } = require('../../constants');
|
|
16
|
-
|
|
17
|
-
class ThemeDependencyValidator extends AbsoluteValidator {
|
|
18
|
-
constructor(options) {
|
|
19
|
-
super(options);
|
|
20
|
-
|
|
21
|
-
this.errors = {
|
|
22
|
-
FAILED_TO_FETCH_DEPS: {
|
|
23
|
-
key: 'failedDepFetch',
|
|
24
|
-
getCopy: ({ filePath }) =>
|
|
25
|
-
`Internal Error. Failed to fetch dependencies for ${filePath}. Please try again`,
|
|
26
|
-
},
|
|
27
|
-
EXTERNAL_DEPENDENCY: {
|
|
28
|
-
key: 'externalDependency',
|
|
29
|
-
getCopy: ({ filePath, referencedFilePath }) =>
|
|
30
|
-
`External dependency. ${filePath} references a file (${referencedFilePath}) that is outside of the theme`,
|
|
31
|
-
},
|
|
32
|
-
ABSOLUTE_DEPENDENCY_PATH: {
|
|
33
|
-
key: 'absoluteDependencyPath',
|
|
34
|
-
getCopy: ({ filePath, referencedFilePath }) =>
|
|
35
|
-
`Relative path required. ${filePath} references a file (${referencedFilePath}) using an absolute path`,
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
failedToFetchDependencies(err, file, validationErrors) {
|
|
41
|
-
logger.debug(`Failed to fetch dependencies for ${file}: `, err.error);
|
|
42
|
-
|
|
43
|
-
validationErrors.push(
|
|
44
|
-
this.getError(this.errors.FAILED_TO_FETCH_DEPS, file)
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async getAllDependenciesByFile(files, accountId, validationErrors) {
|
|
49
|
-
return Promise.all(
|
|
50
|
-
files
|
|
51
|
-
.filter(file => HUBL_EXTENSIONS.has(getExt(file)))
|
|
52
|
-
.map(async file => {
|
|
53
|
-
const source = await fs.readFile(file, { encoding: 'utf8' });
|
|
54
|
-
let deps = [];
|
|
55
|
-
if (!(source && source.trim())) {
|
|
56
|
-
return { file, deps };
|
|
57
|
-
}
|
|
58
|
-
const file_deps = await fetchTemplateDependencies(
|
|
59
|
-
accountId,
|
|
60
|
-
source
|
|
61
|
-
).catch(err => {
|
|
62
|
-
this.failedToFetchDependencies(err, file, validationErrors);
|
|
63
|
-
return null;
|
|
64
|
-
});
|
|
65
|
-
if (file_deps) {
|
|
66
|
-
deps = file_deps.dependencies || [];
|
|
67
|
-
}
|
|
68
|
-
return { file, deps };
|
|
69
|
-
})
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
isExternalDep(file, relativeDepPath) {
|
|
74
|
-
// Get dir of file that references the dep
|
|
75
|
-
const { dir } = path.parse(file);
|
|
76
|
-
// Use dir to get the dep's absolute path
|
|
77
|
-
const absoluteDepPath = path.resolve(dir, relativeDepPath);
|
|
78
|
-
// Get relative path to dep using theme absolute path and dep absolute path
|
|
79
|
-
const relativePath = this.getRelativePath(absoluteDepPath);
|
|
80
|
-
// Check that dep is not within the theme
|
|
81
|
-
return relativePath && relativePath.startsWith('..');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Validates:
|
|
85
|
-
// - Theme does not contain external dependencies
|
|
86
|
-
// - All paths are either @hubspot or relative
|
|
87
|
-
async validate(files, accountId) {
|
|
88
|
-
let validationErrors = [];
|
|
89
|
-
|
|
90
|
-
const dependencyData = await this.getAllDependenciesByFile(
|
|
91
|
-
files,
|
|
92
|
-
accountId,
|
|
93
|
-
validationErrors
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
dependencyData.forEach(depData => {
|
|
97
|
-
const { file, deps } = depData;
|
|
98
|
-
deps.forEach(dependency => {
|
|
99
|
-
// Ignore:
|
|
100
|
-
// - Hubspot modules
|
|
101
|
-
if (!dependency.startsWith(HUBSPOT_FOLDER)) {
|
|
102
|
-
if (!isRelativePath(dependency)) {
|
|
103
|
-
validationErrors.push(
|
|
104
|
-
this.getError(this.errors.ABSOLUTE_DEPENDENCY_PATH, file, {
|
|
105
|
-
referencedFilePath: dependency,
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
} else if (this.isExternalDep(file, dependency)) {
|
|
109
|
-
validationErrors.push(
|
|
110
|
-
this.getError(this.errors.EXTERNAL_DEPENDENCY, file, {
|
|
111
|
-
referencedFilePath: dependency,
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return validationErrors;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
module.exports = new ThemeDependencyValidator({
|
|
124
|
-
name: 'Theme dependency',
|
|
125
|
-
key: VALIDATOR_KEYS.themeDependency,
|
|
126
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const { isModuleFolderChild } = require('@hubspot/cli-lib/modules');
|
|
5
|
-
const AbsoluteValidator = require('../AbsoluteValidator');
|
|
6
|
-
const { VALIDATOR_KEYS } = require('../../constants');
|
|
7
|
-
|
|
8
|
-
const MODULE_LIMIT = 50;
|
|
9
|
-
|
|
10
|
-
class ThemeModuleValidator extends AbsoluteValidator {
|
|
11
|
-
constructor(options) {
|
|
12
|
-
super(options);
|
|
13
|
-
|
|
14
|
-
this.errors = {
|
|
15
|
-
LIMIT_EXCEEDED: {
|
|
16
|
-
key: 'limitExceeded',
|
|
17
|
-
getCopy: ({ limit, total }) =>
|
|
18
|
-
`Module limit exceeded. Themes can only have ${limit} modules, but this theme has ${total}`,
|
|
19
|
-
},
|
|
20
|
-
MISSING_META_JSON: {
|
|
21
|
-
key: 'missingMetaJSON',
|
|
22
|
-
getCopy: ({ filePath }) =>
|
|
23
|
-
`Module ${filePath} is missing the meta.json file`,
|
|
24
|
-
},
|
|
25
|
-
INVALID_META_JSON: {
|
|
26
|
-
key: 'invalidMetaJSON',
|
|
27
|
-
getCopy: ({ filePath }) =>
|
|
28
|
-
`Module ${filePath} has invalid json in the meta.json file`,
|
|
29
|
-
},
|
|
30
|
-
MISSING_LABEL: {
|
|
31
|
-
key: 'missingLabel',
|
|
32
|
-
getCopy: ({ filePath }) =>
|
|
33
|
-
`Missing required property for ${filePath}. The meta.json file is missing the "label" property`,
|
|
34
|
-
},
|
|
35
|
-
MISSING_ICON: {
|
|
36
|
-
key: 'missingIcon',
|
|
37
|
-
getCopy: ({ filePath }) =>
|
|
38
|
-
`Missing required property for ${filePath}. The meta.json file is missing the "icon" property`,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
getUniqueModulesFromFiles(files) {
|
|
44
|
-
const uniqueModules = {};
|
|
45
|
-
|
|
46
|
-
files.forEach(file => {
|
|
47
|
-
if (isModuleFolderChild({ isLocal: true, path: file }, true)) {
|
|
48
|
-
const { base, dir } = path.parse(file);
|
|
49
|
-
if (!uniqueModules[dir]) {
|
|
50
|
-
uniqueModules[dir] = {};
|
|
51
|
-
}
|
|
52
|
-
uniqueModules[dir][base] = file;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
return uniqueModules;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Validates:
|
|
59
|
-
// - Theme does not have more than MODULE_LIMIT modules
|
|
60
|
-
// - Each module folder contains a meta.json file
|
|
61
|
-
// - Each module meta.json file contains valid json
|
|
62
|
-
// - Each module meta.json file has a "label" field
|
|
63
|
-
// - Each module meta.json file has an "icon" field
|
|
64
|
-
validate(files) {
|
|
65
|
-
let validationErrors = [];
|
|
66
|
-
const uniqueModules = this.getUniqueModulesFromFiles(files);
|
|
67
|
-
const numModules = Object.keys(uniqueModules).length;
|
|
68
|
-
|
|
69
|
-
if (numModules > MODULE_LIMIT) {
|
|
70
|
-
validationErrors.push(
|
|
71
|
-
this.getError(this.errors.LIMIT_EXCEEDED, null, {
|
|
72
|
-
limit: MODULE_LIMIT,
|
|
73
|
-
total: numModules,
|
|
74
|
-
})
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
Object.keys(uniqueModules).forEach(modulePath => {
|
|
79
|
-
const metaJSONFile = uniqueModules[modulePath]['meta.json'];
|
|
80
|
-
|
|
81
|
-
if (!metaJSONFile) {
|
|
82
|
-
validationErrors.push(
|
|
83
|
-
this.getError(this.errors.MISSING_META_JSON, modulePath)
|
|
84
|
-
);
|
|
85
|
-
} else {
|
|
86
|
-
let metaJSON;
|
|
87
|
-
try {
|
|
88
|
-
metaJSON = JSON.parse(fs.readFileSync(metaJSONFile));
|
|
89
|
-
} catch (err) {
|
|
90
|
-
validationErrors.push(
|
|
91
|
-
this.getError(this.errors.INVALID_META_JSON, modulePath)
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (metaJSON) {
|
|
96
|
-
if (!metaJSON.label) {
|
|
97
|
-
validationErrors.push(
|
|
98
|
-
this.getError(this.errors.MISSING_LABEL, modulePath)
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
if (!metaJSON.icon) {
|
|
102
|
-
validationErrors.push(
|
|
103
|
-
this.getError(this.errors.MISSING_ICON, modulePath)
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return validationErrors;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
module.exports = new ThemeModuleValidator({
|
|
115
|
-
name: 'Theme modules',
|
|
116
|
-
key: VALIDATOR_KEYS.themeModule,
|
|
117
|
-
});
|