@hubspot/cli 7.9.0-experimental.0 → 7.9.1-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +5 -4
- package/commands/__tests__/getStarted.test.js +10 -0
- package/commands/account/__tests__/rename.test.js +42 -0
- package/commands/account/auth.js +10 -14
- package/commands/account/clean.js +11 -19
- package/commands/account/createOverride.js +15 -11
- package/commands/account/info.js +8 -5
- package/commands/account/list.js +13 -18
- package/commands/account/remove.js +23 -22
- package/commands/account/removeOverride.js +6 -6
- package/commands/account/rename.d.ts +1 -1
- package/commands/account/rename.js +6 -3
- package/commands/account/use.js +19 -8
- package/commands/app/__tests__/migrate.test.js +8 -4
- package/commands/app/migrate.js +2 -2
- package/commands/auth.js +18 -14
- package/commands/config/migrate.js +5 -5
- package/commands/config/set.js +1 -2
- package/commands/customObject/createSchema.js +2 -3
- package/commands/customObject/updateSchema.js +2 -3
- package/commands/getStarted.js +10 -5
- package/commands/hubdb/__tests__/list.test.js +1 -0
- package/commands/hubdb/list.js +2 -2
- package/commands/hubdb.d.ts +1 -1
- package/commands/init.js +36 -32
- package/commands/project/__tests__/deploy.test.js +10 -5
- package/commands/project/__tests__/devUnifiedFlow.test.js +6 -4
- package/commands/project/__tests__/logs.test.js +4 -0
- package/commands/project/__tests__/validate.test.js +2 -2
- package/commands/project/cloneApp.js +2 -2
- package/commands/project/deploy.js +2 -2
- package/commands/project/dev/deprecatedFlow.js +4 -5
- package/commands/project/dev/index.js +14 -4
- package/commands/project/dev/unifiedFlow.js +4 -5
- package/commands/project/listBuilds.js +7 -1
- package/commands/project/logs.js +2 -3
- package/commands/project/profile/add.js +6 -7
- package/commands/project/profile/delete.js +2 -2
- package/commands/project/upload.js +9 -3
- package/commands/project/validate.js +9 -3
- package/commands/project/watch.js +7 -2
- package/commands/sandbox/__tests__/create.test.js +14 -5
- package/commands/sandbox/create.js +4 -5
- package/commands/sandbox/delete.js +23 -20
- package/commands/testAccount/__tests__/create.test.js +68 -0
- package/commands/testAccount/create.d.ts +8 -0
- package/commands/testAccount/create.js +134 -44
- package/commands/testAccount/delete.js +9 -8
- package/commands/testAccount/importData.d.ts +1 -1
- package/lang/en.d.ts +3204 -3205
- package/lang/en.js +33 -9
- package/lib/__tests__/buildAccount.test.js +22 -30
- package/lib/__tests__/commonOpts.test.js +9 -13
- package/lib/__tests__/developerTestAccounts.test.js +29 -17
- package/lib/__tests__/importData.test.js +20 -10
- package/lib/__tests__/oauth.test.js +19 -8
- package/lib/__tests__/sandboxSync.test.js +33 -11
- package/lib/__tests__/sandboxes.test.js +30 -19
- package/lib/__tests__/usageTracking.test.js +10 -10
- package/lib/__tests__/validation.test.js +32 -32
- package/lib/accountTypes.d.ts +9 -9
- package/lib/accountTypes.js +2 -4
- package/lib/app/__tests__/migrate.test.js +15 -0
- package/lib/app/__tests__/migrate_legacy.test.js +9 -0
- package/lib/app/migrate_legacy.d.ts +2 -2
- package/lib/buildAccount.d.ts +4 -4
- package/lib/buildAccount.js +7 -14
- package/lib/commonOpts.js +3 -3
- package/lib/configMigrate.d.ts +2 -2
- package/lib/configMigrate.js +42 -18
- package/lib/configOptions.js +3 -2
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +6 -0
- package/lib/developerTestAccounts.d.ts +3 -3
- package/lib/developerTestAccounts.js +4 -7
- package/lib/doctor/DiagnosticInfoBuilder.d.ts +1 -1
- package/lib/doctor/DiagnosticInfoBuilder.js +9 -6
- package/lib/doctor/Doctor.js +4 -3
- package/lib/doctor/__tests__/Diagnosis.test.js +4 -3
- package/lib/doctor/__tests__/DiagnosticInfoBuilder.test.js +17 -9
- package/lib/doctor/__tests__/Doctor.test.js +14 -0
- package/lib/importData.js +8 -7
- package/lib/links.js +5 -5
- 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/__test__/commandTargetingUtils.test.js +3 -3
- package/lib/middleware/__test__/configMiddleware.test.js +23 -22
- package/lib/middleware/__test__/gitMiddleware.test.js +9 -7
- package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
- package/lib/middleware/autoUpdateMiddleware.js +10 -2
- package/lib/middleware/commandTargetingUtils.js +2 -2
- package/lib/middleware/configMiddleware.d.ts +6 -1
- package/lib/middleware/configMiddleware.js +36 -15
- package/lib/middleware/gitMiddleware.js +8 -4
- package/lib/oauth.d.ts +2 -2
- package/lib/oauth.js +8 -10
- package/lib/projects/__tests__/AppDevModeInterface.test.js +17 -6
- package/lib/projects/__tests__/DevServerManager.test.js +1 -0
- package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
- package/lib/projects/__tests__/components.test.js +148 -24
- package/lib/projects/__tests__/deploy.test.js +1 -0
- 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/projects/create/__tests__/v2.test.js +11 -0
- package/lib/projects/localDev/AppDevModeInterface.js +2 -2
- package/lib/projects/localDev/DevServerManager_DEPRECATED.js +2 -2
- package/lib/projects/localDev/LocalDevLogger.js +4 -4
- package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -3
- package/lib/projects/localDev/helpers/account.d.ts +10 -10
- package/lib/projects/localDev/helpers/account.js +6 -11
- package/lib/projects/urls.js +5 -6
- package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
- package/lib/prompts/__tests__/downloadProjectPrompt.test.js +7 -5
- package/lib/prompts/accountNamePrompt.js +3 -3
- package/lib/prompts/accountsPrompt.d.ts +1 -1
- package/lib/prompts/accountsPrompt.js +6 -7
- package/lib/prompts/confirmImportDataPrompt.js +2 -2
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
- package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
- package/lib/prompts/downloadProjectPrompt.d.ts +1 -0
- package/lib/prompts/downloadProjectPrompt.js +5 -2
- package/lib/prompts/importDataTestAccountSelectPrompt.js +4 -5
- package/lib/prompts/personalAccessKeyPrompt.js +2 -2
- package/lib/prompts/projectDevTargetAccountPrompt.d.ts +3 -3
- package/lib/prompts/projectDevTargetAccountPrompt.js +5 -7
- package/lib/prompts/sandboxesPrompt.js +7 -8
- package/lib/prompts/setAsDefaultAccountPrompt.js +7 -6
- package/lib/sandboxSync.d.ts +2 -2
- package/lib/sandboxSync.js +3 -9
- package/lib/sandboxes.d.ts +4 -4
- package/lib/sandboxes.js +6 -11
- package/lib/serverlessLogs.js +2 -2
- package/lib/theme/__tests__/migrate.test.js +15 -0
- package/lib/ui/index.js +6 -3
- package/lib/usageTracking.js +15 -8
- package/lib/validation.js +13 -11
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +8 -2
- package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
- package/mcp-server/tools/cms/HsCreateModuleTool.js +8 -2
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +8 -2
- package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +6 -2
- package/mcp-server/tools/cms/HsListFunctionsTool.js +5 -1
- package/mcp-server/tools/cms/HsListTool.js +5 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -0
- package/mcp-server/tools/index.js +4 -0
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +9 -3
- package/mcp-server/tools/project/CreateProjectTool.js +8 -2
- 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.d.ts +1 -1
- package/mcp-server/tools/project/DeployProjectTool.js +8 -2
- package/mcp-server/tools/project/DocFetchTool.d.ts +1 -1
- package/mcp-server/tools/project/DocFetchTool.js +9 -5
- package/mcp-server/tools/project/DocsSearchTool.d.ts +1 -1
- package/mcp-server/tools/project/DocsSearchTool.js +12 -8
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +1 -1
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +11 -7
- package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +1 -1
- package/mcp-server/tools/project/GetApplicationInfoTool.js +11 -7
- package/mcp-server/tools/project/GetBuildStatusTool.d.ts +26 -0
- package/mcp-server/tools/project/GetBuildStatusTool.js +164 -0
- package/mcp-server/tools/project/GetConfigValuesTool.d.ts +1 -1
- package/mcp-server/tools/project/GetConfigValuesTool.js +11 -7
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +1 -1
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +7 -3
- package/mcp-server/tools/project/UploadProjectTools.d.ts +9 -3
- package/mcp-server/tools/project/UploadProjectTools.js +51 -5
- package/mcp-server/tools/project/ValidateProjectTool.d.ts +1 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +7 -3
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +454 -0
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +5 -1
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +25 -13
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +7 -5
- package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +7 -5
- package/mcp-server/tools/project/__tests__/GetBuildStatusTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/GetBuildStatusTool.test.js +240 -0
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +8 -6
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +56 -4
- package/mcp-server/utils/__tests__/content.test.js +21 -20
- package/mcp-server/utils/__tests__/feedbackTracking.test.js +33 -28
- package/mcp-server/utils/content.d.ts +1 -1
- package/mcp-server/utils/content.js +2 -2
- package/mcp-server/utils/feedbackTracking.d.ts +1 -1
- package/mcp-server/utils/feedbackTracking.js +3 -3
- package/mcp-server/utils/toolUsageTracking.js +4 -3
- package/package.json +8 -7
- package/mcp-server/utils/__tests__/cliConfig.test.js +0 -110
- package/mcp-server/utils/cliConfig.d.ts +0 -1
- package/mcp-server/utils/cliConfig.js +0 -12
- /package/{mcp-server/utils/__tests__/cliConfig.test.d.ts → lib/mcp/__tests__/setup.test.d.ts} +0 -0
|
@@ -12,7 +12,7 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => {
|
|
|
12
12
|
});
|
|
13
13
|
import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
14
14
|
import { fetchAppMetadataByUid, fetchPublicAppProductionInstallCounts, installStaticAuthAppOnTestAccount, } from '@hubspot/local-dev-lib/api/appsDev';
|
|
15
|
-
import {
|
|
15
|
+
import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
|
|
16
16
|
import AppDevModeInterface from '../localDev/AppDevModeInterface.js';
|
|
17
17
|
import LocalDevState from '../localDev/LocalDevState.js';
|
|
18
18
|
import LocalDevLogger from '../localDev/LocalDevLogger.js';
|
|
@@ -121,7 +121,7 @@ describe('AppDevModeInterface', () => {
|
|
|
121
121
|
previouslyAuthorizedScopeGroups: [],
|
|
122
122
|
},
|
|
123
123
|
});
|
|
124
|
-
|
|
124
|
+
getConfigAccountById.mockReturnValue({
|
|
125
125
|
parentAccountId: 12345,
|
|
126
126
|
});
|
|
127
127
|
isDeveloperTestAccount.mockReturnValue(true);
|
|
@@ -290,13 +290,14 @@ describe('AppDevModeInterface', () => {
|
|
|
290
290
|
});
|
|
291
291
|
it('should handle app reinstallation', async () => {
|
|
292
292
|
// Set up conditions for non-automatic installation
|
|
293
|
-
getAccountConfig.mockReturnValue(null);
|
|
294
293
|
fetchAppInstallationData.mockResolvedValue({
|
|
295
294
|
data: {
|
|
296
295
|
isInstalledWithScopeGroups: false,
|
|
297
296
|
previouslyAuthorizedScopeGroups: ['old-scope'],
|
|
298
297
|
},
|
|
299
298
|
});
|
|
299
|
+
// Make it not automatically installable by making it not a test account
|
|
300
|
+
isDeveloperTestAccount.mockReturnValue(false);
|
|
300
301
|
await appDevModeInterface.setup();
|
|
301
302
|
expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true);
|
|
302
303
|
});
|
|
@@ -308,7 +309,7 @@ describe('AppDevModeInterface', () => {
|
|
|
308
309
|
});
|
|
309
310
|
it('should exit if user declines auto-install', async () => {
|
|
310
311
|
// Set up conditions for automatic installation
|
|
311
|
-
|
|
312
|
+
getConfigAccountById.mockReturnValue({
|
|
312
313
|
parentAccountId: 12345, // matches targetProjectAccountId
|
|
313
314
|
});
|
|
314
315
|
isDeveloperTestAccount.mockReturnValue(true);
|
|
@@ -375,6 +376,15 @@ describe('AppDevModeInterface', () => {
|
|
|
375
376
|
data: { uniquePortalInstallCount: 5 },
|
|
376
377
|
});
|
|
377
378
|
getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
|
|
379
|
+
getConfigAccountById.mockReturnValue({
|
|
380
|
+
parentAccountId: 12345,
|
|
381
|
+
});
|
|
382
|
+
isDeveloperTestAccount.mockReturnValue(true);
|
|
383
|
+
isSandbox.mockReturnValue(false);
|
|
384
|
+
installAppAutoPrompt.mockResolvedValue(true);
|
|
385
|
+
confirmPrompt.mockResolvedValue(true);
|
|
386
|
+
installStaticAuthAppOnTestAccount.mockResolvedValue(undefined);
|
|
387
|
+
isServerRunningAtUrl.mockResolvedValue(true);
|
|
378
388
|
installAppBrowserPrompt.mockImplementation(async () => {
|
|
379
389
|
const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
|
|
380
390
|
if (addListenerCall) {
|
|
@@ -387,8 +397,8 @@ describe('AppDevModeInterface', () => {
|
|
|
387
397
|
mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
|
|
388
398
|
mockLocalDevState.setAppDataForUid = vi.fn();
|
|
389
399
|
mockLocalDevState.addListener = vi.fn();
|
|
390
|
-
// Target account config is missing
|
|
391
|
-
|
|
400
|
+
// Target account config is missing - make it not a test account so isAutomaticallyInstallable returns false
|
|
401
|
+
isDeveloperTestAccount.mockReturnValue(false);
|
|
392
402
|
// App is not installed
|
|
393
403
|
fetchAppInstallationData.mockResolvedValue({
|
|
394
404
|
data: {
|
|
@@ -401,6 +411,7 @@ describe('AppDevModeInterface', () => {
|
|
|
401
411
|
localDevState: mockLocalDevState,
|
|
402
412
|
localDevLogger: mockLocalDevLogger,
|
|
403
413
|
});
|
|
414
|
+
// Remove the spy to see if that's causing the timeout
|
|
404
415
|
await newAppDevModeInterface.setup();
|
|
405
416
|
expect(installAppBrowserPrompt).toHaveBeenCalled();
|
|
406
417
|
});
|
|
@@ -28,6 +28,7 @@ vi.mock('@hubspot/local-dev-lib/config', () => ({
|
|
|
28
28
|
getAccountId: vi.fn().mockReturnValue(123),
|
|
29
29
|
hasLocalStateFlag: vi.fn().mockReturnValue(false),
|
|
30
30
|
getConfigDefaultAccount: vi.fn().mockReturnValue({ accountId: 123 }),
|
|
31
|
+
globalConfigFileExists: vi.fn().mockReturnValue(true),
|
|
31
32
|
}));
|
|
32
33
|
vi.mock('@hubspot/local-dev-lib/urls', () => ({
|
|
33
34
|
getHubSpotApiOrigin: vi.fn().mockReturnValue('https://api.hubspot.com'),
|
|
@@ -26,6 +26,7 @@ vi.mock('../deploy');
|
|
|
26
26
|
vi.mock('../config');
|
|
27
27
|
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
28
28
|
vi.mock('@hubspot/local-dev-lib/errors/index');
|
|
29
|
+
vi.mock('@hubspot/local-dev-lib/config');
|
|
29
30
|
vi.mock('../localDev/LocalDevLogger');
|
|
30
31
|
vi.mock('../localDev/DevServerManager');
|
|
31
32
|
// Tests for LocalDevProcess and LocalDevState
|
|
@@ -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
|
});
|
|
@@ -8,6 +8,7 @@ import { pollDeployStatus } from '../pollProjectBuildAndDeploy.js';
|
|
|
8
8
|
// Mock external dependencies
|
|
9
9
|
vi.mock('../../ui/logger.js');
|
|
10
10
|
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
11
|
+
vi.mock('@hubspot/local-dev-lib/config');
|
|
11
12
|
vi.mock('../pollProjectBuildAndDeploy.js');
|
|
12
13
|
const mockUiLogger = vi.mocked(uiLogger);
|
|
13
14
|
const mockDeployProject = vi.mocked(deployProject);
|
|
@@ -1,83 +1,54 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
5
4
|
import { validateProjectConfig } from '../../projects/config.js';
|
|
6
|
-
import
|
|
5
|
+
import ProjectValidationError from '../../errors/ProjectValidationError.js';
|
|
7
6
|
vi.mock('../../ui/logger.js');
|
|
8
7
|
describe('lib/projects', () => {
|
|
9
8
|
describe('validateProjectConfig()', () => {
|
|
10
9
|
let projectDir;
|
|
11
|
-
let exitMock;
|
|
12
10
|
beforeAll(() => {
|
|
13
11
|
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'projects-'));
|
|
14
12
|
fs.mkdirSync(path.join(projectDir, 'src'));
|
|
15
13
|
});
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
// @ts-expect-error - Mocking process.exit
|
|
18
|
-
exitMock = vi
|
|
19
|
-
.spyOn(process, 'exit')
|
|
20
|
-
.mockImplementation(() => undefined);
|
|
21
|
-
});
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
exitMock.mockRestore();
|
|
24
|
-
});
|
|
25
14
|
it('rejects undefined configuration', () => {
|
|
26
15
|
// @ts-ignore Testing invalid input
|
|
27
|
-
validateProjectConfig(null, projectDir);
|
|
28
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
29
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*Unable to locate a project configuration file. Try running again from a project directory, or run*/));
|
|
16
|
+
expect(() => validateProjectConfig(null, projectDir)).toThrow(/.*Unable to locate a project configuration file. Try running again from a project directory, or run*/);
|
|
30
17
|
});
|
|
31
18
|
it('rejects configuration with missing name', () => {
|
|
32
19
|
// @ts-ignore Testing invalid input
|
|
33
|
-
validateProjectConfig({ srcDir: '.' }, projectDir);
|
|
34
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
35
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*missing required fields*/));
|
|
20
|
+
expect(() => validateProjectConfig({ srcDir: '.' }, projectDir)).toThrow(/.*missing required fields*/);
|
|
36
21
|
});
|
|
37
22
|
it('rejects configuration with missing srcDir', () => {
|
|
23
|
+
expect(() =>
|
|
38
24
|
// @ts-ignore Testing invalid input
|
|
39
|
-
validateProjectConfig({ name: 'hello' }, projectDir);
|
|
40
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
41
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*missing required fields.*/));
|
|
25
|
+
validateProjectConfig({ name: 'hello' }, projectDir)).toThrow(/.*missing required fields.*/);
|
|
42
26
|
});
|
|
43
27
|
describe('rejects configuration with srcDir outside project directory', () => {
|
|
44
28
|
it('for parent directory', () => {
|
|
45
|
-
validateProjectConfig({ name: 'hello', srcDir: '..', platformVersion: '' }, projectDir);
|
|
46
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
47
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('srcDir: ".."'));
|
|
29
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: '..', platformVersion: '' }, projectDir)).toThrow(/srcDir: "\.\."/);
|
|
48
30
|
});
|
|
49
31
|
it('for root directory', () => {
|
|
50
|
-
validateProjectConfig({ name: 'hello', srcDir: '/', platformVersion: '' }, projectDir);
|
|
51
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
52
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining('srcDir: "/"'));
|
|
32
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: '/', platformVersion: '' }, projectDir)).toThrow(/srcDir: "\/"/);
|
|
53
33
|
});
|
|
54
34
|
it('for complicated directory', () => {
|
|
55
35
|
const srcDir = './src/././../src/../../src';
|
|
56
|
-
validateProjectConfig({ name: 'hello', srcDir, platformVersion: '' }, projectDir);
|
|
57
|
-
expect(
|
|
58
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringContaining(`srcDir: "${srcDir}"`));
|
|
36
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir, platformVersion: '' }, projectDir)).toThrow(ProjectValidationError);
|
|
37
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir, platformVersion: '' }, projectDir)).toThrow(/srcDir:/);
|
|
59
38
|
});
|
|
60
39
|
});
|
|
61
40
|
it('rejects configuration with srcDir that does not exist', () => {
|
|
62
|
-
validateProjectConfig({ name: 'hello', srcDir: 'foo', platformVersion: '' }, projectDir);
|
|
63
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
64
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/.*could not be found in.*/));
|
|
41
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: 'foo', platformVersion: '' }, projectDir)).toThrow(/.*could not be found in.*/);
|
|
65
42
|
});
|
|
66
43
|
describe('accepts configuration with valid srcDir', () => {
|
|
67
44
|
it('for current directory', () => {
|
|
68
|
-
validateProjectConfig({ name: 'hello', srcDir: '.', platformVersion: '' }, projectDir);
|
|
69
|
-
expect(exitMock).not.toHaveBeenCalled();
|
|
70
|
-
expect(uiLogger.error).not.toHaveBeenCalled();
|
|
45
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: '.', platformVersion: '' }, projectDir)).not.toThrow();
|
|
71
46
|
});
|
|
72
47
|
it('for relative directory', () => {
|
|
73
|
-
validateProjectConfig({ name: 'hello', srcDir: './src', platformVersion: '' }, projectDir);
|
|
74
|
-
expect(exitMock).not.toHaveBeenCalled();
|
|
75
|
-
expect(uiLogger.error).not.toHaveBeenCalled();
|
|
48
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: './src', platformVersion: '' }, projectDir)).not.toThrow();
|
|
76
49
|
});
|
|
77
50
|
it('for implied relative directory', () => {
|
|
78
|
-
validateProjectConfig({ name: 'hello', srcDir: 'src', platformVersion: '' }, projectDir);
|
|
79
|
-
expect(exitMock).not.toHaveBeenCalled();
|
|
80
|
-
expect(uiLogger.error).not.toHaveBeenCalled();
|
|
51
|
+
expect(() => validateProjectConfig({ name: 'hello', srcDir: 'src', platformVersion: '' }, projectDir)).not.toThrow();
|
|
81
52
|
});
|
|
82
53
|
});
|
|
83
54
|
});
|
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { coerceToValidUid, metafileExtension, } from '@hubspot/project-parsing-lib';
|
|
4
|
+
import { fileExists } from '../validation.js';
|
|
4
5
|
import { uiLogger } from '../ui/logger.js';
|
|
5
6
|
import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
|
|
6
7
|
import { lib } from '../../lang/en.js';
|
|
8
|
+
import { debugError } from '../errorHandlers/index.js';
|
|
9
|
+
// Prefix for the metafile extension
|
|
10
|
+
const metafileExtensionPrefix = path.parse(metafileExtension).name;
|
|
11
|
+
function applyDifferentiatorToFilename(filename, differentiator, isHsMetaFile) {
|
|
12
|
+
const { name, ext, dir } = path.parse(filename);
|
|
13
|
+
if (isHsMetaFile) {
|
|
14
|
+
return path.join(dir, `${name.replace(metafileExtensionPrefix, '')}-${differentiator}${metafileExtension}`);
|
|
15
|
+
}
|
|
16
|
+
return path.join(dir, `${name}-${differentiator}${ext}`);
|
|
17
|
+
}
|
|
18
|
+
// Generates safe filename differentiators, avoiding collisions with existing filenames
|
|
19
|
+
// E.x. "NewCard.tsx" -> "NewCard-1.tsx"
|
|
20
|
+
function generateSafeFilenameDifferentiator(sourceFiles, hsMetaFiles) {
|
|
21
|
+
let differentiator = 1;
|
|
22
|
+
let isDifferentiatorUnique = false;
|
|
23
|
+
let maxAttempts = 10;
|
|
24
|
+
while (!isDifferentiatorUnique) {
|
|
25
|
+
differentiator++;
|
|
26
|
+
maxAttempts--;
|
|
27
|
+
try {
|
|
28
|
+
const isDifferentiatorUniqueForSourceFiles = sourceFiles.every(file => {
|
|
29
|
+
return !fileExists(applyDifferentiatorToFilename(file, differentiator, false));
|
|
30
|
+
});
|
|
31
|
+
const isDifferentiatorUniqueForHsMetaFiles = hsMetaFiles.every(file => {
|
|
32
|
+
return !fileExists(applyDifferentiatorToFilename(file, differentiator, true));
|
|
33
|
+
});
|
|
34
|
+
isDifferentiatorUnique =
|
|
35
|
+
isDifferentiatorUniqueForSourceFiles &&
|
|
36
|
+
isDifferentiatorUniqueForHsMetaFiles;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
uiLogger.debug(lib.projects.generateSafeFilenameDifferentiator.failedToCheckFiles);
|
|
40
|
+
maxAttempts = 0;
|
|
41
|
+
}
|
|
42
|
+
// If we've tried too many times, just use a timestamp
|
|
43
|
+
if (maxAttempts <= 0) {
|
|
44
|
+
return Date.now();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return differentiator;
|
|
48
|
+
}
|
|
7
49
|
// Handles a collision between component source files
|
|
8
50
|
export function handleComponentCollision({ dest, src, collisions }) {
|
|
9
51
|
const hsMetaFiles = [];
|
|
@@ -20,19 +62,20 @@ export function handleComponentCollision({ dest, src, collisions }) {
|
|
|
20
62
|
sourceFiles.push(collision);
|
|
21
63
|
}
|
|
22
64
|
});
|
|
23
|
-
const
|
|
24
|
-
|
|
65
|
+
const filenameDifferentiator = generateSafeFilenameDifferentiator(sourceFiles, hsMetaFiles);
|
|
66
|
+
// Exclude markdown files fromthe rename process because they should not be duplicated
|
|
67
|
+
const sourceFilenameMapping = sourceFiles
|
|
68
|
+
.filter(filename => !filename.endsWith('.md'))
|
|
69
|
+
.reduce((acc, filename) => {
|
|
25
70
|
return {
|
|
26
71
|
...acc,
|
|
27
|
-
[filename]:
|
|
72
|
+
[filename]: applyDifferentiatorToFilename(filename, filenameDifferentiator, false),
|
|
28
73
|
};
|
|
29
74
|
}, {});
|
|
30
|
-
const metafileExtensionPrefix = path.parse(metafileExtension).name;
|
|
31
75
|
const metaFilenameMapping = hsMetaFiles.reduce((acc, filename) => {
|
|
32
|
-
const { name, dir } = path.parse(filename);
|
|
33
76
|
return {
|
|
34
77
|
...acc,
|
|
35
|
-
[filename]:
|
|
78
|
+
[filename]: applyDifferentiatorToFilename(filename, filenameDifferentiator, true),
|
|
36
79
|
};
|
|
37
80
|
}, {});
|
|
38
81
|
// Update the metafiles that might contain references to the old filenames
|
|
@@ -81,22 +124,35 @@ export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFile
|
|
|
81
124
|
uiLogger.log('');
|
|
82
125
|
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.header);
|
|
83
126
|
for (const hsMetaFile of hsMetaFilePaths) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
try {
|
|
128
|
+
const component = JSON.parse(fs.readFileSync(hsMetaFile).toString());
|
|
129
|
+
const getBaseUid = () => {
|
|
130
|
+
const customUid = coerceToValidUid(`${projectName}_${component.type}`);
|
|
131
|
+
if (customUid) {
|
|
132
|
+
return customUid.replace(/-/g, '_');
|
|
133
|
+
}
|
|
134
|
+
return component.uid;
|
|
135
|
+
};
|
|
136
|
+
let uid = getBaseUid();
|
|
137
|
+
let differentiator = 1;
|
|
138
|
+
while (existingUids.includes(uid)) {
|
|
139
|
+
differentiator++;
|
|
140
|
+
uid = `${getBaseUid()}_${differentiator}`;
|
|
141
|
+
}
|
|
142
|
+
component.uid = uid;
|
|
143
|
+
if (component.type === AppKey && component.config) {
|
|
144
|
+
component.config.name = `${projectName}-Application`;
|
|
145
|
+
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
|
|
149
|
+
}
|
|
150
|
+
fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
|
|
90
151
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
|
|
152
|
+
catch (error) {
|
|
153
|
+
debugError(error);
|
|
154
|
+
uiLogger.error(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.failedToUpdate(hsMetaFile));
|
|
98
155
|
}
|
|
99
|
-
fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
|
|
100
156
|
}
|
|
101
157
|
uiLogger.log('');
|
|
102
158
|
}
|