@hubspot/cli 7.10.1-experimental.0 → 7.11.0-beta.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.
Files changed (32) hide show
  1. package/commands/project/__tests__/deploy.test.js +6 -6
  2. package/commands/project/__tests__/validate.test.js +27 -285
  3. package/commands/project/create.js +20 -14
  4. package/commands/project/deploy.js +6 -14
  5. package/commands/project/dev/index.js +4 -13
  6. package/commands/project/dev/unifiedFlow.js +7 -1
  7. package/commands/project/upload.js +2 -8
  8. package/commands/project/validate.js +12 -72
  9. package/lang/en.d.ts +19 -14
  10. package/lang/en.js +21 -16
  11. package/lib/__tests__/projectProfiles.test.js +32 -273
  12. package/lib/errorHandlers/index.js +10 -7
  13. package/lib/projectProfiles.d.ts +3 -4
  14. package/lib/projectProfiles.js +32 -78
  15. package/lib/projects/__tests__/components.test.js +2 -22
  16. package/lib/projects/__tests__/deploy.test.js +15 -13
  17. package/lib/projects/add/__tests__/legacyAddComponent.test.js +1 -1
  18. package/lib/projects/add/__tests__/v2AddComponent.test.js +30 -4
  19. package/lib/projects/add/legacyAddComponent.js +1 -1
  20. package/lib/projects/add/v2AddComponent.js +16 -5
  21. package/lib/projects/components.d.ts +8 -1
  22. package/lib/projects/components.js +91 -8
  23. package/lib/projects/deploy.js +21 -8
  24. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +9 -1
  25. package/lib/projects/localDev/helpers/process.js +5 -3
  26. package/lib/ui/SpinniesManager.d.ts +5 -7
  27. package/lib/ui/SpinniesManager.js +9 -12
  28. package/lib/ui/__tests__/SpinniesManager.test.d.ts +1 -0
  29. package/lib/ui/__tests__/SpinniesManager.test.js +489 -0
  30. package/mcp-server/utils/config.js +1 -1
  31. package/package.json +4 -4
  32. package/ui/components/BoxWithTitle.js +1 -1
@@ -8,19 +8,13 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
8
8
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
9
9
  import { validateSourceDirectory, handleTranslate, } from '../../lib/projects/upload.js';
10
10
  import { commands } from '../../lang/en.js';
11
- import { validateProjectForProfile } from '../../lib/projectProfiles.js';
11
+ import { loadAndValidateProfile } from '../../lib/projectProfiles.js';
12
12
  import { logError } from '../../lib/errorHandlers/index.js';
13
- import { getAllHsProfiles } from '@hubspot/project-parsing-lib';
14
- import SpinniesManager from '../../lib/ui/SpinniesManager.js';
15
13
  const command = 'validate';
16
14
  const describe = commands.project.validate.describe;
