@hubspot/cli 5.4.0 → 6.0.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/commands/__tests__/projects.test.js +105 -0
- package/commands/accounts/clean.js +1 -1
- package/commands/project/__tests__/deploy.test.js +1 -1
- package/commands/project/__tests__/installDeps.test.js +168 -0
- package/commands/project/__tests__/logs.test.js +305 -0
- package/commands/project/cloneApp.js +3 -16
- package/commands/project/deploy.js +4 -1
- package/commands/project/dev.js +15 -6
- package/commands/project/download.js +4 -1
- package/commands/project/installDeps.js +78 -0
- package/commands/project/logs.js +80 -242
- package/commands/project/migrateApp.js +3 -6
- package/commands/project/upload.js +5 -3
- package/commands/project/watch.js +3 -9
- package/commands/project.js +2 -0
- package/commands/sandbox/create.js +1 -0
- package/commands/sandbox.js +0 -2
- package/lang/en.lyaml +25 -70
- package/lib/LocalDevManager.js +1 -22
- package/lib/__tests__/dependencyManagement.test.js +245 -0
- package/lib/__tests__/projectLogsManager.test.js +210 -0
- package/lib/dependencyManagement.js +157 -0
- package/lib/errorHandlers/apiErrors.js +1 -3
- package/lib/errorHandlers/overrideErrors.js +57 -36
- package/lib/localDev.js +1 -7
- package/lib/projectLogsManager.js +144 -0
- package/lib/projects.js +3 -6
- package/lib/projectsWatch.js +2 -5
- package/lib/prompts/__tests__/projectsLogsPrompt.test.js +46 -0
- package/lib/prompts/createProjectPrompt.js +4 -0
- package/lib/prompts/projectDevTargetAccountPrompt.js +16 -25
- package/lib/prompts/projectsLogsPrompt.js +17 -108
- package/lib/sandboxSync.js +13 -15
- package/package.json +6 -6
- package/commands/sandbox/sync.js +0 -225
package/lang/en.lyaml
CHANGED
|
@@ -483,6 +483,7 @@ en:
|
|
|
483
483
|
errors:
|
|
484
484
|
noProjectConfig: "No project detected. Please run this command again from a project directory."
|
|
485
485
|
invalidProjectComponents: "Projects cannot contain both private and public apps. Move your apps to separate projects before attempting local development."
|
|
486
|
+
noRunnableComponents: "No supported components were found in this project. Run {{ command }} to see a list of available components and add one to your project."
|
|
486
487
|
parentAccountNotConfigured: "To develop this project locally, run {{ authCommand }} to authenticate the App Developer Account {{ accountId }} associated with {{ accountIdentifier }}."
|
|
487
488
|
examples:
|
|
488
489
|
default: "Start local dev for the current project"
|
|
@@ -591,16 +592,18 @@ en:
|
|
|
591
592
|
logs:
|
|
592
593
|
describe: "Get execution logs for a serverless function within a project"
|
|
593
594
|
errors:
|
|
594
|
-
|
|
595
|
+
noProjectConfig: "No project detected. Run this command again from a project directory."
|
|
596
|
+
failedToFetchProjectDetails: "There was an error fetching project details"
|
|
597
|
+
noFunctionsLinkText: "Visit developer docs"
|
|
598
|
+
noFunctionsInProject: "There aren't any functions in this project\n\t- Run `{{#orange}}hs project logs --help{{/orange}}` to learn more about logs\n\t- {{link}} to learn more about serverless functions"
|
|
599
|
+
noFunctionWithName: "No function with name \"{{ name }}\""
|
|
600
|
+
functionNotDeployed: "The function with name \"{{ name }}\" is not deployed"
|
|
595
601
|
logs:
|
|
596
602
|
showingLogs: "Showing logs for:"
|
|
597
|
-
|
|
598
|
-
hubspotLogsDirectLink: "View logs in HubSpot"
|
|
603
|
+
hubspotLogsDirectLink: "View function logs in HubSpot"
|
|
599
604
|
noLogsFound: "No logs were found for \"{{ name }}\""
|
|
600
605
|
table:
|
|
601
606
|
accountHeader: "Account"
|
|
602
|
-
projectHeader: "Project"
|
|
603
|
-
appHeader: "App"
|
|
604
607
|
functionHeader: "Function"
|
|
605
608
|
endpointHeader: "Endpoint"
|
|
606
609
|
examples:
|
|
@@ -617,12 +620,8 @@ en:
|
|
|
617
620
|
describe: "Retrieve most recent log only"
|
|
618
621
|
limit:
|
|
619
622
|
describe: "Limit the number of logs to output"
|
|
620
|
-
project:
|
|
621
|
-
describe: "Project name"
|
|
622
623
|
function:
|
|
623
624
|
describe: "App function name"
|
|
624
|
-
endpoint:
|
|
625
|
-
describe: "Public endpoint path"
|
|
626
625
|
upload:
|
|
627
626
|
describe: "Upload your project files and create a new build"
|
|
628
627
|
examples:
|
|
@@ -708,6 +707,20 @@ en:
|
|
|
708
707
|
describe: "Open Github issues in your browser to report a bug."
|
|
709
708
|
general:
|
|
710
709
|
describe: "Open Github issues in your browser to give feedback."
|
|
710
|
+
installDeps:
|
|
711
|
+
help:
|
|
712
|
+
describe: "Install the dependencies for your project, or add a dependency to a subcomponent of a project"
|
|
713
|
+
installAppDepsExample: "Install the dependencies for the project"
|
|
714
|
+
addDepToSubComponentExample: "Install the dependencies to one or more project subcomponents"
|
|
715
|
+
installLocationPrompt: "Choose the project components to install the dependencies:"
|
|
716
|
+
installLocationPromptRequired: "You must choose at least one subcomponent"
|
|
717
|
+
installingDependencies: "Installing dependencies in {{directory}}"
|
|
718
|
+
installationSuccessful: "Installed dependencies in {{directory}}"
|
|
719
|
+
addingDependenciesToLocation: "Installing {{dependencies}} in {{directory}}"
|
|
720
|
+
installingDependenciesFailed: "Installing dependencies for {{directory}} failed"
|
|
721
|
+
noProjectConfig: "No project detected. Run this command from a project directory."
|
|
722
|
+
noPackageJsonInProject: "No dependencies to install. The project {{ projectName }} folder might be missing component or subcomponent files. {{ link }}"
|
|
723
|
+
packageManagerNotInstalled: "This command depends on {{ packageManager }}, install {{#bold}}{{ link }}{{/bold}}"
|
|
711
724
|
remove:
|
|
712
725
|
describe: "Delete a file or folder from HubSpot."
|
|
713
726
|
deleted: "Deleted \"{{ path }}\" from account {{ accountId }}"
|
|
@@ -765,30 +778,6 @@ en:
|
|
|
765
778
|
options:
|
|
766
779
|
account:
|
|
767
780
|
describe: "Account name or id to delete"
|
|
768
|
-
sync:
|
|
769
|
-
describe: "Sync to a sandbox account"
|
|
770
|
-
examples:
|
|
771
|
-
default: "Initiates a sync to a sandbox account."
|
|
772
|
-
force: "Skips all confirmation prompts when initiating a sync."
|
|
773
|
-
info:
|
|
774
|
-
developmentSandbox: "This will sync CRM object definitions."
|
|
775
|
-
standardSandbox: "This will sync all supported assets.
|
|
776
|
-
\nTo sync only specific assets, follow this link: {{#bold}}{{ url }}{{/bold}}"
|
|
777
|
-
sync: "\nSync direction:
|
|
778
|
-
\n- Target sandbox: {{#cyan}}{{#bold}}{{ sandboxName }}{{/bold}}{{/cyan}}
|
|
779
|
-
\n- Source account: {{#bold}}{{ parentAccountName }}{{/bold}}
|
|
780
|
-
\n\nRun {{#bold}}hs accounts use{{/bold}} to change your default account and sync to a different target sandbox."
|
|
781
|
-
warning:
|
|
782
|
-
developmentSandbox: "Syncing will update previously synced object definitions and add new ones from production to your development sandbox. Object definitions that were created in your sandbox will stay the same."
|
|
783
|
-
standardSandbox: "Syncing can have a big impact. Updates from your production account may overwrite changes in your standard sandbox. Standard sandboxes are usually shared with other Super Admins."
|
|
784
|
-
confirm:
|
|
785
|
-
developmentSandbox: "Sync CRM object definitions to {{#cyan}}{{#bold}}{{ sandboxName }}{{/bold}}{{/cyan}} from {{#bold}}{{ parentAccountName }}{{/bold}}?"
|
|
786
|
-
standardSandbox: "Sync all supported assets to {{#cyan}}{{#bold}}{{ sandboxName }}{{/bold}}{{/cyan}} from {{#bold}}{{ parentAccountName }}{{/bold}}?"
|
|
787
|
-
failure:
|
|
788
|
-
invalidAccountType: "Sync must be run in a sandbox account. Your default account is a {{ accountType }}. Run {{#bold}}hs auth{{/bold}} to connect your sandbox account to the CLI or {{#bold}}hs accounts use{{/bold}} to change your default account, then try again."
|
|
789
|
-
missingParentPortal: "The production account associated to {{#bold}}{{ sandboxName }}{{/bold}} is not connected to your HubSpot CLI.
|
|
790
|
-
\n- Run {{#bold}}hs auth{{/bold}} to connect that account to your terminal, then try again.
|
|
791
|
-
\n- Run {{#bold}}hs accounts use{{/bold}} to change your default account, if you want to sync to a different sandbox. Then try again.\n"
|
|
792
781
|
secrets:
|
|
793
782
|
describe: "Manage HubSpot secrets."
|
|
794
783
|
subcommands:
|
|
@@ -984,7 +973,6 @@ en:
|
|
|
984
973
|
failedToInitialize: "Missing required arguments to initialize Local Dev"
|
|
985
974
|
noDeployedBuild: "Your project {{#bold}}{{ projectName }}{{/bold}} exists in {{ accountIdentifier }}, but has no deployed build. Projects must be successfully deployed to be developed locally. Address any build and deploy errors your project may have, then run {{ uploadCommand }} to upload and deploy your project."
|
|
986
975
|
noComponents: "There are no components in this project."
|
|
987
|
-
noRunnableComponents: "No supported components were found under {{#bold}}{{ projectSourceDir }}{{/bold}}. Run {{ command }} to see a list of available components."
|
|
988
976
|
betaMessage: "HubSpot projects local development"
|
|
989
977
|
learnMoreLocalDevServer: "Learn more about the projects local dev server"
|
|
990
978
|
running: "Running {{#bold}}{{ projectName }}{{/bold}} locally on {{ accountIdentifier }}, waiting for changes ..."
|
|
@@ -1024,7 +1012,6 @@ en:
|
|
|
1024
1012
|
invalidPrivateAppAccount: "This project contains a private app. The account specified with the \"--account\" flag points to a developer account, which do not support the local development of private apps. Update the \"--account\" flag to point to a standard, sandbox, or developer test account, or change your default account by running {{ useCommand }}."
|
|
1025
1013
|
nonSandboxWarning: "Testing in a sandbox is strongly recommended. To switch the target account, select an option below or run {{#bold}}`hs accounts use`{{/bold}} before running the command again."
|
|
1026
1014
|
publicAppNonDeveloperTestAccountWarning: "Local development of public apps is only supported in {{#bold}}developer test accounts{{/bold}}."
|
|
1027
|
-
privateAppInAppDeveloperAccountError: "Local development of private apps is not supported in {{#bold}}app developer accounts{{/bold}}"
|
|
1028
1015
|
createNewProjectForLocalDev:
|
|
1029
1016
|
projectMustExistExplanation: "The project {{ projectName }} does not exist in the target account {{ accountIdentifier}}. This command requires the project to exist in the target account."
|
|
1030
1017
|
publicAppProjectMustExistExplanation: "The project {{ projectName }} does not exist in {{ accountIdentifier}}, the app developer account associated with your target account. This command requires the project to exist in this app developer account."
|
|
@@ -1120,12 +1107,6 @@ en:
|
|
|
1120
1107
|
projectDevCommand:
|
|
1121
1108
|
command: "hs project dev"
|
|
1122
1109
|
message: "Run {{ command }} to set up your test environment and start local development"
|
|
1123
|
-
sandboxSyncDevelopmentCommand:
|
|
1124
|
-
command: "hs sandbox sync"
|
|
1125
|
-
message: "Run {{ command }} to to update CRM object definitions in your sandbox"
|
|
1126
|
-
sandboxSyncStandardCommand:
|
|
1127
|
-
command: "hs sandbox sync"
|
|
1128
|
-
message: "Run {{ command }} to to update all supported assets to your sandbox from production"
|
|
1129
1110
|
sampleProjects:
|
|
1130
1111
|
linkText: "HubSpot's sample projects"
|
|
1131
1112
|
url: "https://developers.hubspot.com/docs/platform/sample-projects?utm_source=cli&utm_content=project_create_whats_next"
|
|
@@ -1170,16 +1151,7 @@ en:
|
|
|
1170
1151
|
confirmDefaultAccount: "Continue testing on {{#bold}}{{ accountName }} ({{ accountType }}){{/bold}}? (Y/n)"
|
|
1171
1152
|
confirmUseExistingDeveloperTestAccount: "Continue with {{ accountName }}? This account isn't currently connected to the HubSpot CLI. By continuing, you'll be prompted to generate a personal access key and connect it."
|
|
1172
1153
|
projectLogsPrompt:
|
|
1173
|
-
projectName
|
|
1174
|
-
message: "[--project] Enter the project name:"
|
|
1175
|
-
error: "Project name is required"
|
|
1176
|
-
logType:
|
|
1177
|
-
message: "Select the type of serverless function"
|
|
1178
|
-
function: "App function"
|
|
1179
|
-
endpoint: "Public endpoint"
|
|
1180
|
-
appName: "[--app] Select the app"
|
|
1181
|
-
functionName: "[--function] Enter the app function name:"
|
|
1182
|
-
endpointName: "[--endpoint] Enter the public endpoint path:"
|
|
1154
|
+
functionName: "[--function] Select function in {{#bold}}{{projectName}}{{/bold}} project"
|
|
1183
1155
|
setAsDefaultAccountPrompt:
|
|
1184
1156
|
setAsDefaultAccountMessage: "Set this account as the default?"
|
|
1185
1157
|
setAsDefaultAccount: "Account \"{{ accountName }}\" set as the default account"
|
|
@@ -1421,8 +1393,7 @@ en:
|
|
|
1421
1393
|
successDevSbInfo: "Initiated sync of object definitions from production to {{ accountName }}. It may take some time. {{ url }}"
|
|
1422
1394
|
failure:
|
|
1423
1395
|
invalidUser: "Couldn't sync {{ accountName }} because your account has been removed from {{ parentAccountName }} or your permission set doesn't allow you to sync the sandbox. To update your permissions, contact a super admin in {{ parentAccountName }}."
|
|
1424
|
-
|
|
1425
|
-
syncInProgress: "Couldn’t run the sync because there’s another sync in progress. Wait for the current sync to finish and then try again. To check the sync status, visit the sync activity log: {{ url }}."
|
|
1396
|
+
syncInProgress: "Couldn't run the sync because there's another sync in progress. Wait for the current sync to finish and then try again. To check the sync status, visit the sync activity log: {{ url }}."
|
|
1426
1397
|
notSuperAdmin: "Couldn't run the sync because you are not a super admin in {{ account }}. Ask the account owner for super admin access to the sandbox."
|
|
1427
1398
|
objectNotFound: "Couldn't sync the sandbox because {{#bold}}{{ account }}{{/bold}} may have been deleted through the UI. Run {{#bold}}hs sandbox delete{{/bold}} to remove this account from the config. "
|
|
1428
1399
|
errorHandlers:
|
|
@@ -1437,21 +1408,6 @@ en:
|
|
|
1437
1408
|
errorOccurred: "An error occurred while {{ fileAction }} {{ filepath }}."
|
|
1438
1409
|
errorExplanation: "This is the result of a system error: {{ errorMessage }}"
|
|
1439
1410
|
apiErrors:
|
|
1440
|
-
messageDetail: "{{ request }} in account {{ accountId }}"
|
|
1441
|
-
unableToUpload: 'Unable to upload "{{ payload }}.'
|
|
1442
|
-
codes:
|
|
1443
|
-
400: "The {{ messageDetail }} was bad."
|
|
1444
|
-
401: "The {{ messageDetail }} was unauthorized."
|
|
1445
|
-
403MissingScope: "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key."
|
|
1446
|
-
403Gating: "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/product-updates/in-beta?update=13899236."
|
|
1447
|
-
403: "The {{ messageDetail }} was forbidden."
|
|
1448
|
-
404Request: 'The {{ action }} failed because "{{ request }}" was not found in account {{ accountId }}.'
|
|
1449
|
-
404: "The {{ messageDetail }} was not found."
|
|
1450
|
-
429: "The {{ messageDetail }} surpassed the rate limit. Retry in one minute."
|
|
1451
|
-
503: "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists."
|
|
1452
|
-
500Generic: "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists."
|
|
1453
|
-
400Generic: "The {{ messageDetail }} failed due to a client error."
|
|
1454
|
-
generic: "The {{ messageDetail }} failed."
|
|
1455
1411
|
verifyAccessKeyAndUserAccess:
|
|
1456
1412
|
fetchScopeDataError: "Error verifying access of scopeGroup {{ scopeGroup }}: {{ error }}"
|
|
1457
1413
|
portalMissingScope: "Your account does not have access to this action. Talk to an account admin to request it."
|
|
@@ -1466,5 +1422,4 @@ en:
|
|
|
1466
1422
|
updateProject: "Please update your project to the latest version and try again."
|
|
1467
1423
|
docsLink: "Projects platform versioning (BETA)"
|
|
1468
1424
|
betaLink: "For more info, see {{ docsLink }}."
|
|
1469
|
-
|
|
1470
|
-
|
|
1425
|
+
missingScopeError: "Couldn't execute the {{ request }} because the access key for {{ accountName }} is missing required scopes. To update scopes, run {{ authCommand }}. Then deactivate the existing key and generate a new one that includes the missing scopes."
|
package/lib/LocalDevManager.js
CHANGED
|
@@ -63,7 +63,7 @@ class LocalDevManager {
|
|
|
63
63
|
this.isGithubLinked = options.isGithubLinked;
|
|
64
64
|
this.watcher = null;
|
|
65
65
|
this.uploadWarnings = {};
|
|
66
|
-
this.runnableComponents =
|
|
66
|
+
this.runnableComponents = options.runnableComponents;
|
|
67
67
|
this.activeApp = null;
|
|
68
68
|
this.activePublicAppData = null;
|
|
69
69
|
this.env = options.env;
|
|
@@ -78,27 +78,6 @@ class LocalDevManager {
|
|
|
78
78
|
logger.log(i18n(`${i18nKey}.failedToInitialize`));
|
|
79
79
|
process.exit(EXIT_CODES.ERROR);
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
// The project is empty, there is nothing to run locally
|
|
83
|
-
if (!options.components.length) {
|
|
84
|
-
logger.error(i18n(`${i18nKey}.noComponents`));
|
|
85
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// The project does not contain any components that support local development
|
|
89
|
-
if (!this.runnableComponents.length) {
|
|
90
|
-
logger.error(
|
|
91
|
-
i18n(`${i18nKey}.noRunnableComponents`, {
|
|
92
|
-
projectSourceDir: this.projectSourceDir,
|
|
93
|
-
command: uiCommandReference('hs project add'),
|
|
94
|
-
})
|
|
95
|
-
);
|
|
96
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getRunnableComponents(components) {
|
|
101
|
-
return components.filter(component => component.runnable);
|
|
102
81
|
}
|
|
103
82
|
|
|
104
83
|
async setActiveApp(appUid) {
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
jest.mock('../projects');
|
|
2
|
+
jest.mock('@hubspot/local-dev-lib/logger');
|
|
3
|
+
jest.mock('@hubspot/local-dev-lib/fs');
|
|
4
|
+
jest.mock('../ui/SpinniesManager');
|
|
5
|
+
jest.mock('fs', () => ({
|
|
6
|
+
...jest.requireActual('fs'),
|
|
7
|
+
existsSync: jest.fn().mockReturnValue(true),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const util = require('util');
|
|
11
|
+
const {
|
|
12
|
+
isGloballyInstalled,
|
|
13
|
+
installPackages,
|
|
14
|
+
getProjectPackageJsonLocations,
|
|
15
|
+
} = require('../dependencyManagement');
|
|
16
|
+
const { walk } = require('@hubspot/local-dev-lib/fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { getProjectConfig } = require('../projects');
|
|
19
|
+
const SpinniesManager = require('../ui/SpinniesManager');
|
|
20
|
+
const { existsSync } = require('fs');
|
|
21
|
+
|
|
22
|
+
describe('cli/lib/dependencyManagement', () => {
|
|
23
|
+
let execMock;
|
|
24
|
+
|
|
25
|
+
const projectDir = path.join('path', 'to', 'project');
|
|
26
|
+
const srcDir = 'src';
|
|
27
|
+
const appDir = path.join(projectDir, srcDir, 'app');
|
|
28
|
+
const appFunctionsDir = path.join(appDir, 'app.functions');
|
|
29
|
+
const extensionsDir = path.join(appDir, 'exensions');
|
|
30
|
+
const projectName = 'super cool test project';
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
execMock = jest.fn();
|
|
34
|
+
util.promisify = jest.fn().mockReturnValue(execMock);
|
|
35
|
+
getProjectConfig.mockResolvedValue({
|
|
36
|
+
projectDir,
|
|
37
|
+
projectConfig: {
|
|
38
|
+
srcDir,
|
|
39
|
+
name: projectName,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isGloballyInstalled', () => {
|
|
45
|
+
it('should return true when exec is successful', async () => {
|
|
46
|
+
const actual = await isGloballyInstalled('npm');
|
|
47
|
+
expect(actual).toBe(true);
|
|
48
|
+
expect(execMock).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(execMock).toHaveBeenCalledWith('npm --version');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return false when exec is unsuccessful', async () => {
|
|
53
|
+
execMock = jest.fn().mockImplementationOnce(() => {
|
|
54
|
+
throw new Error('unsuccessful');
|
|
55
|
+
});
|
|
56
|
+
util.promisify = jest.fn().mockReturnValueOnce(execMock);
|
|
57
|
+
const actual = await isGloballyInstalled('npm');
|
|
58
|
+
expect(actual).toBe(false);
|
|
59
|
+
expect(execMock).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(execMock).toHaveBeenCalledWith('npm --version');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('installPackages', () => {
|
|
65
|
+
it('should setup a loading spinner', async () => {
|
|
66
|
+
const packages = ['package1', 'package2'];
|
|
67
|
+
const installLocations = ['src/app/app.functions', 'src/app/extensions'];
|
|
68
|
+
await installPackages({ packages, installLocations });
|
|
69
|
+
expect(SpinniesManager.init).toHaveBeenCalledTimes(
|
|
70
|
+
installLocations.length
|
|
71
|
+
);
|
|
72
|
+
expect(SpinniesManager.add).toHaveBeenCalledTimes(
|
|
73
|
+
installLocations.length
|
|
74
|
+
);
|
|
75
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledTimes(
|
|
76
|
+
installLocations.length
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should install the provided packages in all the provided install locations', async () => {
|
|
81
|
+
const packages = ['package1', 'package2'];
|
|
82
|
+
const installLocations = ['src/app/app.functions', 'src/app/extensions'];
|
|
83
|
+
await installPackages({ packages, installLocations });
|
|
84
|
+
|
|
85
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
86
|
+
expect(SpinniesManager.add).toHaveBeenCalledTimes(
|
|
87
|
+
installLocations.length
|
|
88
|
+
);
|
|
89
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledTimes(
|
|
90
|
+
installLocations.length
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
for (const location of installLocations) {
|
|
94
|
+
expect(execMock).toHaveBeenCalledWith(
|
|
95
|
+
`npm --prefix=${location} install package1 package2`
|
|
96
|
+
);
|
|
97
|
+
expect(SpinniesManager.add).toHaveBeenCalledWith(
|
|
98
|
+
`installingDependencies-${location}`,
|
|
99
|
+
{
|
|
100
|
+
text: `Installing [package1, package2] in ${location}`,
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
expect(SpinniesManager.succeed).toHaveBeenCalledWith(
|
|
104
|
+
`installingDependencies-${location}`,
|
|
105
|
+
{
|
|
106
|
+
text: `Installed dependencies in ${location}`,
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should use the provided install locations', async () => {
|
|
113
|
+
const installLocations = ['src/app/app.functions', 'src/app/extensions'];
|
|
114
|
+
await installPackages({ installLocations });
|
|
115
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length);
|
|
116
|
+
expect(execMock).toHaveBeenCalledWith(
|
|
117
|
+
`npm --prefix=${installLocations[0]} install`
|
|
118
|
+
);
|
|
119
|
+
expect(execMock).toHaveBeenCalledWith(
|
|
120
|
+
`npm --prefix=${installLocations[1]} install`
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should locate the projects package.json files when install locations is not provided', async () => {
|
|
125
|
+
const installLocations = [
|
|
126
|
+
path.join(appFunctionsDir, 'package.json'),
|
|
127
|
+
path.join(extensionsDir, 'package.json'),
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
walk.mockResolvedValue(installLocations);
|
|
131
|
+
|
|
132
|
+
getProjectConfig.mockResolvedValue({
|
|
133
|
+
projectDir,
|
|
134
|
+
projectConfig: {
|
|
135
|
+
srcDir,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await installPackages({});
|
|
140
|
+
// Its called once per each install location, plus once to check if npm installed
|
|
141
|
+
expect(execMock).toHaveBeenCalledTimes(installLocations.length + 1);
|
|
142
|
+
expect(execMock).toHaveBeenCalledWith(
|
|
143
|
+
`npm --prefix=${appFunctionsDir} install`
|
|
144
|
+
);
|
|
145
|
+
expect(execMock).toHaveBeenCalledWith(
|
|
146
|
+
`npm --prefix=${extensionsDir} install`
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should throw an error when installing the dependencies fails', async () => {
|
|
151
|
+
execMock = jest.fn().mockImplementation(command => {
|
|
152
|
+
if (command !== 'npm --version') {
|
|
153
|
+
throw new Error('OH NO');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
util.promisify = jest.fn().mockReturnValue(execMock);
|
|
158
|
+
|
|
159
|
+
const installLocations = [
|
|
160
|
+
path.join(appFunctionsDir, 'package.json'),
|
|
161
|
+
path.join(extensionsDir, 'package.json'),
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
walk.mockResolvedValue(installLocations);
|
|
165
|
+
|
|
166
|
+
getProjectConfig.mockResolvedValue({
|
|
167
|
+
projectDir,
|
|
168
|
+
projectConfig: {
|
|
169
|
+
srcDir,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(() => installPackages({})).rejects.toThrowError(
|
|
174
|
+
`Installing dependencies for ${appFunctionsDir} failed`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(SpinniesManager.fail).toHaveBeenCalledTimes(
|
|
178
|
+
installLocations.length
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(
|
|
182
|
+
`installingDependencies-${appFunctionsDir}`,
|
|
183
|
+
{
|
|
184
|
+
text: `Installing dependencies for ${appFunctionsDir} failed`,
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
expect(SpinniesManager.fail).toHaveBeenCalledWith(
|
|
188
|
+
`installingDependencies-${extensionsDir}`,
|
|
189
|
+
{
|
|
190
|
+
text: `Installing dependencies for ${extensionsDir} failed`,
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('getProjectPackageJsonFiles', () => {
|
|
197
|
+
it('should throw an error when ran outside the boundary of a project', async () => {
|
|
198
|
+
getProjectConfig.mockResolvedValue({});
|
|
199
|
+
await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(
|
|
200
|
+
'No project detected. Run this command from a project directory.'
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should throw an error if npm is not globally installed', async () => {
|
|
205
|
+
execMock = jest.fn().mockImplementation(() => {
|
|
206
|
+
throw new Error('OH NO');
|
|
207
|
+
});
|
|
208
|
+
util.promisify = jest.fn().mockReturnValue(execMock);
|
|
209
|
+
await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(
|
|
210
|
+
/This command depends on npm, install/
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should throw an error if the project directory does not exist', async () => {
|
|
215
|
+
existsSync.mockReturnValueOnce(false);
|
|
216
|
+
await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(
|
|
217
|
+
`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files. Learn how to create a project from scratch.: https://developers.hubspot.com/beta-docs/guides/crm/intro/create-a-project`
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should ignore package.json files in certain directories', async () => {
|
|
222
|
+
const nodeModulesDir = path.join(appDir, 'node_modules');
|
|
223
|
+
const viteDir = path.join(appDir, '.vite');
|
|
224
|
+
const installLocations = [
|
|
225
|
+
path.join(appFunctionsDir, 'package.json'),
|
|
226
|
+
path.join(extensionsDir, 'package.json'),
|
|
227
|
+
path.join(viteDir, 'package.json'),
|
|
228
|
+
path.join(nodeModulesDir, 'package.json'),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
walk.mockResolvedValue(installLocations);
|
|
232
|
+
|
|
233
|
+
const actual = await getProjectPackageJsonLocations();
|
|
234
|
+
expect(actual).toEqual([appFunctionsDir, extensionsDir]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should throw an error if no package.json files are found', async () => {
|
|
238
|
+
walk.mockResolvedValue([]);
|
|
239
|
+
|
|
240
|
+
await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(
|
|
241
|
+
`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files. Learn how to create a project from scratch.: https://developers.hubspot.com/beta-docs/guides/crm/intro/create-a-project`
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
jest.mock('../projects');
|
|
2
|
+
jest.mock('@hubspot/local-dev-lib/api/projects');
|
|
3
|
+
|
|
4
|
+
const ProjectLogsManager = require('../projectLogsManager');
|
|
5
|
+
const { getProjectConfig, ensureProjectExists } = require('../projects');
|
|
6
|
+
const {
|
|
7
|
+
fetchProjectComponentsMetadata,
|
|
8
|
+
} = require('@hubspot/local-dev-lib/api/projects');
|
|
9
|
+
|
|
10
|
+
describe('cli/lib/projectLogsManager', () => {
|
|
11
|
+
const accountId = 12345678;
|
|
12
|
+
const appId = 999999;
|
|
13
|
+
const projectName = 'super cool test project';
|
|
14
|
+
const projectConfig = { projectConfig: { name: projectName } };
|
|
15
|
+
const projectId = 987654321;
|
|
16
|
+
const projectDetails = {
|
|
17
|
+
project: {
|
|
18
|
+
id: projectId,
|
|
19
|
+
deployedBuild: {
|
|
20
|
+
subbuildStatuses: {},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const function1 = {
|
|
26
|
+
componentName: 'function1',
|
|
27
|
+
type: {
|
|
28
|
+
name: 'APP_FUNCTION',
|
|
29
|
+
},
|
|
30
|
+
deployOutput: {
|
|
31
|
+
appId,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const functions = [
|
|
35
|
+
function1,
|
|
36
|
+
{
|
|
37
|
+
componentName: 'function2',
|
|
38
|
+
type: {
|
|
39
|
+
name: 'APP_FUNCTION',
|
|
40
|
+
},
|
|
41
|
+
deployOutput: {
|
|
42
|
+
appId,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
ProjectLogsManager.reset();
|
|
49
|
+
|
|
50
|
+
getProjectConfig.mockResolvedValue(projectConfig);
|
|
51
|
+
ensureProjectExists.mockResolvedValue(projectDetails);
|
|
52
|
+
fetchProjectComponentsMetadata.mockResolvedValue({
|
|
53
|
+
topLevelComponentMetadata: [
|
|
54
|
+
{
|
|
55
|
+
type: {
|
|
56
|
+
name: 'PRIVATE_APP',
|
|
57
|
+
},
|
|
58
|
+
deployOutput: {
|
|
59
|
+
appId,
|
|
60
|
+
},
|
|
61
|
+
featureComponents: [
|
|
62
|
+
...functions,
|
|
63
|
+
{
|
|
64
|
+
type: {
|
|
65
|
+
name: 'NOT_AN_APP_FUNCTION',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('init', () => {
|
|
75
|
+
it('should load the project config', async () => {
|
|
76
|
+
await ProjectLogsManager.init(accountId);
|
|
77
|
+
expect(getProjectConfig).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw an error if there is a problem with the config', async () => {
|
|
81
|
+
getProjectConfig.mockResolvedValue({});
|
|
82
|
+
await expect(async () =>
|
|
83
|
+
ProjectLogsManager.init(accountId)
|
|
84
|
+
).rejects.toThrow(
|
|
85
|
+
'No project detected. Run this command again from a project directory.'
|
|
86
|
+
);
|
|
87
|
+
expect(getProjectConfig).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should ensure the project exists', async () => {
|
|
91
|
+
await ProjectLogsManager.init(accountId);
|
|
92
|
+
expect(ensureProjectExists).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(ensureProjectExists).toHaveBeenCalledWith(accountId, projectName, {
|
|
94
|
+
allowCreate: false,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should throw an error if there is data missing from the project details', async () => {
|
|
99
|
+
ensureProjectExists.mockResolvedValue({});
|
|
100
|
+
await expect(async () =>
|
|
101
|
+
ProjectLogsManager.init(accountId)
|
|
102
|
+
).rejects.toThrow(/There was an error fetching project details/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should set all of the expected fields correctly', async () => {
|
|
106
|
+
await ProjectLogsManager.init(accountId);
|
|
107
|
+
expect(ProjectLogsManager.projectId).toEqual(projectId);
|
|
108
|
+
expect(ProjectLogsManager.projectName).toEqual(projectName);
|
|
109
|
+
expect(ProjectLogsManager.accountId).toEqual(accountId);
|
|
110
|
+
expect(ProjectLogsManager.functions).toEqual(functions);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('fetchFunctionDetails', () => {
|
|
115
|
+
it('should throw an error if the projectId is null when the method is called', async () => {
|
|
116
|
+
await expect(async () =>
|
|
117
|
+
ProjectLogsManager.fetchFunctionDetails()
|
|
118
|
+
).rejects.toThrow(
|
|
119
|
+
'No project detected. Run this command again from a project directory.'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should fetch the component metadata', async () => {
|
|
124
|
+
ProjectLogsManager.projectId = projectId;
|
|
125
|
+
ProjectLogsManager.accountId = accountId;
|
|
126
|
+
await ProjectLogsManager.fetchFunctionDetails();
|
|
127
|
+
expect(fetchProjectComponentsMetadata).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(fetchProjectComponentsMetadata).toHaveBeenCalledWith(
|
|
129
|
+
accountId,
|
|
130
|
+
projectId
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should set the functions correctly', async () => {
|
|
135
|
+
ProjectLogsManager.projectId = projectId;
|
|
136
|
+
ProjectLogsManager.accountId = accountId;
|
|
137
|
+
await ProjectLogsManager.fetchFunctionDetails();
|
|
138
|
+
expect(ProjectLogsManager.functions).toEqual(functions);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('getFunctionNames', () => {
|
|
143
|
+
it('should return an empty array if functions is nullable', async () => {
|
|
144
|
+
ProjectLogsManager.functions = undefined;
|
|
145
|
+
expect(ProjectLogsManager.getFunctionNames()).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return an array of the componentNames', async () => {
|
|
149
|
+
ProjectLogsManager.functions = functions;
|
|
150
|
+
expect(ProjectLogsManager.getFunctionNames()).toEqual([
|
|
151
|
+
'function1',
|
|
152
|
+
'function2',
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('setFunction', () => {
|
|
158
|
+
it('should throw an error when functions is nullable', async () => {
|
|
159
|
+
ProjectLogsManager.functions = undefined;
|
|
160
|
+
expect(() => ProjectLogsManager.setFunction('foo')).toThrow(
|
|
161
|
+
`There aren't any functions in this project`
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should throw an error when the provided function is invalid', async () => {
|
|
166
|
+
ProjectLogsManager.functions = functions;
|
|
167
|
+
const badName = 'foo';
|
|
168
|
+
expect(() => ProjectLogsManager.setFunction(badName)).toThrow(
|
|
169
|
+
`No function with name "${badName}"`
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should set the data correctly for public functions', async () => {
|
|
174
|
+
const functionToChoose = {
|
|
175
|
+
componentName: 'function1',
|
|
176
|
+
type: {
|
|
177
|
+
name: 'APP_FUNCTION',
|
|
178
|
+
},
|
|
179
|
+
deployOutput: {
|
|
180
|
+
endpoint: { path: 'yooooooo', method: ['GET'] },
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
ProjectLogsManager.functions = [functionToChoose];
|
|
184
|
+
ProjectLogsManager.setFunction('function1');
|
|
185
|
+
expect(ProjectLogsManager.functionName).toEqual('function1');
|
|
186
|
+
expect(ProjectLogsManager.endpointName).toEqual('yooooooo');
|
|
187
|
+
expect(ProjectLogsManager.selectedFunction).toEqual(functionToChoose);
|
|
188
|
+
expect(ProjectLogsManager.method).toEqual(['GET']);
|
|
189
|
+
expect(ProjectLogsManager.isPublicFunction).toEqual(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should set the data correctly for public functions', async () => {
|
|
193
|
+
ProjectLogsManager.functions = functions;
|
|
194
|
+
ProjectLogsManager.setFunction('function1');
|
|
195
|
+
expect(ProjectLogsManager.selectedFunction).toEqual(function1);
|
|
196
|
+
expect(ProjectLogsManager.functionName).toEqual('function1');
|
|
197
|
+
expect(ProjectLogsManager.isPublicFunction).toEqual(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('reset', () => {
|
|
202
|
+
it('should reset all the values', async () => {
|
|
203
|
+
ProjectLogsManager.someRandomField = 'value';
|
|
204
|
+
expect(ProjectLogsManager.someRandomField).toBeDefined();
|
|
205
|
+
|
|
206
|
+
ProjectLogsManager.reset();
|
|
207
|
+
expect(ProjectLogsManager.isPublicFunction).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|