@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.
@@ -1,9 +1,7 @@
1
- const fs = require('fs');
2
- const path = require('path');
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
- logValidatorResults,
18
- } = require('../../lib/validators/logValidatorResults');
19
- const {
20
- applyAbsoluteValidators,
21
- } = require('../../lib/validators/applyValidators');
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
- const absoluteSrcPath = path.resolve(getCwd(), src);
39
- let stats;
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
- stats = fs.statSync(absoluteSrcPath);
42
- if (!stats.isDirectory()) {
43
- logger.error(
44
- i18n(`${i18nKey}.errors.invalidPath`, {
45
- path: src,
46
- })
47
- );
48
- return;
49
- }
50
- } catch (e) {
51
- logger.error(
52
- i18n(`${i18nKey}.errors.invalidPath`, {
53
- path: src,
54
- })
55
- );
56
- return;
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
- if (!options.json) {
60
- logger.log(
61
- i18n(`${i18nKey}.logs.validatingTheme`, {
62
- path: src,
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 themeFiles = await walk(absoluteSrcPath);
69
-
70
- applyAbsoluteValidators(
71
- MARKETPLACE_VALIDATORS.theme,
72
- absoluteSrcPath,
73
- themeFiles,
74
- accountId
75
- ).then(groupedResults => {
76
- logValidatorResults(groupedResults, { logAsJson: options.json });
77
-
78
- if (
79
- groupedResults
80
- .flat()
81
- .some(result => result.result === VALIDATION_RESULT.FATAL)
82
- ) {
83
- process.exit(EXIT_CODES.WARNING);
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 = { applyAbsoluteValidators, applyRelativeValidators };
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
  };
@@ -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.2-beta.7",
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.2-beta.7",
12
- "@hubspot/serverless-dev-runtime": "4.0.2-beta.7",
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": "0fe48550b57c3f8326262ed19decaeb4930f8c3a"
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
- });