-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1133 from HubSpot/jy/poc-hs-project-install
feat: Add `hs project install-deps` command
- Loading branch information
Showing
7 changed files
with
769 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
const { command, describe: projectDescribe, builder } = require('../project'); | ||
|
||
jest.mock('../project/deploy'); | ||
jest.mock('../project/create'); | ||
jest.mock('../project/upload'); | ||
jest.mock('../project/listBuilds'); | ||
jest.mock('../project/logs'); | ||
jest.mock('../project/watch'); | ||
jest.mock('../project/download'); | ||
jest.mock('../project/open'); | ||
jest.mock('../project/dev'); | ||
jest.mock('../project/add'); | ||
jest.mock('../project/migrateApp'); | ||
jest.mock('../project/cloneApp'); | ||
jest.mock('../project/installDeps'); | ||
jest.mock('../../lib/commonOpts'); | ||
|
||
const deploy = require('../project/deploy'); | ||
const create = require('../project/create'); | ||
const upload = require('../project/upload'); | ||
const listBuilds = require('../project/listBuilds'); | ||
const logs = require('../project/logs'); | ||
const watch = require('../project/watch'); | ||
const download = require('../project/download'); | ||
const open = require('../project/open'); | ||
const dev = require('../project/dev'); | ||
const add = require('../project/add'); | ||
const migrateApp = require('../project/migrateApp'); | ||
const cloneApp = require('../project/cloneApp'); | ||
const installDeps = require('../project/installDeps'); | ||
const { addConfigOptions, addAccountOptions } = require('../../lib/commonOpts'); | ||
|
||
describe('commands/projects', () => { | ||
describe('command', () => { | ||
it('should have the correct command structure', () => { | ||
expect(command).toEqual('project'); | ||
}); | ||
}); | ||
|
||
describe('describe', () => { | ||
it('should contain the beta tag', () => { | ||
expect(projectDescribe).toContain('[BETA]'); | ||
}); | ||
it('should provide an accurate description of what the command is doing', () => { | ||
expect(projectDescribe).toContain( | ||
'Commands for working with projects. For more information, visit our documentation: https://developers.hubspot.com/docs/platform/build-and-deploy-using-hubspot-projects' | ||
); | ||
}); | ||
}); | ||
|
||
describe('builder', () => { | ||
let yargs; | ||
|
||
const subcommands = [ | ||
['create', create], | ||
['add', add], | ||
['watch', watch], | ||
['dev', dev], | ||
['upload', upload], | ||
['deploy', deploy], | ||
['logs', logs], | ||
['listBuilds', listBuilds], | ||
['download', download], | ||
['open', open], | ||
['migrateApp', migrateApp], | ||
['cloneApp', cloneApp], | ||
['installDeps', installDeps], | ||
]; | ||
|
||
beforeEach(() => { | ||
yargs = { | ||
command: jest.fn().mockImplementation(() => yargs), | ||
demandCommand: jest.fn().mockImplementation(() => yargs), | ||
}; | ||
}); | ||
|
||
it('should add the config options', () => { | ||
builder(yargs); | ||
expect(addConfigOptions).toHaveBeenCalledTimes(1); | ||
expect(addConfigOptions).toHaveBeenCalledWith(yargs); | ||
}); | ||
|
||
it('should add the account options', () => { | ||
builder(yargs); | ||
expect(addAccountOptions).toHaveBeenCalledTimes(1); | ||
expect(addAccountOptions).toHaveBeenCalledWith(yargs); | ||
}); | ||
|
||
it('should add the correct number of sub commands', () => { | ||
builder(yargs); | ||
expect(yargs.command).toHaveBeenCalledTimes(subcommands.length); | ||
}); | ||
|
||
it.each(subcommands)('should attach the %s subcommand', (name, module) => { | ||
builder(yargs); | ||
expect(yargs.command).toHaveBeenCalledWith(module); | ||
}); | ||
|
||
it('should demand the command takes one positional argument', () => { | ||
builder(yargs); | ||
expect(yargs.demandCommand).toHaveBeenCalledTimes(1); | ||
expect(yargs.demandCommand).toHaveBeenCalledWith(1, ''); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
packages/cli/commands/project/__tests__/installDeps.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
jest.mock('../../../lib/projects'); | ||
jest.mock('@hubspot/local-dev-lib/logger'); | ||
jest.mock('../../../lib/dependencyManagement'); | ||
jest.mock('../../../lib/prompts/promptUtils'); | ||
jest.mock('../../../lib/usageTracking'); | ||
jest.mock('../../../lib/commonOpts'); | ||
|
||
const { getProjectConfig } = require('../../../lib/projects'); | ||
const { EXIT_CODES } = require('../../../lib/enums/exitCodes'); | ||
const { logger } = require('@hubspot/local-dev-lib/logger'); | ||
const { trackCommandUsage } = require('../../../lib/usageTracking'); | ||
const { getAccountId } = require('../../../lib/commonOpts'); | ||
const { | ||
installPackages, | ||
getProjectPackageJsonLocations, | ||
} = require('../../../lib/dependencyManagement'); | ||
const { promptUser } = require('../../../lib/prompts/promptUtils'); | ||
const { | ||
command, | ||
describe: installDepsDescribe, | ||
builder, | ||
handler, | ||
} = require('../installDeps'); | ||
const path = require('path'); | ||
|
||
describe('commands/project/installDeps', () => { | ||
describe('command', () => { | ||
it('should have the correct command string', () => { | ||
expect(command).toEqual('install-deps [packages..]'); | ||
}); | ||
}); | ||
|
||
describe('describe', () => { | ||
it('should have the correct description', () => { | ||
expect(installDepsDescribe).toEqual(null); | ||
}); | ||
}); | ||
|
||
describe('builder', () => { | ||
let yargs; | ||
beforeEach(() => { | ||
yargs = { | ||
example: jest.fn().mockImplementation(() => yargs), | ||
}; | ||
}); | ||
|
||
it('should add correct examples', () => { | ||
builder(yargs); | ||
expect(yargs.example).toHaveBeenCalledTimes(1); | ||
expect(yargs.example).toHaveBeenCalledWith([ | ||
['$0 project install-deps', 'Install the dependencies for the project'], | ||
[ | ||
'$0 project install-deps dependency1 dependency2', | ||
'Install the dependencies to one or more project subcomponents', | ||
], | ||
]); | ||
}); | ||
}); | ||
|
||
describe('handler', () => { | ||
let processExitSpy; | ||
|
||
beforeEach(() => { | ||
processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); | ||
}); | ||
|
||
it('should track the command usage', async () => { | ||
const accountId = 999999; | ||
getAccountId.mockReturnValue(accountId); | ||
await handler({}); | ||
|
||
expect(getAccountId).toHaveBeenCalledTimes(1); | ||
expect(trackCommandUsage).toHaveBeenCalledTimes(1); | ||
expect(trackCommandUsage).toHaveBeenCalledWith( | ||
'project-install-deps', | ||
null, | ||
accountId | ||
); | ||
}); | ||
|
||
it('should handle exceptions', async () => { | ||
const error = new Error('Something went super wrong'); | ||
|
||
getProjectConfig.mockImplementationOnce(() => { | ||
throw error; | ||
}); | ||
|
||
await handler({}); | ||
|
||
expect(logger.debug).toHaveBeenCalledTimes(1); | ||
expect(logger.debug).toHaveBeenCalledWith(error); | ||
|
||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
expect(logger.error).toHaveBeenCalledWith(error.message); | ||
|
||
expect(processExitSpy).toHaveBeenCalledTimes(1); | ||
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR); | ||
}); | ||
|
||
it('should log an error and exit when the project config is not defined', async () => { | ||
getProjectConfig.mockResolvedValueOnce(null); | ||
await handler({}); | ||
|
||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
expect(logger.error).toHaveBeenCalledWith( | ||
'No project detected. Run this command from a project directory.' | ||
); | ||
expect(processExitSpy).toHaveBeenCalledTimes(1); | ||
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR); | ||
}); | ||
|
||
it('should log an error and exit when the project config has no projectDir', async () => { | ||
getProjectConfig.mockResolvedValueOnce({ projectDir: null }); | ||
await handler({}); | ||
|
||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
expect(logger.error).toHaveBeenCalledWith( | ||
'No project detected. Run this command from a project directory.' | ||
); | ||
expect(processExitSpy).toHaveBeenCalledTimes(1); | ||
expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR); | ||
}); | ||
|
||
it('should prompt for input when packages is defined', async () => { | ||
const projectDir = 'src'; | ||
getProjectConfig.mockResolvedValue({ projectDir }); | ||
const packageJsonLocation = path.join(projectDir, 'directory1'); | ||
promptUser.mockResolvedValueOnce(packageJsonLocation); | ||
getProjectPackageJsonLocations.mockResolvedValue([packageJsonLocation]); | ||
await handler({ packages: ['@hubspot/local-dev-lib'] }); | ||
expect(getProjectPackageJsonLocations).toHaveBeenCalledTimes(1); | ||
expect(promptUser).toHaveBeenCalledTimes(1); | ||
expect(promptUser).toHaveBeenCalledWith([ | ||
{ | ||
name: 'selectedInstallLocations', | ||
type: 'checkbox', | ||
when: expect.any(Function), | ||
choices: [ | ||
{ | ||
name: 'directory1', | ||
value: packageJsonLocation, | ||
}, | ||
], | ||
message: 'Choose the project components to install the dependencies:', | ||
validate: expect.any(Function), | ||
}, | ||
]); | ||
}); | ||
|
||
it('should call installPackages correctly', async () => { | ||
const projectDir = 'src'; | ||
const packageJsonLocation = path.join(projectDir, 'directory1'); | ||
const installLocations = [packageJsonLocation]; | ||
const packages = ['@hubspot/local-dev-lib']; | ||
|
||
getProjectConfig.mockResolvedValue({ projectDir }); | ||
promptUser.mockResolvedValueOnce(packageJsonLocation); | ||
getProjectPackageJsonLocations.mockResolvedValue(installLocations); | ||
await handler({ packages }); | ||
|
||
expect(installPackages).toHaveBeenCalledTimes(1); | ||
expect(installPackages).toHaveBeenCalledWith({ | ||
packages, | ||
installLocations, | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
const { | ||
installPackages, | ||
getProjectPackageJsonLocations, | ||
} = require('../../lib/dependencyManagement'); | ||
const { logger } = require('@hubspot/local-dev-lib/logger'); | ||
const { EXIT_CODES } = require('../../lib/enums/exitCodes'); | ||
const { getProjectConfig } = require('../../lib/projects'); | ||
const { promptUser } = require('../../lib/prompts/promptUtils'); | ||
const path = require('path'); | ||
const { i18n } = require('../../lib/lang'); | ||
const { trackCommandUsage } = require('../../lib/usageTracking'); | ||
const { getAccountId } = require('../../lib/commonOpts'); | ||
|
||
const i18nKey = `commands.project.subcommands.installDeps`; | ||
|
||
exports.command = 'install-deps [packages..]'; | ||
// Intentionally making this null to hide command | ||
exports.describe = null; | ||
// exports.describe = uiBetaTag(i18n(`${i18nKey}.help.describe`), false); | ||
|
||
exports.handler = async ({ packages }) => { | ||
try { | ||
const accountId = getAccountId(); | ||
trackCommandUsage('project-install-deps', null, accountId); | ||
|
||
const projectConfig = await getProjectConfig(); | ||
if (!projectConfig || !projectConfig.projectDir) { | ||
logger.error(i18n(`${i18nKey}.noProjectConfig`)); | ||
return process.exit(EXIT_CODES.ERROR); | ||
} | ||
|
||
const { projectDir } = projectConfig; | ||
|
||
let installLocations = await getProjectPackageJsonLocations(); | ||
if (packages) { | ||
const { selectedInstallLocations } = await promptUser([ | ||
{ | ||
name: 'selectedInstallLocations', | ||
type: 'checkbox', | ||
when: () => packages && packages.length > 0, | ||
message: i18n(`${i18nKey}.installLocationPrompt`), | ||
choices: installLocations.map(dir => ({ | ||
name: path.relative(projectDir, dir), | ||
value: dir, | ||
})), | ||
validate: choices => { | ||
if (choices === undefined || choices.length === 0) { | ||
return i18n(`${i18nKey}.installLocationPromptRequired`); | ||
} | ||
return true; | ||
}, | ||
}, | ||
]); | ||
if (selectedInstallLocations) { | ||
installLocations = selectedInstallLocations; | ||
} | ||
} | ||
|
||
await installPackages({ | ||
packages, | ||
installLocations, | ||
}); | ||
} catch (e) { | ||
logger.debug(e); | ||
logger.error(e.message); | ||
return process.exit(EXIT_CODES.ERROR); | ||
} | ||
}; | ||
|
||
exports.builder = yargs => { | ||
yargs.example([ | ||
['$0 project install-deps', i18n(`${i18nKey}.help.installAppDepsExample`)], | ||
[ | ||
'$0 project install-deps dependency1 dependency2', | ||
i18n(`${i18nKey}.help.addDepToSubComponentExample`), | ||
], | ||
]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.