@hubspot/cli 8.0.10-experimental.2 → 8.0.10-experimental.4
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/api/migrate.js +5 -1
- package/commands/app/migrate.js +2 -2
- package/commands/cms/__tests__/upload.test.js +4 -0
- package/commands/cms/theme/preview.js +9 -64
- package/commands/project/__tests__/migrate.test.js +2 -2
- package/commands/project/migrate.js +6 -6
- package/lang/en.d.ts +12 -2
- package/lang/en.js +12 -2
- package/lib/__tests__/serverlessLogs.test.js +10 -1
- package/lib/theme/__tests__/migrate.test.js +6 -11
- package/lib/theme/cmsDevServerProcess.d.ts +12 -0
- package/lib/theme/cmsDevServerProcess.js +148 -0
- package/lib/theme/cmsDevServerRunner.d.ts +14 -0
- package/lib/theme/cmsDevServerRunner.js +90 -0
- package/lib/theme/migrate.d.ts +1 -1
- package/lib/theme/migrate.js +1 -5
- package/lib/usageTracking.js +8 -5
- package/package.json +3 -4
package/api/migrate.js
CHANGED
|
@@ -23,7 +23,11 @@ function mapPlatformVersionToEnum(platformVersion) {
|
|
|
23
23
|
if (platformVersion === PLATFORM_VERSIONS.unstable) {
|
|
24
24
|
return PLATFORM_VERSIONS.unstable.toUpperCase();
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const reformattedPlatformVersion = platformVersion
|
|
27
|
+
.replace('.', '_')
|
|
28
|
+
.replace('-', '_')
|
|
29
|
+
.toUpperCase();
|
|
30
|
+
return `V${reformattedPlatformVersion}`;
|
|
27
31
|
}
|
|
28
32
|
export async function initializeAppMigration(accountId, applicationId, platformVersion) {
|
|
29
33
|
return http.post(accountId, {
|
package/commands/app/migrate.js
CHANGED
|
@@ -8,7 +8,7 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
|
|
|
8
8
|
import { migrateApp } from '../../lib/app/migrate.js';
|
|
9
9
|
import { getIsInProject } from '../../lib/projects/config.js';
|
|
10
10
|
import { makeYargsBuilder } from '../../lib/yargsUtils.js';
|
|
11
|
-
const { v2025_2 } = PLATFORM_VERSIONS;
|
|
11
|
+
const { v2025_2, v2026_03_beta } = PLATFORM_VERSIONS;
|
|
12
12
|
const command = 'migrate';
|
|
13
13
|
const describe = commands.project.migrateApp.describe;
|
|
14
14
|
export function handlerGenerator(commandTrackingName) {
|
|
@@ -67,7 +67,7 @@ function appMigrateBuilder(yargs) {
|
|
|
67
67
|
},
|
|
68
68
|
'platform-version': {
|
|
69
69
|
type: 'string',
|
|
70
|
-
choices: [v2025_2],
|
|
70
|
+
choices: [v2025_2, v2026_03_beta],
|
|
71
71
|
default: v2025_2,
|
|
72
72
|
},
|
|
73
73
|
unstable: {
|
|
@@ -8,6 +8,7 @@ import * as modulesLib from '@hubspot/local-dev-lib/cms/modules';
|
|
|
8
8
|
import * as ignoreRulesLib from '@hubspot/local-dev-lib/ignoreRules';
|
|
9
9
|
import * as themesLib from '@hubspot/local-dev-lib/cms/themes';
|
|
10
10
|
import * as configLib from '@hubspot/local-dev-lib/config';
|
|
11
|
+
import * as handleFieldsJSLib from '@hubspot/local-dev-lib/cms/handleFieldsJS';
|
|
11
12
|
import { uiLogger } from '../../../lib/ui/logger.js';
|
|
12
13
|
import * as errorHandlers from '../../../lib/errorHandlers/index.js';
|
|
13
14
|
import * as commonOpts from '../../../lib/commonOpts.js';
|
|
@@ -26,6 +27,7 @@ vi.mock('@hubspot/local-dev-lib/cms/modules');
|
|
|
26
27
|
vi.mock('@hubspot/local-dev-lib/ignoreRules');
|
|
27
28
|
vi.mock('@hubspot/local-dev-lib/cms/themes');
|
|
28
29
|
vi.mock('@hubspot/local-dev-lib/config');
|
|
30
|
+
vi.mock('@hubspot/local-dev-lib/cms/handleFieldsJS');
|
|
29
31
|
vi.mock('../../../lib/errorHandlers/index.js');
|
|
30
32
|
vi.mock('../../../lib/commonOpts.js');
|
|
31
33
|
vi.mock('../../../lib/prompts/uploadPrompt.js');
|
|
@@ -53,6 +55,7 @@ const hasUploadErrorsSpy = vi.spyOn(uploadFolderLib, 'hasUploadErrors');
|
|
|
53
55
|
const processExitSpy = vi.spyOn(process, 'exit');
|
|
54
56
|
const logErrorSpy = vi.spyOn(errorHandlers, 'logError');
|
|
55
57
|
const getConfigAccountIfExistsSpy = vi.spyOn(configLib, 'getConfigAccountIfExists');
|
|
58
|
+
const isConvertableFieldJsSpy = vi.spyOn(handleFieldsJSLib, 'isConvertableFieldJs');
|
|
56
59
|
describe('commands/cms/upload', () => {
|
|
57
60
|
beforeEach(() => {
|
|
58
61
|
// @ts-expect-error Mock implementation
|
|
@@ -67,6 +70,7 @@ describe('commands/cms/upload', () => {
|
|
|
67
70
|
getThemePreviewUrlSpy.mockReturnValue(undefined);
|
|
68
71
|
// Mock config to prevent reading actual config file in CI
|
|
69
72
|
getConfigAccountIfExistsSpy.mockReturnValue(undefined);
|
|
73
|
+
isConvertableFieldJsSpy.mockReturnValue(false);
|
|
70
74
|
});
|
|
71
75
|
describe('command', () => {
|
|
72
76
|
it('should have the correct command structure', () => {
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import cliProgress from 'cli-progress';
|
|
4
3
|
import { commands } from '../../../lang/en.js';
|
|
5
4
|
import { getCwd } from '@hubspot/local-dev-lib/path';
|
|
6
|
-
import { FILE_UPLOAD_RESULT_TYPES } from '@hubspot/local-dev-lib/constants/files';
|
|
7
5
|
import { getThemeJSONPath } from '@hubspot/local-dev-lib/cms/themes';
|
|
8
|
-
import {
|
|
9
|
-
import { getUploadableFileList } from '../../../lib/upload.js';
|
|
6
|
+
import { spawnDevServer } from '../../../lib/theme/cmsDevServerProcess.js';
|
|
10
7
|
import { trackCommandUsage } from '../../../lib/usageTracking.js';
|
|
11
8
|
import { previewPrompt, previewProjectPrompt, } from '../../../lib/prompts/previewPrompt.js';
|
|
12
9
|
import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
|
|
13
|
-
import { ApiErrorContext, logError } from '../../../lib/errorHandlers/index.js';
|
|
14
10
|
import { getProjectConfig } from '../../../lib/projects/config.js';
|
|
15
11
|
import { findProjectComponents } from '../../../lib/projects/structure.js';
|
|
16
12
|
import { ComponentTypes } from '../../../types/Projects.js';
|
|
@@ -73,67 +69,16 @@ async function determineSrcAndDest(args) {
|
|
|
73
69
|
async function handler(args) {
|
|
74
70
|
const { derivedAccountId, noSsl, resetSession, port, generateFieldsTypes } = args;
|
|
75
71
|
const { absoluteSrc, dest } = await determineSrcAndDest(args);
|
|
76
|
-
const filePaths = await getUploadableFileList(absoluteSrc, false);
|
|
77
|
-
function startProgressBar(numFiles) {
|
|
78
|
-
const initialUploadProgressBar = new cliProgress.SingleBar({
|
|
79
|
-
gracefulExit: true,
|
|
80
|
-
format: '[{bar}] {percentage}% | {value}/{total} | {label}',
|
|
81
|
-
hideCursor: true,
|
|
82
|
-
}, cliProgress.Presets.rect);
|
|
83
|
-
initialUploadProgressBar.start(numFiles, 0, {
|
|
84
|
-
label: commands.cms.subcommands.theme.subcommands.preview
|
|
85
|
-
.initialUploadProgressBar.start,
|
|
86
|
-
});
|
|
87
|
-
let uploadsHaveStarted = false;
|
|
88
|
-
const uploadOptions = {
|
|
89
|
-
onAttemptCallback: () => {
|
|
90
|
-
/* Intentionally blank */
|
|
91
|
-
},
|
|
92
|
-
onSuccessCallback: () => {
|
|
93
|
-
initialUploadProgressBar.increment();
|
|
94
|
-
if (!uploadsHaveStarted) {
|
|
95
|
-
uploadsHaveStarted = true;
|
|
96
|
-
initialUploadProgressBar.update(0, {
|
|
97
|
-
label: commands.cms.subcommands.theme.subcommands.preview
|
|
98
|
-
.initialUploadProgressBar.uploading,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
onFirstErrorCallback: () => {
|
|
103
|
-
/* Intentionally blank */
|
|
104
|
-
},
|
|
105
|
-
onRetryCallback: () => {
|
|
106
|
-
/* Intentionally blank */
|
|
107
|
-
},
|
|
108
|
-
onFinalErrorCallback: () => initialUploadProgressBar.increment(),
|
|
109
|
-
onFinishCallback: (results) => {
|
|
110
|
-
initialUploadProgressBar.update(numFiles, {
|
|
111
|
-
label: commands.cms.subcommands.theme.subcommands.preview
|
|
112
|
-
.initialUploadProgressBar.finish,
|
|
113
|
-
});
|
|
114
|
-
initialUploadProgressBar.stop();
|
|
115
|
-
results.forEach(result => {
|
|
116
|
-
if (result.resultType == FILE_UPLOAD_RESULT_TYPES.FAILURE) {
|
|
117
|
-
uiLogger.error(commands.cms.subcommands.theme.subcommands.preview.errors.uploadFailed(result.file, dest));
|
|
118
|
-
logError(result.error, new ApiErrorContext({
|
|
119
|
-
accountId: derivedAccountId,
|
|
120
|
-
request: dest,
|
|
121
|
-
payload: result.file,
|
|
122
|
-
}));
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
return uploadOptions;
|
|
128
|
-
}
|
|
129
72
|
trackCommandUsage('preview', {}, derivedAccountId);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
73
|
+
// Spawn dev server in isolated subprocess to avoid React version conflicts
|
|
74
|
+
// File listing and progress bars are handled within the subprocess
|
|
75
|
+
await spawnDevServer({
|
|
76
|
+
absoluteSrc,
|
|
77
|
+
accountName: derivedAccountId?.toString(),
|
|
78
|
+
noSsl,
|
|
79
|
+
port,
|
|
80
|
+
generateFieldsTypes,
|
|
135
81
|
resetSession: resetSession || false,
|
|
136
|
-
startProgressBar,
|
|
137
82
|
dest,
|
|
138
83
|
});
|
|
139
84
|
}
|
|
@@ -9,7 +9,7 @@ import { uiBetaTag, uiCommandReference } from '../../../lib/ui/index.js';
|
|
|
9
9
|
vi.mock('../../../lib/app/migrate');
|
|
10
10
|
vi.mock('../../../lib/projects/config');
|
|
11
11
|
vi.mock('../../../lib/ui');
|
|
12
|
-
const { v2025_2 } = PLATFORM_VERSIONS;
|
|
12
|
+
const { v2025_2, v2026_03_beta, v2026_03 } = PLATFORM_VERSIONS;
|
|
13
13
|
describe('commands/project/migrate', () => {
|
|
14
14
|
const yargsMock = yargs;
|
|
15
15
|
const optionsSpy = vi.spyOn(yargsMock, 'option').mockReturnValue(yargsMock);
|
|
@@ -48,7 +48,7 @@ describe('commands/project/migrate', () => {
|
|
|
48
48
|
migrateCommand.builder(yargsMock);
|
|
49
49
|
expect(optionsSpy).toHaveBeenCalledWith('platform-version', {
|
|
50
50
|
type: 'string',
|
|
51
|
-
choices: [v2025_2],
|
|
51
|
+
choices: [v2025_2, v2026_03_beta, v2026_03],
|
|
52
52
|
default: v2025_2,
|
|
53
53
|
});
|
|
54
54
|
expect(optionsSpy).toHaveBeenCalledWith('unstable', {
|
|
@@ -9,11 +9,11 @@ import { commands, lib } from '../../lang/en.js';
|
|
|
9
9
|
import { uiLogger } from '../../lib/ui/logger.js';
|
|
10
10
|
import { renderInline } from '../../ui/render.js';
|
|
11
11
|
import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
12
|
-
import { getHasMigratableThemes,
|
|
12
|
+
import { getHasMigratableThemes, migrateThemesV2, } from '../../lib/theme/migrate.js';
|
|
13
13
|
import { hasFeature } from '../../lib/hasFeature.js';
|
|
14
14
|
import { FEATURES } from '../../lib/constants.js';
|
|
15
15
|
import { trackCommandMetadataUsage, trackCommandUsage, } from '../../lib/usageTracking.js';
|
|
16
|
-
const { v2025_2 } = PLATFORM_VERSIONS;
|
|
16
|
+
const { v2025_2, v2026_03_beta, v2026_03 } = PLATFORM_VERSIONS;
|
|
17
17
|
const command = 'migrate';
|
|
18
18
|
const describe = commands.project.migrate.describe;
|
|
19
19
|
async function handler(args) {
|
|
@@ -26,8 +26,8 @@ async function handler(args) {
|
|
|
26
26
|
}
|
|
27
27
|
if (projectConfig?.projectConfig) {
|
|
28
28
|
await renderInline(getWarningBox({
|
|
29
|
-
title: lib.migrate.projectMigrationWarningTitle,
|
|
30
|
-
message: lib.migrate.projectMigrationWarning,
|
|
29
|
+
title: lib.migrate.projectMigrationWarningTitle(platformVersion),
|
|
30
|
+
message: lib.migrate.projectMigrationWarning(platformVersion),
|
|
31
31
|
}));
|
|
32
32
|
}
|
|
33
33
|
try {
|
|
@@ -38,7 +38,7 @@ async function handler(args) {
|
|
|
38
38
|
uiLogger.error(commands.project.migrate.errors.noThemeMigrationAccess(derivedAccountId));
|
|
39
39
|
return process.exit(EXIT_CODES.ERROR);
|
|
40
40
|
}
|
|
41
|
-
await
|
|
41
|
+
await migrateThemesV2(derivedAccountId, {
|
|
42
42
|
...args,
|
|
43
43
|
platformVersion: unstable
|
|
44
44
|
? PLATFORM_VERSIONS.unstable
|
|
@@ -67,7 +67,7 @@ function projectMigrateBuilder(yargs) {
|
|
|
67
67
|
yargs
|
|
68
68
|
.option('platform-version', {
|
|
69
69
|
type: 'string',
|
|
70
|
-
choices: [v2025_2],
|
|
70
|
+
choices: [v2025_2, v2026_03_beta, v2026_03],
|
|
71
71
|
default: v2025_2,
|
|
72
72
|
})
|
|
73
73
|
.option('unstable', {
|
package/lang/en.d.ts
CHANGED
|
@@ -3919,8 +3919,8 @@ export declare const lib: {
|
|
|
3919
3919
|
componentsToBeMigrated: (components: string) => string;
|
|
3920
3920
|
componentsThatWillNotBeMigrated: (components: string) => string;
|
|
3921
3921
|
sourceContentsMoved: (newLocation: string) => string;
|
|
3922
|
-
projectMigrationWarningTitle: string;
|
|
3923
|
-
projectMigrationWarning: string;
|
|
3922
|
+
projectMigrationWarningTitle: (platformVersion: string) => string;
|
|
3923
|
+
projectMigrationWarning: (platformVersion: string) => string;
|
|
3924
3924
|
exitWithoutMigrating: string;
|
|
3925
3925
|
success: {
|
|
3926
3926
|
downloadedProject: (projectName: string, projectDest: string) => string;
|
|
@@ -3977,4 +3977,14 @@ export declare const lib: {
|
|
|
3977
3977
|
copyingProjectFilesFailed: string;
|
|
3978
3978
|
};
|
|
3979
3979
|
};
|
|
3980
|
+
theme: {
|
|
3981
|
+
cmsDevServerProcess: {
|
|
3982
|
+
installStarted: (targetVersion: string) => string;
|
|
3983
|
+
installSucceeded: string;
|
|
3984
|
+
installFailed: string;
|
|
3985
|
+
serverStartError: (error: Error) => string;
|
|
3986
|
+
serverExit: (code: number) => string;
|
|
3987
|
+
serverKill: (signal: NodeJS.Signals) => string;
|
|
3988
|
+
};
|
|
3989
|
+
};
|
|
3980
3990
|
};
|
package/lang/en.js
CHANGED
|
@@ -3942,8 +3942,8 @@ export const lib = {
|
|
|
3942
3942
|
componentsToBeMigrated: (components) => `The following features will be migrated: ${components}`,
|
|
3943
3943
|
componentsThatWillNotBeMigrated: (components) => `[NOTE] These features are not yet supported for migration but will be available later: ${components}`,
|
|
3944
3944
|
sourceContentsMoved: (newLocation) => `The contents of your old source directory have been moved to ${newLocation}, move any required files to the new source directory.`,
|
|
3945
|
-
projectMigrationWarningTitle:
|
|
3946
|
-
projectMigrationWarning: uiBetaTag(`Running the ${uiCommandReference('hs project migrate')} command will permanently upgrade your project to platformVersion
|
|
3945
|
+
projectMigrationWarningTitle: (platformVersion) => `Important: Migrating to platformVersion ${platformVersion} is irreversible`,
|
|
3946
|
+
projectMigrationWarning: (platformVersion) => uiBetaTag(`Running the ${uiCommandReference('hs project migrate')} command will permanently upgrade your project to platformVersion ${platformVersion}. This action cannot be undone. To ensure you have access to your original files, they will be copied to a new directory (archive) for safekeeping.\n\nThis command will guide you through the process, prompting you to enter the required fields and will download the new project source code into your project source directory.`, false),
|
|
3947
3947
|
exitWithoutMigrating: 'Exiting without migrating',
|
|
3948
3948
|
success: {
|
|
3949
3949
|
downloadedProject: (projectName, projectDest) => `Saved ${projectName} to ${projectDest}`,
|
|
@@ -4000,4 +4000,14 @@ export const lib = {
|
|
|
4000
4000
|
copyingProjectFilesFailed: 'Unable to copy migrated project files',
|
|
4001
4001
|
},
|
|
4002
4002
|
},
|
|
4003
|
+
theme: {
|
|
4004
|
+
cmsDevServerProcess: {
|
|
4005
|
+
installStarted: (targetVersion) => `Installing cms-dev-server ${targetVersion}...`,
|
|
4006
|
+
installSucceeded: 'cms-dev-server setup complete',
|
|
4007
|
+
installFailed: 'Failed to install cms-dev-server',
|
|
4008
|
+
serverStartError: (error) => `Failed to start dev server: ${error}`,
|
|
4009
|
+
serverExit: (code) => `Dev server exited with code ${code}`,
|
|
4010
|
+
serverKill: (signal) => `Dev server killed with signal ${signal}`,
|
|
4011
|
+
},
|
|
4012
|
+
},
|
|
4003
4013
|
};
|
|
@@ -41,6 +41,7 @@ describe('lib/serverlessLogs', () => {
|
|
|
41
41
|
status: 200,
|
|
42
42
|
statusText: 'OK',
|
|
43
43
|
headers: {},
|
|
44
|
+
// eslint-disable-next-line
|
|
44
45
|
config: { headers: {} },
|
|
45
46
|
}));
|
|
46
47
|
const tailCall = vi.fn(() => Promise.resolve({
|
|
@@ -55,6 +56,7 @@ describe('lib/serverlessLogs', () => {
|
|
|
55
56
|
status: 200,
|
|
56
57
|
statusText: 'OK',
|
|
57
58
|
headers: {},
|
|
59
|
+
// eslint-disable-next-line
|
|
58
60
|
config: { headers: {} },
|
|
59
61
|
}));
|
|
60
62
|
// @ts-ignore - headers is not used in the actual function and does not need to be mocked
|
|
@@ -74,7 +76,12 @@ describe('lib/serverlessLogs', () => {
|
|
|
74
76
|
id: '1234',
|
|
75
77
|
executionTime: 510,
|
|
76
78
|
log: 'Log message',
|
|
77
|
-
error: {
|
|
79
|
+
error: {
|
|
80
|
+
message: '',
|
|
81
|
+
type: '',
|
|
82
|
+
stackTrace: [],
|
|
83
|
+
statusCode: null,
|
|
84
|
+
},
|
|
78
85
|
status: 'SUCCESS',
|
|
79
86
|
createdAt: 1620232011451,
|
|
80
87
|
memory: '70/128 MB',
|
|
@@ -83,6 +90,7 @@ describe('lib/serverlessLogs', () => {
|
|
|
83
90
|
status: 200,
|
|
84
91
|
statusText: 'OK',
|
|
85
92
|
headers: {},
|
|
93
|
+
// eslint-disable-next-line
|
|
86
94
|
config: { headers: {} },
|
|
87
95
|
}));
|
|
88
96
|
const latestLogResponse = {
|
|
@@ -119,6 +127,7 @@ describe('lib/serverlessLogs', () => {
|
|
|
119
127
|
status: 200,
|
|
120
128
|
statusText: 'OK',
|
|
121
129
|
headers: {},
|
|
130
|
+
// eslint-disable-next-line
|
|
122
131
|
config: { headers: {} },
|
|
123
132
|
}));
|
|
124
133
|
// @ts-ignore - headers is not used in the actual function and does not need to be mocked
|
|
@@ -5,7 +5,7 @@ import { writeProjectConfig, } from '../../projects/config.js';
|
|
|
5
5
|
import { ensureProjectExists } from '../../projects/ensureProjectExists.js';
|
|
6
6
|
import { isV2Project } from '../../projects/platformVersion.js';
|
|
7
7
|
import { fetchMigrationApps } from '../../app/migrate.js';
|
|
8
|
-
import { getHasMigratableThemes, validateMigrationAppsAndThemes, handleThemesMigration,
|
|
8
|
+
import { getHasMigratableThemes, validateMigrationAppsAndThemes, handleThemesMigration, migrateThemesV2, } from '../migrate.js';
|
|
9
9
|
import { lib } from '../../../lang/en.js';
|
|
10
10
|
vi.mock('@hubspot/project-parsing-lib/themes');
|
|
11
11
|
vi.mock('../../prompts/promptUtils');
|
|
@@ -144,11 +144,6 @@ describe('lib/theme/migrate', () => {
|
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
describe('validateMigrationAppsAndThemes', () => {
|
|
147
|
-
it('should throw an error when themes are already migrated (v2 API)', async () => {
|
|
148
|
-
mockedUseV2Api.mockReturnValue(true);
|
|
149
|
-
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
150
|
-
await expect(validateMigrationAppsAndThemes(0, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAlreadyMigrated);
|
|
151
|
-
});
|
|
152
147
|
it('should throw an error when apps and themes are both present', async () => {
|
|
153
148
|
const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
|
|
154
149
|
await expect(validateMigrationAppsAndThemes(1, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAndAppsNotAllowed);
|
|
@@ -230,25 +225,25 @@ describe('lib/theme/migrate', () => {
|
|
|
230
225
|
projectConfig: undefined,
|
|
231
226
|
projectDir: MOCK_PROJECT_DIR,
|
|
232
227
|
};
|
|
233
|
-
await expect(
|
|
228
|
+
await expect(migrateThemesV2(ACCOUNT_ID, options, themeCount, invalidProjectConfig)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
|
|
234
229
|
});
|
|
235
230
|
it('should throw an error when project does not exist', async () => {
|
|
236
231
|
mockedEnsureProjectExists.mockResolvedValue({ projectExists: false });
|
|
237
|
-
await expect(
|
|
232
|
+
await expect(migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig)).rejects.toThrow(lib.migrate.errors.project.doesNotExist(ACCOUNT_ID));
|
|
238
233
|
});
|
|
239
234
|
it('should proceed with migration when user confirms', async () => {
|
|
240
|
-
await
|
|
235
|
+
await migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig);
|
|
241
236
|
expect(mockedFetchMigrationApps).toHaveBeenCalledWith(ACCOUNT_ID, PLATFORM_VERSION, { projectConfig });
|
|
242
237
|
expect(mockedConfirmPrompt).toHaveBeenCalledWith(lib.migrate.prompt.proceed, { defaultAnswer: false });
|
|
243
238
|
expect(mockedMigrateThemes).toHaveBeenCalledWith(MOCK_PROJECT_DIR, `${MOCK_PROJECT_DIR}/src`);
|
|
244
239
|
});
|
|
245
240
|
it('should exit without migrating when user cancels', async () => {
|
|
246
241
|
mockedConfirmPrompt.mockResolvedValue(false);
|
|
247
|
-
await
|
|
242
|
+
await migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig);
|
|
248
243
|
expect(mockedMigrateThemes).not.toHaveBeenCalled();
|
|
249
244
|
});
|
|
250
245
|
it('should validate migration apps and themes', async () => {
|
|
251
|
-
await
|
|
246
|
+
await migrateThemesV2(ACCOUNT_ID, options, themeCount, projectConfig);
|
|
252
247
|
// The validation is called internally, so we verify it through the error handling
|
|
253
248
|
expect(mockedFetchMigrationApps).toHaveBeenCalled();
|
|
254
249
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
interface DevServerOptions {
|
|
3
|
+
absoluteSrc: string;
|
|
4
|
+
accountName?: string;
|
|
5
|
+
noSsl?: boolean;
|
|
6
|
+
port?: number;
|
|
7
|
+
generateFieldsTypes?: boolean;
|
|
8
|
+
resetSession?: boolean;
|
|
9
|
+
dest: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function spawnDevServer(options: DevServerOptions): Promise<ChildProcess>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { getConfigFilePath } from '@hubspot/local-dev-lib/config';
|
|
7
|
+
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
8
|
+
import { lib } from '../../lang/en.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
// cms-dev-server version to install to isolated cache
|
|
12
|
+
const TARGET_CMS_DEV_SERVER_VERSION = '1.2.16';
|
|
13
|
+
/**
|
|
14
|
+
* Ensures cms-dev-server is installed in an isolated cache directory.
|
|
15
|
+
* This prevents React version conflicts with the CLI.
|
|
16
|
+
*/
|
|
17
|
+
async function ensureCmsDevServerCache(targetVersion) {
|
|
18
|
+
const cacheDir = path.join(os.homedir(), '.hscli', '.module-cache');
|
|
19
|
+
const packageJsonPath = path.join(cacheDir, 'node_modules', '@hubspot', 'cms-dev-server', 'package.json');
|
|
20
|
+
// Check if already installed with correct version
|
|
21
|
+
let needsInstall = true;
|
|
22
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
23
|
+
try {
|
|
24
|
+
const installedPackage = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
25
|
+
if (installedPackage.version === targetVersion) {
|
|
26
|
+
needsInstall = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
// If we can't read the package.json, reinstall
|
|
31
|
+
needsInstall = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (needsInstall) {
|
|
35
|
+
// Show spinner during install (can take 10-30 seconds)
|
|
36
|
+
SpinniesManager.init({
|
|
37
|
+
succeedColor: 'white',
|
|
38
|
+
});
|
|
39
|
+
SpinniesManager.add('cms-dev-server-install', {
|
|
40
|
+
text: lib.theme.cmsDevServerProcess.installStarted(targetVersion),
|
|
41
|
+
});
|
|
42
|
+
// Create cache directory
|
|
43
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
44
|
+
// Clear old installation if exists
|
|
45
|
+
const nodeModulesDir = path.join(cacheDir, 'node_modules');
|
|
46
|
+
if (fs.existsSync(nodeModulesDir)) {
|
|
47
|
+
fs.rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
// Install cms-dev-server with production dependencies only (async to allow spinner)
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
const installProcess = spawn('npm', [
|
|
52
|
+
'install',
|
|
53
|
+
`@hubspot/cms-dev-server@${targetVersion}`,
|
|
54
|
+
'--production',
|
|
55
|
+
'--no-save',
|
|
56
|
+
'--loglevel=error',
|
|
57
|
+
], {
|
|
58
|
+
cwd: cacheDir,
|
|
59
|
+
stdio: 'ignore', // Suppress npm output
|
|
60
|
+
});
|
|
61
|
+
installProcess.on('close', code => {
|
|
62
|
+
if (code === 0) {
|
|
63
|
+
SpinniesManager.succeed('cms-dev-server-install', {
|
|
64
|
+
text: lib.theme.cmsDevServerProcess.installSucceeded,
|
|
65
|
+
});
|
|
66
|
+
resolve();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
SpinniesManager.fail('cms-dev-server-install', {
|
|
70
|
+
text: lib.theme.cmsDevServerProcess.installFailed,
|
|
71
|
+
});
|
|
72
|
+
reject(new Error(lib.theme.cmsDevServerProcess.installFailed));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
installProcess.on('error', error => {
|
|
76
|
+
SpinniesManager.fail('cms-dev-server-install', {
|
|
77
|
+
text: lib.theme.cmsDevServerProcess.installFailed,
|
|
78
|
+
});
|
|
79
|
+
reject(error);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return cacheDir;
|
|
84
|
+
}
|
|
85
|
+
export async function spawnDevServer(options) {
|
|
86
|
+
const { absoluteSrc, accountName, noSsl, port, generateFieldsTypes, resetSession, dest, } = options;
|
|
87
|
+
// Ensure cms-dev-server is installed in isolated cache
|
|
88
|
+
const cacheDir = await ensureCmsDevServerCache(TARGET_CMS_DEV_SERVER_VERSION);
|
|
89
|
+
// Get config path to pass to createDevServer
|
|
90
|
+
let configPath = '';
|
|
91
|
+
try {
|
|
92
|
+
configPath = process.env.HUBSPOT_CONFIG_PATH || getConfigFilePath();
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Config file doesn't exist - cms-dev-server will handle this gracefully
|
|
96
|
+
}
|
|
97
|
+
// Copy the runner script to the cache directory so imports resolve from there
|
|
98
|
+
// This is critical: Node resolves ES module imports relative to the script location,
|
|
99
|
+
// not the cwd. By copying the script to the cache directory, imports will resolve
|
|
100
|
+
// from the cache's node_modules (React 18) instead of the CLI's node_modules (React 19)
|
|
101
|
+
const sourceRunnerPath = path.join(__dirname, 'cmsDevServerRunner.js');
|
|
102
|
+
const targetRunnerPath = path.join(cacheDir, 'cmsPreviewRunner.js');
|
|
103
|
+
fs.copyFileSync(sourceRunnerPath, targetRunnerPath);
|
|
104
|
+
// Set environment variables to pass configuration to the runner script
|
|
105
|
+
const env = { ...process.env };
|
|
106
|
+
env.CMS_DEV_SERVER_SRC = absoluteSrc;
|
|
107
|
+
env.CMS_DEV_SERVER_DEST = dest;
|
|
108
|
+
env.CMS_DEV_SERVER_CONFIG = configPath;
|
|
109
|
+
env.CMS_DEV_SERVER_ACCOUNT = accountName || '';
|
|
110
|
+
env.CMS_DEV_SERVER_SSL = (!noSsl).toString();
|
|
111
|
+
env.CMS_DEV_SERVER_FIELD_GEN = Boolean(generateFieldsTypes).toString();
|
|
112
|
+
env.CMS_DEV_SERVER_RESET_SESSION = Boolean(resetSession).toString();
|
|
113
|
+
if (port) {
|
|
114
|
+
env.PORT = port.toString();
|
|
115
|
+
}
|
|
116
|
+
// Suppress Node.js deprecation warnings
|
|
117
|
+
env.NODE_NO_WARNINGS = '1';
|
|
118
|
+
// Spawn Node with the runner script from the isolated cache directory
|
|
119
|
+
// This ensures complete isolation from CLI's React 19
|
|
120
|
+
const devServer = spawn('node', [targetRunnerPath], {
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
env,
|
|
123
|
+
cwd: cacheDir,
|
|
124
|
+
});
|
|
125
|
+
// Handle process events
|
|
126
|
+
devServer.on('error', error => {
|
|
127
|
+
console.error(lib.theme.cmsDevServerProcess.serverStartError(error));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
devServer.on('exit', (code, signal) => {
|
|
131
|
+
if (code !== 0 && code !== null) {
|
|
132
|
+
console.error(lib.theme.cmsDevServerProcess.serverExit(code));
|
|
133
|
+
process.exit(code);
|
|
134
|
+
}
|
|
135
|
+
if (signal) {
|
|
136
|
+
console.error(lib.theme.cmsDevServerProcess.serverKill(signal));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Handle CLI termination
|
|
141
|
+
process.once('SIGINT', () => {
|
|
142
|
+
devServer.kill('SIGINT');
|
|
143
|
+
});
|
|
144
|
+
process.once('SIGTERM', () => {
|
|
145
|
+
devServer.kill('SIGTERM');
|
|
146
|
+
});
|
|
147
|
+
return devServer;
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This script runs in an isolated cache directory with cms-dev-server installed.
|
|
3
|
+
* It is spawned as a separate process to avoid React version conflicts with the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Arguments are passed via environment variables:
|
|
6
|
+
* - CMS_DEV_SERVER_SRC: Source directory path
|
|
7
|
+
* - CMS_DEV_SERVER_DEST: Destination path
|
|
8
|
+
* - CMS_DEV_SERVER_CONFIG: Config file path (optional)
|
|
9
|
+
* - CMS_DEV_SERVER_ACCOUNT: Account name (optional)
|
|
10
|
+
* - CMS_DEV_SERVER_SSL: 'true' or 'false'
|
|
11
|
+
* - CMS_DEV_SERVER_FIELD_GEN: 'true' or 'false'
|
|
12
|
+
* - CMS_DEV_SERVER_RESET_SESSION: 'true' or 'false'
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* This script runs in an isolated cache directory with cms-dev-server installed.
|
|
4
|
+
* It is spawned as a separate process to avoid React version conflicts with the CLI.
|
|
5
|
+
*
|
|
6
|
+
* Arguments are passed via environment variables:
|
|
7
|
+
* - CMS_DEV_SERVER_SRC: Source directory path
|
|
8
|
+
* - CMS_DEV_SERVER_DEST: Destination path
|
|
9
|
+
* - CMS_DEV_SERVER_CONFIG: Config file path (optional)
|
|
10
|
+
* - CMS_DEV_SERVER_ACCOUNT: Account name (optional)
|
|
11
|
+
* - CMS_DEV_SERVER_SSL: 'true' or 'false'
|
|
12
|
+
* - CMS_DEV_SERVER_FIELD_GEN: 'true' or 'false'
|
|
13
|
+
* - CMS_DEV_SERVER_RESET_SESSION: 'true' or 'false'
|
|
14
|
+
*/
|
|
15
|
+
// Suppress library deprecation warnings (e.g., body-parser)
|
|
16
|
+
process.noDeprecation = true;
|
|
17
|
+
// Dynamic imports to use the isolated cms-dev-server installation
|
|
18
|
+
const { createDevServer } = await import('@hubspot/cms-dev-server');
|
|
19
|
+
const { walk } = await import('@hubspot/local-dev-lib/fs');
|
|
20
|
+
const { createIgnoreFilter } = await import('@hubspot/local-dev-lib/ignoreRules');
|
|
21
|
+
const { isAllowedExtension } = await import('@hubspot/local-dev-lib/path');
|
|
22
|
+
const { FILE_UPLOAD_RESULT_TYPES } = await import('@hubspot/local-dev-lib/constants/files');
|
|
23
|
+
const cliProgress = (await import('cli-progress')).default;
|
|
24
|
+
// Read configuration from environment variables
|
|
25
|
+
const src = process.env.CMS_DEV_SERVER_SRC;
|
|
26
|
+
const dest = process.env.CMS_DEV_SERVER_DEST;
|
|
27
|
+
const configPath = process.env.CMS_DEV_SERVER_CONFIG || '';
|
|
28
|
+
const accountName = process.env.CMS_DEV_SERVER_ACCOUNT || '';
|
|
29
|
+
const sslEnabled = process.env.CMS_DEV_SERVER_SSL === 'true';
|
|
30
|
+
const fieldGenEnabled = process.env.CMS_DEV_SERVER_FIELD_GEN === 'true';
|
|
31
|
+
const resetSession = process.env.CMS_DEV_SERVER_RESET_SESSION === 'true';
|
|
32
|
+
// Get uploadable files for preview
|
|
33
|
+
let filePaths = [];
|
|
34
|
+
try {
|
|
35
|
+
filePaths = await walk(src);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.error('Error walking directory:', e);
|
|
39
|
+
}
|
|
40
|
+
filePaths = filePaths
|
|
41
|
+
.filter(file => isAllowedExtension(file))
|
|
42
|
+
.filter(createIgnoreFilter(false));
|
|
43
|
+
// Create progress bar for initial upload
|
|
44
|
+
function startProgressBar(numFiles) {
|
|
45
|
+
const initialUploadProgressBar = new cliProgress.SingleBar({
|
|
46
|
+
gracefulExit: true,
|
|
47
|
+
format: '[{bar}] {percentage}% | {value}/{total} | {label}',
|
|
48
|
+
hideCursor: true,
|
|
49
|
+
}, cliProgress.Presets.rect);
|
|
50
|
+
initialUploadProgressBar.start(numFiles, 0, {
|
|
51
|
+
label: 'Preparing upload...',
|
|
52
|
+
});
|
|
53
|
+
let uploadsHaveStarted = false;
|
|
54
|
+
return {
|
|
55
|
+
onAttemptCallback: () => { },
|
|
56
|
+
onSuccessCallback: () => {
|
|
57
|
+
initialUploadProgressBar.increment();
|
|
58
|
+
if (!uploadsHaveStarted) {
|
|
59
|
+
uploadsHaveStarted = true;
|
|
60
|
+
initialUploadProgressBar.update(0, {
|
|
61
|
+
label: 'Uploading files...',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
onFirstErrorCallback: () => { },
|
|
66
|
+
onRetryCallback: () => { },
|
|
67
|
+
onFinalErrorCallback: () => initialUploadProgressBar.increment(),
|
|
68
|
+
// eslint-disable-next-line
|
|
69
|
+
onFinishCallback: (results) => {
|
|
70
|
+
initialUploadProgressBar.update(numFiles, {
|
|
71
|
+
label: 'Upload complete',
|
|
72
|
+
});
|
|
73
|
+
initialUploadProgressBar.stop();
|
|
74
|
+
results.forEach(result => {
|
|
75
|
+
if (result.resultType === FILE_UPLOAD_RESULT_TYPES.FAILURE) {
|
|
76
|
+
console.error(`Failed to upload ${result.file}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const themePreviewOptions = {
|
|
83
|
+
filePaths,
|
|
84
|
+
startProgressBar,
|
|
85
|
+
resetSession,
|
|
86
|
+
dest,
|
|
87
|
+
};
|
|
88
|
+
createDevServer(src, false, // storybook
|
|
89
|
+
configPath, accountName, sslEnabled, fieldGenEnabled, themePreviewOptions);
|
|
90
|
+
export {};
|
package/lib/theme/migrate.d.ts
CHANGED
|
@@ -10,4 +10,4 @@ export declare function getHasMigratableThemes(projectConfig?: LoadedProjectConf
|
|
|
10
10
|
}>;
|
|
11
11
|
export declare function validateMigrationAppsAndThemes(hasApps: number, projectConfig?: LoadedProjectConfig): Promise<void>;
|
|
12
12
|
export declare function handleThemesMigration(projectConfig: LoadedProjectConfig, platformVersion: string): Promise<void>;
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function migrateThemesV2(derivedAccountId: number, options: ArgumentsCamelCase<MigrateThemesArgs>, themeCount: number, projectConfig: LoadedProjectConfig): Promise<void>;
|
package/lib/theme/migrate.js
CHANGED
|
@@ -7,7 +7,6 @@ import { lib } from '../../lang/en.js';
|
|
|
7
7
|
import { PROJECT_CONFIG_FILE } from '../constants.js';
|
|
8
8
|
import { uiLogger } from '../ui/logger.js';
|
|
9
9
|
import { debugError } from '../errorHandlers/index.js';
|
|
10
|
-
import { isV2Project } from '../projects/platformVersion.js';
|
|
11
10
|
import { confirmPrompt } from '../prompts/promptUtils.js';
|
|
12
11
|
import { fetchMigrationApps } from '../app/migrate.js';
|
|
13
12
|
export async function getHasMigratableThemes(projectConfig) {
|
|
@@ -22,9 +21,6 @@ export async function getHasMigratableThemes(projectConfig) {
|
|
|
22
21
|
};
|
|
23
22
|
}
|
|
24
23
|
export async function validateMigrationAppsAndThemes(hasApps, projectConfig) {
|
|
25
|
-
if (isV2Project(projectConfig?.projectConfig?.platformVersion)) {
|
|
26
|
-
throw new Error(lib.migrate.errors.project.themesAlreadyMigrated);
|
|
27
|
-
}
|
|
28
24
|
if (hasApps > 0 && projectConfig) {
|
|
29
25
|
throw new Error(lib.migrate.errors.project.themesAndAppsNotAllowed);
|
|
30
26
|
}
|
|
@@ -61,7 +57,7 @@ export async function handleThemesMigration(projectConfig, platformVersion) {
|
|
|
61
57
|
uiLogger.log('');
|
|
62
58
|
uiLogger.log(lib.migrate.success.themesMigrationSuccess(platformVersion));
|
|
63
59
|
}
|
|
64
|
-
export async function
|
|
60
|
+
export async function migrateThemesV2(derivedAccountId, options, themeCount, projectConfig) {
|
|
65
61
|
if (!projectConfig?.projectConfig || !projectConfig?.projectDir) {
|
|
66
62
|
throw new Error(lib.migrate.errors.project.invalidConfig);
|
|
67
63
|
}
|
package/lib/usageTracking.js
CHANGED
|
@@ -44,11 +44,14 @@ export async function trackCommandUsage(command, meta = {}, accountId) {
|
|
|
44
44
|
uiLogger.debug(`Attempting to track usage of "${command}" command`);
|
|
45
45
|
let authType = 'unknown';
|
|
46
46
|
if (accountId) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
try {
|
|
48
|
+
const accountConfig = getConfigAccountById(accountId);
|
|
49
|
+
authType =
|
|
50
|
+
accountConfig && accountConfig.authType
|
|
51
|
+
? accountConfig.authType
|
|
52
|
+
: API_KEY_AUTH_METHOD.value;
|
|
53
|
+
}
|
|
54
|
+
catch (e) { }
|
|
52
55
|
}
|
|
53
56
|
return trackCliInteraction({
|
|
54
57
|
action: 'cli-command',
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "8.0.10-experimental.
|
|
3
|
+
"version": "8.0.10-experimental.4",
|
|
4
4
|
"description": "The official CLI for developing on HubSpot",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": "https://github.com/HubSpot/hubspot-cli",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@hubspot/
|
|
10
|
-
"@hubspot/
|
|
11
|
-
"@hubspot/project-parsing-lib": "0.2.0-experimental.2",
|
|
9
|
+
"@hubspot/local-dev-lib": "5.2.0",
|
|
10
|
+
"@hubspot/project-parsing-lib": "0.12.1",
|
|
12
11
|
"@hubspot/serverless-dev-runtime": "7.0.7",
|
|
13
12
|
"@hubspot/ui-extensions-dev-server": "2.0.0",
|
|
14
13
|
"@inquirer/prompts": "7.1.0",
|