@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.
- package/commands/project/__tests__/deploy.test.js +6 -6
- package/commands/project/__tests__/validate.test.js +27 -285
- package/commands/project/create.js +20 -14
- package/commands/project/deploy.js +6 -14
- package/commands/project/dev/index.js +4 -13
- package/commands/project/dev/unifiedFlow.js +7 -1
- package/commands/project/upload.js +2 -8
- package/commands/project/validate.js +12 -72
- package/lang/en.d.ts +19 -14
- package/lang/en.js +21 -16
- package/lib/__tests__/projectProfiles.test.js +32 -273
- package/lib/errorHandlers/index.js +10 -7
- package/lib/projectProfiles.d.ts +3 -4
- package/lib/projectProfiles.js +32 -78
- package/lib/projects/__tests__/components.test.js +2 -22
- package/lib/projects/__tests__/deploy.test.js +15 -13
- package/lib/projects/add/__tests__/legacyAddComponent.test.js +1 -1
- package/lib/projects/add/__tests__/v2AddComponent.test.js +30 -4
- package/lib/projects/add/legacyAddComponent.js +1 -1
- package/lib/projects/add/v2AddComponent.js +16 -5
- package/lib/projects/components.d.ts +8 -1
- package/lib/projects/components.js +91 -8
- package/lib/projects/deploy.js +21 -8
- package/lib/projects/localDev/DevServerManager_DEPRECATED.js +9 -1
- package/lib/projects/localDev/helpers/process.js +5 -3
- package/lib/ui/SpinniesManager.d.ts +5 -7
- package/lib/ui/SpinniesManager.js +9 -12
- package/lib/ui/__tests__/SpinniesManager.test.d.ts +1 -0
- package/lib/ui/__tests__/SpinniesManager.test.js +489 -0
- package/mcp-server/utils/config.js +1 -1
- package/package.json +4 -4
- 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 {
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
42
|
+
catch (e) {
|
|
43
|
+
logError(e);
|
|
105
44
|
process.exit(EXIT_CODES.ERROR);
|
|
106
45
|
}
|
|
107
46
|
try {
|
|
108
|
-
await
|
|
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: (
|
|
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
|
|
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) =>
|
|
1526
|
-
success: (
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
7
|
-
import { logProfileHeader, logProfileFooter, loadProfile,
|
|
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
|
-
|
|
18
|
-
vi.
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
118
|
+
await exitIfUsingProfiles(mockProjectConfig, mockProjectDir);
|
|
119
|
+
expect(mockedUiLogger.error).not.toHaveBeenCalled();
|
|
120
|
+
expect(mockExit).not.toHaveBeenCalled();
|
|
137
121
|
});
|
|
138
|
-
it('should
|
|
122
|
+
it('should exit with error when profiles exist', async () => {
|
|
139
123
|
mockedGetAllHsProfiles.mockResolvedValue(['profile1', 'profile2']);
|
|
140
|
-
await expect(
|
|
141
|
-
|
|
142
|
-
|
|
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
|
});
|