@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.
Files changed (158) hide show
  1. package/commands/app/__tests__/migrate.test.js +1 -0
  2. package/commands/getStarted.js +7 -20
  3. package/commands/mcp/setup.d.ts +0 -1
  4. package/commands/mcp/setup.js +11 -11
  5. package/commands/project/__tests__/add.test.js +64 -0
  6. package/commands/project/__tests__/create.test.js +57 -0
  7. package/commands/project/__tests__/deploy.test.js +3 -2
  8. package/commands/project/add.d.ts +1 -1
  9. package/commands/project/add.js +3 -5
  10. package/commands/project/create.js +7 -2
  11. package/commands/project/deploy.js +9 -61
  12. package/commands/project/dev/index.js +1 -1
  13. package/commands/project/dev/unifiedFlow.js +4 -1
  14. package/commands/project/migrate.js +26 -7
  15. package/commands/project/upload.js +2 -2
  16. package/commands/project/validate.js +1 -1
  17. package/commands/project/watch.js +2 -2
  18. package/lang/en.d.ts +20 -5
  19. package/lang/en.js +38 -22
  20. package/lang/en.lyaml +12 -12
  21. package/lib/__tests__/hasFeature.test.js +145 -7
  22. package/lib/__tests__/importData.test.js +1 -1
  23. package/lib/app/__tests__/migrate.test.js +14 -51
  24. package/lib/app/migrate.d.ts +2 -8
  25. package/lib/app/migrate.js +5 -73
  26. package/lib/constants.d.ts +4 -0
  27. package/lib/constants.js +4 -0
  28. package/lib/errorHandlers/index.d.ts +4 -0
  29. package/lib/errorHandlers/index.js +1 -1
  30. package/lib/hasFeature.js +6 -0
  31. package/lib/importData.js +1 -1
  32. package/lib/links.d.ts +1 -0
  33. package/lib/links.js +10 -3
  34. package/lib/mcp/setup.d.ts +0 -2
  35. package/lib/mcp/setup.js +4 -29
  36. package/lib/projects/__tests__/AppDevModeInterface.test.js +71 -44
  37. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  38. package/lib/projects/__tests__/components.test.js +164 -7
  39. package/lib/projects/__tests__/deploy.test.js +164 -0
  40. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  41. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  42. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  43. package/lib/projects/add/__tests__/v3AddComponent.test.js +142 -8
  44. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  45. package/lib/projects/add/legacyAddComponent.js +5 -1
  46. package/lib/projects/add/v3AddComponent.d.ts +2 -1
  47. package/lib/projects/add/v3AddComponent.js +22 -5
  48. package/lib/projects/components.d.ts +1 -0
  49. package/lib/projects/components.js +27 -1
  50. package/lib/projects/create/__tests__/v3.test.js +97 -9
  51. package/lib/projects/create/index.js +2 -2
  52. package/lib/projects/create/legacy.js +1 -1
  53. package/lib/projects/create/v3.d.ts +2 -2
  54. package/lib/projects/create/v3.js +35 -12
  55. package/lib/projects/deploy.d.ts +13 -0
  56. package/lib/projects/deploy.js +63 -0
  57. package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -3
  58. package/lib/projects/localDev/AppDevModeInterface.js +110 -47
  59. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  60. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  61. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  62. package/lib/projects/localDev/LocalDevState.js +9 -1
  63. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  64. package/lib/projects/localDev/helpers/project.js +6 -8
  65. package/lib/projects/platformVersion.d.ts +1 -0
  66. package/lib/projects/platformVersion.js +10 -0
  67. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  68. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  69. package/lib/projects/upload.js +1 -1
  70. package/lib/projects/urls.d.ts +1 -0
  71. package/lib/projects/urls.js +3 -0
  72. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  73. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  74. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  75. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  76. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  77. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  78. package/lib/prompts/installAppPrompt.d.ts +6 -1
  79. package/lib/prompts/installAppPrompt.js +6 -1
  80. package/lib/prompts/projectAddPrompt.js +1 -1
  81. package/lib/prompts/promptUtils.d.ts +5 -0
  82. package/lib/prompts/promptUtils.js +9 -0
  83. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  84. package/lib/theme/__tests__/migrate.test.d.ts +1 -0
  85. package/lib/theme/__tests__/migrate.test.js +233 -0
  86. package/lib/theme/migrate.d.ts +13 -0
  87. package/lib/theme/migrate.js +90 -0
  88. package/lib/ui/index.js +3 -6
  89. package/lib/usageTracking.js +2 -2
  90. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  91. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  92. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  93. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  94. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  95. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  96. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  97. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  98. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  99. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  100. package/mcp-server/tools/cms/HsListTool.js +1 -1
  101. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  102. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  103. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  106. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  107. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  108. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  109. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  110. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  111. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  112. package/mcp-server/tools/index.js +13 -1
  113. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  114. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  115. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  116. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  117. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  118. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  119. package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
  120. package/mcp-server/tools/project/DocsSearchTool.js +7 -7
  121. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
  122. package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
  123. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  124. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  125. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  126. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  127. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  128. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  129. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  130. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
  131. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
  132. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  133. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  134. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  135. package/mcp-server/tools/project/constants.d.ts +1 -1
  136. package/mcp-server/tools/project/constants.js +14 -6
  137. package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
  138. package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
  139. package/mcp-server/utils/cliConfig.d.ts +1 -0
  140. package/mcp-server/utils/cliConfig.js +12 -0
  141. package/package.json +4 -3
  142. package/types/LocalDev.d.ts +2 -1
  143. package/types/Projects.d.ts +1 -0
  144. package/ui/components/BoxWithTitle.d.ts +8 -0
  145. package/ui/components/BoxWithTitle.js +9 -0
  146. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  147. package/ui/components/HorizontalSelectPrompt.js +30 -0
  148. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  149. package/ui/components/StatusMessageBoxes.js +31 -0
  150. package/ui/lib/ui-testing-utils.d.ts +9 -0
  151. package/ui/lib/ui-testing-utils.js +47 -0
  152. package/ui/lib/useTerminalSize.d.ts +13 -0
  153. package/ui/lib/useTerminalSize.js +31 -0
  154. package/ui/styles.d.ts +18 -0
  155. package/ui/styles.js +18 -0
  156. package/ui/views/UiSandbox.d.ts +5 -0
  157. package/ui/views/UiSandbox.js +25 -0
  158. /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
