@hubspot/cli 3.0.13-beta.1 → 3.0.13-beta.2

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.
Files changed (26) hide show
  1. package/bin/cli.js +2 -0
  2. package/commands/module/marketplace-validate.js +77 -0
  3. package/commands/module.js +17 -0
  4. package/commands/theme/marketplace-validate.js +4 -2
  5. package/lib/validators/__tests__/{BaseValidator.js → AbsoluteValidator.js} +3 -3
  6. package/lib/validators/__tests__/ModuleDependencyValidator.js +79 -0
  7. package/lib/validators/__tests__/ModuleValidator.js +22 -55
  8. package/lib/validators/__tests__/RelativeValidator.js +28 -0
  9. package/lib/validators/__tests__/TemplateValidator.js +1 -1
  10. package/lib/validators/__tests__/ThemeConfigValidator.js +1 -1
  11. package/lib/validators/__tests__/{DependencyValidator.js → ThemeDependencyValidator.js} +14 -12
  12. package/lib/validators/__tests__/ThemeModuleValidator.js +84 -0
  13. package/lib/validators/__tests__/validatorTestUtils.js +2 -0
  14. package/lib/validators/applyValidators.js +20 -4
  15. package/lib/validators/constants.js +4 -2
  16. package/lib/validators/index.js +8 -4
  17. package/lib/validators/marketplaceValidators/{BaseValidator.js → AbsoluteValidator.js} +8 -8
  18. package/lib/validators/marketplaceValidators/RelativeValidator.js +46 -0
  19. package/lib/validators/marketplaceValidators/module/ModuleDependencyValidator.js +101 -0
  20. package/lib/validators/marketplaceValidators/module/ModuleValidator.js +102 -0
  21. package/lib/validators/marketplaceValidators/theme/SectionValidator.js +2 -2
  22. package/lib/validators/marketplaceValidators/theme/TemplateValidator.js +2 -2
  23. package/lib/validators/marketplaceValidators/theme/ThemeConfigValidator.js +3 -3
  24. package/lib/validators/marketplaceValidators/theme/{DependencyValidator.js → ThemeDependencyValidator.js} +15 -12
  25. package/lib/validators/marketplaceValidators/theme/{ModuleValidator.js → ThemeModuleValidator.js} +5 -5
  26. package/package.json +4 -4
package/bin/cli.js CHANGED
@@ -31,6 +31,7 @@ const openCommand = require('../commands/open');
31
31
  const mvCommand = require('../commands/mv');
32
32
  const projectCommands = require('../commands/project');
33
33
  const themeCommand = require('../commands/theme');
34
+ const moduleCommand = require('../commands/module');
34
35
  const configCommand = require('../commands/config');
35
36
  const accountsCommand = require('../commands/accounts');
36
37
  const sandboxesCommand = require('../commands/sandbox');
@@ -120,6 +121,7 @@ const argv = yargs
120
121
  .command(mvCommand)
121
122
  .command(projectCommands)
122
123
  .command(themeCommand)
124
+ .command(moduleCommand)
123
125
  .command(configCommand)
124
126
  .command(accountsCommand)
125
127
  .command(sandboxesCommand)
@@ -0,0 +1,77 @@
1
+ const { logger } = require('@hubspot/cli-lib/logger');
2
+
3
+ const {
4
+ addConfigOptions,
5
+ addAccountOptions,
6
+ addUseEnvironmentOptions,
7
+ getAccountId,
8
+ } = require('../../lib/commonOpts');
9
+ const { loadAndValidateOptions } = require('../../lib/validation');
10
+ const { trackCommandUsage } = require('../../lib/usageTracking');
11
+ const {
12
+ logValidatorResults,
13
+ } = require('../../lib/validators/logValidatorResults');
14
+ const {
15
+ applyRelativeValidators,
16
+ } = require('../../lib/validators/applyValidators');
17
+ const MARKETPLACE_VALIDATORS = require('../../lib/validators');
18
+ const { VALIDATION_RESULT } = require('../../lib/validators/constants');
19
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
20
+
21
+ const i18nKey = 'cli.commands.module.subcommands.marketplaceValidate';
22
+ const { EXIT_CODES } = require('../../lib/enums/exitCodes');
23
+
24
+ exports.command = 'marketplace-validate <src>';
25
+ exports.describe = i18n(`${i18nKey}.describe`);
26
+
27
+ exports.handler = async options => {
28
+ const { src } = options;
29
+
30
+ await loadAndValidateOptions(options);
31
+
32
+ const accountId = getAccountId(options);
33
+
34
+ if (!options.json) {
35
+ logger.log(
36
+ i18n(`${i18nKey}.logs.validatingModule`, {
37
+ path: src,
38
+ })
39
+ );
40
+ }
41
+ trackCommandUsage('validate', {}, accountId);
42
+
43
+ applyRelativeValidators(
44
+ MARKETPLACE_VALIDATORS.module,
45
+ src,
46
+ src,
47
+ accountId
48
+ ).then(groupedResults => {
49
+ logValidatorResults(groupedResults, { logAsJson: options.json });
50
+
51
+ if (
52
+ groupedResults
53
+ .flat()
54
+ .some(result => result.result === VALIDATION_RESULT.FATAL)
55
+ ) {
56
+ process.exit(EXIT_CODES.WARNING);
57
+ }
58
+ });
59
+ };
60
+
61
+ exports.builder = yargs => {
62
+ addConfigOptions(yargs, true);
63
+ addAccountOptions(yargs, true);
64
+ addUseEnvironmentOptions(yargs, true);
65
+
66
+ yargs.options({
67
+ json: {
68
+ describe: i18n(`${i18nKey}.options.json.describe`),
69
+ type: 'boolean',
70
+ },
71
+ });
72
+ yargs.positional('src', {
73
+ describe: i18n(`${i18nKey}.positionals.src.describe`),
74
+ type: 'string',
75
+ });
76
+ return yargs;
77
+ };
@@ -0,0 +1,17 @@
1
+ const marketplaceValidate = require('./module/marketplace-validate');
2
+ const { addConfigOptions, addAccountOptions } = require('../lib/commonOpts');
3
+ // const { i18n } = require('@hubspot/cli-lib/lib/lang');
4
+
5
+ // const i18nKey = 'cli.commands.module';
6
+
7
+ exports.command = 'module';
8
+ exports.describe = false; //i18n(`${i18nKey}.describe`);
9
+
10
+ exports.builder = yargs => {
11
+ addConfigOptions(yargs, true);
12
+ addAccountOptions(yargs, true);
13
+
14
+ yargs.command(marketplaceValidate).demandCommand(1, '');
15
+
16
+ return yargs;
17
+ };
@@ -16,7 +16,9 @@ const { trackCommandUsage } = require('../../lib/usageTracking');
16
16
  const {
17
17
  logValidatorResults,
18
18
  } = require('../../lib/validators/logValidatorResults');
