@hubspot/cli 7.7.32-experimental.0 → 7.7.33-experimental.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/getStarted.js +5 -4
- package/commands/project/__tests__/add.test.js +3 -5
- package/commands/project/__tests__/deploy.test.js +3 -2
- package/commands/project/add.js +2 -4
- package/commands/project/deploy.js +9 -61
- package/commands/project/dev/index.js +1 -1
- package/commands/project/dev/unifiedFlow.js +3 -0
- package/commands/project/upload.js +2 -2
- package/commands/project/validate.js +1 -1
- package/commands/project/watch.js +2 -2
- package/lang/en.d.ts +7 -3
- package/lang/en.js +8 -4
- package/lib/__tests__/hasFeature.test.js +145 -7
- package/lib/__tests__/importData.test.js +1 -1
- package/lib/app/migrate.js +9 -2
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +2 -0
- package/lib/errorHandlers/index.d.ts +4 -0
- package/lib/errorHandlers/index.js +1 -1
- package/lib/hasFeature.js +6 -0
- package/lib/importData.js +1 -1
- package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
- package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
- package/lib/projects/__tests__/deploy.test.js +164 -0
- package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
- package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
- package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
- package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
- package/lib/projects/add/legacyAddComponent.d.ts +1 -1
- package/lib/projects/add/legacyAddComponent.js +5 -1
- package/lib/projects/add/v3AddComponent.d.ts +1 -0
- package/lib/projects/add/v3AddComponent.js +2 -2
- package/lib/projects/create/__tests__/v3.test.js +97 -9
- package/lib/projects/create/index.js +2 -2
- package/lib/projects/create/legacy.js +1 -1
- package/lib/projects/create/v3.d.ts +2 -2
- package/lib/projects/create/v3.js +35 -12
- package/lib/projects/deploy.d.ts +13 -0
- package/lib/projects/deploy.js +63 -0
- package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
- package/lib/projects/localDev/AppDevModeInterface.js +65 -36
- package/lib/projects/localDev/DevServerManagerV2.js +1 -0
- package/lib/projects/localDev/LocalDevProcess.js +3 -1
- package/lib/projects/localDev/LocalDevState.d.ts +5 -2
- package/lib/projects/localDev/LocalDevState.js +9 -1
- package/lib/projects/localDev/helpers/project.js +1 -1
- package/lib/projects/platformVersion.d.ts +1 -0
- package/lib/projects/platformVersion.js +10 -0
- package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
- package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
- package/lib/projects/upload.js +1 -1
- package/lib/projects/urls.d.ts +1 -0
- package/lib/projects/urls.js +3 -0
- package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
- package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
- package/lib/prompts/importDataFilePathPrompt.js +4 -2
- package/lib/prompts/installAppPrompt.d.ts +6 -1
- package/lib/prompts/installAppPrompt.js +6 -1
- package/lib/prompts/projectAddPrompt.js +1 -1
- package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
- package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
- package/mcp-server/tools/index.js +2 -0
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/GetConfigValuesTool.js +3 -3
- package/mcp-server/tools/project/constants.d.ts +1 -1
- package/mcp-server/tools/project/constants.js +6 -4
- package/package.json +3 -3
- package/types/LocalDev.d.ts +2 -1
- package/types/Projects.d.ts +1 -0
- /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
package/lib/app/migrate.js
CHANGED
|
@@ -22,7 +22,7 @@ import { hasUnfiedAppsAccess } from '../hasFeature.js';
|
|
|
22
22
|
import { getProjectBuildDetailUrl, getProjectDetailUrl, } from '../projects/urls.js';
|
|
23
23
|
import { uiLogger } from '../ui/logger.js';
|
|
24
24
|
import { debugError } from '../errorHandlers/index.js';
|
|
25
|
-
import { useV3Api } from '../projects/
|
|
25
|
+
import { useV3Api } from '../projects/platformVersion.js';
|
|
26
26
|
export function getUnmigratableReason(reasonCode, projectName, accountId) {
|
|
27
27
|
switch (reasonCode) {
|
|
28
28
|
case UNMIGRATABLE_REASONS.UP_TO_DATE:
|
|
@@ -236,13 +236,20 @@ export async function handleThemesMigration(projectConfig, platformVersion) {
|
|
|
236
236
|
throw new Error(lib.migrate.errors.project.invalidConfig);
|
|
237
237
|
}
|
|
238
238
|
const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
|
|
239
|
+
let migrated = false;
|
|
240
|
+
let failureReason;
|
|
239
241
|
try {
|
|
240
|
-
await migrateThemes(projectConfig.projectDir, projectSrcDir);
|
|
242
|
+
const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
|
|
243
|
+
migrated = migrationResult.migrated;
|
|
244
|
+
failureReason = migrationResult.failureReason;
|
|
241
245
|
}
|
|
242
246
|
catch (error) {
|
|
243
247
|
debugError(error);
|
|
244
248
|
throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
|
|
245
249
|
}
|
|
250
|
+
if (!migrated) {
|
|
251
|
+
throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
|
|
252
|
+
}
|
|
246
253
|
const newProjectConfig = { ...projectConfig.projectConfig };
|
|
247
254
|
newProjectConfig.platformVersion = platformVersion;
|
|
248
255
|
const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
|
package/lib/constants.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare const FEATURES: {
|
|
|
80
80
|
readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
|
|
81
81
|
readonly SANDBOXES_V2: "sandboxes:v2:enabled";
|
|
82
82
|
readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
|
|
83
|
+
readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
|
|
84
|
+
readonly APPS_HOME: "UIE:AppHome";
|
|
83
85
|
};
|
|
84
86
|
export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
|
|
85
87
|
UPLOAD_SUCCESS: string;
|
package/lib/constants.js
CHANGED
|
@@ -72,6 +72,8 @@ export const FEATURES = {
|
|
|
72
72
|
UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
|
|
73
73
|
SANDBOXES_V2: 'sandboxes:v2:enabled',
|
|
74
74
|
SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
|
|
75
|
+
APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
|
|
76
|
+
APPS_HOME: 'UIE:AppHome',
|
|
75
77
|
};
|
|
76
78
|
export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
|
|
77
79
|
UPLOAD_SUCCESS: 'server:uploadSuccess',
|
|
@@ -85,7 +85,7 @@ export class ApiErrorContext {
|
|
|
85
85
|
this.projectName = props.projectName || '';
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
function isErrorWithMessageOrReason(error) {
|
|
88
|
+
export function isErrorWithMessageOrReason(error) {
|
|
89
89
|
return (typeof error === 'object' &&
|
|
90
90
|
error !== null &&
|
|
91
91
|
('message' in error || 'reason' in error));
|
package/lib/hasFeature.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { http } from '@hubspot/local-dev-lib/http';
|
|
2
2
|
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
3
|
+
import { FEATURES } from './constants.js';
|
|
4
|
+
const FEATURES_THAT_DEFAULT_ON = [FEATURES.APPS_HOME];
|
|
3
5
|
export async function hasFeature(accountId, feature) {
|
|
4
6
|
const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
|
|
7
|
+
if (enabledFeatures[feature] === undefined &&
|
|
8
|
+
FEATURES_THAT_DEFAULT_ON.includes(feature)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
5
11
|
return Boolean(enabledFeatures[feature]);
|
|
6
12
|
}
|
|
7
13
|
export async function hasUnfiedAppsAccess(accountId) {
|
package/lib/importData.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function handleImportData(targetAccountId, dataFileNames, importReq
|
|
|
10
10
|
const baseUrl = getHubSpotWebsiteOrigin(getEnv());
|
|
11
11
|
const response = await createImport(targetAccountId, importRequest, dataFileNames);
|
|
12
12
|
const importId = response.data.id;
|
|
13
|
-
uiLogger.
|
|
13
|
+
uiLogger.info(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
|
|
14
14
|
}
|
|
15
15
|
catch (error) {
|
|
16
16
|
uiLogger.error(lib.importData.errors.failedToImportData);
|
|
@@ -240,16 +240,21 @@ describe('AppDevModeInterface', () => {
|
|
|
240
240
|
await newAppDevModeInterface.setup({});
|
|
241
241
|
expect(process.exit).toHaveBeenCalledWith(0);
|
|
242
242
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
243
|
+
// @TODO: Restore test account auto install functionality
|
|
244
|
+
// it('should auto-install static auth app on test account', async () => {
|
|
245
|
+
// (fetchAppInstallationData as Mock).mockResolvedValue({
|
|
246
|
+
// data: {
|
|
247
|
+
// isInstalledWithScopeGroups: false,
|
|
248
|
+
// previouslyAuthorizedScopeGroups: [],
|
|
249
|
+
// },
|
|
250
|
+
// });
|
|
251
|
+
// await appDevModeInterface.setup({});
|
|
252
|
+
// expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
|
|
253
|
+
// 123,
|
|
254
|
+
// 67890,
|
|
255
|
+
// [1, 2, 3]
|
|
256
|
+
// );
|
|
257
|
+
// });
|
|
253
258
|
it('should open browser for OAuth app installation', async () => {
|
|
254
259
|
const oauthAppNode = {
|
|
255
260
|
...mockAppNode,
|
|
@@ -288,7 +293,12 @@ describe('AppDevModeInterface', () => {
|
|
|
288
293
|
},
|
|
289
294
|
});
|
|
290
295
|
await appDevModeInterface.setup({});
|
|
291
|
-
expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true
|
|
296
|
+
expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
|
|
297
|
+
appUid: 'test-app-uid',
|
|
298
|
+
projectAccountId: 12345,
|
|
299
|
+
projectName: 'test-project',
|
|
300
|
+
testingAccountId: 67890,
|
|
301
|
+
});
|
|
292
302
|
});
|
|
293
303
|
it('should handle errors during setup', async () => {
|
|
294
304
|
const error = new Error('Setup failed');
|
|
@@ -318,39 +328,46 @@ describe('AppDevModeInterface', () => {
|
|
|
318
328
|
await appDevModeInterface.setup({});
|
|
319
329
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
320
330
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
331
|
+
// @TODO: Restore test account auto install functionality
|
|
332
|
+
// it('should exit if user declines auto-install', async () => {
|
|
333
|
+
// // Set up conditions for automatic installation
|
|
334
|
+
// (getAccountConfig as Mock).mockReturnValue({
|
|
335
|
+
// parentAccountId: 12345, // matches targetProjectAccountId
|
|
336
|
+
// });
|
|
337
|
+
// (isDeveloperTestAccount as Mock).mockReturnValue(true);
|
|
338
|
+
// (fetchAppInstallationData as Mock).mockResolvedValue({
|
|
339
|
+
// data: {
|
|
340
|
+
// isInstalledWithScopeGroups: false,
|
|
341
|
+
// previouslyAuthorizedScopeGroups: [],
|
|
342
|
+
// },
|
|
343
|
+
// });
|
|
344
|
+
// (installAppAutoPrompt as Mock).mockResolvedValue(false);
|
|
345
|
+
// // Create a new instance to trigger the exit during setup
|
|
346
|
+
// const newAppDevModeInterface = new AppDevModeInterface({
|
|
347
|
+
// localDevState: mockLocalDevState,
|
|
348
|
+
// localDevLogger: mockLocalDevLogger,
|
|
349
|
+
// });
|
|
350
|
+
// // The setup method catches the error, so we check that process.exit was called
|
|
351
|
+
// await newAppDevModeInterface.setup({});
|
|
352
|
+
// expect(process.exit).toHaveBeenCalledWith(0);
|
|
353
|
+
// });
|
|
354
|
+
// @TODO: Restore test account auto install functionality
|
|
355
|
+
// it('should fallback to browser install if auto-install fails', async () => {
|
|
356
|
+
// (fetchAppInstallationData as Mock).mockResolvedValue({
|
|
357
|
+
// data: {
|
|
358
|
+
// isInstalledWithScopeGroups: false,
|
|
359
|
+
// previouslyAuthorizedScopeGroups: [],
|
|
360
|
+
// },
|
|
361
|
+
// });
|
|
362
|
+
// (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
|
|
363
|
+
// new Error('Install failed')
|
|
364
|
+
// );
|
|
365
|
+
// await appDevModeInterface.setup({});
|
|
366
|
+
// expect(installAppBrowserPrompt).toHaveBeenCalledWith(
|
|
367
|
+
// 'http://static-install-url',
|
|
368
|
+
// false
|
|
369
|
+
// );
|
|
370
|
+
// });
|
|
354
371
|
});
|
|
355
372
|
describe('start()', () => {
|
|
356
373
|
it('should return early if no app node exists', async () => {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { validateBuildIdForDeploy, logDeployErrors, handleProjectDeploy, } from '../deploy.js';
|
|
3
|
+
import { uiLogger } from '../../ui/logger.js';
|
|
4
|
+
import { commands } from '../../../lang/en.js';
|
|
5
|
+
import { PROJECT_ERROR_TYPES } from '../../constants.js';
|
|
6
|
+
import { deployProject } from '@hubspot/local-dev-lib/api/projects';
|
|
7
|
+
import { pollDeployStatus } from '../pollProjectBuildAndDeploy.js';
|
|
8
|
+
// Mock external dependencies
|
|
9
|
+
vi.mock('../../ui/logger.js');
|
|
10
|
+
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
11
|
+
vi.mock('../pollProjectBuildAndDeploy.js');
|
|
12
|
+
const mockUiLogger = vi.mocked(uiLogger);
|
|
13
|
+
const mockDeployProject = vi.mocked(deployProject);
|
|
14
|
+
const mockPollDeployStatus = vi.mocked(pollDeployStatus);
|
|
15
|
+
describe('lib/projects/deploy', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.resetAllMocks();
|
|
18
|
+
});
|
|
19
|
+
describe('validateBuildIdForDeploy()', () => {
|
|
20
|
+
const accountId = 12345;
|
|
21
|
+
const projectName = 'test-project';
|
|
22
|
+
it('returns true when build ID is valid for deployment', () => {
|
|
23
|
+
const buildId = 5;
|
|
24
|
+
const deployedBuildId = 3;
|
|
25
|
+
const latestBuildId = 10;
|
|
26
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
27
|
+
expect(result).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('returns error message when build ID does not exist', () => {
|
|
30
|
+
const buildId = 15;
|
|
31
|
+
const deployedBuildId = 3;
|
|
32
|
+
const latestBuildId = 10;
|
|
33
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
34
|
+
expect(result).toBe(commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName));
|
|
35
|
+
});
|
|
36
|
+
it('returns error message when build is already deployed', () => {
|
|
37
|
+
const buildId = 3;
|
|
38
|
+
const deployedBuildId = 3;
|
|
39
|
+
const latestBuildId = 10;
|
|
40
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
41
|
+
expect(result).toBe(commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName));
|
|
42
|
+
});
|
|
43
|
+
it('handles edge case when deployedBuildId is undefined', () => {
|
|
44
|
+
const buildId = 5;
|
|
45
|
+
const deployedBuildId = undefined;
|
|
46
|
+
const latestBuildId = 10;
|
|
47
|
+
const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
|
|
48
|
+
expect(result).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('logDeployErrors()', () => {
|
|
52
|
+
it('logs main error message and individual error messages', () => {
|
|
53
|
+
const errorData = {
|
|
54
|
+
message: 'Deploy failed with errors',
|
|
55
|
+
errors: [
|
|
56
|
+
{
|
|
57
|
+
message: 'Component error 1',
|
|
58
|
+
subCategory: 'SOME_ERROR',
|
|
59
|
+
context: { COMPONENT_NAME: 'test-component' },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
message: 'Component error 2',
|
|
63
|
+
subCategory: 'ANOTHER_ERROR',
|
|
64
|
+
context: { COMPONENT_NAME: 'another-component' },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
logDeployErrors(errorData);
|
|
69
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy failed with errors');
|
|
70
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 1');
|
|
71
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 2');
|
|
72
|
+
});
|
|
73
|
+
it('handles DEPLOY_CONTAINS_REMOVALS error type specially', () => {
|
|
74
|
+
const errorData = {
|
|
75
|
+
message: 'Deploy contains removals',
|
|
76
|
+
errors: [
|
|
77
|
+
{
|
|
78
|
+
message: 'Component will be removed',
|
|
79
|
+
subCategory: PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS,
|
|
80
|
+
context: { COMPONENT_NAME: 'removed-component' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
logDeployErrors(errorData);
|
|
85
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy contains removals');
|
|
86
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployContainsRemovals('removed-component'));
|
|
87
|
+
});
|
|
88
|
+
it('handles empty errors array', () => {
|
|
89
|
+
const errorData = {
|
|
90
|
+
message: 'No specific errors',
|
|
91
|
+
errors: [],
|
|
92
|
+
};
|
|
93
|
+
logDeployErrors(errorData);
|
|
94
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith('No specific errors');
|
|
95
|
+
expect(mockUiLogger.log).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('handleProjectDeploy()', () => {
|
|
99
|
+
const targetAccountId = 12345;
|
|
100
|
+
const projectName = 'test-project';
|
|
101
|
+
const buildId = 5;
|
|
102
|
+
const useV3Api = true;
|
|
103
|
+
const force = false;
|
|
104
|
+
it('successfully deploys and returns deploy result', async () => {
|
|
105
|
+
const mockDeployResponseData = {
|
|
106
|
+
id: 'deploy-123',
|
|
107
|
+
buildResultType: 'DEPLOY_QUEUED',
|
|
108
|
+
links: {
|
|
109
|
+
status: 'http://status-url',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const mockDeployResult = {
|
|
113
|
+
deployId: 123,
|
|
114
|
+
buildId: 5,
|
|
115
|
+
status: 'SUCCESS',
|
|
116
|
+
enqueuedAt: '2023-01-01T00:00:00Z',
|
|
117
|
+
startedAt: '2023-01-01T00:01:00Z',
|
|
118
|
+
finishedAt: '2023-01-01T00:05:00Z',
|
|
119
|
+
portalId: targetAccountId,
|
|
120
|
+
projectName: 'test-project',
|
|
121
|
+
userId: 456,
|
|
122
|
+
source: 'HUBSPOT_USER',
|
|
123
|
+
subdeployStatuses: [],
|
|
124
|
+
};
|
|
125
|
+
mockDeployProject.mockResolvedValue({
|
|
126
|
+
data: mockDeployResponseData,
|
|
127
|
+
});
|
|
128
|
+
mockPollDeployStatus.mockResolvedValue(mockDeployResult);
|
|
129
|
+
const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
130
|
+
expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
|
|
131
|
+
expect(result).toEqual(mockDeployResult);
|
|
132
|
+
});
|
|
133
|
+
it('handles blocked deploy with warnings', async () => {
|
|
134
|
+
const mockBlockedResponse = {
|
|
135
|
+
buildResultType: 'DEPLOY_BLOCKED',
|
|
136
|
+
issues: [
|
|
137
|
+
{
|
|
138
|
+
uid: 'component-1',
|
|
139
|
+
componentTypeName: 'module',
|
|
140
|
+
errorMessages: [],
|
|
141
|
+
blockingMessages: [
|
|
142
|
+
{
|
|
143
|
+
message: 'This is a warning',
|
|
144
|
+
isWarning: true,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
mockDeployProject.mockResolvedValue({
|
|
151
|
+
data: mockBlockedResponse,
|
|
152
|
+
});
|
|
153
|
+
const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
154
|
+
expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
|
|
155
|
+
expect(result).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
it('handles general deploy failure', async () => {
|
|
158
|
+
mockDeployProject.mockResolvedValue({ data: null });
|
|
159
|
+
const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
|
|
160
|
+
expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
|
|
161
|
+
expect(result).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useV3Api } from '../
|
|
2
|
-
describe('
|
|
1
|
+
import { useV3Api } from '../platformVersion.js';
|
|
2
|
+
describe('platformVersion', () => {
|
|
3
3
|
describe('useV3Api', () => {
|
|
4
4
|
it('returns true if platform version is UNSTABLE', () => {
|
|
5
5
|
expect(useV3Api('UNSTABLE')).toBe(true);
|
|
@@ -4,6 +4,7 @@ import { getProjectComponentListFromRepo } from '../../create/legacy.js';
|
|
|
4
4
|
import { projectAddPrompt } from '../../../prompts/projectAddPrompt.js';
|
|
5
5
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
6
6
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
7
|
+
import { trackCommandUsage } from '../../../usageTracking.js';
|
|
7
8
|
import { ComponentTypes, } from '../../../../types/Projects.js';
|
|
8
9
|
import { commands } from '../../../../lang/en.js';
|
|
9
10
|
vi.mock('../../structure');
|
|
@@ -11,21 +12,25 @@ vi.mock('../../create/legacy');
|
|
|
11
12
|
vi.mock('../../../prompts/projectAddPrompt');
|
|
12
13
|
vi.mock('@hubspot/local-dev-lib/logger');
|
|
13
14
|
vi.mock('@hubspot/local-dev-lib/github');
|
|
15
|
+
vi.mock('../../../usageTracking.js');
|
|
14
16
|
const mockedFindProjectComponents = vi.mocked(findProjectComponents);
|
|
15
17
|
const mockedGetProjectComponentListFromRepo = vi.mocked(getProjectComponentListFromRepo);
|
|
16
18
|
const mockedProjectAddPrompt = vi.mocked(projectAddPrompt);
|
|
17
19
|
const mockedLogger = vi.mocked(logger);
|
|
18
20
|
const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
|
|
21
|
+
const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
|
|
19
22
|
describe('lib/projects/add/legacyAddComponent', () => {
|
|
20
23
|
const mockProjectConfig = {
|
|
21
24
|
name: 'test-project',
|
|
22
25
|
srcDir: 'src',
|
|
23
26
|
platformVersion: 'v1',
|
|
24
27
|
};
|
|
28
|
+
const accountId = 1234567890;
|
|
25
29
|
const mockArgs = { name: 'test-component', type: 'module' };
|
|
26
30
|
const projectDir = '/path/to/project';
|
|
27
31
|
beforeEach(() => {
|
|
28
32
|
vi.resetAllMocks();
|
|
33
|
+
mockedTrackCommandUsage.mockResolvedValue();
|
|
29
34
|
});
|
|
30
35
|
describe('legacyAddComponent()', () => {
|
|
31
36
|
it('successfully adds a component to a project without public apps', async () => {
|
|
@@ -58,7 +63,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
58
63
|
mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
|
|
59
64
|
mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
|
|
60
65
|
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
61
|
-
await legacyAddComponent(mockArgs, projectDir, mockProjectConfig);
|
|
66
|
+
await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
|
|
62
67
|
expect(mockedFindProjectComponents).toHaveBeenCalledWith(projectDir);
|
|
63
68
|
expect(mockedGetProjectComponentListFromRepo).toHaveBeenCalledWith('v1');
|
|
64
69
|
expect(mockedProjectAddPrompt).toHaveBeenCalledWith(mockComponentList, mockArgs);
|
|
@@ -67,6 +72,9 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
67
72
|
branch: 'main',
|
|
68
73
|
hideLogs: true,
|
|
69
74
|
}));
|
|
75
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
76
|
+
type: 'module',
|
|
77
|
+
}, accountId);
|
|
70
78
|
expect(mockedLogger.log).toHaveBeenCalledWith(commands.project.add.creatingComponent('test-project'));
|
|
71
79
|
expect(mockedLogger.success).toHaveBeenCalledWith(commands.project.add.success('new-component'));
|
|
72
80
|
});
|
|
@@ -97,7 +105,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
97
105
|
},
|
|
98
106
|
];
|
|
99
107
|
mockedFindProjectComponents.mockResolvedValue(mockComponents);
|
|
100
|
-
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.projectContainsPublicApp);
|
|
108
|
+
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.projectContainsPublicApp);
|
|
101
109
|
expect(mockedGetProjectComponentListFromRepo).not.toHaveBeenCalled();
|
|
102
110
|
expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
|
|
103
111
|
expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
|
|
@@ -118,7 +126,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
118
126
|
mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
|
|
119
127
|
mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
|
|
120
128
|
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
121
|
-
await legacyAddComponent(mockArgs, projectDir, mockProjectConfig);
|
|
129
|
+
await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
|
|
122
130
|
expect(mockedGetProjectComponentListFromRepo).toHaveBeenCalledWith('v1');
|
|
123
131
|
expect(mockedProjectAddPrompt).toHaveBeenCalledWith(mockComponentList, mockArgs);
|
|
124
132
|
expect(mockedCloneGithubRepo).toHaveBeenCalled();
|
|
@@ -140,7 +148,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
140
148
|
];
|
|
141
149
|
mockedFindProjectComponents.mockResolvedValue(mockComponents);
|
|
142
150
|
mockedGetProjectComponentListFromRepo.mockResolvedValue([]);
|
|
143
|
-
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
|
|
151
|
+
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
|
|
144
152
|
expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
|
|
145
153
|
expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
|
|
146
154
|
});
|
|
@@ -162,7 +170,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
162
170
|
mockedFindProjectComponents.mockResolvedValue(mockComponents);
|
|
163
171
|
// @ts-expect-error Breaking stuff on purpose
|
|
164
172
|
mockedGetProjectComponentListFromRepo.mockResolvedValue(null);
|
|
165
|
-
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
|
|
173
|
+
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
|
|
166
174
|
expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
|
|
167
175
|
expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
|
|
168
176
|
});
|
|
@@ -196,9 +204,44 @@ describe('lib/projects/add/legacyAddComponent', () => {
|
|
|
196
204
|
mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
|
|
197
205
|
mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
|
|
198
206
|
mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
|
|
199
|
-
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
|
|
207
|
+
await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
|
|
200
208
|
expect(mockedCloneGithubRepo).toHaveBeenCalled();
|
|
201
209
|
expect(mockedLogger.success).not.toHaveBeenCalled();
|
|
202
210
|
});
|
|
211
|
+
it('calls trackCommandUsage with correct component type', async () => {
|
|
212
|
+
const mockComponents = [
|
|
213
|
+
{
|
|
214
|
+
type: ComponentTypes.PrivateApp,
|
|
215
|
+
config: {
|
|
216
|
+
name: 'private-app',
|
|
217
|
+
description: '',
|
|
218
|
+
uid: '',
|
|
219
|
+
scopes: [],
|
|
220
|
+
public: false,
|
|
221
|
+
},
|
|
222
|
+
runnable: true,
|
|
223
|
+
path: '/path/to/private-app',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
const mockComponentList = [
|
|
227
|
+
{ label: 'Card Component', path: 'card-component', type: 'card' },
|
|
228
|
+
];
|
|
229
|
+
const mockPromptResponse = {
|
|
230
|
+
name: 'new-card',
|
|
231
|
+
componentTemplate: {
|
|
232
|
+
label: 'Card Component',
|
|
233
|
+
path: 'card-template-path',
|
|
234
|
+
type: 'card',
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
mockedFindProjectComponents.mockResolvedValue(mockComponents);
|
|
238
|
+
mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
|
|
239
|
+
mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
|
|
240
|
+
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
241
|
+
await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
|
|
242
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
243
|
+
type: 'card',
|
|
244
|
+
}, accountId);
|
|
245
|
+
});
|
|
203
246
|
});
|
|
204
247
|
});
|
|
@@ -33,7 +33,11 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
33
33
|
srcDir: 'src',
|
|
34
34
|
platformVersion: 'v3',
|
|
35
35
|
};
|
|
36
|
-
const mockArgs = {
|
|
36
|
+
const mockArgs = {
|
|
37
|
+
name: 'test-component',
|
|
38
|
+
type: 'module',
|
|
39
|
+
derivedAccountId: 1234,
|
|
40
|
+
};
|
|
37
41
|
const projectDir = '/path/to/project';
|
|
38
42
|
const mockAccountId = 123;
|
|
39
43
|
const mockComponentTemplate = {
|
|
@@ -245,5 +249,71 @@ describe('lib/projects/add/v3AddComponent', () => {
|
|
|
245
249
|
type: '',
|
|
246
250
|
}, mockAccountId);
|
|
247
251
|
});
|
|
252
|
+
it('should track usage with cliSelector when available', async () => {
|
|
253
|
+
const mockAppMeta = {
|
|
254
|
+
config: {
|
|
255
|
+
distribution: 'private',
|
|
256
|
+
auth: { type: 'oauth' },
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
const mockComponentTemplateWithCliSelector = {
|
|
260
|
+
label: 'Workflow Action Tool',
|
|
261
|
+
path: 'workflow-action-tool',
|
|
262
|
+
type: 'workflow-action',
|
|
263
|
+
cliSelector: 'workflow-action-tool',
|
|
264
|
+
supportedAuthTypes: ['oauth'],
|
|
265
|
+
supportedDistributions: ['private'],
|
|
266
|
+
};
|
|
267
|
+
const mockPromptResponse = {
|
|
268
|
+
componentTemplate: [mockComponentTemplateWithCliSelector],
|
|
269
|
+
};
|
|
270
|
+
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
271
|
+
mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
|
|
272
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
|
|
273
|
+
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
274
|
+
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
275
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
276
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
277
|
+
type: 'workflow-action-tool',
|
|
278
|
+
}, mockAccountId);
|
|
279
|
+
});
|
|
280
|
+
it('should track usage with cliSelector for multiple components', async () => {
|
|
281
|
+
const mockAppMeta = {
|
|
282
|
+
config: {
|
|
283
|
+
distribution: 'private',
|
|
284
|
+
auth: { type: 'oauth' },
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const mockComponentWithCliSelector = {
|
|
288
|
+
label: 'Workflow Action Tool',
|
|
289
|
+
path: 'workflow-action-tool',
|
|
290
|
+
type: 'workflow-action',
|
|
291
|
+
cliSelector: 'workflow-action-tool',
|
|
292
|
+
supportedAuthTypes: ['oauth'],
|
|
293
|
+
supportedDistributions: ['private'],
|
|
294
|
+
};
|
|
295
|
+
const mockComponentWithoutCliSelector = {
|
|
296
|
+
label: 'Regular Module',
|
|
297
|
+
path: 'module',
|
|
298
|
+
type: 'module',
|
|
299
|
+
supportedAuthTypes: ['oauth'],
|
|
300
|
+
supportedDistributions: ['private'],
|
|
301
|
+
};
|
|
302
|
+
const mockPromptResponse = {
|
|
303
|
+
componentTemplate: [
|
|
304
|
+
mockComponentWithCliSelector,
|
|
305
|
+
mockComponentWithoutCliSelector,
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
|
|
309
|
+
mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
|
|
310
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
|
|
311
|
+
mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
|
|
312
|
+
mockedCloneGithubRepo.mockResolvedValue(true);
|
|
313
|
+
await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
|
|
314
|
+
expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
|
|
315
|
+
type: 'workflow-action-tool,module',
|
|
316
|
+
}, mockAccountId);
|
|
317
|
+
});
|
|
248
318
|
});
|
|
249
319
|
});
|
|
@@ -2,4 +2,4 @@ import { ProjectConfig } from '../../../types/Projects.js';
|
|
|
2
2
|
export declare function legacyAddComponent(args: {
|
|
3
3
|
name?: string;
|
|
4
4
|
type?: string;
|
|
5
|
-
}, projectDir: string, projectConfig: ProjectConfig): Promise<void>;
|
|
5
|
+
}, projectDir: string, projectConfig: ProjectConfig, derivedAccountId: number): Promise<void>;
|