@hubspot/cli 7.6.0-beta.11 → 7.6.0-beta.13
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/app/__tests__/migrate.test.js +1 -0
- package/commands/getStarted.js +7 -20
- package/commands/mcp/setup.d.ts +0 -1
- package/commands/mcp/setup.js +11 -11
- package/commands/project/__tests__/add.test.js +64 -0
- package/commands/project/__tests__/create.test.js +57 -0
- package/commands/project/__tests__/deploy.test.js +3 -2
- package/commands/project/add.d.ts +1 -1
- package/commands/project/add.js +3 -5
- package/commands/project/create.js +7 -2
- package/commands/project/deploy.js +9 -61
- package/commands/project/dev/index.js +1 -1
- package/commands/project/dev/unifiedFlow.js +4 -1
- package/commands/project/migrate.js +26 -7
- 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 +20 -5
- package/lang/en.js +38 -22
- package/lang/en.lyaml +12 -12
- package/lib/__tests__/hasFeature.test.js +145 -7
- package/lib/__tests__/importData.test.js +1 -1
- package/lib/app/__tests__/migrate.test.js +14 -51
- package/lib/app/migrate.d.ts +2 -8
- package/lib/app/migrate.js +5 -73
- package/lib/constants.d.ts +4 -0
- package/lib/constants.js +4 -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/links.d.ts +1 -0
- package/lib/links.js +10 -3
- package/lib/mcp/setup.d.ts +0 -2
- package/lib/mcp/setup.js +4 -29
- package/lib/projects/__tests__/AppDevModeInterface.test.js +71 -44
- package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
- package/lib/projects/__tests__/components.test.js +164 -7
- 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 +142 -8
- package/lib/projects/add/legacyAddComponent.d.ts +1 -1
- package/lib/projects/add/legacyAddComponent.js +5 -1
- package/lib/projects/add/v3AddComponent.d.ts +2 -1
- package/lib/projects/add/v3AddComponent.js +22 -5
- package/lib/projects/components.d.ts +1 -0
- package/lib/projects/components.js +27 -1
- 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 +5 -3
- package/lib/projects/localDev/AppDevModeInterface.js +110 -47
- 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.d.ts +2 -2
- package/lib/projects/localDev/helpers/project.js +6 -8
- 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/promptUtils.d.ts +5 -0
- package/lib/prompts/promptUtils.js +9 -0
- package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
- package/lib/theme/__tests__/migrate.test.d.ts +1 -0
- package/lib/theme/__tests__/migrate.test.js +233 -0
- package/lib/theme/migrate.d.ts +13 -0
- package/lib/theme/migrate.js +90 -0
- package/lib/ui/index.js +3 -6
- package/lib/usageTracking.js +2 -2
- package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
- package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
- package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
- package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -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/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
- package/mcp-server/tools/index.js +13 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
- package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
- package/mcp-server/tools/project/CreateProjectTool.js +5 -5
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/DocFetchTool.js +2 -2
- package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
- package/mcp-server/tools/project/DocsSearchTool.js +7 -7
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
- package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
- package/mcp-server/tools/project/UploadProjectTools.js +2 -2
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
- package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +1 -1
- package/mcp-server/tools/project/constants.js +14 -6
- package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
- package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
- package/mcp-server/utils/cliConfig.d.ts +1 -0
- package/mcp-server/utils/cliConfig.js +12 -0
- package/package.json +4 -3
- package/types/LocalDev.d.ts +2 -1
- package/types/Projects.d.ts +1 -0
- package/ui/components/BoxWithTitle.d.ts +8 -0
- package/ui/components/BoxWithTitle.js +9 -0
- package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
- package/ui/components/HorizontalSelectPrompt.js +30 -0
- package/ui/components/StatusMessageBoxes.d.ts +12 -0
- package/ui/components/StatusMessageBoxes.js +31 -0
- package/ui/lib/ui-testing-utils.d.ts +9 -0
- package/ui/lib/ui-testing-utils.js +47 -0
- package/ui/lib/useTerminalSize.d.ts +13 -0
- package/ui/lib/useTerminalSize.js +31 -0
- package/ui/styles.d.ts +18 -0
- package/ui/styles.js +18 -0
- package/ui/views/UiSandbox.d.ts +5 -0
- package/ui/views/UiSandbox.js +25 -0
- /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
|
@@ -100,6 +100,7 @@ describe('AppDevModeInterface', () => {
|
|
|
100
100
|
setAppDataForUid: vi.fn(),
|
|
101
101
|
addListener: vi.fn(),
|
|
102
102
|
addUploadWarning: vi.fn(),
|
|
103
|
+
removeListener: vi.fn(),
|
|
103
104
|
};
|
|
104
105
|
mockLocalDevLogger = {};
|
|
105
106
|
// Mock constructors
|
|
@@ -239,16 +240,21 @@ describe('AppDevModeInterface', () => {
|
|
|
239
240
|
await newAppDevModeInterface.setup({});
|
|
240
241
|
expect(process.exit).toHaveBeenCalledWith(0);
|
|
241
242
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
// });
|
|
252
258
|
it('should open browser for OAuth app installation', async () => {
|
|
253
259
|
const oauthAppNode = {
|
|
254
260
|
...mockAppNode,
|
|
@@ -287,7 +293,12 @@ describe('AppDevModeInterface', () => {
|
|
|
287
293
|
},
|
|
288
294
|
});
|
|
289
295
|
await appDevModeInterface.setup({});
|
|
290
|
-
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
|
+
});
|
|
291
302
|
});
|
|
292
303
|
it('should handle errors during setup', async () => {
|
|
293
304
|
const error = new Error('Setup failed');
|
|
@@ -317,39 +328,46 @@ describe('AppDevModeInterface', () => {
|
|
|
317
328
|
await appDevModeInterface.setup({});
|
|
318
329
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
319
330
|
});
|
|
320
|
-
|
|
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
|
-
|
|
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
|
+
// });
|
|
353
371
|
});
|
|
354
372
|
describe('start()', () => {
|
|
355
373
|
it('should return early if no app node exists', async () => {
|
|
@@ -387,6 +405,15 @@ describe('AppDevModeInterface', () => {
|
|
|
387
405
|
await appDevModeInterface.cleanup();
|
|
388
406
|
expect(UIEDevModeInterface.cleanup).toHaveBeenCalled();
|
|
389
407
|
});
|
|
408
|
+
it('should remove state listeners', async () => {
|
|
409
|
+
await appDevModeInterface.cleanup();
|
|
410
|
+
expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('devServerMessage',
|
|
411
|
+
// @ts-expect-error access private method for testing
|
|
412
|
+
appDevModeInterface.onDevServerMessage);
|
|
413
|
+
expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('projectNodes',
|
|
414
|
+
// @ts-expect-error
|
|
415
|
+
appDevModeInterface.onChangeProjectNodes);
|
|
416
|
+
});
|
|
390
417
|
});
|
|
391
418
|
describe('isAutomaticallyInstallable()', () => {
|
|
392
419
|
it('should return true for static auth app on test account with correct parent', () => {
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import { handleComponentCollision } from '../components.js';
|
|
2
|
+
import { handleComponentCollision, updateHsMetaFilesWithAutoGeneratedFields, } from '../components.js';
|
|
3
|
+
import { uiLogger } from '../../ui/logger.js';
|
|
4
|
+
import { coerceToValidUid } from '@hubspot/project-parsing-lib';
|
|
3
5
|
vi.mock('fs');
|
|
6
|
+
vi.mock('../../ui/logger.js');
|
|
7
|
+
vi.mock('@hubspot/project-parsing-lib', () => ({
|
|
8
|
+
coerceToValidUid: vi.fn(),
|
|
9
|
+
metafileExtension: '.module.meta.json',
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('@hubspot/project-parsing-lib/src/lib/constants.js', () => ({
|
|
12
|
+
AppKey: 'app',
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('../../../lang/en.js', () => ({
|
|
15
|
+
lib: {
|
|
16
|
+
projects: {
|
|
17
|
+
updateHsMetaFilesWithAutoGeneratedFields: {
|
|
18
|
+
header: 'Updating component metadata files...',
|
|
19
|
+
applicationLog: (type, uid, name) => `Updated ${type} component with uid: ${uid} and name: ${name}`,
|
|
20
|
+
componentLog: (type, uid) => `Updated ${type} component with uid: ${uid}`,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
4
25
|
const mockedFs = vi.mocked(fs);
|
|
26
|
+
const mockCoerceToValidUid = vi.mocked(coerceToValidUid);
|
|
5
27
|
describe('lib/projects/components', () => {
|
|
6
28
|
describe('handleComponentCollision()', () => {
|
|
7
29
|
const mockCollision = {
|
|
@@ -152,10 +174,9 @@ describe('lib/projects/components', () => {
|
|
|
152
174
|
collisions: [
|
|
153
175
|
'regular.js',
|
|
154
176
|
'nested/path/file.ts',
|
|
155
|
-
'component.
|
|
177
|
+
'component.meta.json',
|
|
156
178
|
'another.meta.json',
|
|
157
179
|
'package.json',
|
|
158
|
-
'nested/package.json',
|
|
159
180
|
],
|
|
160
181
|
};
|
|
161
182
|
// Mock metafileExtension
|
|
@@ -166,16 +187,152 @@ describe('lib/projects/components', () => {
|
|
|
166
187
|
const mockMetaContent = '{}';
|
|
167
188
|
mockedFs.readFileSync.mockReturnValue(mockMetaContent);
|
|
168
189
|
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
190
|
+
// Track what files are being copied to debug the issue
|
|
169
191
|
mockedFs.copyFileSync.mockImplementation(() => { });
|
|
170
192
|
// Mock console.log for package.json handling
|
|
171
193
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
172
194
|
handleComponentCollision(collision);
|
|
173
|
-
|
|
174
|
-
expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2);
|
|
195
|
+
expect(mockedFs.readFileSync).toHaveBeenCalledTimes(2);
|
|
175
196
|
// Should handle 2 metafiles
|
|
176
|
-
expect(mockedFs.readFileSync).toHaveBeenCalledWith('/
|
|
177
|
-
expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/
|
|
197
|
+
expect(mockedFs.readFileSync).toHaveBeenCalledWith('/dest/path/package.json', 'utf-8');
|
|
198
|
+
expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/package.json', 'utf-8');
|
|
178
199
|
consoleSpy.mockRestore();
|
|
179
200
|
});
|
|
180
201
|
});
|
|
202
|
+
describe('updateHsMetaFilesWithAutoGeneratedFields()', () => {
|
|
203
|
+
const mockUiLogger = vi.mocked(uiLogger);
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
vi.resetAllMocks();
|
|
206
|
+
mockCoerceToValidUid.mockImplementation((input) => input);
|
|
207
|
+
});
|
|
208
|
+
afterEach(() => {
|
|
209
|
+
vi.restoreAllMocks();
|
|
210
|
+
});
|
|
211
|
+
it('updates component metadata files with project-specific UIDs', () => {
|
|
212
|
+
const projectName = 'my-project';
|
|
213
|
+
const hsMetaFilePaths = [
|
|
214
|
+
'/path/to/component1.meta.json',
|
|
215
|
+
'/path/to/component2.meta.json',
|
|
216
|
+
];
|
|
217
|
+
const component1 = {
|
|
218
|
+
type: 'card',
|
|
219
|
+
uid: 'old-uid-1',
|
|
220
|
+
config: {
|
|
221
|
+
name: 'Old Name',
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const component2 = {
|
|
225
|
+
type: 'function',
|
|
226
|
+
uid: 'old-uid-2',
|
|
227
|
+
};
|
|
228
|
+
mockedFs.readFileSync
|
|
229
|
+
.mockReturnValueOnce(JSON.stringify(component1))
|
|
230
|
+
.mockReturnValueOnce(JSON.stringify(component2));
|
|
231
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
232
|
+
mockCoerceToValidUid
|
|
233
|
+
.mockReturnValueOnce('card-my-project')
|
|
234
|
+
.mockReturnValueOnce('function-my-project');
|
|
235
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
236
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
|
|
237
|
+
type: 'card',
|
|
238
|
+
uid: 'card-my-project',
|
|
239
|
+
config: {
|
|
240
|
+
name: 'Old Name',
|
|
241
|
+
},
|
|
242
|
+
}, null, 2));
|
|
243
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component2.meta.json', JSON.stringify({
|
|
244
|
+
type: 'function',
|
|
245
|
+
uid: 'function-my-project',
|
|
246
|
+
}, null, 2));
|
|
247
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
|
|
248
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated card component with uid: card-my-project');
|
|
249
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated function component with uid: function-my-project');
|
|
250
|
+
});
|
|
251
|
+
it('handles app components by updating both uid and config.name', () => {
|
|
252
|
+
const projectName = 'test-app';
|
|
253
|
+
const hsMetaFilePaths = ['/path/to/app.meta.json'];
|
|
254
|
+
const appComponent = {
|
|
255
|
+
type: 'app',
|
|
256
|
+
uid: 'old-app-uid',
|
|
257
|
+
config: {
|
|
258
|
+
name: 'Old App Name',
|
|
259
|
+
other: 'property',
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(appComponent));
|
|
263
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
264
|
+
mockCoerceToValidUid.mockReturnValue('app-test-app');
|
|
265
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
266
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
|
|
267
|
+
type: 'app',
|
|
268
|
+
uid: 'app-test-app',
|
|
269
|
+
config: {
|
|
270
|
+
name: 'test-app-Application',
|
|
271
|
+
other: 'property',
|
|
272
|
+
},
|
|
273
|
+
}, null, 2));
|
|
274
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-test-app and name: test-app-Application');
|
|
275
|
+
});
|
|
276
|
+
it('handles UID collisions by using timestamps', () => {
|
|
277
|
+
const projectName = 'collision-project';
|
|
278
|
+
const hsMetaFilePaths = ['/path/to/component1.meta.json'];
|
|
279
|
+
const existingUids = ['card-collision-project'];
|
|
280
|
+
const component1 = { type: 'card', uid: 'old-uid-1' };
|
|
281
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component1));
|
|
282
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
283
|
+
// Mock Date.now to return consistent value for testing
|
|
284
|
+
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
|
|
285
|
+
mockCoerceToValidUid
|
|
286
|
+
.mockReturnValueOnce('card-collision-project')
|
|
287
|
+
.mockReturnValueOnce('card-1234567890-collision-project');
|
|
288
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids);
|
|
289
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
|
|
290
|
+
type: 'card',
|
|
291
|
+
uid: 'card-1234567890-collision-project',
|
|
292
|
+
}, null, 2));
|
|
293
|
+
});
|
|
294
|
+
it('falls back to original uid when coerceToValidUid returns null', () => {
|
|
295
|
+
const projectName = 'fallback-project';
|
|
296
|
+
const hsMetaFilePaths = ['/path/to/component.meta.json'];
|
|
297
|
+
const component = {
|
|
298
|
+
type: 'card',
|
|
299
|
+
uid: 'original-uid',
|
|
300
|
+
};
|
|
301
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
|
|
302
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
303
|
+
mockCoerceToValidUid.mockReturnValue(undefined);
|
|
304
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
305
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
|
|
306
|
+
type: 'card',
|
|
307
|
+
uid: 'original-uid',
|
|
308
|
+
}, null, 2));
|
|
309
|
+
});
|
|
310
|
+
it('handles empty hsMetaFilePaths array', () => {
|
|
311
|
+
const projectName = 'empty-project';
|
|
312
|
+
const hsMetaFilePaths = [];
|
|
313
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
314
|
+
expect(mockedFs.readFileSync).not.toHaveBeenCalled();
|
|
315
|
+
expect(mockedFs.writeFileSync).not.toHaveBeenCalled();
|
|
316
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
|
|
317
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('');
|
|
318
|
+
});
|
|
319
|
+
it('handles components without config property for app type', () => {
|
|
320
|
+
const projectName = 'no-config-project';
|
|
321
|
+
const hsMetaFilePaths = ['/path/to/app.meta.json'];
|
|
322
|
+
const appComponent = {
|
|
323
|
+
type: 'app',
|
|
324
|
+
uid: 'app-uid',
|
|
325
|
+
// No config property
|
|
326
|
+
};
|
|
327
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(appComponent));
|
|
328
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
329
|
+
mockCoerceToValidUid.mockReturnValue('app-no-config-project');
|
|
330
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
331
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
|
|
332
|
+
type: 'app',
|
|
333
|
+
uid: 'app-no-config-project',
|
|
334
|
+
}, null, 2));
|
|
335
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-no-config-project');
|
|
336
|
+
});
|
|
337
|
+
});
|
|
181
338
|
});
|
|
@@ -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);
|