@hubspot/cli 7.7.27-experimental.2 → 7.7.28-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.
Files changed (131) hide show
  1. package/README.md +0 -4
  2. package/api/__tests__/migrate.test.js +5 -5
  3. package/api/migrate.d.ts +10 -4
  4. package/api/migrate.js +2 -2
  5. package/commands/__tests__/create.test.js +20 -0
  6. package/commands/__tests__/testAccount.test.js +2 -0
  7. package/commands/app/__tests__/migrate.test.js +1 -0
  8. package/commands/create/function.js +2 -2
  9. package/commands/create/module.js +2 -2
  10. package/commands/create/template.js +2 -2
  11. package/commands/create.js +47 -0
  12. package/commands/getStarted.js +66 -4
  13. package/commands/mcp/setup.d.ts +0 -1
  14. package/commands/mcp/setup.js +3 -11
  15. package/commands/project/__tests__/create.test.js +57 -0
  16. package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
  17. package/commands/project/create.js +6 -1
  18. package/commands/project/deploy.js +31 -1
  19. package/commands/project/dev/deprecatedFlow.js +2 -1
  20. package/commands/project/dev/index.js +32 -12
  21. package/commands/project/dev/unifiedFlow.d.ts +1 -1
  22. package/commands/project/dev/unifiedFlow.js +10 -16
  23. package/commands/project/profile/delete.js +26 -14
  24. package/commands/project/upload.d.ts +2 -2
  25. package/commands/project/upload.js +1 -1
  26. package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
  27. package/commands/testAccount/__tests__/importData.test.js +93 -0
  28. package/commands/testAccount/create.js +23 -13
  29. package/commands/testAccount/importData.d.ts +9 -0
  30. package/commands/testAccount/importData.js +61 -0
  31. package/commands/testAccount.js +2 -0
  32. package/lang/en.d.ts +160 -46
  33. package/lang/en.js +175 -59
  34. package/lang/en.lyaml +35 -14
  35. package/lib/__tests__/importData.test.d.ts +1 -0
  36. package/lib/__tests__/importData.test.js +89 -0
  37. package/lib/accountTypes.js +2 -3
  38. package/lib/app/__tests__/migrate.test.js +81 -36
  39. package/lib/app/migrate.d.ts +17 -4
  40. package/lib/app/migrate.js +97 -19
  41. package/lib/constants.d.ts +1 -0
  42. package/lib/constants.js +1 -0
  43. package/lib/hasFeature.d.ts +1 -0
  44. package/lib/hasFeature.js +7 -0
  45. package/lib/importData.d.ts +3 -0
  46. package/lib/importData.js +50 -0
  47. package/lib/mcp/setup.d.ts +0 -2
  48. package/lib/mcp/setup.js +0 -24
  49. package/lib/process.js +15 -4
  50. package/lib/projectProfiles.d.ts +1 -1
  51. package/lib/projectProfiles.js +10 -2
  52. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
  53. package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
  54. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  55. package/lib/projects/__tests__/components.test.js +164 -7
  56. package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
  57. package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
  58. package/lib/projects/add/v3AddComponent.js +16 -4
  59. package/lib/projects/components.d.ts +1 -0
  60. package/lib/projects/components.js +27 -1
  61. package/lib/projects/localDev/AppDevModeInterface.js +35 -3
  62. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
  63. package/lib/projects/localDev/LocalDevLogger.js +2 -19
  64. package/lib/projects/localDev/LocalDevManager.js +1 -1
  65. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
  66. package/lib/projects/localDev/LocalDevProcess.js +3 -26
  67. package/lib/projects/localDev/LocalDevState.d.ts +6 -7
  68. package/lib/projects/localDev/LocalDevState.js +16 -15
  69. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  70. package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
  71. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
  72. package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
  73. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  74. package/lib/projects/localDev/helpers/project.js +173 -0
  75. package/lib/projects/urls.d.ts +1 -0
  76. package/lib/projects/urls.js +4 -0
  77. package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
  78. package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
  79. package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
  80. package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
  81. package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
  82. package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
  83. package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
  84. package/lib/prompts/confirmImportDataPrompt.js +12 -0
  85. package/lib/prompts/createFunctionPrompt.d.ts +2 -1
  86. package/lib/prompts/createFunctionPrompt.js +36 -7
  87. package/lib/prompts/createModulePrompt.d.ts +2 -1
  88. package/lib/prompts/createModulePrompt.js +48 -1
  89. package/lib/prompts/createTemplatePrompt.d.ts +3 -24
  90. package/lib/prompts/createTemplatePrompt.js +9 -1
  91. package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
  92. package/lib/prompts/importDataFilePathPrompt.js +24 -0
  93. package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
  94. package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
  95. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  96. package/lib/prompts/promptUtils.d.ts +7 -1
  97. package/lib/prompts/promptUtils.js +14 -1
  98. package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
  99. package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
  100. package/lib/ui/index.js +3 -6
  101. package/lib/ui/removeAnsiCodes.d.ts +1 -0
  102. package/lib/ui/removeAnsiCodes.js +4 -0
  103. package/mcp-server/server.js +2 -1
  104. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  105. package/mcp-server/tools/cms/HsListTool.js +58 -0
  106. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  107. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  108. package/mcp-server/tools/index.d.ts +1 -0
  109. package/mcp-server/tools/index.js +8 -0
  110. package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
  111. package/mcp-server/tools/project/DocFetchTool.js +49 -0
  112. package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
  113. package/mcp-server/tools/project/DocsSearchTool.js +62 -0
  114. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
  115. package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
  116. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
  117. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
  118. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
  119. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  120. package/mcp-server/tools/project/constants.d.ts +2 -0
  121. package/mcp-server/tools/project/constants.js +6 -0
  122. package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
  123. package/mcp-server/utils/toolUsageTracking.js +2 -1
  124. package/package.json +9 -6
  125. package/types/Cms.d.ts +16 -0
  126. package/types/Cms.js +25 -1
  127. package/types/LocalDev.d.ts +0 -3
  128. package/types/Prompts.d.ts +1 -0
  129. package/types/Yargs.d.ts +1 -1
  130. package/ui/index.d.ts +1 -0
  131. package/ui/index.js +6 -0
