@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
|
@@ -14,13 +14,11 @@ export function logError(error, context) {
|
|
|
14
14
|
if (shouldSuppressError(error, context)) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
if (isHubSpotHttpError(error)
|
|
17
|
+
if (isHubSpotHttpError(error)) {
|
|
18
18
|
if (shouldSuppressError(error, error.context)) {
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
if (isHubSpotHttpError(error) && context) {
|
|
23
|
-
error.updateContext(context);
|
|
21
|
+
error.updateContext(context || {}, lib.errorHandlers.index.additionalDebugContext);
|
|
24
22
|
}
|
|
25
23
|
if (isHubSpotHttpError(error) && isValidationError(error)) {
|
|
26
24
|
uiLogger.error(error.formattedValidationErrors());
|
|
@@ -57,9 +55,14 @@ export function debugError(error, context) {
|
|
|
57
55
|
}
|
|
58
56
|
else {
|
|
59
57
|
uiLogger.debug(lib.errorHandlers.index.errorOccurred(String(error)));
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
if (error instanceof Error && error.cause) {
|
|
59
|
+
if (isHubSpotHttpError(error.cause)) {
|
|
60
|
+
uiLogger.debug(error.cause.toString());
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
uiLogger.debug(lib.errorHandlers.index.errorCause(util.inspect(error.cause, false, null, true)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
63
66
|
}
|
|
64
67
|
if (context) {
|
|
65
68
|
uiLogger.debug(lib.errorHandlers.index.errorContext(util.inspect(context, false, null, true)));
|
package/lib/projectProfiles.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { HsProfileFile } from '@hubspot/project-parsing-lib/src/lib/types.js';
|
|
|
2
2
|
import { ProjectConfig } from '../types/Projects.js';
|
|
3
3
|
export declare function logProfileHeader(profileName: string): void;
|
|
4
4
|
export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
|
|
5
|
-
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile |
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null,
|
|
8
|
-
export declare function validateProjectForProfile(projectConfig: ProjectConfig, projectDir: string, profileName: string, derivedAccountId: number, indentSpinners?: boolean): Promise<(string | Error)[]>;
|
|
5
|
+
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
|
|
6
|
+
export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
|
|
7
|
+
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
|
package/lib/projectProfiles.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, } from '@hubspot/project-parsing-lib';
|
|
3
|
-
import {
|
|
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 { getConfigAccountById } from '@hubspot/local-dev-lib/config';
|
|
8
|
-
import SpinniesManager from './ui/SpinniesManager.js';
|
|
9
|
-
import { handleTranslate } from './projects/upload.js';
|
|
6
|
+
import { EXIT_CODES } from './enums/exitCodes.js';
|
|
10
7
|
export function logProfileHeader(profileName) {
|
|
11
8
|
uiLine();
|
|
12
9
|
uiBetaTag(lib.projectProfiles.logs.usingProfile(getHsProfileFilename(profileName)));
|
|
@@ -26,94 +23,51 @@ export function logProfileFooter(profile, includeVariables = false) {
|
|
|
26
23
|
}
|
|
27
24
|
export function loadProfile(projectConfig, projectDir, profileName) {
|
|
28
25
|
if (!projectConfig || !projectDir) {
|
|
29
|
-
|
|
26
|
+
uiLogger.error(lib.projectProfiles.loadProfile.errors.noProjectConfig);
|
|
27
|
+
return;
|
|
30
28
|
}
|
|
31
29
|
const projectSourceDir = path.join(projectDir, projectConfig.srcDir);
|
|
32
30
|
const profileFilename = getHsProfileFilename(profileName);
|
|
33
|
-
let profile;
|
|
34
31
|
try {
|
|
35
|
-
profile = loadHsProfileFile(projectSourceDir, profileName);
|
|
32
|
+
const profile = loadHsProfileFile(projectSourceDir, profileName);
|
|
33
|
+
if (!profile) {
|
|
34
|
+
uiLogger.error(lib.projectProfiles.loadProfile.errors.profileNotFound(profileFilename));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!profile.accountId) {
|
|
38
|
+
uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
return profile;
|
|
36
42
|
}
|
|
37
43
|
catch (e) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!profile) {
|
|
41
|
-
throw new Error(lib.projectProfiles.loadProfile.errors.profileNotFound(profileFilename));
|
|
42
|
-
}
|
|
43
|
-
if (!profile.accountId) {
|
|
44
|
-
throw new Error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
getConfigAccountById(profile.accountId);
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
throw new Error(lib.projectProfiles.loadProfile.errors.listedAccountNotFound(profile.accountId, profileFilename));
|
|
44
|
+
uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
|
|
45
|
+
return;
|
|
51
46
|
}
|
|
52
|
-
return profile;
|
|
53
47
|
}
|
|
54
|
-
export async function
|
|
48
|
+
export async function exitIfUsingProfiles(projectConfig, projectDir) {
|
|
55
49
|
if (projectConfig && projectDir) {
|
|
56
50
|
const existingProfiles = await getAllHsProfiles(path.join(projectDir, projectConfig.srcDir));
|
|
57
51
|
if (existingProfiles.length > 0) {
|
|
58
|
-
|
|
52
|
+
uiLogger.error(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
|
|
53
|
+
process.exit(EXIT_CODES.ERROR);
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
|
-
export async function loadAndValidateProfile(projectConfig, projectDir,
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
const profile = loadProfile(projectConfig, projectDir, profileName);
|
|
71
|
-
if (!silent) {
|
|
72
|
-
logProfileFooter(profile, true);
|
|
73
|
-
}
|
|
74
|
-
if (profile.variables) {
|
|
75
|
-
const validationResult = validateProfileVariables(profile.variables, profileName);
|
|
76
|
-
if (!validationResult.success) {
|
|
77
|
-
throw new Error(lib.projectProfiles.loadProfile.errors.profileNotValid(getHsProfileFilename(profileName), validationResult.errors));
|
|
57
|
+
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
|
|
58
|
+
if (argsProfile) {
|
|
59
|
+
logProfileHeader(argsProfile);
|
|
60
|
+
const profile = loadProfile(projectConfig, projectDir, argsProfile);
|
|
61
|
+
if (!profile) {
|
|
62
|
+
uiLine();
|
|
63
|
+
process.exit(EXIT_CODES.ERROR);
|
|
78
64
|
}
|
|
65
|
+
logProfileFooter(profile, true);
|
|
66
|
+
return profile.accountId;
|
|
79
67
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
export async function validateProjectForProfile(projectConfig, projectDir, profileName, derivedAccountId, indentSpinners = false) {
|
|
84
|
-
let targetAccountId = derivedAccountId;
|
|
85
|
-
const spinnerName = `validatingProfile-${profileName}`;
|
|
86
|
-
const profileFilename = getHsProfileFilename(profileName);
|
|
87
|
-
SpinniesManager.init();
|
|
88
|
-
SpinniesManager.add(spinnerName, {
|
|
89
|
-
text: commands.project.validate.spinners.validatingProfile(profileFilename),
|
|
90
|
-
indent: indentSpinners ? 4 : 0,
|
|
91
|
-
});
|
|
92
|
-
try {
|
|
93
|
-
const accountId = await loadAndValidateProfile(projectConfig, projectDir, profileName, true);
|
|
94
|
-
targetAccountId = accountId || derivedAccountId;
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
SpinniesManager.fail(spinnerName, {
|
|
98
|
-
text: commands.project.validate.spinners.profileValidationFailed(profileFilename),
|
|
99
|
-
});
|
|
100
|
-
return [error instanceof Error ? error.message : `${error}`];
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
await handleTranslate(projectDir, projectConfig, targetAccountId, false, profileName);
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
SpinniesManager.fail(spinnerName, {
|
|
107
|
-
text: commands.project.validate.spinners.invalidWithProfile(profileFilename, projectConfig.name),
|
|
108
|
-
});
|
|
109
|
-
const errors = [
|
|
110
|
-
commands.project.validate.failure(projectConfig.name),
|
|
111
|
-
];
|
|
112
|
-
errors.push(error instanceof Error ? error : `${error}`);
|
|
113
|
-
return errors;
|
|
68
|
+
else {
|
|
69
|
+
// A profile must be specified if this project has profiles configured
|
|
70
|
+
await exitIfUsingProfiles(projectConfig, projectDir);
|
|
114
71
|
}
|
|
115
|
-
|
|
116
|
-
text: commands.project.validate.spinners.profileValidationSucceeded(profileFilename),
|
|
117
|
-
});
|
|
118
|
-
return [];
|
|
72
|
+
return undefined;
|
|
119
73
|
}
|
|
@@ -13,20 +13,6 @@ vi.mock('@hubspot/project-parsing-lib', () => ({
|
|
|
13
13
|
vi.mock('@hubspot/project-parsing-lib/src/lib/constants.js', () => ({
|
|
14
14
|
AppKey: 'app',
|
|
15
15
|
}));
|
|
16
|
-
vi.mock('../../../lang/en.js', () => ({
|
|
17
|
-
lib: {
|
|
18
|
-
projects: {
|
|
19
|
-
updateHsMetaFilesWithAutoGeneratedFields: {
|
|
20
|
-
header: 'Updating component metadata files...',
|
|
21
|
-
applicationLog: (type, uid, name) => `Updated ${type} component with uid: ${uid} and name: ${name}`,
|
|
22
|
-
componentLog: (type, uid) => `Updated ${type} component with uid: ${uid}`,
|
|
23
|
-
},
|
|
24
|
-
generateSafeFilenameDifferentiator: {
|
|
25
|
-
failedToCheckFiles: 'Failed to check files for filename differentiator. Falling back to timestamp.',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
16
|
const mockedFs = vi.mocked(fs);
|
|
31
17
|
const mockCoerceToValidUid = vi.mocked(coerceToValidUid);
|
|
32
18
|
const mockedFileExists = vi.mocked(fileExists);
|
|
@@ -247,7 +233,6 @@ describe('lib/projects/components', () => {
|
|
|
247
233
|
});
|
|
248
234
|
});
|
|
249
235
|
describe('updateHsMetaFilesWithAutoGeneratedFields()', () => {
|
|
250
|
-
const mockUiLogger = vi.mocked(uiLogger);
|
|
251
236
|
beforeEach(() => {
|
|
252
237
|
vi.resetAllMocks();
|
|
253
238
|
mockCoerceToValidUid.mockImplementation((input) => input);
|
|
@@ -282,6 +267,8 @@ describe('lib/projects/components', () => {
|
|
|
282
267
|
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
283
268
|
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_card');
|
|
284
269
|
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_function');
|
|
270
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_card');
|
|
271
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_function');
|
|
285
272
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
|
|
286
273
|
type: 'card',
|
|
287
274
|
uid: 'card_my_project',
|
|
@@ -293,9 +280,6 @@ describe('lib/projects/components', () => {
|
|
|
293
280
|
type: 'function',
|
|
294
281
|
uid: 'function_my_project',
|
|
295
282
|
}, null, 2));
|
|
296
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
|
|
297
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated card component with uid: card_my_project');
|
|
298
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated function component with uid: function_my_project');
|
|
299
283
|
});
|
|
300
284
|
it('handles app components by updating both uid and config.name', () => {
|
|
301
285
|
const projectName = 'test-app';
|
|
@@ -321,7 +305,6 @@ describe('lib/projects/components', () => {
|
|
|
321
305
|
other: 'property',
|
|
322
306
|
},
|
|
323
307
|
}, null, 2));
|
|
324
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app_test_app and name: test-app-Application');
|
|
325
308
|
});
|
|
326
309
|
it('handles UID collisions by using differentiators', () => {
|
|
327
310
|
const projectName = 'collision-project';
|
|
@@ -366,8 +349,6 @@ describe('lib/projects/components', () => {
|
|
|
366
349
|
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
367
350
|
expect(mockedFs.readFileSync).not.toHaveBeenCalled();
|
|
368
351
|
expect(mockedFs.writeFileSync).not.toHaveBeenCalled();
|
|
369
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
|
|
370
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('');
|
|
371
352
|
});
|
|
372
353
|
it('handles components without config property for app type', () => {
|
|
373
354
|
const projectName = 'no-config-project';
|
|
@@ -386,7 +367,6 @@ describe('lib/projects/components', () => {
|
|
|
386
367
|
type: 'app',
|
|
387
368
|
uid: 'app_no_config_project',
|
|
388
369
|
}, null, 2));
|
|
389
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app_no_config_project');
|
|
390
370
|
});
|
|
391
371
|
it('replaces hyphens with underscores in coerced UIDs', () => {
|
|
392
372
|
const projectName = 'my-project';
|
|
@@ -3,7 +3,7 @@ import { validateBuildIdForDeploy, logDeployErrors, handleProjectDeploy, } from
|
|
|
3
3
|
import { uiLogger } from '../../ui/logger.js';
|
|
4
4
|
import { commands } from '../../../lang/en.js';
|
|
5
5
|
import { PROJECT_ERROR_TYPES } from '../../constants.js';
|
|
6
|
-
import {
|
|
6
|
+
import { deployProjectV1, deployProjectV2, } from '@hubspot/local-dev-lib/api/projects';
|
|
7
7
|
import { pollDeployStatus } from '../pollProjectBuildAndDeploy.js';
|
|
8
8
|
// Mock external dependencies
|
|
9
9
|
vi.mock('../../ui/logger.js');
|
|
@@ -11,7 +11,8 @@ vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
|
11
11
|
vi.mock('@hubspot/local-dev-lib/config');
|
|
12
12
|
vi.mock('../pollProjectBuildAndDeploy.js');
|
|
13
13
|
const mockUiLogger = vi.mocked(uiLogger);
|
|
14
|
-
const
|
|
14
|
+
const mockDeployProjectV1 = vi.mocked(deployProjectV1);
|
|
15
|
+
const mockDeployProjectV2 = vi.mocked(deployProjectV2);
|
|
15
16
|
const mockPollDeployStatus = vi.mocked(pollDeployStatus);
|
|
16
17
|
describe('lib/projects/deploy', () => {
|
|
17
18
|
beforeEach(() => {
|
|
@@ -123,12 +124,12 @@ describe('lib/projects/deploy', () => {
|
|
|
123
124
|
source: 'HUBSPOT_USER',
|
|
124
125
|
subdeployStatuses: [],
|
|
125
126
|
};
|
|
126
|
-
|
|
127
|
+
mockDeployProjectV2.mockResolvedValue({
|
|
127
128
|
data: mockDeployResponseData,
|
|
128
129
|
});
|
|
129
130
|
mockPollDeployStatus.mockResolvedValue(mockDeployResult);
|
|
130
131
|
const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
|
|
131
|
-
expect(
|
|
132
|
+
expect(mockDeployProjectV2).toHaveBeenCalledWith(targetAccountId, projectName, buildId, force);
|
|
132
133
|
expect(deploy).toEqual(mockDeployResult);
|
|
133
134
|
});
|
|
134
135
|
it('handles blocked deploy with warnings', async () => {
|
|
@@ -148,7 +149,7 @@ describe('lib/projects/deploy', () => {
|
|
|
148
149
|
},
|
|
149
150
|
],
|
|
150
151
|
};
|
|
151
|
-
|
|
152
|
+
mockDeployProjectV2.mockResolvedValue({
|
|
152
153
|
data: mockBlockedResponse,
|
|
153
154
|
});
|
|
154
155
|
await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
|
|
@@ -171,7 +172,7 @@ describe('lib/projects/deploy', () => {
|
|
|
171
172
|
},
|
|
172
173
|
],
|
|
173
174
|
};
|
|
174
|
-
|
|
175
|
+
mockDeployProjectV2.mockResolvedValue({
|
|
175
176
|
data: mockBlockedResponse,
|
|
176
177
|
});
|
|
177
178
|
await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
|
|
@@ -190,7 +191,7 @@ describe('lib/projects/deploy', () => {
|
|
|
190
191
|
},
|
|
191
192
|
],
|
|
192
193
|
};
|
|
193
|
-
|
|
194
|
+
mockDeployProjectV2.mockResolvedValue({
|
|
194
195
|
data: mockBlockedResponse,
|
|
195
196
|
});
|
|
196
197
|
await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
|
|
@@ -198,18 +199,18 @@ describe('lib/projects/deploy', () => {
|
|
|
198
199
|
expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentGeneric('component-1', 'module'));
|
|
199
200
|
});
|
|
200
201
|
it('handles general deploy failure', async () => {
|
|
201
|
-
|
|
202
|
+
mockDeployProjectV2.mockResolvedValue({ data: null });
|
|
202
203
|
const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
|
|
203
204
|
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
|
|
204
205
|
expect(deploy).toBeUndefined();
|
|
205
206
|
});
|
|
206
207
|
it('handles undefined deploy response', async () => {
|
|
207
|
-
|
|
208
|
+
mockDeployProjectV2.mockResolvedValue({ data: undefined });
|
|
208
209
|
const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV2Api, force);
|
|
209
210
|
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
|
|
210
211
|
expect(deploy).toBeUndefined();
|
|
211
212
|
});
|
|
212
|
-
it('passes correct parameters to
|
|
213
|
+
it('passes correct parameters to deployProjectV1', async () => {
|
|
213
214
|
const mockDeployResponseData = {
|
|
214
215
|
id: 'deploy-123',
|
|
215
216
|
buildResultType: 'DEPLOY_QUEUED',
|
|
@@ -217,14 +218,15 @@ describe('lib/projects/deploy', () => {
|
|
|
217
218
|
status: 'http://status-url',
|
|
218
219
|
},
|
|
219
220
|
};
|
|
220
|
-
|
|
221
|
+
mockDeployProjectV1.mockResolvedValue({
|
|
221
222
|
data: mockDeployResponseData,
|
|
222
223
|
});
|
|
223
224
|
mockPollDeployStatus.mockResolvedValue({});
|
|
224
|
-
await handleProjectDeploy(targetAccountId, projectName, buildId, false,
|
|
225
|
-
expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, false, // useV2Api
|
|
225
|
+
await handleProjectDeploy(targetAccountId, projectName, buildId, false, // isV2Project
|
|
226
226
|
true // force
|
|
227
227
|
);
|
|
228
|
+
expect(mockDeployProjectV1).toHaveBeenCalledWith(targetAccountId, projectName, buildId, true // force
|
|
229
|
+
);
|
|
228
230
|
});
|
|
229
231
|
});
|
|
230
232
|
});
|
|
@@ -76,7 +76,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
76
76
|
type: 'module',
|
|
77
77
|
}, accountId);
|
|
78
78
|
expect(mockedUiLogger.log).toHaveBeenCalledWith(commands.project.add.creatingComponent('test-project'));
|
|
79
|
-
expect(mockedUiLogger.success).toHaveBeenCalledWith(commands.project.add.success('
|
|
79
|
+
expect(mockedUiLogger.success).toHaveBeenCalledWith(commands.project.add.success('test-project'));
|
|
80
80
|
});
|
|
81
81
|
it('throws an error when project contains a public app', async () => {
|
|
82
82
|
const mockComponents = [
|
|
@@ -5,9 +5,9 @@ import { createV2App } from '../../create/v2.js';
|
|
|
5
5
|
import { confirmPrompt } from '../../../prompts/promptUtils.js';
|
|
6
6
|
import { projectAddPromptV2 } from '../../../prompts/projectAddPrompt.js';
|
|
7
7
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
8
|
-
import { uiLogger } from '../../../ui/logger.js';
|
|
9
8
|
import { getProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
|
|
10
9
|
import { trackCommandUsage } from '../../../usageTracking.js';
|
|
10
|
+
import { updateHsMetaFilesWithAutoGeneratedFields } from '../../components.js';
|
|
11
11
|
import { commands } from '../../../../lang/en.js';
|
|
12
12
|
vi.mock('fs');
|
|
13
13
|
vi.mock('../../../prompts/promptUtils');
|
|
@@ -16,17 +16,19 @@ vi.mock('../../create/v2');
|
|
|
16
16
|
vi.mock('../../../prompts/projectAddPrompt');
|
|
17
17
|
vi.mock('@hubspot/local-dev-lib/github');
|
|
18
18
|
vi.mock('../../../ui/logger.js');
|
|
19
|
+
vi.mock('../../../ui/SpinniesManager.js');
|
|
19
20
|
vi.mock('@hubspot/project-parsing-lib/src/lib/project');
|
|
20
21
|
vi.mock('../../../usageTracking');
|
|
22
|
+
vi.mock('../../components.js');
|
|
21
23
|
const mockedFs = vi.mocked(fs);
|
|
22
24
|
const mockedGetConfigForPlatformVersion = vi.mocked(getConfigForPlatformVersion);
|
|
23
25
|
const mockedConfirmPrompt = vi.mocked(confirmPrompt);
|
|
24
26
|
const mockedCreateV2App = vi.mocked(createV2App);
|
|
25
27
|
const mockedProjectAddPromptV2 = vi.mocked(projectAddPromptV2);
|
|
26
28
|
const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
|
|
27
|
-
const mockedUiLogger = vi.mocked(uiLogger);
|
|
28
29
|
const mockedGetProjectMetadata = vi.mocked(getProjectMetadata);
|
|
29
30
|
const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
|
|
31
|
+
const mockedUpdateHsMetaFilesWithAutoGeneratedFields = vi.mocked(updateHsMetaFilesWithAutoGeneratedFields);
|
|
30
32
|
describe('lib/projects/add/v2AddComponent', () => {
|
|
31
33
|
const mockProjectConfig = {
|
|
32
34
|
name: 'test-project',
|
|
@@ -75,6 +77,7 @@ describe('lib/projects/add/v2AddComponent', () => {
|
|
|
75
77
|
describe('v2AddComponent()', () => {
|
|
76
78
|
it('successfully adds a component when app already exists', async () => {
|
|
77
79
|
const mockAppMeta = {
|
|
80
|
+
name: 'Test App',
|
|
78
81
|
config: {
|
|
79
82
|
distribution: 'private',
|
|
80
83
|
auth: { type: 'oauth' },
|
|
@@ -83,14 +86,33 @@ describe('lib/projects/add/v2AddComponent', () => {
|
|
|
83
86
|
const mockPromptResponse = {
|
|
84
87
|
componentTemplate: [mockComponentTemplate],
|
|
85
88
|
};
|
|
89
|
+
const mockUpdatedProjectMetadata = {
|
|
90
|
+
hsMetaFiles: ['/path/to/new-module.meta.json'],
|
|
91
|
+
components: {
|
|
92
|
+
app: {
|
|
93
|
+
count: 1,
|
|
94
|
+
maxCount: 1,
|
|
95
|
+
hsMetaFiles: ['/path/to/app.meta.json'],
|
|
96
|
+
},
|
|
97
|
+
module: {
|
|
98
|
+
count: 1,
|
|
99
|
+
maxCount: 5,
|
|
100
|
+
hsMetaFiles: ['/path/to/new-module.meta.json'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
86
104
|
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
87
|
-
mockedGetProjectMetadata
|
|
105
|
+
mockedGetProjectMetadata
|
|
106
|
+
.mockResolvedValueOnce(mockProjectMetadata)
|
|
107
|
+
.mockResolvedValueOnce(mockUpdatedProjectMetadata);
|
|
88
108
|
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
|
|
89
109
|
mockedProjectAddPromptV2.mockResolvedValue(mockPromptResponse);
|
|
90
110
|
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
111
|
+
mockedUpdateHsMetaFilesWithAutoGeneratedFields.mockResolvedValue(undefined);
|
|
91
112
|
await v2AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
92
113
|
expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('2025.2');
|
|
93
114
|
expect(mockedGetProjectMetadata).toHaveBeenCalledWith('/path/to/project/src');
|
|
115
|
+
expect(mockedGetProjectMetadata).toHaveBeenCalledTimes(2);
|
|
94
116
|
expect(mockedProjectAddPromptV2).toHaveBeenCalled();
|
|
95
117
|
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
96
118
|
type: 'module',
|
|
@@ -100,7 +122,11 @@ describe('lib/projects/add/v2AddComponent', () => {
|
|
|
100
122
|
hideLogs: true,
|
|
101
123
|
branch: 'main',
|
|
102
124
|
}));
|
|
103
|
-
expect(
|
|
125
|
+
expect(mockedUpdateHsMetaFilesWithAutoGeneratedFields).toHaveBeenCalledWith('test-project', ['/path/to/new-module.meta.json'], [], expect.objectContaining({
|
|
126
|
+
currentProjectMetadata: mockProjectMetadata,
|
|
127
|
+
updatedProjectMetadata: mockUpdatedProjectMetadata,
|
|
128
|
+
showSuccessMessage: true,
|
|
129
|
+
}));
|
|
104
130
|
});
|
|
105
131
|
it('creates an app when no app exists and user confirms', async () => {
|
|
106
132
|
const mockProjectMetadataNoApps = {
|
|
@@ -38,7 +38,7 @@ export async function legacyAddComponent(args, projectDir, projectConfig, derive
|
|
|
38
38
|
branch: DEFAULT_PROJECT_TEMPLATE_BRANCH,
|
|
39
39
|
hideLogs: true,
|
|
40
40
|
});
|
|
41
|
-
uiLogger.success(commands.project.add.success(
|
|
41
|
+
uiLogger.success(commands.project.add.success(projectConfig.name));
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
44
44
|
throw new Error(commands.project.add.error.failedToDownloadComponent);
|
|
@@ -13,8 +13,8 @@ import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
|
13
13
|
import { debugError } from '../../errorHandlers/index.js';
|
|
14
14
|
import { uiLogger } from '../../ui/logger.js';
|
|
15
15
|
import { trackCommandUsage } from '../../usageTracking.js';
|
|
16
|
+
import SpinniesManager from '../../ui/SpinniesManager.js';
|
|
16
17
|
export async function v2AddComponent(args, projectDir, projectConfig, accountId) {
|
|
17
|
-
uiLogger.log(commands.project.add.creatingComponent(projectConfig.name));
|
|
18
18
|
const config = await getConfigForPlatformVersion(projectConfig.platformVersion);
|
|
19
19
|
const { components, parentComponents } = config;
|
|
20
20
|
if (!components || !components.length) {
|
|
@@ -52,6 +52,10 @@ export async function v2AddComponent(args, projectDir, projectConfig, accountId)
|
|
|
52
52
|
await trackCommandUsage('project-add', {
|
|
53
53
|
type: componentTypes?.join(','),
|
|
54
54
|
}, accountId);
|
|
55
|
+
SpinniesManager.init();
|
|
56
|
+
SpinniesManager.add('project-add', {
|
|
57
|
+
text: commands.project.add.creatingComponent(projectConfig.name),
|
|
58
|
+
});
|
|
55
59
|
try {
|
|
56
60
|
const components = projectAddPromptResponse.componentTemplate?.map((componentTemplate) => {
|
|
57
61
|
return path.join(projectConfig.platformVersion, componentTemplate.path);
|
|
@@ -87,12 +91,19 @@ export async function v2AddComponent(args, projectDir, projectConfig, accountId)
|
|
|
87
91
|
return '';
|
|
88
92
|
}
|
|
89
93
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
SpinniesManager.succeed('project-add', {
|
|
95
|
+
text: commands.project.add.success(projectConfig.name),
|
|
96
|
+
});
|
|
97
|
+
await updateHsMetaFilesWithAutoGeneratedFields(projectConfig.name, newHsMetaFiles, existingUids, {
|
|
98
|
+
currentProjectMetadata,
|
|
99
|
+
updatedProjectMetadata,
|
|
100
|
+
showSuccessMessage: true,
|
|
101
|
+
});
|
|
94
102
|
}
|
|
95
103
|
catch (error) {
|
|
104
|
+
SpinniesManager.fail('project-add', {
|
|
105
|
+
text: commands.project.add.failure(projectConfig.name),
|
|
106
|
+
});
|
|
96
107
|
debugError(error);
|
|
97
108
|
throw new Error(commands.project.add.error.failedToDownloadComponent, {
|
|
98
109
|
cause: error,
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import { Collision } from '@hubspot/local-dev-lib/types/Archive';
|
|
2
|
+
import { ProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
|
|
2
3
|
export declare function handleComponentCollision({ dest, src, collisions }: Collision): void;
|
|
3
|
-
export declare function updateHsMetaFilesWithAutoGeneratedFields(projectName: string, hsMetaFilePaths: string[], existingUids?: string[]
|
|
4
|
+
export declare function updateHsMetaFilesWithAutoGeneratedFields(projectName: string, hsMetaFilePaths: string[], existingUids?: string[], options?: {
|
|
5
|
+
currentProjectMetadata?: ProjectMetadata;
|
|
6
|
+
updatedProjectMetadata?: ProjectMetadata;
|
|
7
|
+
showSuccessMessage?: boolean;
|
|
8
|
+
isProjectEmpty?: boolean;
|
|
9
|
+
projectDest?: string;
|
|
10
|
+
}): Promise<void>;
|
|
@@ -6,8 +6,59 @@ import { uiLogger } from '../ui/logger.js';
|
|
|
6
6
|
import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
|
|
7
7
|
import { lib } from '../../lang/en.js';
|
|
8
8
|
import { debugError } from '../errorHandlers/index.js';
|
|
9
|
+
import { uiLink } from '../ui/index.js';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { renderInline } from '../../ui/index.js';
|
|
12
|
+
import { getSuccessBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
9
13
|
// Prefix for the metafile extension
|
|
10
14
|
const metafileExtensionPrefix = path.parse(metafileExtension).name;
|
|
15
|
+
function buildProjectTree(projectName, uids, componentsByType, showOnlyNew) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
lines.push(chalk.bold(projectName));
|
|
18
|
+
const types = Array.from(componentsByType.keys());
|
|
19
|
+
for (const uid of uids) {
|
|
20
|
+
lines.push(`├─ [app] ${uid}`);
|
|
21
|
+
for (let i = 0; i < types.length; i++) {
|
|
22
|
+
const type = types[i];
|
|
23
|
+
const allComponents = componentsByType.get(type) || [];
|
|
24
|
+
const components = showOnlyNew
|
|
25
|
+
? allComponents.filter(c => c.isNew)
|
|
26
|
+
: allComponents;
|
|
27
|
+
if (components.length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
const isLastType = i === types.length - 1;
|
|
30
|
+
const typePrefix = isLastType ? '│ └─' : '│ ├─';
|
|
31
|
+
const typeConnector = isLastType ? ' ' : '│ ';
|
|
32
|
+
lines.push(`${typePrefix} ${type}`);
|
|
33
|
+
for (let j = 0; j < components.length; j++) {
|
|
34
|
+
const component = components[j];
|
|
35
|
+
const isLastComponent = j === components.length - 1;
|
|
36
|
+
const componentPrefix = isLastComponent ? '└─' : '├─';
|
|
37
|
+
const addedLabel = component.isNew ? chalk.green(' (added)') : '';
|
|
38
|
+
lines.push(`│ ${typeConnector}${componentPrefix} ${component.filename}${addedLabel}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
44
|
+
function buildSuccessMessage(projectName, uids, componentsByType, newComponentsCount, showOnlyNew, isProjectEmpty, projectDest) {
|
|
45
|
+
const messages = lib.projects.components.buildSuccessMessage;
|
|
46
|
+
const tree = buildProjectTree(projectName, uids, componentsByType, showOnlyNew);
|
|
47
|
+
const featureText = `${newComponentsCount} feature${newComponentsCount > 1 ? 's' : ''}`;
|
|
48
|
+
const uid = uids.length === 1 ? uids[0] : projectName;
|
|
49
|
+
const docsLink = uiLink(messages.seeOurDocs, messages.docsUrl);
|
|
50
|
+
const header = projectDest
|
|
51
|
+
? messages.headerCreated(projectName, projectDest)
|
|
52
|
+
: messages.headerAdded(featureText, uid, newComponentsCount > 1);
|
|
53
|
+
// Use \n\n between sections to create gaps, \n within sections for no gaps
|
|
54
|
+
const sections = [
|
|
55
|
+
header,
|
|
56
|
+
isProjectEmpty ? null : tree,
|
|
57
|
+
messages.docsDetails(docsLink),
|
|
58
|
+
`${messages.uploadPrompt}\n${messages.devPrompt}`,
|
|
59
|
+
].filter(Boolean);
|
|
60
|
+
return sections.join('\n\n');
|
|
61
|
+
}
|
|
11
62
|
function applyDifferentiatorToFilename(filename, differentiator, isHsMetaFile) {
|
|
12
63
|
const { name, ext, dir } = path.parse(filename);
|
|
13
64
|
if (isHsMetaFile) {
|
|
@@ -120,9 +171,7 @@ function handlePackageJsonCollisions(dest, src, packageJsonFiles) {
|
|
|
120
171
|
fs.writeFileSync(path.join(dest, file), JSON.stringify(existingPackageJsonContents, null, 2));
|
|
121
172
|
});
|
|
122
173
|
}
|
|
123
|
-
export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids = []) {
|
|
124
|
-
uiLogger.log('');
|
|
125
|
-
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.header);
|
|
174
|
+
export async function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids = [], options) {
|
|
126
175
|
for (const hsMetaFile of hsMetaFilePaths) {
|
|
127
176
|
try {
|
|
128
177
|
const component = JSON.parse(fs.readFileSync(hsMetaFile).toString());
|
|
@@ -142,10 +191,6 @@ export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFile
|
|
|
142
191
|
component.uid = uid;
|
|
143
192
|
if (component.type === AppKey && component.config) {
|
|
144
193
|
component.config.name = `${projectName}-Application`;
|
|
145
|
-
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
|
|
149
194
|
}
|
|
150
195
|
fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
|
|
151
196
|
}
|
|
@@ -154,5 +199,43 @@ export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFile
|
|
|
154
199
|
uiLogger.error(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.failedToUpdate(hsMetaFile));
|
|
155
200
|
}
|
|
156
201
|
}
|
|
157
|
-
|
|
202
|
+
if (options?.showSuccessMessage && options?.updatedProjectMetadata) {
|
|
203
|
+
const { currentProjectMetadata, updatedProjectMetadata, isProjectEmpty, projectDest, } = options;
|
|
204
|
+
const uids = [];
|
|
205
|
+
const updatedAppsMetadata = updatedProjectMetadata.components[AppKey];
|
|
206
|
+
// Get UID(s) from -hsmeta.json files
|
|
207
|
+
if (updatedAppsMetadata?.hsMetaFiles) {
|
|
208
|
+
updatedAppsMetadata.hsMetaFiles.forEach(appLocation => {
|
|
209
|
+
try {
|
|
210
|
+
const appConfig = JSON.parse(fs.readFileSync(appLocation, 'utf-8'));
|
|
211
|
+
uids.push(appConfig.uid || 'unknown app'); // fallback to unknown app incase we can't get uid
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
uiLogger.debug(lib.projects.components.unableToGetUidFromHsmeta);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Fallback if no app hsmeta files found or all failed to parse
|
|
219
|
+
if (uids.length === 0) {
|
|
220
|
+
uids.push('unknown app');
|
|
221
|
+
}
|
|
222
|
+
const componentsByType = new Map();
|
|
223
|
+
const addComponent = (hsMetaPath, isNew) => {
|
|
224
|
+
const type = path.basename(path.dirname(hsMetaPath));
|
|
225
|
+
const existing = componentsByType.get(type) || [];
|
|
226
|
+
existing.push({ filename: path.basename(hsMetaPath), isNew });
|
|
227
|
+
componentsByType.set(type, existing);
|
|
228
|
+
};
|
|
229
|
+
// Add new files
|
|
230
|
+
hsMetaFilePaths.forEach(hsMetaFile => addComponent(hsMetaFile, true));
|
|
231
|
+
if (currentProjectMetadata) {
|
|
232
|
+
Object.entries(currentProjectMetadata.components)
|
|
233
|
+
.filter(([type, metadata]) => type !== AppKey && metadata.count > 0)
|
|
234
|
+
.flatMap(([, metadata]) => metadata.hsMetaFiles)
|
|
235
|
+
.forEach(hsMetaFile => addComponent(hsMetaFile, false));
|
|
236
|
+
}
|
|
237
|
+
const newComponentsCount = hsMetaFilePaths.length;
|
|
238
|
+
const successMessage = buildSuccessMessage(projectName, uids, componentsByType, newComponentsCount, false, isProjectEmpty, projectDest);
|
|
239
|
+
await renderInline(getSuccessBox({ title: 'SUCCESS', message: successMessage }));
|
|
240
|
+
}
|
|
158
241
|
}
|