@hubspot/cli 7.9.0 → 7.10.0-beta.1
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/__tests__/project.test.js +2 -0
- package/commands/account/__tests__/rename.test.js +35 -0
- package/commands/account/createOverride.js +2 -12
- package/commands/account/removeOverride.js +2 -10
- package/commands/account/rename.d.ts +1 -1
- package/commands/account/rename.js +5 -2
- package/commands/cms/theme/preview.js +1 -4
- package/commands/config/set.js +1 -2
- package/commands/getStarted.js +13 -19
- package/commands/hubdb.d.ts +1 -1
- package/commands/project/__tests__/updateDeps.test.d.ts +1 -0
- package/commands/project/__tests__/updateDeps.test.js +142 -0
- package/commands/project/create.js +0 -1
- package/commands/project/dev/index.js +8 -1
- package/commands/project/listBuilds.js +7 -1
- package/commands/project/updateDeps.d.ts +6 -0
- package/commands/project/updateDeps.js +80 -0
- package/commands/project/upload.js +7 -1
- package/commands/project/validate.js +7 -1
- package/commands/project/watch.js +7 -2
- package/commands/project.js +2 -0
- 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/importData.d.ts +1 -1
- package/lang/en.d.ts +3194 -3184
- package/lang/en.js +43 -8
- package/lib/__tests__/dependencyManagement.test.js +273 -1
- package/lib/commonOpts.js +2 -5
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +6 -0
- package/lib/dependencyManagement.d.ts +8 -2
- package/lib/dependencyManagement.js +75 -12
- 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/npm.d.ts +3 -0
- package/lib/npm.js +6 -0
- package/lib/projects/__tests__/components.test.js +148 -24
- package/lib/projects/__tests__/platformVersion.test.js +5 -1
- 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/platformVersion.js +1 -1
- 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 +137 -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.d.ts +9 -3
- package/mcp-server/tools/project/UploadProjectTools.js +50 -4
- 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 +231 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +56 -4
- package/package.json +2 -2
- package/lang/en.lyaml +0 -1508
- package/lib/lang.d.ts +0 -8
- package/lib/lang.js +0 -72
package/lang/en.js
CHANGED
|
@@ -51,12 +51,11 @@ export const commands = {
|
|
|
51
51
|
uploadProject: (accountName) => `Would you like to upload this project to account "${accountName}" now?`,
|
|
52
52
|
projectCreated: {
|
|
53
53
|
title: chalk.bold('Next steps:'),
|
|
54
|
-
description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project
|
|
54
|
+
description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project upload')} to ${chalk.bold('upload')} your project.`,
|
|
55
55
|
},
|
|
56
56
|
},
|
|
57
57
|
logs: {
|
|
58
58
|
appSelected: `We'll create a new project with a sample app for you.\nProjects are what you can use to create apps with HubSpot.\nUsually you'll use the ${uiCommandReference('hs project create')} command, but we'll go ahead and make one now.`,
|
|
59
|
-
dependenciesInstalled: 'Dependencies installed successfully.',
|
|
60
59
|
uploadingProject: 'Uploading your project to HubSpot...',
|
|
61
60
|
uploadSuccess: 'Project uploaded successfully!',
|
|
62
61
|
developerOverviewLink: 'Open this link to navigate to your HubSpot developer portal',
|
|
@@ -64,7 +63,6 @@ export const commands = {
|
|
|
64
63
|
errors: {
|
|
65
64
|
uploadFailed: 'Failed to upload project to HubSpot.',
|
|
66
65
|
configFileNotFound: 'Could not find project configuration for upload.',
|
|
67
|
-
installDepsFailed: 'Failed to install dependencies.',
|
|
68
66
|
},
|
|
69
67
|
},
|
|
70
68
|
completion: {
|
|
@@ -120,7 +118,7 @@ export const commands = {
|
|
|
120
118
|
},
|
|
121
119
|
},
|
|
122
120
|
success: {
|
|
123
|
-
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)' : ''}.`,
|
|
124
122
|
},
|
|
125
123
|
},
|
|
126
124
|
use: {
|
|
@@ -1198,6 +1196,7 @@ export const commands = {
|
|
|
1198
1196
|
setup: {
|
|
1199
1197
|
describe: 'Setup the HubSpot development MCP servers.',
|
|
1200
1198
|
installingDocSearch: 'Adding the docs-search mcp server',
|
|
1199
|
+
codex: 'Codex CLI',
|
|
1201
1200
|
claudeCode: 'Claude Code',
|
|
1202
1201
|
cursor: 'Cursor',
|
|
1203
1202
|
windsurf: 'Windsurf',
|
|
@@ -1219,7 +1218,11 @@ export const commands = {
|
|
|
1219
1218
|
configuredClaudeCode: 'Configured Claude Code',
|
|
1220
1219
|
claudeCodeNotFound: 'Claude Code not found - skipping configuration',
|
|
1221
1220
|
claudeCodeInstallFailed: 'Claude Code CLI not working - skipping configuration',
|
|
1222
|
-
|
|
1221
|
+
// Codex
|
|
1222
|
+
configuringCodex: 'Configuring Codex...',
|
|
1223
|
+
configuredCodex: 'Configured Codex',
|
|
1224
|
+
codexNotFound: 'Codex command not found - skipping configuration',
|
|
1225
|
+
codexInstallFailed: 'Failed to configure Codex',
|
|
1223
1226
|
// Cursor
|
|
1224
1227
|
configuringCursor: 'Configuring Cursor...',
|
|
1225
1228
|
failedToConfigureCursor: 'Failed to configure Cursor',
|
|
@@ -1765,6 +1768,22 @@ export const commands = {
|
|
|
1765
1768
|
noPackageJsonInProject: (projectName) => `No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files. ${uiLink('Learn how to create a project from scratch', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app#customize-a-new-project-using-the-cli')}`,
|
|
1766
1769
|
packageManagerNotInstalled: (packageManager) => `This command depends on ${packageManager}, install ${uiLink(packageManager, 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm')}`,
|
|
1767
1770
|
},
|
|
1771
|
+
updateDeps: {
|
|
1772
|
+
help: {
|
|
1773
|
+
describe: 'Update the npm dependencies for your project, or update specific dependencies in a subcomponent of a project.',
|
|
1774
|
+
updateAppDepsExample: 'Update the dependencies for the project',
|
|
1775
|
+
updateDepToSubComponentExample: 'Update the npm dependencies in one or more project subcomponents',
|
|
1776
|
+
},
|
|
1777
|
+
installLocationPrompt: 'Choose which project components you would like to update the dependencies for:',
|
|
1778
|
+
installLocationPromptRequired: 'You must choose at least one subcomponent',
|
|
1779
|
+
updatingDependencies: (directory) => `Updating dependencies in ${directory}`,
|
|
1780
|
+
updateSuccessful: (directory) => `Updated dependencies in ${directory}`,
|
|
1781
|
+
updatingDependenciesToLocation: (dependencies, directory) => `Updating ${dependencies} in ${directory}`,
|
|
1782
|
+
updatingDependenciesFailed: (directory) => `Updating dependencies for ${directory} failed`,
|
|
1783
|
+
noProjectConfig: 'No project detected. Run this command from a project directory.',
|
|
1784
|
+
noPackageJsonInProject: (projectName) => `No dependencies to update. The project ${projectName} folder might be missing component or subcomponent files. ${uiLink('Learn how to create a project from scratch', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app#customize-a-new-project-using-the-cli')}`,
|
|
1785
|
+
packageManagerNotInstalled: (packageManager) => `This command depends on ${packageManager}, install ${uiLink(packageManager, 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm')}`,
|
|
1786
|
+
},
|
|
1768
1787
|
validate: {
|
|
1769
1788
|
describe: 'Validate the project before uploading',
|
|
1770
1789
|
mustBeRanWithinAProject: 'This command must be run from within a project directory.',
|
|
@@ -2082,7 +2101,7 @@ export const commands = {
|
|
|
2082
2101
|
},
|
|
2083
2102
|
},
|
|
2084
2103
|
create: {
|
|
2085
|
-
describe:
|
|
2104
|
+
describe: `Create a test account from scratch or from a config file. Use ${uiCommandReference('hs test-account create-config')} to generate a config file. \n${uiLink('Learn more', 'https://developers.hubspot.com/docs/developer-tooling/local-development/configurable-test-accounts')}`,
|
|
2086
2105
|
configPathPrompt: '[--config-path] Enter the path to the test account config: ',
|
|
2087
2106
|
createTestAccountFromConfigPrompt: 'How would you like to create your test account?',
|
|
2088
2107
|
createFromConfigOption: 'Create test account from config file',
|
|
@@ -2099,9 +2118,21 @@ export const commands = {
|
|
|
2099
2118
|
createFailure: 'Failed to create test account.',
|
|
2100
2119
|
},
|
|
2101
2120
|
options: {
|
|
2102
|
-
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. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2125
|
+
opsLevel: 'Operations Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2126
|
+
serviceLevel: 'Service Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2127
|
+
salesLevel: 'Sales Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2128
|
+
contentLevel: 'CMS Hub tier. Unprovided tiers default to ENTERPRISE. Choices: FREE, STARTER, PROFESSIONAL, ENTERPRISE',
|
|
2103
2129
|
},
|
|
2104
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.`,
|
|
2105
2136
|
},
|
|
2106
2137
|
createConfig: {
|
|
2107
2138
|
describe: 'Create a test account config file.',
|
|
@@ -2964,12 +2995,16 @@ export const lib = {
|
|
|
2964
2995
|
},
|
|
2965
2996
|
},
|
|
2966
2997
|
add: {
|
|
2967
|
-
nothingAdded: 'No features added.',
|
|
2998
|
+
nothingAdded: 'No features were added to the project. Use the space bar to select features from the list.',
|
|
2968
2999
|
},
|
|
2969
3000
|
updateHsMetaFilesWithAutoGeneratedFields: {
|
|
2970
3001
|
header: 'Created the following components and features:',
|
|
2971
3002
|
applicationLog: (componentType, uid, name) => ` - Created ${chalk.bold(componentType)} with uid ${chalk.bold(uid)} and name ${chalk.bold(name)}`,
|
|
2972
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.',
|
|
2973
3008
|
},
|
|
2974
3009
|
validateProjectConfig: {
|
|
2975
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.`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import util from 'util';
|
|
2
|
-
import { installPackages, getProjectPackageJsonLocations, } from '../dependencyManagement.js';
|
|
2
|
+
import { installPackages, updatePackages, getProjectPackageJsonLocations, isPackageInstalled, } from '../dependencyManagement.js';
|
|
3
3
|
import { walk } from '@hubspot/local-dev-lib/fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { getProjectConfig } from '../projects/config.js';
|
|
@@ -105,6 +105,54 @@ describe('lib/dependencyManagement', () => {
|
|
|
105
105
|
cwd: extensionsDir,
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
+
it('should install packages as dev dependencies when dev flag is true', async () => {
|
|
109
|
+
const packages = ['eslint', 'prettier'];
|
|
110
|
+
await installPackages({ packages, installLocations, dev: true });
|
|
111
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
112
|
+
for (const location of installLocations) {
|
|
113
|
+
expect(execMock).toHaveBeenCalledWith(`npm install --save-dev eslint prettier`, {
|
|
114
|
+
cwd: location,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
it('should install packages as regular dependencies when dev flag is false', async () => {
|
|
119
|
+
const packages = ['react', 'react-dom'];
|
|
120
|
+
await installPackages({ packages, installLocations, dev: false });
|
|
121
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
122
|
+
for (const location of installLocations) {
|
|
123
|
+
expect(execMock).toHaveBeenCalledWith(`npm install react react-dom`, {
|
|
124
|
+
cwd: location,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
it('should install packages as regular dependencies when dev flag is not provided', async () => {
|
|
129
|
+
const packages = ['axios'];
|
|
130
|
+
await installPackages({ packages, installLocations });
|
|
131
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
132
|
+
for (const location of installLocations) {
|
|
133
|
+
expect(execMock).toHaveBeenCalledWith(`npm install axios`, {
|
|
134
|
+
cwd: location,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
it('should not use --save-dev flag when dev is true but no packages are provided', async () => {
|
|
139
|
+
await installPackages({ installLocations, dev: true });
|
|
140
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
141
|
+
for (const location of installLocations) {
|
|
142
|
+
expect(execMock).toHaveBeenCalledWith(`npm install `, {
|
|
143
|
+
cwd: location,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
it('should not use --save-dev flag when dev is true but packages array is empty', async () => {
|
|
148
|
+
await installPackages({ packages: [], installLocations, dev: true });
|
|
149
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
150
|
+
for (const location of installLocations) {
|
|
151
|
+
expect(execMock).toHaveBeenCalledWith(`npm install `, {
|
|
152
|
+
cwd: location,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
108
156
|
it('should throw an error when installing the dependencies fails', async () => {
|
|
109
157
|
execMock = vi.fn().mockImplementation(command => {
|
|
110
158
|
if (command === 'npm --version') {
|
|
@@ -133,6 +181,92 @@ describe('lib/dependencyManagement', () => {
|
|
|
133
181
|
});
|
|
134
182
|
});
|
|
135
183
|
});
|
|
184
|
+
describe('updatePackages()', () => {
|
|
185
|
+
it('should setup a loading spinner', async () => {
|
|
186
|
+
const packages = ['package1', 'package2'];
|
|
187
|
+
await updatePackages({ packages, installLocations });
|
|
188
|
+
expect(SpinniesManager.init).toHaveBeenCalledTimes(installLocations.length);
|
|
189
|
+
expect(SpinniesManager.add).toHaveBeenCalledTimes(installLocations.length);
|
|
190
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledTimes(installLocations.length);
|
|
191
|
+
});
|
|
192
|
+
it('should update the provided packages in all the provided install locations', async () => {
|
|
193
|
+
const packages = ['package1', 'package2'];
|
|
194
|
+
await updatePackages({ packages, installLocations });
|
|
195
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
196
|
+
expect(SpinniesManager.add).toHaveBeenCalledTimes(installLocations.length);
|
|
197
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledTimes(installLocations.length);
|
|
198
|
+
for (const location of installLocations) {
|
|
199
|
+
expect(execMock).toHaveBeenCalledWith(`npm update package1 package2`, {
|
|
200
|
+
cwd: location,
|
|
201
|
+
});
|
|
202
|
+
expect(SpinniesManager.add).toHaveBeenCalledWith(`updatingDependencies-${location}`, {
|
|
203
|
+
text: `Updating [package1, package2] in ${location}`,
|
|
204
|
+
});
|
|
205
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledWith(`updatingDependencies-${location}`, {
|
|
206
|
+
text: `Updated dependencies in ${location}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
it('should use the provided install locations', async () => {
|
|
211
|
+
await updatePackages({ installLocations });
|
|
212
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
213
|
+
expect(execMock).toHaveBeenCalledWith(`npm update `, {
|
|
214
|
+
cwd: appFunctionsDir,
|
|
215
|
+
});
|
|
216
|
+
expect(execMock).toHaveBeenCalledWith(`npm update `, {
|
|
217
|
+
cwd: extensionsDir,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
it('should locate the projects package.json files when install locations is not provided', async () => {
|
|
221
|
+
const installLocations = [
|
|
222
|
+
path.join(appFunctionsDir, 'package.json'),
|
|
223
|
+
path.join(extensionsDir, 'package.json'),
|
|
224
|
+
];
|
|
225
|
+
mockedWalk.mockResolvedValue(installLocations);
|
|
226
|
+
mockedGetProjectConfig.mockResolvedValue({
|
|
227
|
+
projectDir,
|
|
228
|
+
projectConfig: {
|
|
229
|
+
srcDir,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
await updatePackages({});
|
|
233
|
+
// It's called once per each install location, plus once to check if npm installed
|
|
234
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length + 1);
|
|
235
|
+
expect(execMock).toHaveBeenCalledWith(`npm update `, {
|
|
236
|
+
cwd: appFunctionsDir,
|
|
237
|
+
});
|
|
238
|
+
expect(execMock).toHaveBeenCalledWith(`npm update `, {
|
|
239
|
+
cwd: extensionsDir,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
it('should throw an error when updating the dependencies fails', async () => {
|
|
243
|
+
execMock = vi.fn().mockImplementation(command => {
|
|
244
|
+
if (command === 'npm --version') {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
throw new Error('OH NO');
|
|
248
|
+
});
|
|
249
|
+
util.promisify = mockedPromisify(execMock);
|
|
250
|
+
// Mock walk to return the directory paths instead of package.json paths
|
|
251
|
+
mockedWalk.mockResolvedValue([appFunctionsDir, extensionsDir]);
|
|
252
|
+
mockedFs.existsSync.mockImplementation(filePath => {
|
|
253
|
+
const pathStr = filePath.toString();
|
|
254
|
+
if (pathStr === projectDir ||
|
|
255
|
+
pathStr === path.join(projectDir, srcDir)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
});
|
|
260
|
+
await expect(() => updatePackages({ installLocations: [appFunctionsDir, extensionsDir] })).rejects.toThrowError(`Updating dependencies for ${appFunctionsDir} failed`);
|
|
261
|
+
expect(SpinniesManager.fail).toHaveBeenCalledTimes(installLocations.length);
|
|
262
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(`updatingDependencies-${appFunctionsDir}`, {
|
|
263
|
+
text: `Updating dependencies for ${appFunctionsDir} failed`,
|
|
264
|
+
});
|
|
265
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(`updatingDependencies-${extensionsDir}`, {
|
|
266
|
+
text: `Updating dependencies for ${extensionsDir} failed`,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
136
270
|
describe('getProjectPackageJsonFiles()', () => {
|
|
137
271
|
it('should throw an error when ran outside the boundary of a project', async () => {
|
|
138
272
|
mockedGetProjectConfig.mockResolvedValue({});
|
|
@@ -149,6 +283,30 @@ describe('lib/dependencyManagement', () => {
|
|
|
149
283
|
mockedFs.existsSync.mockReturnValueOnce(false);
|
|
150
284
|
await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(new RegExp(`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files.`));
|
|
151
285
|
});
|
|
286
|
+
it('should throw "install" error message when isUpdate=false and no package.json files found', async () => {
|
|
287
|
+
mockedWalk.mockResolvedValue([]);
|
|
288
|
+
mockedFs.existsSync.mockImplementation(filePath => {
|
|
289
|
+
const pathStr = filePath.toString();
|
|
290
|
+
if (pathStr === projectDir ||
|
|
291
|
+
pathStr === path.join(projectDir, srcDir)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
});
|
|
296
|
+
await expect(() => getProjectPackageJsonLocations(undefined, false)).rejects.toThrowError(new RegExp(`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files.`));
|
|
297
|
+
});
|
|
298
|
+
it('should throw "update" error message when isUpdate=true and no package.json files found', async () => {
|
|
299
|
+
mockedWalk.mockResolvedValue([]);
|
|
300
|
+
mockedFs.existsSync.mockImplementation(filePath => {
|
|
301
|
+
const pathStr = filePath.toString();
|
|
302
|
+
if (pathStr === projectDir ||
|
|
303
|
+
pathStr === path.join(projectDir, srcDir)) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
});
|
|
308
|
+
await expect(() => getProjectPackageJsonLocations(undefined, true)).rejects.toThrowError(new RegExp(`No dependencies to update. The project ${projectName} folder might be missing component or subcomponent files.`));
|
|
309
|
+
});
|
|
152
310
|
it('should ignore package.json files in certain directories', async () => {
|
|
153
311
|
const nodeModulesDir = path.join(appDir, 'node_modules');
|
|
154
312
|
const viteDir = path.join(appDir, '.vite');
|
|
@@ -172,4 +330,118 @@ describe('lib/dependencyManagement', () => {
|
|
|
172
330
|
expect(actual).toEqual([appFunctionsDir, extensionsDir]);
|
|
173
331
|
});
|
|
174
332
|
});
|
|
333
|
+
describe('isPackageInstalled()', () => {
|
|
334
|
+
const testDir = '/test/directory';
|
|
335
|
+
const readFileSyncSpy = vi.spyOn(fs, 'readFileSync');
|
|
336
|
+
const existsSyncSpy = vi.spyOn(fs, 'existsSync');
|
|
337
|
+
function mockNodeModulesExists(packageName, exists = true) {
|
|
338
|
+
existsSyncSpy.mockImplementation(filePath => {
|
|
339
|
+
const pathStr = filePath.toString();
|
|
340
|
+
return (exists && pathStr === path.join(testDir, 'node_modules', packageName));
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
beforeEach(() => {
|
|
344
|
+
vi.clearAllMocks();
|
|
345
|
+
readFileSyncSpy.mockReset();
|
|
346
|
+
existsSyncSpy.mockReset();
|
|
347
|
+
});
|
|
348
|
+
it('should return true if package is in dependencies and in node_modules', () => {
|
|
349
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
350
|
+
dependencies: {
|
|
351
|
+
eslint: '^9.0.0',
|
|
352
|
+
},
|
|
353
|
+
}));
|
|
354
|
+
mockNodeModulesExists('eslint', true);
|
|
355
|
+
const result = isPackageInstalled(testDir, 'eslint');
|
|
356
|
+
expect(result).toBe(true);
|
|
357
|
+
expect(readFileSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'package.json'), 'utf-8');
|
|
358
|
+
expect(existsSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'node_modules', 'eslint'));
|
|
359
|
+
});
|
|
360
|
+
it('should return true if package is in devDependencies and in node_modules', () => {
|
|
361
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
362
|
+
devDependencies: {
|
|
363
|
+
prettier: '^3.0.0',
|
|
364
|
+
},
|
|
365
|
+
}));
|
|
366
|
+
mockNodeModulesExists('prettier', true);
|
|
367
|
+
const result = isPackageInstalled(testDir, 'prettier');
|
|
368
|
+
expect(result).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
it('should return false if package is in package.json but not in node_modules', () => {
|
|
371
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
372
|
+
dependencies: {
|
|
373
|
+
react: '^18.0.0',
|
|
374
|
+
},
|
|
375
|
+
}));
|
|
376
|
+
mockNodeModulesExists('react', false);
|
|
377
|
+
const result = isPackageInstalled(testDir, 'react');
|
|
378
|
+
expect(result).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
it('should return false if package is not in package.json but is in node_modules', () => {
|
|
381
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
382
|
+
dependencies: {
|
|
383
|
+
typescript: '^5.0.0',
|
|
384
|
+
},
|
|
385
|
+
}));
|
|
386
|
+
mockNodeModulesExists('lodash', true);
|
|
387
|
+
const result = isPackageInstalled(testDir, 'lodash');
|
|
388
|
+
expect(result).toBe(false);
|
|
389
|
+
});
|
|
390
|
+
it('should return false if package is not in package.json and not in node_modules', () => {
|
|
391
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
392
|
+
dependencies: {},
|
|
393
|
+
}));
|
|
394
|
+
mockNodeModulesExists('nonexistent-package', false);
|
|
395
|
+
const result = isPackageInstalled(testDir, 'nonexistent-package');
|
|
396
|
+
expect(result).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
it('should return false if package.json cannot be read', () => {
|
|
399
|
+
readFileSyncSpy.mockImplementationOnce(() => {
|
|
400
|
+
throw new Error('File not found');
|
|
401
|
+
});
|
|
402
|
+
const result = isPackageInstalled(testDir, 'eslint');
|
|
403
|
+
expect(result).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
it('should return false if package.json has invalid JSON', () => {
|
|
406
|
+
readFileSyncSpy.mockReturnValueOnce('invalid json{');
|
|
407
|
+
const result = isPackageInstalled(testDir, 'eslint');
|
|
408
|
+
expect(result).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
it('should return false if checking node_modules throws an error', () => {
|
|
411
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
412
|
+
dependencies: {
|
|
413
|
+
eslint: '^9.0.0',
|
|
414
|
+
},
|
|
415
|
+
}));
|
|
416
|
+
existsSyncSpy.mockImplementation(() => {
|
|
417
|
+
throw new Error('Permission denied');
|
|
418
|
+
});
|
|
419
|
+
const result = isPackageInstalled(testDir, 'eslint');
|
|
420
|
+
expect(result).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
it('should handle scoped packages correctly', () => {
|
|
423
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
424
|
+
dependencies: {
|
|
425
|
+
'@typescript-eslint/parser': '^8.0.0',
|
|
426
|
+
},
|
|
427
|
+
}));
|
|
428
|
+
mockNodeModulesExists('@typescript-eslint/parser', true);
|
|
429
|
+
const result = isPackageInstalled(testDir, '@typescript-eslint/parser');
|
|
430
|
+
expect(result).toBe(true);
|
|
431
|
+
expect(existsSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'node_modules', '@typescript-eslint/parser'));
|
|
432
|
+
});
|
|
433
|
+
it('should check both dependencies and devDependencies', () => {
|
|
434
|
+
readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
|
|
435
|
+
dependencies: {
|
|
436
|
+
react: '^18.0.0',
|
|
437
|
+
},
|
|
438
|
+
devDependencies: {
|
|
439
|
+
eslint: '^9.0.0',
|
|
440
|
+
},
|
|
441
|
+
}));
|
|
442
|
+
mockNodeModulesExists('eslint', true);
|
|
443
|
+
const result = isPackageInstalled(testDir, 'eslint');
|
|
444
|
+
expect(result).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
175
447
|
});
|
package/lib/commonOpts.js
CHANGED
|
@@ -149,16 +149,13 @@ export function setCLILogLevel(options) {
|
|
|
149
149
|
setLogLevel(LOG_LEVEL.ERROR);
|
|
150
150
|
SpinniesManager.setDisableOutput(true);
|
|
151
151
|
}
|
|
152
|
-
else if (debug) {
|
|
152
|
+
else if (debug || networkDebug) {
|
|
153
153
|
setLogLevel(LOG_LEVEL.DEBUG);
|
|
154
|
+
process.env.HUBSPOT_NETWORK_LOGGING = 'true';
|
|
154
155
|
}
|
|
155
156
|
else {
|
|
156
157
|
setLogLevel(LOG_LEVEL.LOG);
|
|
157
158
|
}
|
|
158
|
-
if (networkDebug) {
|
|
159
|
-
process.env.HUBSPOT_NETWORK_LOGGING = 'true';
|
|
160
|
-
setLogLevel(LOG_LEVEL.DEBUG);
|
|
161
|
-
}
|
|
162
159
|
}
|
|
163
160
|
export function getCommandName(argv) {
|
|
164
161
|
return String(argv && argv._ && argv._[0]) || '';
|
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
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
export declare function installPackages({ packages, installLocations, }: {
|
|
1
|
+
export declare function installPackages({ packages, installLocations, dev, }: {
|
|
2
2
|
packages?: string[];
|
|
3
3
|
installLocations?: string[];
|
|
4
|
+
dev?: boolean;
|
|
4
5
|
}): Promise<void>;
|
|
5
|
-
export declare function
|
|
6
|
+
export declare function updatePackages({ packages, installLocations, }: {
|
|
7
|
+
packages?: string[];
|
|
8
|
+
installLocations?: string[];
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export declare function getProjectPackageJsonLocations(dir?: string, isUpdate?: boolean): Promise<string[]>;
|
|
11
|
+
export declare function isPackageInstalled(directory: string, packageName: string): boolean;
|
|
6
12
|
export declare function hasMissingPackages(directory: string): Promise<boolean>;
|
|
@@ -6,19 +6,21 @@ import { walk } from '@hubspot/local-dev-lib/fs';
|
|
|
6
6
|
import { getProjectConfig } from './projects/config.js';
|
|
7
7
|
import { commands } from '../lang/en.js';
|
|
8
8
|
import SpinniesManager from './ui/SpinniesManager.js';
|
|
9
|
-
import { isGloballyInstalled, executeInstall, DEFAULT_PACKAGE_MANAGER, } from './npm.js';
|
|
9
|
+
import { isGloballyInstalled, executeInstall, executeUpdate, DEFAULT_PACKAGE_MANAGER, } from './npm.js';
|
|
10
10
|
class NoPackageJsonFilesError extends Error {
|
|
11
|
-
constructor(projectName) {
|
|
12
|
-
super(
|
|
11
|
+
constructor(projectName, isUpdate = false) {
|
|
12
|
+
super(isUpdate
|
|
13
|
+
? commands.project.updateDeps.noPackageJsonInProject(projectName)
|
|
14
|
+
: commands.project.installDeps.noPackageJsonInProject(projectName));
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
|
-
export async function installPackages({ packages, installLocations, }) {
|
|
17
|
+
export async function installPackages({ packages, installLocations, dev = false, }) {
|
|
16
18
|
const installDirs = installLocations || (await getProjectPackageJsonLocations());
|
|
17
19
|
await Promise.all(installDirs.map(async (dir) => {
|
|
18
|
-
await installPackagesInDirectory(dir, packages);
|
|
20
|
+
await installPackagesInDirectory(dir, packages, dev);
|
|
19
21
|
}));
|
|
20
22
|
}
|
|
21
|
-
async function installPackagesInDirectory(directory, packages) {
|
|
23
|
+
async function installPackagesInDirectory(directory, packages, dev = false) {
|
|
22
24
|
const spinner = `installingDependencies-${directory}`;
|
|
23
25
|
const relativeDir = path.relative(process.cwd(), directory);
|
|
24
26
|
SpinniesManager.init();
|
|
@@ -28,7 +30,8 @@ async function installPackagesInDirectory(directory, packages) {
|
|
|
28
30
|
: commands.project.installDeps.installingDependencies(relativeDir),
|
|
29
31
|
});
|
|
30
32
|
try {
|
|
31
|
-
|
|
33
|
+
const flags = dev && packages && packages.length > 0 ? '--save-dev' : null;
|
|
34
|
+
await executeInstall(packages, flags, { cwd: directory });
|
|
32
35
|
SpinniesManager.succeed(spinner, {
|
|
33
36
|
text: commands.project.installDeps.installationSuccessful(relativeDir),
|
|
34
37
|
});
|
|
@@ -42,26 +45,60 @@ async function installPackagesInDirectory(directory, packages) {
|
|
|
42
45
|
});
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
export async function
|
|
48
|
+
export async function updatePackages({ packages, installLocations, }) {
|
|
49
|
+
const installDirs = installLocations || (await getProjectPackageJsonLocations(undefined, true));
|
|
50
|
+
await Promise.all(installDirs.map(async (dir) => {
|
|
51
|
+
await updatePackagesInDirectory(dir, packages);
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
async function updatePackagesInDirectory(directory, packages) {
|
|
55
|
+
const spinner = `updatingDependencies-${directory}`;
|
|
56
|
+
const relativeDir = path.relative(process.cwd(), directory);
|
|
57
|
+
SpinniesManager.init();
|
|
58
|
+
SpinniesManager.add(spinner, {
|
|
59
|
+
text: packages && packages.length
|
|
60
|
+
? commands.project.updateDeps.updatingDependenciesToLocation(`[${packages.join(', ')}]`, relativeDir)
|
|
61
|
+
: commands.project.updateDeps.updatingDependencies(relativeDir),
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
await executeUpdate(packages, null, { cwd: directory });
|
|
65
|
+
SpinniesManager.succeed(spinner, {
|
|
66
|
+
text: commands.project.updateDeps.updateSuccessful(relativeDir),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
SpinniesManager.fail(spinner, {
|
|
71
|
+
text: commands.project.updateDeps.updatingDependenciesFailed(relativeDir),
|
|
72
|
+
});
|
|
73
|
+
throw new Error(commands.project.updateDeps.updatingDependenciesFailed(relativeDir), {
|
|
74
|
+
cause: e,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function getProjectPackageJsonLocations(dir, isUpdate = false) {
|
|
46
79
|
const projectConfig = await getProjectConfig(dir);
|
|
47
80
|
if (!projectConfig ||
|
|
48
81
|
!projectConfig.projectDir ||
|
|
49
82
|
!projectConfig.projectConfig) {
|
|
50
|
-
throw new Error(
|
|
83
|
+
throw new Error(isUpdate
|
|
84
|
+
? commands.project.updateDeps.noProjectConfig
|
|
85
|
+
: commands.project.installDeps.noProjectConfig);
|
|
51
86
|
}
|
|
52
87
|
const { projectDir, projectConfig: { srcDir, name }, } = projectConfig;
|
|
53
88
|
if (!(await isGloballyInstalled(DEFAULT_PACKAGE_MANAGER))) {
|
|
54
|
-
throw new Error(
|
|
89
|
+
throw new Error(isUpdate
|
|
90
|
+
? commands.project.updateDeps.packageManagerNotInstalled(DEFAULT_PACKAGE_MANAGER)
|
|
91
|
+
: commands.project.installDeps.packageManagerNotInstalled(DEFAULT_PACKAGE_MANAGER));
|
|
55
92
|
}
|
|
56
93
|
if (!fs.existsSync(projectConfig.projectDir) ||
|
|
57
94
|
!fs.existsSync(path.join(projectDir, srcDir))) {
|
|
58
|
-
throw new NoPackageJsonFilesError(name);
|
|
95
|
+
throw new NoPackageJsonFilesError(name, isUpdate);
|
|
59
96
|
}
|
|
60
97
|
const packageJsonFiles = (await walk(path.join(projectDir, srcDir))).filter(file => file.includes('package.json') &&
|
|
61
98
|
!file.includes('node_modules') &&
|
|
62
99
|
!file.includes('.vite'));
|
|
63
100
|
if (packageJsonFiles.length === 0) {
|
|
64
|
-
throw new NoPackageJsonFilesError(name);
|
|
101
|
+
throw new NoPackageJsonFilesError(name, isUpdate);
|
|
65
102
|
}
|
|
66
103
|
const packageParentDirs = [];
|
|
67
104
|
packageJsonFiles.forEach(packageJsonFile => {
|
|
@@ -70,6 +107,32 @@ export async function getProjectPackageJsonLocations(dir) {
|
|
|
70
107
|
});
|
|
71
108
|
return packageParentDirs;
|
|
72
109
|
}
|
|
110
|
+
function isPackageInPackageJson(directory, packageName) {
|
|
111
|
+
const packageJsonPath = path.join(directory, 'package.json');
|
|
112
|
+
try {
|
|
113
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
114
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
115
|
+
return !!((packageJson.dependencies && packageJson.dependencies[packageName]) ||
|
|
116
|
+
(packageJson.devDependencies && packageJson.devDependencies[packageName]));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function isPackageInNodeModules(directory, packageName) {
|
|
123
|
+
const packagePath = path.join(directory, 'node_modules', packageName);
|
|
124
|
+
try {
|
|
125
|
+
return fs.existsSync(packagePath);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function isPackageInstalled(directory, packageName) {
|
|
132
|
+
const inPackageJson = isPackageInPackageJson(directory, packageName);
|
|
133
|
+
const actuallyInstalled = isPackageInNodeModules(directory, packageName);
|
|
134
|
+
return inPackageJson && actuallyInstalled;
|
|
135
|
+
}
|
|
73
136
|
export async function hasMissingPackages(directory) {
|
|
74
137
|
const exec = util.promisify(execAsync);
|
|
75
138
|
const { stdout } = await exec(`npm install --ignore-scripts --dry-run`, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|