19
- const { applyValidators } = require('../../lib/validators/applyValidators');
19
+ const {
20
+ applyAbsoluteValidators,
21
+ } = require('../../lib/validators/applyValidators');
20
22
  const MARKETPLACE_VALIDATORS = require('../../lib/validators');
21
23
  const { VALIDATION_RESULT } = require('../../lib/validators/constants');
22
24
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
@@ -65,7 +67,7 @@ exports.handler = async options => {
65
67
 
66
68
  const themeFiles = await walk(absoluteSrcPath);
67
69
 
68
- applyValidators(
70
+ applyAbsoluteValidators(
69
71
  MARKETPLACE_VALIDATORS.theme,
70
72
  absoluteSrcPath,
71
73
  themeFiles,
@@ -1,12 +1,12 @@
1
- const BaseValidator = require('../marketplaceValidators/BaseValidator');
1
+ const AbsoluteValidator = require('../marketplaceValidators/AbsoluteValidator');
2
2
  const { VALIDATION_RESULT } = require('../constants');
3
3
 
4
- const Validator = new BaseValidator({
4
+ const Validator = new AbsoluteValidator({
5
5
  name: 'Test validator',
6
6
  key: 'validatorKey',
7
7
  });
8
8
 
9
- describe('validators/marketplaceValidators/BaseValidator', () => {
9
+ describe('validators/marketplaceValidators/AbsoluteValidator', () => {
10
10
  it('getSuccess returns expected object', async () => {
11
11
  const success = Validator.getSuccess();
12
12
 
@@ -0,0 +1,79 @@
1
+ const marketplace = require('@hubspot/cli-lib/api/marketplace');
2
+ const path = require('path');
3
+
4
+ const ModuleDependencyValidator = require('../marketplaceValidators/module/ModuleDependencyValidator');
5
+ const { VALIDATION_RESULT } = require('../constants');
6
+ const { MODULE_PATH } = require('./validatorTestUtils');
7
+
8
+ jest.mock('@hubspot/cli-lib/api/marketplace');
9
+
10
+ const getMockDependencyResult = (customPaths = []) => {
11
+ const result = {
12
+ dependencies: [...customPaths],
13
+ };
14
+ return Promise.resolve(result);
15
+ };
16
+
17
+ describe('validators/marketplaceValidators/module/ModuleDependencyValidator', () => {
18
+ beforeEach(() => {
19
+ ModuleDependencyValidator.setRelativePath(MODULE_PATH);
20
+ });
21
+
22
+ describe('isExternalDep', () => {
23
+ beforeEach(() => {
24
+ ModuleDependencyValidator.setRelativePath(MODULE_PATH);
25
+ });
26
+
27
+ it('returns true if dep is external to the provided absolute path', () => {
28
+ const isExternal = ModuleDependencyValidator.isExternalDep(
29
+ MODULE_PATH,
30
+ 'SomeOtherFolder/In/The/DesignManager.css'
31
+ );
32
+ expect(isExternal).toBe(true);
33
+ });
34
+
35
+ it('returns false if dep is not external to the provided absolute path', () => {
36
+ const isExternal = ModuleDependencyValidator.isExternalDep(
37
+ MODULE_PATH,
38
+ `${path.parse(MODULE_PATH).dir}/Internal/Folder/style.css`
39
+ );
40
+ expect(isExternal).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe('validate', () => {
45
+ it('returns error if any referenced path is absolute', async () => {
46
+ marketplace.fetchModuleDependencies.mockReturnValue(
47
+ getMockDependencyResult(['/absolute/path'])
48
+ );
49
+ const validationErrors = await ModuleDependencyValidator.validate(
50
+ MODULE_PATH
51
+ );
52
+
53
+ expect(validationErrors.length).toBe(1);
54
+ expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
55
+ });
56
+
57
+ it('returns error if any referenced path is external to the theme', async () => {
58
+ marketplace.fetchModuleDependencies.mockReturnValue(
59
+ getMockDependencyResult(['../../external/file-3.js'])
60
+ );
61
+ const validationErrors = await ModuleDependencyValidator.validate(
62
+ MODULE_PATH
63
+ );
64
+
65
+ expect(validationErrors.length).toBe(1);
66
+ expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
67
+ });
68
+
69
+ it('returns no errors if paths are relative and internal', async () => {
70
+ marketplace.fetchModuleDependencies.mockReturnValue(
71
+ getMockDependencyResult(['module/style.css', 'module/another/test.js'])
72
+ );
73
+ const validationErrors = await ModuleDependencyValidator.validate(
74
+ MODULE_PATH
75
+ );
76
+ expect(validationErrors.length).toBe(0);
77
+ });
78
+ });
79
+ });
@@ -1,84 +1,51 @@
1
- const fs = require('fs');
2
- const ModuleValidator = require('../marketplaceValidators/theme/ModuleValidator');
1
+ const ModuleValidator = require('../marketplaceValidators/module/ModuleValidator');
3
2
  const { VALIDATION_RESULT } = require('../constants');
4
- const {
5
- generateModulesList,
6
- makeFindError,
7
- THEME_PATH,
8
- } = require('./validatorTestUtils');
3
+ const { MODULE_PATH } = require('./validatorTestUtils');
4
+ const marketplace = require('@hubspot/cli-lib/api/marketplace');
9
5
 
10
- jest.mock('fs');
6
+ jest.mock('@hubspot/cli-lib/api/marketplace');
11
7
 
12
- const MODULE_LIMIT = 50;
13
-
14
- const findError = makeFindError('module');
15
-
16
- describe('validators/marketplaceValidators/theme/ModuleValidator', () => {
8
+ describe('validators/marketplaceValidators/module/ModuleValidator', () => {
17
9
  beforeEach(() => {
18
- ModuleValidator.setThemePath(THEME_PATH);
19
- });
20
-
21
- it('returns error if module limit is exceeded', async () => {
22
- const validationErrors = ModuleValidator.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 = ModuleValidator.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 = ModuleValidator.validate([
40
- 'module.module/module.html',
41
- ]);
42
- expect(validationErrors.length).toBe(1);
43
- expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
10
+ ModuleValidator.setRelativePath(MODULE_PATH);
44
11
  });
45
12
 
46
13
  it('returns error if module meta.json file has invalid json', async () => {
47
- fs.readFileSync.mockReturnValue('{} bad json }');
14
+ marketplace.fetchModuleMeta.mockReturnValue(
15
+ Promise.resolve({ source: '{} bad json }' })
16
+ );
48
17
 
49
- const validationErrors = ModuleValidator.validate([
50
- 'module.module/meta.json',
51
- ]);
18
+ const validationErrors = await ModuleValidator.validate(MODULE_PATH);
52
19
  expect(validationErrors.length).toBe(1);
53
20
  expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
54
21
  });
55
22
 
56
23
  it('returns error if module meta.json file is missing a label field', async () => {
57
- fs.readFileSync.mockReturnValue('{ "icon": "woo" }');
24
+ marketplace.fetchModuleMeta.mockReturnValue(
25
+ Promise.resolve({ source: '{ "icon": "woo" }' })
26
+ );
58
27
 
59
- const validationErrors = ModuleValidator.validate([
60
- 'module.module/meta.json',
61
- ]);
28
+ const validationErrors = await ModuleValidator.validate(MODULE_PATH);
62
29
  expect(validationErrors.length).toBe(1);
63
30
  expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
64
31
  });
65
32
 
66
33
  it('returns error if module meta.json file is missing an icon field', async () => {
67
- fs.readFileSync.mockReturnValue('{ "label": "yay" }');
34
+ marketplace.fetchModuleMeta.mockReturnValue(
35
+ Promise.resolve({ source: '{ "label": "yay" }' })
36
+ );
68
37
 
69
- const validationErrors = ModuleValidator.validate([
70
- 'module.module/meta.json',
71
- ]);
38
+ const validationErrors = await ModuleValidator.validate(MODULE_PATH);
72
39
  expect(validationErrors.length).toBe(1);
73
40
  expect(validationErrors[0].result).toBe(VALIDATION_RESULT.FATAL);
74
41
  });
75
42
 
76
43
  it('returns no error if module meta.json file exists and has all required fields', async () => {
77
- fs.readFileSync.mockReturnValue('{ "label": "yay", "icon": "woo" }');
44
+ marketplace.fetchModuleMeta.mockReturnValue(
45
+ Promise.resolve({ source: '{ "label": "yay", "icon": "woo" }' })
46
+ );
78
47
 
79
- const validationErrors = ModuleValidator.validate([
80
- 'module.module/meta.json',
81
- ]);
48
+ const validationErrors = await ModuleValidator.validate(MODULE_PATH);
82
49
  expect(validationErrors.length).toBe(0);
83
50
  });
84
51
  });
@@ -0,0 +1,28 @@
1
+ const RelativeValidator = require('../marketplaceValidators/RelativeValidator');
2
+ const { VALIDATION_RESULT } = require('../constants');
3
+
4
+ const Validator = new RelativeValidator({
5
+ name: 'Test validator',
6
+ key: 'validatorKey',
7
+ });
8
+
9
+ describe('validators/marketplaceValidators/RelativeValidator', () => {
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
+ });
@@ -29,7 +29,7 @@ const findError = makeFindError('template');
29
29
 
30
30
  describe('validators/marketplaceValidators/theme/TemplateValidator', () => {
31
31
  beforeEach(() => {
32
- TemplateValidator.setThemePath(THEME_PATH);
32
+ TemplateValidator.setAbsolutePath(THEME_PATH);
33
33
  templates.isCodedFile.mockReturnValue(true);
34
34
  });
35
35
 
@@ -10,7 +10,7 @@ jest.mock('path');
10
10
 
11
11
  describe('validators/marketplaceValidators/theme/ThemeConfigValidator', () => {
12
12
  beforeEach(() => {
13
- ThemeConfigValidator.setThemePath(THEME_PATH);
13
+ ThemeConfigValidator.setAbsolutePath(THEME_PATH);
14
14
  });
15
15
 
16
16
  it('returns error if no theme.json file exists', async () => {
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs-extra');
2
2
  const marketplace = require('@hubspot/cli-lib/api/marketplace');
3
3
 
4
- const DependencyValidator = require('../marketplaceValidators/theme/DependencyValidator');
4
+ const ThemeDependencyValidator = require('../marketplaceValidators/theme/ThemeDependencyValidator');
5
5
  const { VALIDATION_RESULT } = require('../constants');
6
6
  const { THEME_PATH } = require('./validatorTestUtils');
7
7
 
@@ -15,20 +15,20 @@ const getMockDependencyResult = (customPaths = []) => {
15
15
  return Promise.resolve(result);
16
16
  };
17
17
 
18
- describe('validators/marketplaceValidators/theme/DependencyValidator', () => {
18
+ describe('validators/marketplaceValidators/theme/ThemeDependencyValidator', () => {
19
19
  beforeEach(() => {
20
- DependencyValidator.setThemePath(THEME_PATH);
20
+ ThemeDependencyValidator.setAbsolutePath(THEME_PATH);
21
21
  });
22
22
 
23
23
  describe('isExternalDep', () => {
24
24
  beforeEach(() => {
25
- DependencyValidator.setThemePath(THEME_PATH);
25
+ ThemeDependencyValidator.setAbsolutePath(THEME_PATH);
26
26
  });
27
27
 
28
28
  it('returns true if dep is external to the provided absolute path', () => {
29
29
  const absoluteFilePath = `${THEME_PATH}/file.js`;
30
30
  const relativeDepPath = '../external/dep/path/file2.js';
31
- const isExternal = DependencyValidator.isExternalDep(
31
+ const isExternal = ThemeDependencyValidator.isExternalDep(
32
32
  absoluteFilePath,
33
33
  relativeDepPath
34
34
  );
@@ -38,7 +38,7 @@ describe('validators/marketplaceValidators/theme/DependencyValidator', () => {
38
38
  it('returns false if dep is not external to the provided absolute path', () => {
39
39
  const absoluteFilePath = `${THEME_PATH}/file.js`;
40
40
  const relativeDepPath = './internal/dep/path/file2.js';
41
- const isExternal = DependencyValidator.isExternalDep(
41
+ const isExternal = ThemeDependencyValidator.isExternalDep(
42
42
  absoluteFilePath,
43
43
  relativeDepPath
44
44
  );
@@ -52,10 +52,10 @@ describe('validators/marketplaceValidators/theme/DependencyValidator', () => {
52
52
  });
53
53
 
54
54
  it('returns error if any referenced path is absolute', async () => {
55
- marketplace.fetchDependencies.mockReturnValue(
55
+ marketplace.fetchTemplateDependencies.mockReturnValue(
56
56
  getMockDependencyResult(['/absolute/file-3.js'])
57
57
  );
58
- const validationErrors = await DependencyValidator.validate([
58
+ const validationErrors = await ThemeDependencyValidator.validate([
59
59
  `${THEME_PATH}/template.html`,
60
60
  ]);
61
61
 
@@ -64,10 +64,10 @@ describe('validators/marketplaceValidators/theme/DependencyValidator', () => {
64
64
  });
65
65
 
66
66
  it('returns error if any referenced path is external to the theme', async () => {
67
- marketplace.fetchDependencies.mockReturnValue(
67
+ marketplace.fetchTemplateDependencies.mockReturnValue(
68
68
  getMockDependencyResult(['../../external/file-3.js'])
69
69
  );
70
- const validationErrors = await DependencyValidator.validate([
70
+ const validationErrors = await ThemeDependencyValidator.validate([
71
71
  `${THEME_PATH}/template.html`,
72
72
  ]);
73
73
 
@@ -76,8 +76,10 @@ describe('validators/marketplaceValidators/theme/DependencyValidator', () => {
76
76
  });
77
77
 
78
78
  it('returns no errors if paths are relative and internal', async () => {
79
- marketplace.fetchDependencies.mockReturnValue(getMockDependencyResult());
80
- const validationErrors = await DependencyValidator.validate([
79
+ marketplace.fetchTemplateDependencies.mockReturnValue(
80
+ getMockDependencyResult()
81
+ );
82
+ const validationErrors = await ThemeDependencyValidator.validate([
81
83
  `${THEME_PATH}/template.html`,
82
84
  ]);
83
85
 
@@ -0,0 +1,84 @@
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
+ });
@@ -2,6 +2,7 @@
2
2
  test.skip('skip', () => null);
3
3
 
4
4
  const THEME_PATH = '/path/to/a/theme';
5
+ const MODULE_PATH = 'module/path';
5
6
 
6
7
  const makeFindError = baseKey => (errors, errorKey) =>
7
8
  errors.find(error => error.key === `${baseKey}.${errorKey}`);
@@ -31,4 +32,5 @@ module.exports = {
31
32
  generateTemplatesList,
32
33
  makeFindError,
33
34
  THEME_PATH,
35
+ MODULE_PATH,
34
36
  };
@@ -1,9 +1,9 @@
1
- async function applyValidators(validators, absoluteThemePath, ...args) {
1
+ async function applyAbsoluteValidators(validators, absolutePath, ...args) {
2
2
  return Promise.all(
3
3
  validators.map(async Validator => {
4
- Validator.setThemePath(absoluteThemePath);
4
+ Validator.setAbsolutePath(absolutePath);
5
5
  const validationResult = await Validator.validate(...args);
6
- Validator.clearThemePath();
6
+ Validator.clearAbsolutePath();
7
7
 
8
8
  if (!validationResult.length) {
9
9
  // Return a success obj so we can log the successes
@@ -14,4 +14,20 @@ async function applyValidators(validators, absoluteThemePath, ...args) {
14
14
  );
15
15
  }
16
16
 
17
- module.exports = { applyValidators };
17
+ async function applyRelativeValidators(validators, relativePath, ...args) {
18
+ return Promise.all(
19
+ validators.map(async Validator => {
20
+ Validator.setRelativePath(relativePath);
21
+ const validationResult = await Validator.validate(...args);
22
+ Validator.clearRelativePath();
23
+
24
+ if (!validationResult.length) {
25
+ // Return a success obj so we can log the successes
26
+ return [Validator.getSuccess()];
27
+ }
28
+ return validationResult;
29
+ })
30
+ );
31
+ }
32
+
33
+ module.exports = { applyAbsoluteValidators, applyRelativeValidators };
@@ -4,11 +4,13 @@ const SUCCESS = 'SUCCESS';
4
4
 
5
5
  const VALIDATION_RESULT = { WARNING, FATAL, SUCCESS };
6
6
  const VALIDATOR_KEYS = {
7
- dependency: 'dependency',
8
- module: 'module',
7
+ themeDependency: 'themeDependency',
8
+ themeModule: 'themeModule',
9
9
  section: 'section',
10
10
  template: 'template',
11
11
  themeConfig: 'themeConfig',
12
+ module: 'module',
13
+ moduleDependency: 'moduleDependency',
12
14
  };
13
15
 
14
16
  module.exports = { VALIDATOR_KEYS, VALIDATION_RESULT };
@@ -1,17 +1,21 @@
1
1
  const ThemeConfigValidator = require('./marketplaceValidators/theme/ThemeConfigValidator');
2
2
  const SectionValidator = require('./marketplaceValidators/theme/SectionValidator');
3
3
  const TemplateValidator = require('./marketplaceValidators/theme/TemplateValidator');
4
- const ModuleValidator = require('./marketplaceValidators/theme/ModuleValidator');
5
- const DependencyValidator = require('./marketplaceValidators/theme/DependencyValidator');
4
+ const ThemeModuleValidator = require('./marketplaceValidators/theme/ThemeModuleValidator');
5
+ const ThemeDependencyValidator = require('./marketplaceValidators/theme/ThemeDependencyValidator');
6
+
7
+ const ModuleValidator = require('./marketplaceValidators/module/ModuleValidator');
8
+ const ModuleDependencyValidator = require('./marketplaceValidators/module/ModuleDependencyValidator');
6
9
 
7
10
  const MARKETPLACE_VALIDATORS = {
8
11
  theme: [
9
12
  ThemeConfigValidator,
10
13
  SectionValidator,
11
14
  TemplateValidator,
12
- ModuleValidator,
13
- DependencyValidator,
15
+ ThemeModuleValidator,
16
+ ThemeDependencyValidator,
14
17
  ],
18
+ module: [ModuleValidator, ModuleDependencyValidator],
15
19
  };
16
20
 
17
21
  module.exports = MARKETPLACE_VALIDATORS;
@@ -2,23 +2,23 @@ const path = require('path');
2
2
 
3
3
  const { VALIDATION_RESULT } = require('../constants');
4
4
 
5
- class BaseValidator {
5
+ class AbsoluteValidator {
6
6
  constructor({ name, key }) {
7
7
  this.name = name;
8
8
  this.key = key;
9
9
  }
10
10
 
11
- clearThemePath() {
12
- this._absoluteThemePath = null;
11
+ clearAbsolutePath() {
12
+ this._absolutePath = null;
13
13
  }
14
14
 
15
- setThemePath(path) {
16
- this._absoluteThemePath = path;
15
+ setAbsolutePath(path) {
16
+ this._absolutePath = path;
17
17
  }
18
18
 
19
19
  getRelativePath(filePath) {
20
- return this._absoluteThemePath
21
- ? path.relative(this._absoluteThemePath, filePath)
20
+ return this._absolutePath
21
+ ? path.relative(this._absolutePath, filePath)
22
22
  : filePath;
23
23
  }
24
24
 
@@ -47,4 +47,4 @@ class BaseValidator {
47
47
  }
48
48
  }
49
49
 
50
- module.exports = BaseValidator;
50
+ module.exports = AbsoluteValidator;
@@ -0,0 +1,46 @@
1
+ const { VALIDATION_RESULT } = require('../constants');
2
+
3
+ class RelativeValidator {
4
+ constructor({ name, key }) {
5
+ this.name = name;
6
+ this.key = key;
7
+ }
8
+
9
+ clearRelativePath() {
10
+ this._relativePath = null;
11
+ }
12
+
13
+ setRelativePath(path) {
14
+ this._relativePath = path;
15
+ }
16
+
17
+ getRelativePath() {
18
+ return this._relativePath;
19
+ }
20
+
21
+ getSuccess() {
22
+ return {
23
+ validatorKey: this.key,
24
+ validatorName: this.name,
25
+ result: VALIDATION_RESULT.SUCCESS,
26
+ };
27
+ }
28
+
29
+ getError(errorObj, file, extraContext = {}) {
30
+ const relativeFilePath = this.getRelativePath();
31
+ const context = {
32
+ filePath: relativeFilePath,
33
+ ...extraContext,
34
+ };
35
+ return {
36
+ validatorKey: this.key,
37
+ validatorName: this.name,
38
+ error: errorObj.getCopy(context),
39
+ result: errorObj.severity || VALIDATION_RESULT.FATAL,
40
+ key: `${this.key}.${errorObj.key}`,
41
+ context,
42
+ };
43
+ }
44
+ }
45
+
46
+ module.exports = RelativeValidator;
@@ -0,0 +1,101 @@
1
+ const path = require('path');
2
+
3
+ const { logger } = require('@hubspot/cli-lib/logger');
4
+ const { HUBSPOT_FOLDER } = require('@hubspot/cli-lib/lib/constants');
5
+ const { fetchModuleDependencies } = require('@hubspot/cli-lib/api/marketplace');
6
+ const { isRelativePath } = require('@hubspot/cli-lib/path');
7
+
8
+ const RelativeValidator = require('../RelativeValidator');
9
+ const { VALIDATOR_KEYS } = require('../../constants');
10
+
11
+ class ModuleDependencyValidator extends RelativeValidator {
12
+ constructor(options) {
13
+ super(options);
14
+
15
+ this.errors = {
16
+ FAILED_TO_FETCH_DEPS: {
17
+ key: 'failedDepFetch',
18
+ getCopy: ({ filePath }) =>
19
+ `Internal Error. Failed to fetch dependencies for ${filePath}. Please try again`,
20
+ },
21
+ EXTERNAL_DEPENDENCY: {
22
+ key: 'externalDependency',
23
+ getCopy: ({ filePath, referencedFilePath }) =>
24
+ `External dependency. ${filePath} references a file (${referencedFilePath}) that is outside of the module's immediate folder.`,
25
+ },
26
+ ABSOLUTE_DEPENDENCY_PATH: {
27
+ key: 'absoluteDependencyPath',
28
+ getCopy: ({ filePath, referencedFilePath }) =>
29
+ `Relative path required. ${filePath} references a file (${referencedFilePath}) using an absolute path`,
30
+ },
31
+ };
32
+ }
33
+
34
+ failedToFetchDependencies(err, relativePath, validationErrors) {
35
+ logger.debug(
36
+ `Failed to fetch dependencies for ${relativePath}: `,
37
+ err.error
38
+ );
39
+
40
+ validationErrors.push(
41
+ this.getError(this.errors.FAILED_TO_FETCH_DEPS, relativePath)
42
+ );
43
+ }
44
+
45
+ async getAllDependenciesByPath(relativePath, accountId, validationErrors) {
46
+ let deps = [];
47
+ const file_deps = await fetchModuleDependencies(
48
+ accountId,
49
+ relativePath
50
+ ).catch(err => {
51
+ this.failedToFetchDependencies(err, relativePath, validationErrors);
52
+ return null;
53
+ });
54
+ if (file_deps) {
55
+ deps = file_deps.dependencies || [];
56
+ }
57
+ return deps;
58
+ }
59
+
60
+ isExternalDep(relPath, relativeDepPath) {
61
+ const moduleDir = path.parse(relPath).dir;
62
+ const depDir = path.parse(relativeDepPath).dir;
63
+ return !depDir.startsWith(moduleDir);
64
+ }
65
+
66
+ // Validates:
67
+ // - Module does not contain external dependencies
68
+ // - All paths are either @hubspot or relative
69
+ async validate(relativePath, accountId) {
70
+ let validationErrors = [];
71
+
72
+ const dependencyData = await this.getAllDependenciesByPath(
73
+ relativePath,
74
+ accountId,
75
+ validationErrors
76
+ );
77
+ dependencyData.forEach(dependency => {
78
+ if (!dependency.startsWith(HUBSPOT_FOLDER)) {
79
+ if (!isRelativePath(dependency)) {
80
+ validationErrors.push(
81
+ this.getError(this.errors.ABSOLUTE_DEPENDENCY_PATH, relativePath, {
82
+ referencedFilePath: dependency,
83
+ })
84
+ );
85
+ } else if (this.isExternalDep(relativePath, dependency)) {
86
+ validationErrors.push(
87
+ this.getError(this.errors.EXTERNAL_DEPENDENCY, relativePath, {
88
+ referencedFilePath: dependency,
89
+ })
90
+ );
91
+ }
92
+ }
93
+ });
94
+ return validationErrors;
95
+ }
96
+ }
97
+
98
+ module.exports = new ModuleDependencyValidator({
99
+ name: 'Module dependency',
100
+ key: VALIDATOR_KEYS.moduleDependency,
101
+ });
@@ -0,0 +1,102 @@
1
+ const { logger } = require('@hubspot/cli-lib/logger');
2
+ const { fetchModuleMeta } = require('@hubspot/cli-lib/api/marketplace');
3
+ const RelativeValidator = require('../RelativeValidator');
4
+ const { VALIDATOR_KEYS } = require('../../constants');
5
+
6
+ class ModuleValidator extends RelativeValidator {
7
+ constructor(options) {
8
+ super(options);
9
+
10
+ this.errors = {
11
+ FAILED_TO_FETCH_META_JSON: {
12
+ key: 'failedMetaFetch',
13
+ getCopy: ({ filePath }) =>
14
+ `Internal error. Failed to fetch meta.json for ${filePath}. Please try again.`,
15
+ },
16
+ MISSING_META_JSON: {
17
+ key: 'missingMetaJSON',
18
+ getCopy: ({ filePath }) =>
19
+ `Module ${filePath} is missing the meta.json file`,
20
+ },
21
+ INVALID_META_JSON: {
22
+ key: 'invalidMetaJSON',
23
+ getCopy: ({ filePath }) =>
24
+ `Module ${filePath} has invalid json in the meta.json file`,
25
+ },
26
+ MISSING_LABEL: {
27
+ key: 'missingLabel',
28
+ getCopy: ({ filePath }) =>
29
+ `Missing required property for ${filePath}. The meta.json file is missing the "label" property`,
30
+ },
31
+ MISSING_ICON: {
32
+ key: 'missingIcon',
33
+ getCopy: ({ filePath }) =>
34
+ `Missing required property for ${filePath}. The meta.json file is missing the "icon" property`,
35
+ },
36
+ };
37
+ }
38
+ failedToFetchDependencies(err, relativePath, validationErrors) {
39
+ logger.debug(
40
+ `Failed to fetch dependencies for ${relativePath}: `,
41
+ err.error
42
+ );
43
+ validationErrors.push(
44
+ this.getError(this.errors.FAILED_TO_FETCH_META_JSON, relativePath)
45
+ );
46
+ }
47
+
48
+ async getModuleMetaByPath(relativePath, accountId, validationErrors) {
49
+ const moduleMeta = await fetchModuleMeta(accountId, relativePath).catch(
50
+ err => {
51
+ this.failedToFetchDependencies(err, relativePath, validationErrors);
52
+ return null;
53
+ }
54
+ );
55
+ return moduleMeta;
56
+ }
57
+
58
+ // Validates:
59
+ // - Module folder contains a meta.json file
60
+ // - Module meta.json file contains valid json
61
+ // - Module meta.json file has a "label" field
62
+ // - Module meta.json file has an "icon" field
63
+ async validate(relativePath, accountId) {
64
+ let validationErrors = [];
65
+ const metaJSONFile = await this.getModuleMetaByPath(
66
+ relativePath,
67
+ accountId,
68
+ validationErrors
69
+ );
70
+ if (!metaJSONFile) {
71
+ validationErrors.push(
72
+ this.getError(this.errors.MISSING_META_JSON, relativePath)
73
+ );
74
+ }
75
+ let metaJSON;
76
+ try {
77
+ metaJSON = JSON.parse(metaJSONFile.source);
78
+ } catch (err) {
79
+ validationErrors.push(
80
+ this.getError(this.errors.INVALID_META_JSON, relativePath)
81
+ );
82
+ }
83
+ if (metaJSON) {
84
+ if (!metaJSON.label) {
85
+ validationErrors.push(
86
+ this.getError(this.errors.MISSING_LABEL, relativePath)
87
+ );
88
+ }
89
+ if (!metaJSON.icon) {
90
+ validationErrors.push(
91
+ this.getError(this.errors.MISSING_ICON, relativePath)
92
+ );
93
+ }
94
+ }
95
+ return validationErrors;
96
+ }
97
+ }
98
+
99
+ module.exports = new ModuleValidator({
100
+ name: 'Module',
101
+ key: VALIDATOR_KEYS.module,
102
+ });
@@ -2,12 +2,12 @@ const {
2
2
  ANNOTATION_KEYS,
3
3
  buildAnnotationValueGetter,
4
4
  } = require('@hubspot/cli-lib/templates');
5
- const BaseValidator = require('../BaseValidator');
5
+ const AbsoluteValidator = require('../AbsoluteValidator');
6
6
  const { VALIDATOR_KEYS } = require('../../constants');
7
7
 
8
8
  const SECTION_LIMIT = 50;
9
9
 
10
- class SectionValidator extends BaseValidator {
10
+ class SectionValidator extends AbsoluteValidator {
11
11
  constructor(options) {
12
12
  super(options);
13
13
 
@@ -3,7 +3,7 @@ const {
3
3
  buildAnnotationValueGetter,
4
4
  isCodedFile,
5
5
  } = require('@hubspot/cli-lib/templates');
6
- const BaseValidator = require('../BaseValidator');
6
+ const AbsoluteValidator = require('../AbsoluteValidator');
7
7
  const { VALIDATOR_KEYS } = require('../../constants');
8
8
 
9
9
  const TEMPLATE_LIMIT = 50;
@@ -50,7 +50,7 @@ const VALIDATIONS_BY_TYPE = {
50
50
  blog_post: { allowed: true, label: true, screenshot: true },
51
51
  };
52
52
 
53
- class TemplateValidator extends BaseValidator {
53
+ class TemplateValidator extends AbsoluteValidator {
54
54
  constructor(options) {
55
55
  super(options);
56
56
 
@@ -2,10 +2,10 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const { isRelativePath } = require('@hubspot/cli-lib/path');
5
- const BaseValidator = require('../BaseValidator');
5
+ const AbsoluteValidator = require('../AbsoluteValidator');
6
6
  const { VALIDATOR_KEYS } = require('../../constants');
7
7
 
8
- class ThemeValidator extends BaseValidator {
8
+ class ThemeValidator extends AbsoluteValidator {
9
9
  constructor(options) {
10
10
  super(options);
11
11
 
@@ -84,7 +84,7 @@ class ThemeValidator extends BaseValidator {
84
84
  );
85
85
  } else {
86
86
  const absoluteScreenshotPath = path.resolve(
87
- this._absoluteThemePath,
87
+ this._absolutePath,
88
88
  themeJSON.screenshot_path
89
89
  );
90
90
  if (!fs.existsSync(absoluteScreenshotPath)) {
@@ -6,13 +6,15 @@ const {
6
6
  HUBL_EXTENSIONS,
7
7
  HUBSPOT_FOLDER,
8
8
  } = require('@hubspot/cli-lib/lib/constants');
9
- const { fetchDependencies } = require('@hubspot/cli-lib/api/marketplace');
9
+ const {
10
+ fetchTemplateDependencies,
11
+ } = require('@hubspot/cli-lib/api/marketplace');
10
12
  const { getExt, isRelativePath } = require('@hubspot/cli-lib/path');
11
13
 
12
- const BaseValidator = require('../BaseValidator');
14
+ const AbsoluteValidator = require('../AbsoluteValidator');
13
15
  const { VALIDATOR_KEYS } = require('../../constants');
14
16
 
15
- class DependencyValidator extends BaseValidator {
17
+ class ThemeDependencyValidator extends AbsoluteValidator {
16
18
  constructor(options) {
17
19
  super(options);
18
20
 
@@ -53,12 +55,13 @@ class DependencyValidator extends BaseValidator {
53
55
  if (!(source && source.trim())) {
54
56
  return { file, deps };
55
57
  }
56
- const file_deps = await fetchDependencies(accountId, source).catch(
57
- err => {
58
- this.failedToFetchDependencies(err, file, validationErrors);
59
- return null;
60
- }
61
- );
58
+ const file_deps = await fetchTemplateDependencies(
59
+ accountId,
60
+ source
61
+ ).catch(err => {
62
+ this.failedToFetchDependencies(err, file, validationErrors);
63
+ return null;
64
+ });
62
65
  if (file_deps) {
63
66
  deps = file_deps.dependencies || [];
64
67
  }
@@ -117,7 +120,7 @@ class DependencyValidator extends BaseValidator {
117
120
  }
118
121
  }
119
122
 
120
- module.exports = new DependencyValidator({
121
- name: 'Dependency',
122
- key: VALIDATOR_KEYS.dependency,
123
+ module.exports = new ThemeDependencyValidator({
124
+ name: 'Theme dependency',
125
+ key: VALIDATOR_KEYS.themeDependency,
123
126
  });
@@ -2,12 +2,12 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const { isModuleFolderChild } = require('@hubspot/cli-lib/modules');
5
- const BaseValidator = require('../BaseValidator');
5
+ const AbsoluteValidator = require('../AbsoluteValidator');
6
6
  const { VALIDATOR_KEYS } = require('../../constants');
7
7
 
8
8
  const MODULE_LIMIT = 50;
9
9
 
10
- class ModuleValidator extends BaseValidator {
10
+ class ThemeModuleValidator extends AbsoluteValidator {
11
11
  constructor(options) {
12
12
  super(options);
13
13
 
@@ -111,7 +111,7 @@ class ModuleValidator extends BaseValidator {
111
111
  }
112
112
  }
113
113
 
114
- module.exports = new ModuleValidator({
115
- name: 'Module',
116
- key: VALIDATOR_KEYS.module,
114
+ module.exports = new ThemeModuleValidator({
115
+ name: 'Theme modules',
116
+ key: VALIDATOR_KEYS.themeModule,
117
117
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "3.0.13-beta.1",
3
+ "version": "3.0.13-beta.2",
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": "3.0.13-beta.1",
12
- "@hubspot/serverless-dev-runtime": "3.0.13-beta.1",
11
+ "@hubspot/cli-lib": "3.0.13-beta.2",
12
+ "@hubspot/serverless-dev-runtime": "3.0.13-beta.2",
13
13
  "archiver": "^5.3.0",
14
14
  "chalk": "^4.1.2",
15
15
  "express": "^4.17.1",
@@ -38,5 +38,5 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "1c84f581aac363971c008254b153613ace1241d9"
41
+ "gitHead": "ff694f8c61c2326159f7ad4d3f2cfdb18f8c1875"
42
42
  }