@@ -1,3 +1,4 @@
1
1
  import { FEATURES } from './constants.js';
2
2
  import { ValueOf } from '@hubspot/local-dev-lib/types/Utils';
3
3
  export declare function hasFeature(accountId: number, feature: ValueOf<typeof FEATURES>): Promise<boolean>;
4
+ export declare function hasUnfiedAppsAccess(accountId: number): Promise<boolean>;
package/lib/hasFeature.js CHANGED
@@ -1,5 +1,12 @@
1
+ import { http } from '@hubspot/local-dev-lib/http';
1
2
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
2
3
  export async function hasFeature(accountId, feature) {
3
4
  const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
4
5
  return Boolean(enabledFeatures[feature]);
5
6
  }
7
+ export async function hasUnfiedAppsAccess(accountId) {
8
+ const response = await http.get(accountId, {
9
+ url: 'developer-tooling/external/developer-portal/has-unified-dev-platform-access',
10
+ });
11
+ return Boolean(response.data);
12
+ }
@@ -0,0 +1,3 @@
1
+ import { ImportRequest } from '@hubspot/local-dev-lib/types/Crm';
2
+ export declare function handleImportData(targetAccountId: number, dataFileNames: string[], importRequest: ImportRequest): Promise<void>;
3
+ export declare function handleTargetTestAccountSelectionFlow(derivedAccountId: number, userProvidedAccount: string | number | undefined): Promise<number>;
@@ -0,0 +1,50 @@
1
+ import { getAccountConfig, getAccountId, getEnv, } from '@hubspot/local-dev-lib/config';
2
+ import { createImport } from '@hubspot/local-dev-lib/api/crm';
3
+ import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
4
+ import { importDataTestAccountSelectPrompt } from './prompts/importDataTestAccountSelectPrompt.js';
5
+ import { lib } from '../lang/en.js';
6
+ import { isAppDeveloperAccount, isDeveloperTestAccount, isStandardAccount, } from './accountTypes.js';
7
+ import { uiLogger } from './ui/logger.js';
8
+ export async function handleImportData(targetAccountId, dataFileNames, importRequest) {
9
+ try {
10
+ const baseUrl = getHubSpotWebsiteOrigin(getEnv());
11
+ const response = await createImport(targetAccountId, importRequest, dataFileNames);
12
+ const importId = response.data.id;
13
+ uiLogger.success(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
14
+ }
15
+ catch (error) {
16
+ uiLogger.error(lib.importData.errors.failedToImportData);
17
+ throw error;
18
+ }
19
+ }
20
+ export async function handleTargetTestAccountSelectionFlow(derivedAccountId, userProvidedAccount) {
21
+ let targetAccountId = null;
22
+ if (userProvidedAccount) {
23
+ targetAccountId = getAccountId(userProvidedAccount);
24
+ }
25
+ // Only allow users to pass in test accounts
26
+ if (targetAccountId) {
27
+ const testAccount = getAccountConfig(targetAccountId);
28
+ if (!testAccount || !isDeveloperTestAccount(testAccount)) {
29
+ throw new Error(lib.importData.errors.notDeveloperTestAccount);
30
+ }
31
+ }
32
+ else {
33
+ const targetProjectAccountConfig = getAccountConfig(derivedAccountId);
34
+ if (!targetProjectAccountConfig) {
35
+ throw new Error(lib.importData.errors.noAccountConfig(derivedAccountId));
36
+ }
37
+ if (isDeveloperTestAccount(targetProjectAccountConfig)) {
38
+ targetAccountId = derivedAccountId;
39
+ }
40
+ else if (!isStandardAccount(targetProjectAccountConfig) &&
41
+ !isAppDeveloperAccount(targetProjectAccountConfig)) {
42
+ throw new Error(lib.importData.errors.incorrectAccountType(derivedAccountId));
43
+ }
44
+ else {
45
+ const { selectedAccountId } = await importDataTestAccountSelectPrompt(derivedAccountId);
46
+ targetAccountId = selectedAccountId;
47
+ }
48
+ }
49
+ return targetAccountId;
50
+ }
@@ -15,8 +15,6 @@ interface McpCommand {
15
15
  command: string;
16
16
  args: string[];
17
17
  }
18
- export declare function addMintlifyMcpServer(installTargets: string[]): Promise<void>;
19
- export declare function setupMintlify(derivedTargets?: string[]): Promise<boolean>;
20
18
  export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
21
19
  export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
22
20
  export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
package/lib/mcp/setup.js CHANGED
@@ -4,7 +4,6 @@ import { promptUser } from '../prompts/promptUtils.js';
4
4
  import SpinniesManager from '../ui/SpinniesManager.js';
5
5
  import { logError } from '../errorHandlers/index.js';
6
6
  import { execAsync } from '../../mcp-server/utils/command.js';
7
- import { spawn } from 'node:child_process';
8
7
  import path from 'path';
9
8
  import os from 'os';
10
9
  import fs from 'fs-extra';
@@ -14,7 +13,6 @@ const claudeCode = 'claude';
14
13
  const windsurf = 'windsurf';
15
14
  const cursor = 'cursor';
16
15
  const vscode = 'vscode';
17
- const supportedMintlifyClients = [windsurf, cursor];
18
16
  export const supportedTools = [
19
17
  { name: commands.mcp.setup.claudeCode, value: claudeCode },
20
18
  { name: commands.mcp.setup.cursor, value: cursor },
@@ -25,28 +23,6 @@ const defaultMcpCommand = {
25
23
  command: 'hs',
26
24
  args: ['mcp', 'start'],
27
25
  };
28
- export async function addMintlifyMcpServer(installTargets) {
29
- await runSetupFunction(() => setupMintlify(installTargets));
30
- }
31
- export async function setupMintlify(derivedTargets = supportedMintlifyClients) {
32
- uiLogger.info(commands.mcp.setup.installingDocSearch);
33
- uiLogger.log('');
34
- return new Promise(resolve => {
35
- const subcommands = ['mint-mcp', 'add', 'hubspot-migration'];
36
- const docsSearchClients = derivedTargets.filter(target => supportedMintlifyClients.includes(target));
37
- const childProcess = spawn(`npx`, docsSearchClients && docsSearchClients.length
38
- ? [...subcommands, '--client', ...docsSearchClients]
39
- : subcommands, {
40
- stdio: 'inherit',
41
- });
42
- childProcess.on('exit', code => {
43
- if (code !== 0) {
44
- resolve(false);
45
- }
46
- resolve(true);
47
- });
48
- });
49
- }
50
26
  export async function addMcpServerToConfig(targets) {
51
27
  try {
52
28
  let derivedTargets = [];
package/lib/process.js CHANGED
@@ -1,29 +1,40 @@
1
1
  import readline from 'readline';
2
2
  import { logger, setLogLevel, LOG_LEVEL } from '@hubspot/local-dev-lib/logger';
3
3
  import { i18n } from './lang.js';
4
+ import { logError } from './errorHandlers/index.js';
5
+ const SIGHUP = 'SIGHUP';
6
+ const uncaughtException = 'uncaughtException';
4
7
  export const TERMINATION_SIGNALS = [
5
8
  'beforeExit',
6
9
  'SIGINT', // Terminal trying to interrupt (Ctrl + C)
7
10
  'SIGUSR1', // Start Debugger User-defined signal 1
8
11
  'SIGUSR2', // User-defined signal 2
9
- 'uncaughtException',
12
+ uncaughtException,
10
13
  'SIGTERM', // Represents a graceful termination
11
- 'SIGHUP', // Parent terminal has been closed
14
+ SIGHUP, // Parent terminal has been closed
12
15
  ];
13
16
  export function handleExit(callback) {
14
17
  let exitInProgress = false;
15
18
  TERMINATION_SIGNALS.forEach(signal => {
16
19
  process.removeAllListeners(signal);
17
- process.on(signal, async () => {
20
+ process.on(signal, async (...args) => {
18
21
  // Prevent duplicate exit handling
19
22
  if (!exitInProgress) {
20
23
  exitInProgress = true;
21
- const isSIGHUP = signal === 'SIGHUP';
24
+ const isSIGHUP = signal === SIGHUP;
22
25
  // Prevent logs when terminal closes
23
26
  if (isSIGHUP) {
24
27
  setLogLevel(LOG_LEVEL.NONE);
25
28
  }
26
29
  logger.debug(i18n(`lib.process.exitDebug`, { signal }));
30
+ if (signal === uncaughtException && args && args.length > 0) {
31
+ try {
32
+ logError(args[0]);
33
+ }
34
+ catch (e) {
35
+ logger.error(args[0]);
36
+ }
37
+ }
27
38
  await callback({ isSIGHUP });
28
39
  }
29
40
  });
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
4
4
  export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
5
5
  export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
6
6
  export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
7
- export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
7
+ export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined, useEnv?: boolean): Promise<number | undefined>;
@@ -38,7 +38,12 @@ export function loadProfile(projectConfig, projectDir, profileName) {
38
38
  uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
39
39
  return;
40
40
  }
41
- return profile;
41
+ return {
42
+ ...profile,
43
+ accountId: process.env.HUBSPOT_ACCOUNT_ID
44
+ ? Number(process.env.HUBSPOT_ACCOUNT_ID)
45
+ : profile.accountId,
46
+ };
42
47
  }
43
48
  catch (e) {
44
49
  uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
@@ -54,7 +59,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
54
59
  }
55
60
  }
56
61
  }
57
- export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
62
+ export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile, useEnv = false) {
58
63
  if (argsProfile) {
59
64
  logProfileHeader(argsProfile);
60
65
  const profile = loadProfile(projectConfig, projectDir, argsProfile);
@@ -63,6 +68,9 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
63
68
  process.exit(EXIT_CODES.ERROR);
64
69
  }
65
70
  logProfileFooter(profile, true);
71
+ if (useEnv) {
72
+ return Number(process.env.HUBSPOT_ACCOUNT_ID);
73
+ }
66
74
  return profile.accountId;
67
75
  }
68
76
  else {
@@ -37,6 +37,7 @@ vi.mock('../../ui/logger');
37
37
  vi.mock('../../errorHandlers/index');
38
38
  vi.mock('../localDev/LocalDevState');
39
39
  vi.mock('../localDev/LocalDevLogger');
40
+ vi.mock('../../ui/SpinniesManager');
40
41
  describe('AppDevModeInterface', () => {
41
42
  let appDevModeInterface;
42
43
  let mockLocalDevState;
@@ -98,10 +99,9 @@ describe('AppDevModeInterface', () => {
98
99
  getAppDataByUid: vi.fn(),
99
100
  setAppDataForUid: vi.fn(),
100
101
  addListener: vi.fn(),
101
- };
102
- mockLocalDevLogger = {
103
102
  addUploadWarning: vi.fn(),
104
103
  };
104
+ mockLocalDevLogger = {};
105
105
  // Mock constructors
106
106
  LocalDevState.mockImplementation(() => mockLocalDevState);
107
107
  LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
@@ -211,7 +211,7 @@ describe('AppDevModeInterface', () => {
211
211
  };
212
212
  await appDevModeInterface.setup({});
213
213
  expect(confirmPrompt).toHaveBeenCalled();
214
- expect(mockLocalDevLogger.addUploadWarning).toHaveBeenCalled();
214
+ expect(mockLocalDevState.addUploadWarning).toHaveBeenCalled();
215
215
  });
216
216
  it('should exit if user declines marketplace warning', async () => {
217
217
  const marketplaceAppNode = {
@@ -32,51 +32,14 @@ describe('LocalDevProcess', () => {
32
32
  srcDir: 'src',
33
33
  platformVersion: '1.0.0',
34
34
  };
35
- const mockSubbuildStatus = {
36
- buildName: 'component1',
37
- buildType: 'APP',
38
- errorMessage: '',
39
- finishedAt: new Date().toISOString(),
40
- rootPath: '/test/path',
41
- startedAt: new Date().toISOString(),
42
- status: 'SUCCESS',
43
- id: '123',
44
- standardError: null,
45
- visible: true,
46
- };
47
- const mockBuild = {
48
- activitySource: { type: 'HUBSPOT' },
49
- projectName: 'test-project',
50
- uploadMessage: 'test-upload-message',
51
- autoDeployId: 123,
52
- buildId: 123,
53
- createdAt: new Date().toISOString(),
54
- deployableState: 'DEPLOYED',
55
- finishedAt: new Date().toISOString(),
56
- startedAt: new Date().toISOString(),
57
- status: 'SUCCESS',
58
- subbuildStatuses: [
59
- { ...mockSubbuildStatus, buildName: 'component1' },
60
- { ...mockSubbuildStatus, buildName: 'component2' },
61
- ],
62
- deployStatusTaskLocator: {
63
- id: '123',
64
- links: [],
65
- },
66
- enqueuedAt: new Date().toISOString(),
67
- isAutoDeployEnabled: false,
68
- portalId: 123,
69
- };
70
35
  const mockOptions = {
71
36
  projectDir: '/test/project',
72
37
  projectConfig: mockProjectConfig,
73
38
  targetProjectAccountId: 123,
74
39
  targetTestingAccountId: 456,
75
40
  projectId: 789,
76
- isGithubLinked: false,
77
41
  initialProjectNodes: {},
78
42
  env: ENVIRONMENTS.PROD,
79
- deployedBuild: mockBuild,
80
43
  projectName: 'test-project',
81
44
  };
82
45
  beforeEach(() => {
@@ -87,7 +50,6 @@ describe('LocalDevProcess', () => {
87
50
  devServerStartError: vi.fn(),
88
51
  devServerCleanupError: vi.fn(),
89
52
  missingComponentsWarning: vi.fn(),
90
- noDeployedBuild: vi.fn(),
91
53
  startupMessage: vi.fn(),
92
54
  monitorConsoleOutput: vi.fn(),
93
55
  cleanupStart: vi.fn(),
@@ -97,7 +59,6 @@ describe('LocalDevProcess', () => {
97
59
  projectConfigMismatch: vi.fn(),
98
60
  uploadError: vi.fn(),
99
61
  uploadSuccess: vi.fn(),
100
- clearUploadWarnings: vi.fn(),
101
62
  fileChangeError: vi.fn(),
102
63
  uploadWarning: vi.fn(),
103
64
  };
@@ -118,14 +79,6 @@ describe('LocalDevProcess', () => {
118
79
  });
119
80
  });
120
81
  describe('start()', () => {
121
- it('should exit if no deployed build exists', async () => {
122
- const processWithoutBuild = new LocalDevProcess({
123
- ...mockOptions,
124
- deployedBuild: undefined,
125
- });
126
- await expect(processWithoutBuild.start()).rejects.toThrow('Process.exit called with code 0');
127
- expect(mockLocalDevLogger.noDeployedBuild).toHaveBeenCalled();
128
- });
129
82
  it('should exit if dev server setup fails', async () => {
130
83
  mockDevServerManager.setup.mockRejectedValue(new Error('Setup failed'));
131
84
  await expect(process.start()).rejects.toThrow('Process.exit called with code 1');
@@ -138,31 +91,6 @@ describe('LocalDevProcess', () => {
138
91
  expect(mockLocalDevLogger.monitorConsoleOutput).toHaveBeenCalled();
139
92
  expect(mockLocalDevLogger.missingComponentsWarning).not.toHaveBeenCalled();
140
93
  });
141
- it('should warn about missing components', async () => {
142
- const mockNode = {
143
- uid: 'component3',
144
- componentType: 'APP',
145
- localDev: {
146
- componentRoot: '/test/path',
147
- componentConfigPath: '/test/path/config.json',
148
- configUpdatedSinceLastUpload: false,
149
- },
150
- componentDeps: {},
151
- metaFilePath: '/test/path',
152
- config: {},
153
- files: [],
154
- };
155
- const processWithNode = new LocalDevProcess({
156
- ...mockOptions,
157
- initialProjectNodes: {
158
- component3: mockNode,
159
- },
160
- });
161
- await processWithNode.start();
162
- expect(mockLocalDevLogger.missingComponentsWarning).toHaveBeenCalledWith([
163
- '[App] component3',
164
- ]);
165
- });
166
94
  });
167
95
  describe('stop()', () => {
168
96
  it('should exit with error if cleanup fails', async () => {
@@ -226,7 +154,8 @@ describe('LocalDevProcess', () => {
226
154
  });
227
155
  const success = await process.uploadProject();
228
156
  expect(mockLocalDevLogger.uploadSuccess).toHaveBeenCalled();
229
- expect(mockLocalDevLogger.clearUploadWarnings).toHaveBeenCalled();
157
+ // @ts-expect-error accessing private property for testing
158
+ expect(process.state.uploadWarnings.size).toBe(0);
230
159
  expect(success).toBe(true);
231
160
  });
232
161
  it('should reset projectNodesAtLastUpload', async () => {
@@ -300,10 +229,10 @@ describe('LocalDevProcess', () => {
300
229
  process.state.projectNodes = {};
301
230
  expect(listener).toHaveBeenCalled();
302
231
  });
303
- it('should call listener immediately when callOnInit is true', () => {
232
+ it('should call listener immediately', () => {
304
233
  const listener = vi.fn();
305
234
  const key = 'projectNodes';
306
- process.addStateListener(key, listener, true);
235
+ process.addStateListener(key, listener);
307
236
  expect(listener).toHaveBeenCalledWith(process.projectNodes);
308
237
  });
309
238
  });
@@ -313,32 +242,13 @@ describe('LocalDevProcess', () => {
313
242
  const key = 'projectNodes';
314
243
  // Add the listener first
315
244
  process.addStateListener(key, listener);
316
- // Trigger state change to verify listener is called
317
- // @ts-expect-error
318
- process.state.projectNodes = {};
319
245
  expect(listener).toHaveBeenCalledTimes(1);
320
246
  // Remove the listener
321
247
  process.removeStateListener(key, listener);
322
248
  // Trigger state change again to verify listener is no longer called
323
249
  // @ts-expect-error
324
250
  process.state.projectNodes = { newNode: { uid: 'newNode' } };
325
- expect(listener).toHaveBeenCalledTimes(1); // Should still be 1, not 2
326
- });
327
- it('should not affect other listeners when removing one', () => {
328
- const listener1 = vi.fn();
329
- const listener2 = vi.fn();
330
- const key = 'projectNodes';
331
- // Add two listeners
332
- process.addStateListener(key, listener1);
333
- process.addStateListener(key, listener2);
334
- // Remove only the first listener
335
- process.removeStateListener(key, listener1);
336
- // Trigger state change
337
- // @ts-expect-error
338
- process.state.projectNodes = {};
339
- // Only listener2 should be called
340
- expect(listener1).not.toHaveBeenCalled();
341
- expect(listener2).toHaveBeenCalled();
251
+ expect(listener).toHaveBeenCalledTimes(1);
342
252
  });
343
253
  });
344
254
  });
@@ -66,7 +66,7 @@ describe('LocalDevWebsocketServer', () => {
66
66
  headers: { origin: 'https://app.hubspot.com' },
67
67
  });
68
68
  expect(mockWebSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
69
- expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function), true);
69
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function));
70
70
  expect(mockWebSocket.close).not.toHaveBeenCalled();
71
71
  });
72
72
  it('should reject connection from invalid origin', async () => {
@@ -215,7 +215,7 @@ describe('LocalDevWebsocketServer', () => {
215
215
  expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
216
216
  expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
217
217
  // Each connection should trigger state listener setup
218
- expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(6); // 2 listeners per connection * 3 connections
218
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(9); // 3 listeners per connection * 3 connections
219
219
  // Each connection should trigger dev server message
220
220
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
221
221
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
@@ -284,16 +284,16 @@ describe('LocalDevWebsocketServer', () => {
284
284
  const closeCallbacks2 = mockWebSocket2.on.mock.calls
285
285
  .filter(call => call[0] === 'close')
286
286
  .map(call => call[1]);
287
- expect(closeCallbacks1).toHaveLength(2); // projectNodes and appData listeners
288
- expect(closeCallbacks2).toHaveLength(2); // projectNodes and appData listeners
287
+ expect(closeCallbacks1).toHaveLength(3); // projectNodes and appData listeners
288
+ expect(closeCallbacks2).toHaveLength(3); // projectNodes and appData listeners
289
289
  // Simulate first connection closing (call all close callbacks)
290
290
  closeCallbacks1.forEach(callback => callback());
291
291
  // Should have removed listeners for first connection (2 listeners: projectNodes and appData)
292
- expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(2);
292
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(3);
293
293
  // Simulate second connection closing
294
294
  closeCallbacks2.forEach(callback => callback());
295
295
  // Should have removed listeners for second connection as well
296
- expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(4);
296
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(6);
297
297
  });
298
298
  it('should broadcast state changes to all connected clients', () => {
299
299
  // Establish connections
@@ -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
  });