@hubspot/cli 7.10.0-beta.0 → 7.10.0-beta.2
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/account/__tests__/rename.test.js +35 -0
- package/commands/account/rename.d.ts +1 -1
- package/commands/account/rename.js +5 -2
- package/commands/config/set.js +1 -2
- package/commands/getStarted.js +8 -2
- package/commands/hubdb.d.ts +1 -1
- package/commands/project/dev/index.js +8 -1
- package/commands/project/listBuilds.js +7 -1
- package/commands/project/upload.js +7 -1
- package/commands/project/validate.js +7 -1
- package/commands/project/watch.js +7 -2
- package/commands/testAccount/__tests__/create.test.js +68 -0
- package/commands/testAccount/create.d.ts +8 -0
- package/commands/testAccount/create.js +133 -43
- package/commands/testAccount/importData.d.ts +1 -1
- package/lang/en.d.ts +3199 -3204
- package/lang/en.js +24 -3
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +6 -0
- package/lib/mcp/__tests__/setup.test.d.ts +1 -0
- package/lib/mcp/__tests__/setup.test.js +127 -0
- package/lib/mcp/setup.d.ts +4 -12
- package/lib/mcp/setup.js +34 -1
- package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
- package/lib/middleware/autoUpdateMiddleware.js +1 -0
- package/lib/projects/__tests__/components.test.js +148 -24
- package/lib/projects/__tests__/projects.test.js +13 -42
- package/lib/projects/components.js +76 -20
- package/lib/projects/config.js +5 -9
- package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +6 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
- package/mcp-server/tools/cms/HsCreateModuleTool.js +6 -0
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +6 -0
- package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +4 -0
- package/mcp-server/tools/cms/HsListFunctionsTool.js +4 -0
- package/mcp-server/tools/cms/HsListTool.js +4 -0
- package/mcp-server/tools/index.js +2 -0
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -0
- package/mcp-server/tools/project/CreateProjectTool.js +6 -0
- package/mcp-server/tools/project/CreateTestAccountTool.d.ts +41 -0
- package/mcp-server/tools/project/CreateTestAccountTool.js +150 -0
- package/mcp-server/tools/project/DeployProjectTool.js +6 -0
- package/mcp-server/tools/project/DocFetchTool.js +4 -0
- package/mcp-server/tools/project/DocsSearchTool.js +4 -0
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +4 -0
- package/mcp-server/tools/project/GetApplicationInfoTool.js +4 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +4 -0
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +4 -0
- package/mcp-server/tools/project/UploadProjectTools.js +6 -0
- package/mcp-server/tools/project/ValidateProjectTool.js +4 -0
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +444 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
- package/package.json +1 -1
package/lang/en.js
CHANGED
|
@@ -118,7 +118,7 @@ export const commands = {
|
|
|
118
118
|
},
|
|
119
119
|
},
|
|
120
120
|
success: {
|
|
121
|
-
renamed: (name, newName) => `Account "${name}" renamed to "${newName}"
|
|
121
|
+
renamed: (name, newName, nameWasSanitized) => `Account "${chalk.bold(name)}" successfully renamed to "${chalk.bold(newName)}"${nameWasSanitized ? ' (Sanitized to remove invalid characters)' : ''}.`,
|
|
122
122
|
},
|
|
123
123
|
},
|
|
124
124
|
use: {
|
|
@@ -1196,6 +1196,7 @@ export const commands = {
|
|
|
1196
1196
|
setup: {
|
|
1197
1197
|
describe: 'Setup the HubSpot development MCP servers.',
|
|
1198
1198
|
installingDocSearch: 'Adding the docs-search mcp server',
|
|
1199
|
+
codex: 'Codex CLI',
|
|
1199
1200
|
claudeCode: 'Claude Code',
|
|
1200
1201
|
cursor: 'Cursor',
|
|
1201
1202
|
windsurf: 'Windsurf',
|
|
@@ -1217,7 +1218,11 @@ export const commands = {
|
|
|
1217
1218
|
configuredClaudeCode: 'Configured Claude Code',
|
|
1218
1219
|
claudeCodeNotFound: 'Claude Code not found - skipping configuration',
|
|
1219
1220
|
claudeCodeInstallFailed: 'Claude Code CLI not working - skipping configuration',
|
|
1220
|
-
|
|
1221
|
+
// Codex
|
|
1222
|
+
configuringCodex: 'Configuring Codex...',
|
|
1223
|
+
configuredCodex: 'Configured Codex',
|
|
1224
|
+
codexNotFound: 'Codex command not found - skipping configuration',
|
|
1225
|
+
codexInstallFailed: 'Failed to configure Codex',
|
|
1221
1226
|
// Cursor
|
|
1222
1227
|
configuringCursor: 'Configuring Cursor...',
|
|
1223
1228
|
failedToConfigureCursor: 'Failed to configure Cursor',
|
|
@@ -2113,9 +2118,21 @@ export const commands = {
|
|
|
2113
2118
|
createFailure: 'Failed to create test account.',
|
|
2114
2119
|
},
|
|
2115
2120
|
options: {
|
|
2116
|
-
configPath: '
|
|
2121
|
+
configPath: 'Path to config file (mutually exclusive with other flags)',
|
|
2122
|
+
accountName: 'Name for the test account',
|
|
2123
|
+
description: 'Description for the test account',
|
|
2124
|
+
marketingLevel: 'Marketing Hub tier. Options: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2125
|
+
opsLevel: 'Operations Hub tier. Options: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2126
|
+
serviceLevel: 'Service Hub tier. Options: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2127
|
+
salesLevel: 'Sales Hub tier. Options: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2128
|
+
contentLevel: 'CMS Hub tier. Options: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2117
2129
|
},
|
|
2118
2130
|
example: (configPath) => `Create a test account from the config file at ${configPath}`,
|
|
2131
|
+
examples: {
|
|
2132
|
+
withAllHubsEnterprise: 'Create a test account with all hubs at ENTERPRISE level',
|
|
2133
|
+
withSpecificHubLevels: 'Create a test account with specific hub levels',
|
|
2134
|
+
},
|
|
2135
|
+
savedAccountNameDiffers: (originalName, savedName) => `Account name "${chalk.bold(originalName)}" was saved as "${chalk.bold(savedName)}" in config.`,
|
|
2119
2136
|
},
|
|
2120
2137
|
createConfig: {
|
|
2121
2138
|
describe: 'Create a test account config file.',
|
|
@@ -2984,6 +3001,10 @@ export const lib = {
|
|
|
2984
3001
|
header: 'Created the following components and features:',
|
|
2985
3002
|
applicationLog: (componentType, uid, name) => ` - Created ${chalk.bold(componentType)} with uid ${chalk.bold(uid)} and name ${chalk.bold(name)}`,
|
|
2986
3003
|
componentLog: (componentType, uid) => ` - Created ${chalk.bold(componentType)} feature with uid ${chalk.bold(uid)}`,
|
|
3004
|
+
failedToUpdate: (hsMetaFile) => `Failed to update the uid in ${chalk.bold(hsMetaFile)}`,
|
|
3005
|
+
},
|
|
3006
|
+
generateSafeFilenameDifferentiator: {
|
|
3007
|
+
failedToCheckFiles: 'Failed to check files for filename differentiator. Falling back to timestamp.',
|
|
2987
3008
|
},
|
|
2988
3009
|
validateProjectConfig: {
|
|
2989
3010
|
configNotFound: `Unable to locate a project configuration file. Try running again from a project directory, or run ${uiCommandReference('hs project create')} to create a new project.`,
|
package/lib/constants.d.ts
CHANGED
|
@@ -139,3 +139,4 @@ export declare const LEGACY_PRIVATE_APP_FILE = "app.json";
|
|
|
139
139
|
export declare const THEME_FILE = "theme.json";
|
|
140
140
|
export declare const CMS_ASSETS_FILE = "cms-assets.json";
|
|
141
141
|
export declare const LEGACY_CONFIG_FILES: string[];
|
|
142
|
+
export declare const ACCOUNT_LEVEL_CHOICES: readonly ["FREE", "STARTER", "PROFESSIONAL", "ENTERPRISE"];
|
package/lib/constants.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execAsync } from '../../../mcp-server/utils/command.js';
|
|
2
|
+
import { setupCodex, supportedTools } from '../setup.js';
|
|
3
|
+
import SpinniesManager from '../../ui/SpinniesManager.js';
|
|
4
|
+
import { logError } from '../../errorHandlers/index.js';
|
|
5
|
+
import { commands } from '../../../lang/en.js';
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('../../../mcp-server/utils/command.js');
|
|
8
|
+
vi.mock('../../ui/SpinniesManager.js');
|
|
9
|
+
vi.mock('../../ui/logger.js');
|
|
10
|
+
vi.mock('../../errorHandlers/index.js');
|
|
11
|
+
vi.mock('../../../lang/en.js', () => ({
|
|
12
|
+
commands: {
|
|
13
|
+
mcp: {
|
|
14
|
+
setup: {
|
|
15
|
+
codex: 'Codex CLI',
|
|
16
|
+
claudeCode: 'Claude Code',
|
|
17
|
+
cursor: 'Cursor',
|
|
18
|
+
vsCode: 'VS Code',
|
|
19
|
+
windsurf: 'Windsurf',
|
|
20
|
+
success: vi.fn(targets => `Success message for ${targets.join(', ')}`),
|
|
21
|
+
spinners: {
|
|
22
|
+
configuringCodex: 'Configuring Codex...',
|
|
23
|
+
configuredCodex: 'Configured Codex',
|
|
24
|
+
codexNotFound: 'Codex command not found - skipping configuration',
|
|
25
|
+
codexInstallFailed: 'Failed to configure Codex',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
const mockedExecAsync = vi.mocked(execAsync);
|
|
32
|
+
const mockedSpinniesManager = vi.mocked(SpinniesManager);
|
|
33
|
+
const mockedLogError = vi.mocked(logError);
|
|
34
|
+
describe('lib/mcp/setup', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.resetAllMocks();
|
|
37
|
+
});
|
|
38
|
+
describe('supportedTools', () => {
|
|
39
|
+
it('should include Codex in the supported tools list', () => {
|
|
40
|
+
const codexTool = supportedTools.find(tool => tool.value === 'codex');
|
|
41
|
+
expect(codexTool).toBeDefined();
|
|
42
|
+
expect(codexTool?.name).toBe(commands.mcp.setup.codex);
|
|
43
|
+
expect(codexTool?.value).toBe('codex');
|
|
44
|
+
});
|
|
45
|
+
it('should have Codex as the first tool in the list', () => {
|
|
46
|
+
expect(supportedTools[0].value).toBe('codex');
|
|
47
|
+
});
|
|
48
|
+
it('should contain all expected tools', () => {
|
|
49
|
+
const toolValues = supportedTools.map(tool => tool.value);
|
|
50
|
+
expect(toolValues).toContain('codex');
|
|
51
|
+
expect(toolValues).toContain('claude');
|
|
52
|
+
expect(toolValues).toContain('cursor');
|
|
53
|
+
expect(toolValues).toContain('vscode');
|
|
54
|
+
expect(toolValues).toContain('windsurf');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('setupCodex', () => {
|
|
58
|
+
const mockMcpCommand = {
|
|
59
|
+
command: 'test-command',
|
|
60
|
+
args: ['--arg1', '--arg2'],
|
|
61
|
+
};
|
|
62
|
+
it('should successfully configure Codex when command is available', async () => {
|
|
63
|
+
mockedExecAsync.mockResolvedValueOnce({
|
|
64
|
+
stdout: 'codex version 1.0.0',
|
|
65
|
+
stderr: '',
|
|
66
|
+
}); // version check
|
|
67
|
+
mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' }); // mcp add command
|
|
68
|
+
const result = await setupCodex(mockMcpCommand);
|
|
69
|
+
expect(result).toBe(true);
|
|
70
|
+
expect(mockedSpinniesManager.add).toHaveBeenCalledWith('codexSpinner', {
|
|
71
|
+
text: commands.mcp.setup.spinners.configuringCodex,
|
|
72
|
+
});
|
|
73
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('codex --version');
|
|
74
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('codex mcp add "HubSpotDev" -- test-command --arg1 --arg2 --ai-agent codex');
|
|
75
|
+
expect(mockedSpinniesManager.succeed).toHaveBeenCalledWith('codexSpinner', {
|
|
76
|
+
text: commands.mcp.setup.spinners.configuredCodex,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('should use default mcp command when none provided', async () => {
|
|
80
|
+
mockedExecAsync.mockResolvedValueOnce({
|
|
81
|
+
stdout: 'codex version 1.0.0',
|
|
82
|
+
stderr: '',
|
|
83
|
+
}); // version check
|
|
84
|
+
mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' }); // mcp add command
|
|
85
|
+
const result = await setupCodex();
|
|
86
|
+
expect(result).toBe(true);
|
|
87
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('codex mcp add "HubSpotDev" -- hs mcp start --ai-agent codex');
|
|
88
|
+
});
|
|
89
|
+
it('should handle codex command not found', async () => {
|
|
90
|
+
const error = new Error('Command not found: codex');
|
|
91
|
+
mockedExecAsync.mockRejectedValueOnce(error);
|
|
92
|
+
const result = await setupCodex(mockMcpCommand);
|
|
93
|
+
expect(result).toBe(false);
|
|
94
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('codexSpinner', {
|
|
95
|
+
text: commands.mcp.setup.spinners.codexNotFound,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it('should handle codex installation failure', async () => {
|
|
99
|
+
const error = new Error('Some other error');
|
|
100
|
+
mockedExecAsync.mockResolvedValueOnce({
|
|
101
|
+
stdout: 'codex version 1.0.0',
|
|
102
|
+
stderr: '',
|
|
103
|
+
}); // version check passes
|
|
104
|
+
mockedExecAsync.mockRejectedValueOnce(error); // mcp add fails
|
|
105
|
+
const result = await setupCodex(mockMcpCommand);
|
|
106
|
+
expect(result).toBe(false);
|
|
107
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('codexSpinner', {
|
|
108
|
+
text: commands.mcp.setup.spinners.codexInstallFailed,
|
|
109
|
+
});
|
|
110
|
+
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
111
|
+
});
|
|
112
|
+
it('should handle unexpected errors during spinner setup', async () => {
|
|
113
|
+
const error = new Error('Unexpected error');
|
|
114
|
+
mockedSpinniesManager.add.mockImplementationOnce(() => {
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
const result = await setupCodex(mockMcpCommand);
|
|
118
|
+
expect(result).toBe(false);
|
|
119
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('codexSpinner', {
|
|
120
|
+
text: commands.mcp.setup.spinners.codexInstallFailed,
|
|
121
|
+
});
|
|
122
|
+
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// Note: addMcpServerToConfig integration tests would require mocking many dependencies
|
|
126
|
+
// and complex setup. The setupCodex function tests above cover the new functionality.
|
|
127
|
+
});
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
export declare const supportedTools:
|
|
2
|
-
name:
|
|
1
|
+
export declare const supportedTools: {
|
|
2
|
+
name: string;
|
|
3
3
|
value: string;
|
|
4
|
-
}
|
|
5
|
-
name: "Cursor";
|
|
6
|
-
value: string;
|
|
7
|
-
} | {
|
|
8
|
-
name: "Windsurf";
|
|
9
|
-
value: string;
|
|
10
|
-
} | {
|
|
11
|
-
name: "VSCode";
|
|
12
|
-
value: string;
|
|
13
|
-
})[];
|
|
4
|
+
}[];
|
|
14
5
|
interface McpCommand {
|
|
15
6
|
command: string;
|
|
16
7
|
args: string[];
|
|
@@ -20,4 +11,5 @@ export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
|
20
11
|
export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
21
12
|
export declare function setupCursor(mcpCommand?: McpCommand): boolean;
|
|
22
13
|
export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
|
|
14
|
+
export declare function setupCodex(mcpCommand?: McpCommand): Promise<boolean>;
|
|
23
15
|
export {};
|
package/lib/mcp/setup.js
CHANGED
|
@@ -13,11 +13,13 @@ const claudeCode = 'claude';
|
|
|
13
13
|
const windsurf = 'windsurf';
|
|
14
14
|
const cursor = 'cursor';
|
|
15
15
|
const vscode = 'vscode';
|
|
16
|
+
const codex = 'codex';
|
|
16
17
|
export const supportedTools = [
|
|
18
|
+
{ name: commands.mcp.setup.codex, value: codex },
|
|
17
19
|
{ name: commands.mcp.setup.claudeCode, value: claudeCode },
|
|
18
20
|
{ name: commands.mcp.setup.cursor, value: cursor },
|
|
19
|
-
{ name: commands.mcp.setup.windsurf, value: windsurf },
|
|
20
21
|
{ name: commands.mcp.setup.vsCode, value: vscode },
|
|
22
|
+
{ name: commands.mcp.setup.windsurf, value: windsurf },
|
|
21
23
|
];
|
|
22
24
|
const defaultMcpCommand = {
|
|
23
25
|
command: 'hs',
|
|
@@ -56,6 +58,9 @@ export async function addMcpServerToConfig(targets) {
|
|
|
56
58
|
if (derivedTargets.includes(vscode)) {
|
|
57
59
|
await runSetupFunction(setupVsCode);
|
|
58
60
|
}
|
|
61
|
+
if (derivedTargets.includes(codex)) {
|
|
62
|
+
await runSetupFunction(setupCodex);
|
|
63
|
+
}
|
|
59
64
|
uiLogger.info(commands.mcp.setup.success(derivedTargets));
|
|
60
65
|
return derivedTargets;
|
|
61
66
|
}
|
|
@@ -231,6 +236,34 @@ export function setupWindsurf(mcpCommand = defaultMcpCommand) {
|
|
|
231
236
|
mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
|
|
232
237
|
});
|
|
233
238
|
}
|
|
239
|
+
export async function setupCodex(mcpCommand = defaultMcpCommand) {
|
|
240
|
+
try {
|
|
241
|
+
SpinniesManager.add('codexSpinner', {
|
|
242
|
+
text: commands.mcp.setup.spinners.configuringCodex,
|
|
243
|
+
});
|
|
244
|
+
// Check if codex command is available
|
|
245
|
+
await execAsync('codex --version');
|
|
246
|
+
await execAsync(`codex mcp add "${mcpServerName}" -- ${mcpCommand.command} ${mcpCommand.args.join(' ')} --ai-agent codex`);
|
|
247
|
+
SpinniesManager.succeed('codexSpinner', {
|
|
248
|
+
text: commands.mcp.setup.spinners.configuredCodex,
|
|
249
|
+
});
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof Error && error.message.includes('codex')) {
|
|
254
|
+
SpinniesManager.fail('codexSpinner', {
|
|
255
|
+
text: commands.mcp.setup.spinners.codexNotFound,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
SpinniesManager.fail('codexSpinner', {
|
|
260
|
+
text: commands.mcp.setup.spinners.codexInstallFailed,
|
|
261
|
+
});
|
|
262
|
+
logError(error);
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
234
267
|
function buildCommandWithAgentString(mcpCommand, agent) {
|
|
235
268
|
const mcpCommandCopy = structuredClone(mcpCommand);
|
|
236
269
|
mcpCommandCopy.args.push('--ai-agent', agent);
|
|
@@ -47,6 +47,7 @@ export async function autoUpdateCLI(argv) {
|
|
|
47
47
|
let showManualInstallHelp = true;
|
|
48
48
|
if (notifier &&
|
|
49
49
|
notifier.update &&
|
|
50
|
+
!argv.useEnv &&
|
|
50
51
|
!process.env.SKIP_HUBSPOT_CLI_AUTO_UPDATES &&
|
|
51
52
|
isConfigFlagEnabled('allowAutoUpdates') &&
|
|
52
53
|
!preventAutoUpdateForCommand(argv._)) {
|
|
@@ -2,8 +2,10 @@ import fs from 'fs';
|
|
|
2
2
|
import { handleComponentCollision, updateHsMetaFilesWithAutoGeneratedFields, } from '../components.js';
|
|
3
3
|
import { uiLogger } from '../../ui/logger.js';
|
|
4
4
|
import { coerceToValidUid } from '@hubspot/project-parsing-lib';
|
|
5
|
+
import { fileExists } from '../../validation.js';
|
|
5
6
|
vi.mock('fs');
|
|
6
7
|
vi.mock('../../ui/logger.js');
|
|
8
|
+
vi.mock('../../validation.js');
|
|
7
9
|
vi.mock('@hubspot/project-parsing-lib', () => ({
|
|
8
10
|
coerceToValidUid: vi.fn(),
|
|
9
11
|
metafileExtension: '.module.meta.json',
|
|
@@ -19,11 +21,15 @@ vi.mock('../../../lang/en.js', () => ({
|
|
|
19
21
|
applicationLog: (type, uid, name) => `Updated ${type} component with uid: ${uid} and name: ${name}`,
|
|
20
22
|
componentLog: (type, uid) => `Updated ${type} component with uid: ${uid}`,
|
|
21
23
|
},
|
|
24
|
+
generateSafeFilenameDifferentiator: {
|
|
25
|
+
failedToCheckFiles: 'Failed to check files for filename differentiator. Falling back to timestamp.',
|
|
26
|
+
},
|
|
22
27
|
},
|
|
23
28
|
},
|
|
24
29
|
}));
|
|
25
30
|
const mockedFs = vi.mocked(fs);
|
|
26
31
|
const mockCoerceToValidUid = vi.mocked(coerceToValidUid);
|
|
32
|
+
const mockedFileExists = vi.mocked(fileExists);
|
|
27
33
|
describe('lib/projects/components', () => {
|
|
28
34
|
describe('handleComponentCollision()', () => {
|
|
29
35
|
const mockCollision = {
|
|
@@ -33,13 +39,13 @@ describe('lib/projects/components', () => {
|
|
|
33
39
|
};
|
|
34
40
|
beforeEach(() => {
|
|
35
41
|
vi.resetAllMocks();
|
|
36
|
-
//
|
|
37
|
-
|
|
42
|
+
// Default: fileExists returns false (file doesn't exist)
|
|
43
|
+
mockedFileExists.mockReturnValue(false);
|
|
38
44
|
});
|
|
39
45
|
afterEach(() => {
|
|
40
46
|
vi.restoreAllMocks();
|
|
41
47
|
});
|
|
42
|
-
it('handles source file collisions by renaming them with
|
|
48
|
+
it('handles source file collisions by renaming them with sequential numbers', () => {
|
|
43
49
|
const collision = {
|
|
44
50
|
...mockCollision,
|
|
45
51
|
collisions: ['component.js', 'utils.ts'],
|
|
@@ -47,8 +53,8 @@ describe('lib/projects/components', () => {
|
|
|
47
53
|
mockedFs.copyFileSync.mockImplementation(() => { });
|
|
48
54
|
handleComponentCollision(collision);
|
|
49
55
|
expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2);
|
|
50
|
-
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/component.js', '/dest/path/component-
|
|
51
|
-
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/utils.ts', '/dest/path/utils-
|
|
56
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/component.js', '/dest/path/component-2.js');
|
|
57
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/utils.ts', '/dest/path/utils-2.ts');
|
|
52
58
|
});
|
|
53
59
|
it('handles metafile collisions by renaming and updating references', () => {
|
|
54
60
|
const collision = {
|
|
@@ -69,8 +75,8 @@ describe('lib/projects/components', () => {
|
|
|
69
75
|
});
|
|
70
76
|
handleComponentCollision(collision);
|
|
71
77
|
expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/component.module.meta.json', 'utf-8');
|
|
72
|
-
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/dest/path/component-
|
|
73
|
-
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/source.js', '/dest/path/source-
|
|
78
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/dest/path/component-2.module.meta.json', expect.stringContaining('source-2.js'));
|
|
79
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/source.js', '/dest/path/source-2.js');
|
|
74
80
|
});
|
|
75
81
|
it('handles package.json collisions by merging dependencies', () => {
|
|
76
82
|
const collision = {
|
|
@@ -150,10 +156,10 @@ describe('lib/projects/components', () => {
|
|
|
150
156
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
151
157
|
handleComponentCollision(collision);
|
|
152
158
|
// Verify source files are copied with new names
|
|
153
|
-
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/component.js', '/dest/path/component-
|
|
154
|
-
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/utils.ts', '/dest/path/utils-
|
|
159
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/component.js', '/dest/path/component-2.js');
|
|
160
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/utils.ts', '/dest/path/utils-2.ts');
|
|
155
161
|
// Verify metafile is updated and written with new name
|
|
156
|
-
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/dest/path/component-
|
|
162
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/dest/path/component-2.module.meta.json', expect.stringContaining('component-2.js'));
|
|
157
163
|
// Verify package.json is merged
|
|
158
164
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/dest/path/package.json', expect.stringContaining('"dependencies"'));
|
|
159
165
|
consoleSpy.mockRestore();
|
|
@@ -198,6 +204,47 @@ describe('lib/projects/components', () => {
|
|
|
198
204
|
expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/package.json', 'utf-8');
|
|
199
205
|
consoleSpy.mockRestore();
|
|
200
206
|
});
|
|
207
|
+
it('falls back to timestamp when maxAttempts is exhausted', () => {
|
|
208
|
+
const collision = {
|
|
209
|
+
...mockCollision,
|
|
210
|
+
collisions: ['component.js'],
|
|
211
|
+
};
|
|
212
|
+
// Mock Date.now to return a consistent timestamp
|
|
213
|
+
const mockTimestamp = 1234567890;
|
|
214
|
+
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp);
|
|
215
|
+
// Mock fileExists to return true 10 times (exhausting maxAttempts)
|
|
216
|
+
// The function starts with differentiator = 1, then increments to 2, 3, etc.
|
|
217
|
+
// It will try 10 times (differentiators 2-11), and if all return true,
|
|
218
|
+
// maxAttempts will be 0 and it will fall back to timestamp
|
|
219
|
+
mockedFileExists.mockReturnValue(true);
|
|
220
|
+
mockedFs.copyFileSync.mockImplementation(() => { });
|
|
221
|
+
handleComponentCollision(collision);
|
|
222
|
+
// Should use timestamp as differentiator
|
|
223
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/component.js', `/dest/path/component-${mockTimestamp}.js`);
|
|
224
|
+
vi.restoreAllMocks();
|
|
225
|
+
});
|
|
226
|
+
it('falls back to timestamp when fileExists throws an error', () => {
|
|
227
|
+
const collision = {
|
|
228
|
+
...mockCollision,
|
|
229
|
+
collisions: ['component.js'],
|
|
230
|
+
};
|
|
231
|
+
// Mock Date.now to return a consistent timestamp
|
|
232
|
+
const mockTimestamp = 9876543210;
|
|
233
|
+
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp);
|
|
234
|
+
// Mock fileExists to throw an error
|
|
235
|
+
const mockError = new Error('File system error');
|
|
236
|
+
mockedFileExists.mockImplementation(() => {
|
|
237
|
+
throw mockError;
|
|
238
|
+
});
|
|
239
|
+
mockedFs.copyFileSync.mockImplementation(() => { });
|
|
240
|
+
const mockUiLogger = vi.mocked(uiLogger);
|
|
241
|
+
handleComponentCollision(collision);
|
|
242
|
+
// Should log debug message about the error
|
|
243
|
+
expect(mockUiLogger.debug).toHaveBeenCalledWith('Failed to check files for filename differentiator. Falling back to timestamp.');
|
|
244
|
+
// Should use timestamp as differentiator
|
|
245
|
+
expect(mockedFs.copyFileSync).toHaveBeenCalledWith('/src/path/component.js', `/dest/path/component-${mockTimestamp}.js`);
|
|
246
|
+
vi.restoreAllMocks();
|
|
247
|
+
});
|
|
201
248
|
});
|
|
202
249
|
describe('updateHsMetaFilesWithAutoGeneratedFields()', () => {
|
|
203
250
|
const mockUiLogger = vi.mocked(uiLogger);
|
|
@@ -233,20 +280,22 @@ describe('lib/projects/components', () => {
|
|
|
233
280
|
.mockReturnValueOnce('card-my-project')
|
|
234
281
|
.mockReturnValueOnce('function-my-project');
|
|
235
282
|
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
283
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_card');
|
|
284
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_function');
|
|
236
285
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
|
|
237
286
|
type: 'card',
|
|
238
|
-
uid: '
|
|
287
|
+
uid: 'card_my_project',
|
|
239
288
|
config: {
|
|
240
289
|
name: 'Old Name',
|
|
241
290
|
},
|
|
242
291
|
}, null, 2));
|
|
243
292
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component2.meta.json', JSON.stringify({
|
|
244
293
|
type: 'function',
|
|
245
|
-
uid: '
|
|
294
|
+
uid: 'function_my_project',
|
|
246
295
|
}, null, 2));
|
|
247
296
|
expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
|
|
248
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated card component with uid:
|
|
249
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated function component with uid:
|
|
297
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated card component with uid: card_my_project');
|
|
298
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated function component with uid: function_my_project');
|
|
250
299
|
});
|
|
251
300
|
it('handles app components by updating both uid and config.name', () => {
|
|
252
301
|
const projectName = 'test-app';
|
|
@@ -263,32 +312,36 @@ describe('lib/projects/components', () => {
|
|
|
263
312
|
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
264
313
|
mockCoerceToValidUid.mockReturnValue('app-test-app');
|
|
265
314
|
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
315
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('test-app_app');
|
|
266
316
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
|
|
267
317
|
type: 'app',
|
|
268
|
-
uid: '
|
|
318
|
+
uid: 'app_test_app',
|
|
269
319
|
config: {
|
|
270
320
|
name: 'test-app-Application',
|
|
271
321
|
other: 'property',
|
|
272
322
|
},
|
|
273
323
|
}, null, 2));
|
|
274
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid:
|
|
324
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app_test_app and name: test-app-Application');
|
|
275
325
|
});
|
|
276
|
-
it('handles UID collisions by using
|
|
326
|
+
it('handles UID collisions by using differentiators', () => {
|
|
277
327
|
const projectName = 'collision-project';
|
|
278
328
|
const hsMetaFilePaths = ['/path/to/component1.meta.json'];
|
|
279
|
-
const existingUids = ['
|
|
329
|
+
const existingUids = ['card_collision_project'];
|
|
280
330
|
const component1 = { type: 'card', uid: 'old-uid-1' };
|
|
281
331
|
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component1));
|
|
282
332
|
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
283
|
-
//
|
|
284
|
-
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
|
|
333
|
+
// First call for getBaseUid() check, second call when adding differentiator
|
|
285
334
|
mockCoerceToValidUid
|
|
286
335
|
.mockReturnValueOnce('card-collision-project')
|
|
287
|
-
.mockReturnValueOnce('card-
|
|
336
|
+
.mockReturnValueOnce('card-collision-project');
|
|
288
337
|
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids);
|
|
338
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('collision-project_card');
|
|
339
|
+
// getBaseUid() is called twice - once for initial check, once when adding differentiator
|
|
340
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledTimes(2);
|
|
341
|
+
// The differentiator is appended with a hyphen, so the final UID has a hyphen before the number
|
|
289
342
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
|
|
290
343
|
type: 'card',
|
|
291
|
-
uid: '
|
|
344
|
+
uid: 'card_collision_project_2',
|
|
292
345
|
}, null, 2));
|
|
293
346
|
});
|
|
294
347
|
it('falls back to original uid when coerceToValidUid returns null', () => {
|
|
@@ -328,11 +381,82 @@ describe('lib/projects/components', () => {
|
|
|
328
381
|
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
329
382
|
mockCoerceToValidUid.mockReturnValue('app-no-config-project');
|
|
330
383
|
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
384
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('no-config-project_app');
|
|
331
385
|
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
|
|
332
386
|
type: 'app',
|
|
333
|
-
uid: '
|
|
387
|
+
uid: 'app_no_config_project',
|
|
388
|
+
}, null, 2));
|
|
389
|
+
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app_no_config_project');
|
|
390
|
+
});
|
|
391
|
+
it('replaces hyphens with underscores in coerced UIDs', () => {
|
|
392
|
+
const projectName = 'my-project';
|
|
393
|
+
const hsMetaFilePaths = ['/path/to/component.meta.json'];
|
|
394
|
+
const component = {
|
|
395
|
+
type: 'card',
|
|
396
|
+
uid: 'old-uid',
|
|
397
|
+
};
|
|
398
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
|
|
399
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
400
|
+
// coerceToValidUid returns a value with hyphens that should be converted to underscores
|
|
401
|
+
mockCoerceToValidUid.mockReturnValue('my-project-card-with-hyphens');
|
|
402
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
403
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-project_card');
|
|
404
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
|
|
405
|
+
type: 'card',
|
|
406
|
+
uid: 'my_project_card_with_hyphens',
|
|
407
|
+
}, null, 2));
|
|
408
|
+
});
|
|
409
|
+
it('handles UIDs with multiple hyphens correctly', () => {
|
|
410
|
+
const projectName = 'test-project';
|
|
411
|
+
const hsMetaFilePaths = ['/path/to/component.meta.json'];
|
|
412
|
+
const component = {
|
|
413
|
+
type: 'custom-object',
|
|
414
|
+
uid: 'old-uid',
|
|
415
|
+
};
|
|
416
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
|
|
417
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
418
|
+
mockCoerceToValidUid.mockReturnValue('test-project-custom-object-type');
|
|
419
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
420
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('test-project_custom-object');
|
|
421
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
|
|
422
|
+
type: 'custom-object',
|
|
423
|
+
uid: 'test_project_custom_object_type',
|
|
424
|
+
}, null, 2));
|
|
425
|
+
});
|
|
426
|
+
it('preserves UIDs without hyphens unchanged', () => {
|
|
427
|
+
const projectName = 'simpleproject';
|
|
428
|
+
const hsMetaFilePaths = ['/path/to/component.meta.json'];
|
|
429
|
+
const component = {
|
|
430
|
+
type: 'card',
|
|
431
|
+
uid: 'old-uid',
|
|
432
|
+
};
|
|
433
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
|
|
434
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
435
|
+
// coerceToValidUid returns a value without hyphens
|
|
436
|
+
mockCoerceToValidUid.mockReturnValue('simpleprojectcard');
|
|
437
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
438
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('simpleproject_card');
|
|
439
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
|
|
440
|
+
type: 'card',
|
|
441
|
+
uid: 'simpleprojectcard',
|
|
442
|
+
}, null, 2));
|
|
443
|
+
});
|
|
444
|
+
it('handles project names with hyphens in UID generation', () => {
|
|
445
|
+
const projectName = 'my-super-project';
|
|
446
|
+
const hsMetaFilePaths = ['/path/to/component.meta.json'];
|
|
447
|
+
const component = {
|
|
448
|
+
type: 'function',
|
|
449
|
+
uid: 'old-uid',
|
|
450
|
+
};
|
|
451
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
|
|
452
|
+
mockedFs.writeFileSync.mockImplementation(() => { });
|
|
453
|
+
mockCoerceToValidUid.mockReturnValue('my-super-project-function');
|
|
454
|
+
updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
|
|
455
|
+
expect(mockCoerceToValidUid).toHaveBeenCalledWith('my-super-project_function');
|
|
456
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
|
|
457
|
+
type: 'function',
|
|
458
|
+
uid: 'my_super_project_function',
|
|
334
459
|
}, null, 2));
|
|
335
|
-
expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-no-config-project');
|
|
336
460
|
});
|
|
337
461
|
});
|
|
338
462
|
});
|