- it('should auto-install static auth app on test account', async () => {
243
- fetchAppInstallationData.mockResolvedValue({
244
- data: {
245
- isInstalledWithScopeGroups: false,
246
- previouslyAuthorizedScopeGroups: [],
247
- },
248
- });
249
- await appDevModeInterface.setup({});
250
- expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(123, 67890, [1, 2, 3]);
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
- it('should exit if user declines auto-install', async () => {
321
- // Set up conditions for automatic installation
322
- getAccountConfig.mockReturnValue({
323
- parentAccountId: 12345, // matches targetProjectAccountId
324
- });
325
- isDeveloperTestAccount.mockReturnValue(true);
326
- fetchAppInstallationData.mockResolvedValue({
327
- data: {
328
- isInstalledWithScopeGroups: false,
329
- previouslyAuthorizedScopeGroups: [],
330
- },
331
- });
332
- installAppAutoPrompt.mockResolvedValue(false);
333
- // Create a new instance to trigger the exit during setup
334
- const newAppDevModeInterface = new AppDevModeInterface({
335
- localDevState: mockLocalDevState,
336
- localDevLogger: mockLocalDevLogger,
337
- });
338
- // The setup method catches the error, so we check that process.exit was called
339
- await newAppDevModeInterface.setup({});
340
- expect(process.exit).toHaveBeenCalledWith(0);
341
- });
342
- it('should fallback to browser install if auto-install fails', async () => {
343
- fetchAppInstallationData.mockResolvedValue({
344
- data: {
345
- isInstalledWithScopeGroups: false,
346
- previouslyAuthorizedScopeGroups: [],
347
- },
348
- });
349
- installStaticAuthAppOnTestAccount.mockRejectedValue(new Error('Install failed'));
350
- await appDevModeInterface.setup({});
351
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', false);
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', () => {
@@ -39,6 +39,7 @@ describe('LocalDevProcess', () => {
39
39
  targetTestingAccountId: 456,
40
40
  projectId: 789,
41
41
  initialProjectNodes: {},
42
+ initialProjectProfileData: {},
42
43
  env: ENVIRONMENTS.PROD,
43
44
  projectName: 'test-project',
44
45
  };
@@ -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.module.meta.json',
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
- // Should copy 2 source files
174
- expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2);
195
+ expect(mockedFs.readFileSync).toHaveBeenCalledTimes(2);
175
196
  // Should handle 2 metafiles
176
- expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/component.module.meta.json', 'utf-8');
177
- expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/another.meta.json', 'utf-8');
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 '../buildAndDeploy.js';
2
- describe('buildAndDeploy', () => {
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);