17
15
  async function handler(args) {
18
- SpinniesManager.init();
19
16
  const { derivedAccountId, profile } = args;
20
17
  const { projectConfig, projectDir } = await getProjectConfig();
21
- const accountConfig = getConfigAccountById(derivedAccountId);
22
- const accountType = accountConfig && accountConfig.accountType;
23
- trackCommandUsage('project-validate', { type: accountType }, derivedAccountId);
24
18
  if (!projectConfig || !projectDir) {
25
19
  uiLogger.error(commands.project.validate.mustBeRanWithinAProject);
26
20
  process.exit(EXIT_CODES.ERROR);
@@ -36,79 +30,25 @@ async function handler(args) {
36
30
  logError(error);
37
31
  process.exit(EXIT_CODES.ERROR);
38
32
  }
39
- let validationSucceeded = true;
33
+ let targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
34
+ targetAccountId = targetAccountId || derivedAccountId;
35
+ const accountConfig = getConfigAccountById(targetAccountId);
36
+ const accountType = accountConfig && accountConfig.accountType;
37
+ trackCommandUsage('project-validate', { type: accountType }, targetAccountId);
40
38
  const srcDir = path.resolve(projectDir, projectConfig.srcDir);
41
- // Get all of the profiles except the provided profile
42
- const profiles = (await getAllHsProfiles(path.join(projectDir, projectConfig.srcDir))).filter(profileName => profileName !== profile);
43
- // If a profile is specified, only validate that profile
44
- if (profile) {
45
- const validationErrors = await validateProjectForProfile(projectConfig, projectDir, profile, derivedAccountId);
46
- if (validationErrors.length) {
47
- validationErrors.forEach(error => {
48
- uiLogger.log('');
49
- if (error instanceof Error) {
50
- logError(error);
51
- }
52
- else {
53
- uiLogger.error(error);
54
- }
55
- });
56
- validationSucceeded = false;
57
- }
58
- }
59
- else if (profiles.length > 0) {
60
- // If no profile was specified and the project has profiles, validate all of them
61
- SpinniesManager.add('validatingAllProfiles', {
62
- text: commands.project.validate.spinners.validatingAllProfiles,
63
- });
64
- const errors = [];
65
- for (const profileName of profiles) {
66
- const validationErrors = await validateProjectForProfile(projectConfig, projectDir, profileName, derivedAccountId, true);
67
- if (validationErrors.length) {
68
- errors.push(...validationErrors);
69
- validationSucceeded = false;
70
- }
71
- }
72
- if (validationSucceeded) {
73
- SpinniesManager.succeed('validatingAllProfiles', {
74
- text: commands.project.validate.spinners.allProfilesValidationSucceeded,
75
- });
76
- }
77
- else {
78
- SpinniesManager.fail('validatingAllProfiles', {
79
- text: commands.project.validate.spinners.allProfilesValidationFailed,
80
- });
81
- }
82
- errors.forEach(error => {
83
- uiLogger.log('');
84
- if (error instanceof Error) {
85
- logError(error);
86
- }
87
- else {
88
- uiLogger.error(error);
89
- }
90
- });
91
- }
92
- else if (profiles.length === 0) {
93
- // If the project has no profiles, validate the project without a profile
94
- try {
95
- await handleTranslate(projectDir, projectConfig, derivedAccountId, false, undefined);
96
- }
97
- catch (e) {
98
- uiLogger.error(commands.project.validate.failure(projectConfig.name));
99
- logError(e);
100
- validationSucceeded = false;
101
- uiLogger.log('');
102
- }
39
+ try {
40
+ await validateSourceDirectory(srcDir, projectConfig, projectDir);
103
41
  }
104
- if (!validationSucceeded) {
42
+ catch (e) {
43
+ logError(e);
105
44
  process.exit(EXIT_CODES.ERROR);
106
45
  }
107
46
  try {
108
- await validateSourceDirectory(srcDir, projectConfig, projectDir);
47
+ await handleTranslate(projectDir, projectConfig, targetAccountId, false, profile);
109
48
  }
110
49
  catch (e) {
111
50
  logError(e);
51
+ uiLogger.error(commands.project.validate.failure(projectConfig.name));
112
52
  process.exit(EXIT_CODES.ERROR);
113
53
  }
114
54
  uiLogger.success(commands.project.validate.success(projectConfig.name));
package/lang/en.d.ts CHANGED
@@ -1366,8 +1366,10 @@ export declare const commands: {
1366
1366
  };
1367
1367
  logs: {
1368
1368
  success: (projectName: string, projectDest: string) => string;
1369
- welcomeMessage: string;
1370
1369
  };
1370
+ creatingComponent: (isProjectEmpty: boolean, projectName: string) => string;
1371
+ success: (isProjectEmpty: boolean, projectName: string) => string;
1372
+ failure: (isProjectEmpty: boolean, projectName: string) => string;
1371
1373
  prompts: {
1372
1374
  parentComponents: string;
1373
1375
  emptyProject: string;
@@ -1508,7 +1510,8 @@ export declare const commands: {
1508
1510
  };
1509
1511
  };
1510
1512
  creatingComponent: (projectName: string) => string;
1511
- success: (componentName: string, multiple?: boolean) => string;
1513
+ success: (projectName: string) => string;
1514
+ failure: (projectName: string) => string;
1512
1515
  error: {
1513
1516
  failedToDownloadComponent: string;
1514
1517
  invalidComponentType: (componentType: string) => string;
@@ -1802,16 +1805,7 @@ export declare const commands: {
1802
1805
  default: string;
1803
1806
  };
1804
1807
  success: (projectName: string) => string;
1805
- failure: (projectName: string, profileName?: string) => string;
1806
- spinners: {
1807
- validatingProfile: (profileName: string) => string;
1808
- profileValidationFailed: (profileName: string) => string;
1809
- profileValidationSucceeded: (profileName: string) => string;
1810
- invalidWithProfile: (profileName: string, projectName: string) => string;
1811
- validatingAllProfiles: string;
1812
- allProfilesValidationSucceeded: string;
1813
- allProfilesValidationFailed: string;
1814
- };
1808
+ failure: (projectName: string) => string;
1815
1809
  options: {
1816
1810
  profile: {
1817
1811
  describe: string;
@@ -2990,9 +2984,7 @@ export declare const lib: {
2990
2984
  noProjectConfig: string;
2991
2985
  profileNotFound: (profileName: string) => string;
2992
2986
  missingAccountId: (profileName: string) => string;
2993
- listedAccountNotFound: (accountId: number, profileName: string) => string;
2994
2987
  failedToLoadProfile: (profileName: string) => string;
2995
- profileNotValid: (profileName: string, errors: string[]) => string;
2996
2988
  };
2997
2989
  };
2998
2990
  };
@@ -3049,6 +3041,18 @@ export declare const lib: {
3049
3041
  feedbackHeader: string;
3050
3042
  feedbackMessage: string;
3051
3043
  };
3044
+ components: {
3045
+ unableToGetUidFromHsmeta: string;
3046
+ buildSuccessMessage: {
3047
+ seeOurDocs: string;
3048
+ docsUrl: string;
3049
+ headerCreated: (projectName: string, projectDest: string) => string;
3050
+ headerAdded: (featureText: string, uid: string, plural: boolean) => string;
3051
+ docsDetails: (docsLink: string) => string;
3052
+ uploadPrompt: string;
3053
+ devPrompt: string;
3054
+ };
3055
+ };
3052
3056
  };
3053
3057
  projectBuildAndDeploy: {
3054
3058
  makePollTaskStatusFunc: {
@@ -3657,6 +3661,7 @@ export declare const lib: {
3657
3661
  unknownErrorOccurred: string;
3658
3662
  configTimeoutErrorOccurred: (timeout: number, configSetCommand: string) => string;
3659
3663
  genericTimeoutErrorOccurred: string;
3664
+ additionalDebugContext: string;
3660
3665
  };
3661
3666
  suppressErrors: {
3662
3667
  platformVersionErrors: {
package/lang/en.js CHANGED
@@ -1381,8 +1381,10 @@ export const commands = {
1381
1381
  },
1382
1382
  logs: {
1383
1383
  success: (projectName, projectDest) => `Project ${chalk.bold(projectName)} was successfully created in ${projectDest}`,
1384
- welcomeMessage: `\n${chalk.bold('Welcome to HubSpot Developer Projects!')}`,
1385
1384
  },
1385
+ creatingComponent: (isProjectEmpty, projectName) => `${isProjectEmpty ? `Creating empty project [${chalk.bold(projectName)}]` : `Adding feature(s) to app [${chalk.bold(projectName)}]`}\n`,
1386
+ success: (isProjectEmpty, projectName) => `${isProjectEmpty ? `Created empty project [${chalk.bold(projectName)}]` : `Added feature(s) to app [${chalk.bold(projectName)}]`}\n`,
1387
+ failure: (isProjectEmpty, projectName) => `${isProjectEmpty ? `Failed to create project [${chalk.bold(projectName)}]` : `Failed to add feature(s) to app [${chalk.bold(projectName)}]`}\n`,
1386
1388
  prompts: {
1387
1389
  parentComponents: '[--project-base] Choose what to include in your project:',
1388
1390
  emptyProject: 'Empty Project',
@@ -1522,8 +1524,9 @@ export const commands = {
1522
1524
  describe: 'Which features to include with the application.',
1523
1525
  },
1524
1526
  },
1525
- creatingComponent: (projectName) => `\nAdding a new app feature to ${chalk.bold(projectName)}\n`,
1526
- success: (componentName, multiple = false) => `${componentName || 'An app'} ${multiple ? 'were' : 'was'} successfully added to your ${componentName ? 'app' : 'project'}.`,
1527
+ creatingComponent: (projectName) => `Adding feature(s) to app [${chalk.bold(projectName)}]\n`,
1528
+ success: (projectName) => `Added feature(s) to app [${chalk.bold(projectName)}]\n`,
1529
+ failure: (projectName) => `Failed to add feature(s) to app [${chalk.bold(projectName)}]\n`,
1527
1530
  error: {
1528
1531
  failedToDownloadComponent: 'Failed to download project. Please try again later.',
1529
1532
  invalidComponentType: (componentType) => `'${componentType}' is not a valid project component type.`,
@@ -1824,19 +1827,10 @@ export const commands = {
1824
1827
  default: 'Validate the project before uploading',
1825
1828
  },
1826
1829
  success: (projectName) => `Project ${projectName} is valid and ready to upload`,
1827
- failure: (projectName, profileName) => `Project ${projectName} is invalid${profileName ? `with profile ${profileName} applied` : ''}`,
1828
- spinners: {
1829
- validatingProfile: (profileName) => `Validating project with profile "${profileName}"`,
1830
- profileValidationFailed: (profileName) => `Profile "${profileName}" failed validation`,
1831
- profileValidationSucceeded: (profileName) => `Project valid with profile "${profileName}" applied`,
1832
- invalidWithProfile: (profileName, projectName) => `Project is invalid with profile "${profileName}" applied \n ${commands.project.validate.failure(projectName)}`,
1833
- validatingAllProfiles: 'Validating the project with all profiles',
1834
- allProfilesValidationSucceeded: 'Project profile validation succeeded',
1835
- allProfilesValidationFailed: 'Project profile validation failed',
1836
- },
1830
+ failure: (projectName) => `Project ${projectName} is invalid`,
1837
1831
  options: {
1838
1832
  profile: {
1839
- describe: 'The profile to target for this validation. If no profile is provided, all profiles will be validated.',
1833
+ describe: 'The profile to target for this validation',
1840
1834
  },
1841
1835
  },
1842
1836
  },
@@ -3012,9 +3006,7 @@ export const lib = {
3012
3006
  noProjectConfig: 'No project config found. Please run this command from a project directory.',
3013
3007
  profileNotFound: (profileName) => `Profile ${chalk.bold(profileName)} not found.`,
3014
3008
  missingAccountId: (profileName) => `Profile ${chalk.bold(profileName)} is missing an account id.`,
3015
- listedAccountNotFound: (accountId, profileName) => `The account ${uiAccountDescription(accountId)} is defined in your profile ${chalk.bold(profileName)}, but is missing in your config file`,
3016
3009
  failedToLoadProfile: (profileName) => `Failed to load profile ${chalk.bold(profileName)}.`,
3017
- profileNotValid: (profileName, errors) => `Profile "${profileName}" is not valid:\n\t- ${errors.join('\n\t- ')}`,
3018
3010
  },
3019
3011
  },
3020
3012
  },
@@ -3071,6 +3063,18 @@ export const lib = {
3071
3063
  feedbackHeader: "We'd love to hear your feedback!",
3072
3064
  feedbackMessage: `How are you liking the new projects and developer tools? \n > Run ${uiCommandReference('hs feedback')} to let us know what you think!\n`,
3073
3065
  },
3066
+ components: {
3067
+ unableToGetUidFromHsmeta: 'Unable to get UID from hsmeta',
3068
+ buildSuccessMessage: {
3069
+ seeOurDocs: 'See our docs',
3070
+ docsUrl: 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/overview',
3071
+ headerCreated: (projectName, projectDest) => `${chalk.bold(projectName)} was successfully created in ${projectDest}`,
3072
+ headerAdded: (featureText, uid, plural) => `${featureText} ${plural ? 'were' : 'was'} successfully added to ${uid}:`,
3073
+ docsDetails: (docsLink) => `📖 ${docsLink} for more details about building and testing these features.`,
3074
+ uploadPrompt: `🚀 Run ${uiCommandReference('hs project upload')} when you're ready to deploy.`,
3075
+ devPrompt: `🧪 Run ${uiCommandReference('hs project dev')} to start local development.`,
3076
+ },
3077
+ },
3074
3078
  },
3075
3079
  projectBuildAndDeploy: {
3076
3080
  makePollTaskStatusFunc: {
@@ -3679,6 +3683,7 @@ export const lib = {
3679
3683
  unknownErrorOccurred: 'An unknown error has occurred.',
3680
3684
  configTimeoutErrorOccurred: (timeout, configSetCommand) => `This error occurred because a request exceeded the default HTTP timeout of ${timeout}ms. To increase the default HTTP timeout, run ${uiCommandReference(configSetCommand)}.`,
3681
3685
  genericTimeoutErrorOccurred: 'This error occurred because an HTTP request timed out. Re-running the command may resolve this issue.',
3686
+ additionalDebugContext: chalk.bold(`For more information, run the command again with the ${uiCommandReference('--debug')} flag.`),
3682
3687
  },
3683
3688
  suppressErrors: {
3684
3689
  platformVersionErrors: {
@@ -1,26 +1,22 @@
1
1
  import path from 'path';
2
- import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, validateProfileVariables, } from '@hubspot/project-parsing-lib';
2
+ import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, } from '@hubspot/project-parsing-lib';
3
3
  import { lib } from '../../lang/en.js';
4
4
  import { uiBetaTag, uiLine } from '../ui/index.js';
5
5
  import { uiLogger } from '../ui/logger.js';
6
- import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
7
- import { logProfileHeader, logProfileFooter, loadProfile, enforceProfileUsage, loadAndValidateProfile, validateProjectForProfile, } from '../projectProfiles.js';
8
- import { handleTranslate } from '../projects/upload.js';
9
- import SpinniesManager from '../ui/SpinniesManager.js';
10
- import { commands } from '../../lang/en.js';
6
+ import { EXIT_CODES } from '../enums/exitCodes.js';
7
+ import { logProfileHeader, logProfileFooter, loadProfile, exitIfUsingProfiles, } from '../projectProfiles.js';
11
8
  // Mock dependencies
12
9
  vi.mock('@hubspot/project-parsing-lib');
13
- vi.mock('@hubspot/local-dev-lib/config');
14
10
  vi.mock('../ui');
15
11
  vi.mock('../ui/logger');
16
12
  vi.mock('../../lang/en');
17
- vi.mock('../projects/upload');
18
- vi.mock('../ui/SpinniesManager');
13
+ // Mock process.exit
14
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(code => {
15
+ throw new Error(`Process.exit called with code ${code}`);
16
+ });
19
17
  const mockedLoadHsProfileFile = loadHsProfileFile;
20
18
  const mockedGetHsProfileFilename = getHsProfileFilename;
21
19
  const mockedGetAllHsProfiles = getAllHsProfiles;
22
- const mockedValidateProfileVariables = validateProfileVariables;
23
- const mockedGetConfigAccountById = getConfigAccountById;
24
20
  const mockedUiBetaTag = uiBetaTag;
25
21
  const mockedUiLine = uiLine;
26
22
  const mockedUiLogger = uiLogger;
@@ -72,299 +68,62 @@ describe('lib/projectProfiles', () => {
72
68
  const mockProfile = {
73
69
  accountId: 123,
74
70
  };
75
- beforeEach(() => {
76
- vi.clearAllMocks();
77
- });
78
- it('should throw error when project config is missing', () => {
79
- expect(() => loadProfile(null, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.noProjectConfig);
80
- });
81
- it('should throw error when project dir is missing', () => {
82
- expect(() => loadProfile(mockProjectConfig, null, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.noProjectConfig);
71
+ it('should return undefined when project config is missing', () => {
72
+ const result = loadProfile(null, mockProjectDir, mockProfileName);
73
+ expect(result).toBeUndefined();
74
+ expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.noProjectConfig);
83
75
  });
84
- it('should throw error when profile is not found', () => {
76
+ it('should return undefined when profile is not found', () => {
85
77
  mockedLoadHsProfileFile.mockReturnValue(null);
86
78
  const filename = 'test-profile.hsprofile';
87
79
  mockedGetHsProfileFilename.mockReturnValue(filename);
88
- expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.profileNotFound(filename));
80
+ const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
81
+ expect(result).toBeUndefined();
82
+ expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.profileNotFound(filename));
89
83
  });
90
- it('should throw error when profile has no account ID', () => {
84
+ it('should return undefined when profile has no account ID', () => {
91
85
  mockedLoadHsProfileFile.mockReturnValue({});
92
86
  const filename = 'test-profile.hsprofile';
93
87
  mockedGetHsProfileFilename.mockReturnValue(filename);
94
- expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.missingAccountId(filename));
88
+ const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
89
+ expect(result).toBeUndefined();
90
+ expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.missingAccountId(filename));
95
91
  });
96
- it('should throw error when profile loading fails', () => {
92
+ it('should return undefined when profile loading fails', () => {
97
93
  mockedLoadHsProfileFile.mockImplementation(() => {
98
94
  throw new Error('Load failed');
99
95
  });
100
96
  const filename = 'test-profile.hsprofile';
101
97
  mockedGetHsProfileFilename.mockReturnValue(filename);
102
- expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(filename));
103
- });
104
- it('should throw error when account is not found in config', () => {
105
- mockedLoadHsProfileFile.mockReturnValue(mockProfile);
106
- mockedGetConfigAccountById.mockImplementation(() => {
107
- throw new Error('Account not found');
108
- });
109
- const filename = 'test-profile.hsprofile';
110
- mockedGetHsProfileFilename.mockReturnValue(filename);
111
- expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.listedAccountNotFound(mockProfile.accountId, filename));
98
+ const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
99
+ expect(result).toBeUndefined();
100
+ expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(filename));
112
101
  });
113
102
  it('should return profile when loading succeeds', () => {
114
103
  mockedLoadHsProfileFile.mockReturnValue(mockProfile);
115
- mockedGetConfigAccountById.mockReturnValue({
116
- accountId: mockProfile.accountId,
117
- });
118
104
  const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
119
105
  expect(result).toEqual(mockProfile);
120
106
  expect(mockedLoadHsProfileFile).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir), mockProfileName);
121
- expect(mockedGetConfigAccountById).toHaveBeenCalledWith(mockProfile.accountId);
122
107
  });
123
108
  });
124
- describe('enforceProfileUsage()', () => {
109
+ describe('exitIfUsingProfiles()', () => {
125
110
  const mockProjectConfig = {
126
111
  srcDir: 'src',
127
112
  name: 'test-project',
128
113
  platformVersion: '1.0.0',
129
114
  };
130
115
  const mockProjectDir = '/test/project';
131
- beforeEach(() => {
132
- vi.clearAllMocks();
133
- });
134
- it('should not throw when no profiles exist', async () => {
116
+ it('should not exit when no profiles exist', async () => {
135
117
  mockedGetAllHsProfiles.mockResolvedValue([]);
136
- await expect(enforceProfileUsage(mockProjectConfig, mockProjectDir)).resolves.toBeUndefined();
118
+ await exitIfUsingProfiles(mockProjectConfig, mockProjectDir);
119
+ expect(mockedUiLogger.error).not.toHaveBeenCalled();
120
+ expect(mockExit).not.toHaveBeenCalled();
137
121
  });
138
- it('should throw error when profiles exist', async () => {
122
+ it('should exit with error when profiles exist', async () => {
139
123
  mockedGetAllHsProfiles.mockResolvedValue(['profile1', 'profile2']);
140
- await expect(enforceProfileUsage(mockProjectConfig, mockProjectDir)).rejects.toThrow(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
141
- });
142
- it('should not throw when project config is null', async () => {
143
- await expect(enforceProfileUsage(null, mockProjectDir)).resolves.toBeUndefined();
144
- });
145
- it('should not throw when project dir is null', async () => {
146
- await expect(enforceProfileUsage(mockProjectConfig, null)).resolves.toBeUndefined();
147
- });
148
- });
149
- describe('loadAndValidateProfile()', () => {
150
- const mockProjectConfig = {
151
- srcDir: 'src',
152
- name: 'test-project',
153
- platformVersion: '1.0.0',
154
- };
155
- const mockProjectDir = '/test/project';
156
- const mockProfileName = 'test-profile';
157
- const mockProfile = {
158
- accountId: 123,
159
- variables: {
160
- key1: 'value1',
161
- key2: 'value2',
162
- },
163
- };
164
- beforeEach(() => {
165
- vi.clearAllMocks();
166
- });
167
- it('should enforce profile usage when no profile name provided', async () => {
168
- mockedGetAllHsProfiles.mockResolvedValue([]);
169
- const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, undefined);
170
- expect(result).toBeUndefined();
171
- expect(mockedGetAllHsProfiles).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir));
172
- });
173
- it('should throw when profiles exist but no profile name provided', async () => {
174
- mockedGetAllHsProfiles.mockResolvedValue(['profile1']);
175
- await expect(loadAndValidateProfile(mockProjectConfig, mockProjectDir, undefined)).rejects.toThrow(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
176
- });
177
- it('should load and return account ID when profile is valid', async () => {
178
- mockedLoadHsProfileFile.mockReturnValue(mockProfile);
179
- mockedGetConfigAccountById.mockReturnValue({
180
- accountId: mockProfile.accountId,
181
- });
182
- mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
183
- mockedValidateProfileVariables.mockReturnValue({ success: true });
184
- const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName);
185
- expect(result).toBe(mockProfile.accountId);
186
- expect(mockedLoadHsProfileFile).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir), mockProfileName);
187
- expect(mockedValidateProfileVariables).toHaveBeenCalledWith(mockProfile.variables, mockProfileName);
188
- });
189
- it('should log profile header and footer when not silent', async () => {
190
- mockedLoadHsProfileFile.mockReturnValue(mockProfile);
191
- mockedGetConfigAccountById.mockReturnValue({
192
- accountId: mockProfile.accountId,
193
- });
194
- mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
195
- mockedValidateProfileVariables.mockReturnValue({ success: true });
196
- await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName, false);
197
- expect(mockedUiBetaTag).toHaveBeenCalled();
198
- expect(mockedUiLine).toHaveBeenCalled();
199
- expect(mockedUiLogger.log).toHaveBeenCalled();
200
- });
201
- it('should not log when silent is true', async () => {
202
- mockedLoadHsProfileFile.mockReturnValue(mockProfile);
203
- mockedGetConfigAccountById.mockReturnValue({
204
- accountId: mockProfile.accountId,
205
- });
206
- mockedValidateProfileVariables.mockReturnValue({ success: true });
207
- await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName, true);
208
- expect(mockedUiBetaTag).not.toHaveBeenCalled();
209
- expect(mockedUiLine).not.toHaveBeenCalled();
210
- });
211
- it('should throw error when profile variables are invalid', async () => {
212
- const invalidProfile = {
213
- accountId: 123,
214
- variables: {
215
- invalid: 'value',
216
- },
217
- };
218
- const validationErrors = ['Variable "invalid" is not allowed'];
219
- mockedLoadHsProfileFile.mockReturnValue(invalidProfile);
220
- mockedGetConfigAccountById.mockReturnValue({
221
- accountId: invalidProfile.accountId,
222
- });
223
- mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
224
- mockedValidateProfileVariables.mockReturnValue({
225
- success: false,
226
- errors: validationErrors,
227
- });
228
- await expect(loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName)).rejects.toThrow(lib.projectProfiles.loadProfile.errors.profileNotValid('test-profile.hsprofile', validationErrors));
229
- });
230
- it('should not validate when profile has no variables', async () => {
231
- const profileWithoutVars = {
232
- accountId: 123,
233
- };
234
- mockedLoadHsProfileFile.mockReturnValue(profileWithoutVars);
235
- mockedGetConfigAccountById.mockReturnValue({
236
- accountId: profileWithoutVars.accountId,
237
- });
238
- mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
239
- const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName);
240
- expect(result).toBe(profileWithoutVars.accountId);
241
- expect(mockedValidateProfileVariables).not.toHaveBeenCalled();
242
- });
243
- });
244
- describe('validateProjectForProfile()', () => {
245
- const mockProjectConfig = {
246
- srcDir: 'src',
247
- name: 'test-project',
248
- platformVersion: '2025.2',
249
- };
250
- const mockProjectDir = '/test/project';
251
- const mockProfileName = 'test-profile';
252
- const mockDerivedAccountId = 123;
253
- const mockProfileFilename = 'test-profile.hsprofile';
254
- const mockProfile = {
255
- accountId: mockDerivedAccountId,
256
- };
257
- beforeEach(() => {
258
- vi.clearAllMocks();
259
- mockedGetHsProfileFilename.mockReturnValue(mockProfileFilename);
260
- vi.mocked(SpinniesManager.init);
261
- vi.mocked(SpinniesManager.add);
262
- vi.mocked(SpinniesManager.succeed);
263
- vi.mocked(SpinniesManager.fail);
264
- // Mock dependencies for loadAndValidateProfile
265
- mockedGetAllHsProfiles.mockResolvedValue([]);
266
- mockedLoadHsProfileFile.mockReturnValue(mockProfile);
267
- mockedGetConfigAccountById.mockReturnValue({
268
- accountId: mockDerivedAccountId,
269
- });
270
- mockedValidateProfileVariables.mockReturnValue({ success: true });
271
- vi.mocked(handleTranslate).mockResolvedValue(undefined);
272
- });
273
- it('should return empty array when validation succeeds', async () => {
274
- const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
275
- expect(result).toEqual([]);
276
- expect(SpinniesManager.init).toHaveBeenCalled();
277
- expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
278
- text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
279
- indent: 0,
280
- });
281
- expect(SpinniesManager.succeed).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
282
- text: commands.project.validate.spinners.profileValidationSucceeded(mockProfileFilename),
283
- });
284
- });
285
- it('should call handleTranslate with profile account ID from profile', async () => {
286
- await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
287
- expect(handleTranslate).toHaveBeenCalledWith(mockProjectDir, mockProjectConfig, mockDerivedAccountId, false, mockProfileName);
288
- });
289
- it('should call handleTranslate with different profile account ID when profile has different ID', async () => {
290
- const profileAccountId = 456;
291
- const profileWithDifferentId = {
292
- accountId: profileAccountId,
293
- };
294
- mockedLoadHsProfileFile.mockReturnValue(profileWithDifferentId);
295
- mockedGetConfigAccountById.mockReturnValue({
296
- accountId: profileAccountId,
297
- });
298
- await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
299
- expect(handleTranslate).toHaveBeenCalledWith(mockProjectDir, mockProjectConfig, profileAccountId, false, mockProfileName);
300
- });
301
- it('should return error when profile has no accountId', async () => {
302
- // @ts-expect-error causing an error on purpose
303
- const profileWithoutId = {};
304
- mockedLoadHsProfileFile.mockReturnValue(profileWithoutId);
305
- const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
306
- expect(result.length).toBeGreaterThan(0);
307
- expect(SpinniesManager.fail).toHaveBeenCalled();
308
- expect(handleTranslate).not.toHaveBeenCalled();
309
- });
310
- it('should indent spinners when indentSpinners is true', async () => {
311
- await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId, true);
312
- expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
313
- text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
314
- indent: 4,
315
- });
316
- });
317
- it('should not indent spinners when indentSpinners is false', async () => {
318
- await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId, false);
319
- expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
320
- text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
321
- indent: 0,
322
- });
323
- });
324
- it('should return error array when profile loading fails', async () => {
325
- mockedLoadHsProfileFile.mockReturnValue(null);
326
- const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
327
- expect(result.length).toBeGreaterThan(0);
328
- expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
329
- text: commands.project.validate.spinners.profileValidationFailed(mockProfileFilename),
330
- });
331
- expect(handleTranslate).not.toHaveBeenCalled();
332
- });
333
- it('should return error when profile file loading throws', async () => {
334
- mockedLoadHsProfileFile.mockImplementation(() => {
335
- throw new Error('Failed to load profile file');
336
- });
337
- const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
338
- expect(result.length).toBeGreaterThan(0);
339
- expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
340
- text: commands.project.validate.spinners.profileValidationFailed(mockProfileFilename),
341
- });
342
- expect(handleTranslate).not.toHaveBeenCalled();
343
- });
344
- it('should return error array when translation fails', async () => {
345
- const error = new Error('Translation failed');
346
- vi.mocked(handleTranslate).mockRejectedValue(error);
347
- const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
348
- expect(result).toHaveLength(2);
349
- expect(result[0]).toBe(commands.project.validate.failure(mockProjectConfig.name));
350
- expect(result[1]).toBe(error);
351
- expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
352
- text: commands.project.validate.spinners.invalidWithProfile(mockProfileFilename, mockProjectConfig.name),
353
- });
354
- });
355
- it('should return string error when translation fails with non-Error', async () => {
356
- const error = 'Translation error';
357
- vi.mocked(handleTranslate).mockRejectedValue(error);
358
- const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
359
- expect(result).toHaveLength(2);
360
- expect(result[0]).toBe(commands.project.validate.failure(mockProjectConfig.name));
361
- expect(result[1]).toBe(error);
362
- });
363
- it('should use correct spinner name based on profile name', async () => {
364
- const customProfileName = 'custom-profile';
365
- await validateProjectForProfile(mockProjectConfig, mockProjectDir, customProfileName, mockDerivedAccountId);
366
- expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${customProfileName}`, expect.any(Object));
367
- expect(SpinniesManager.succeed).toHaveBeenCalledWith(`validatingProfile-${customProfileName}`, expect.any(Object));
124
+ await expect(exitIfUsingProfiles(mockProjectConfig, mockProjectDir)).rejects.toThrow(`Process.exit called with code ${EXIT_CODES.ERROR}`);
125
+ expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
126
+ expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
368
127
  });
369
128
  });
370
129
  });