@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.
Files changed (35) hide show
  1. package/commands/__tests__/projects.test.js +105 -0
  2. package/commands/accounts/clean.js +1 -1
  3. package/commands/project/__tests__/deploy.test.js +1 -1
  4. package/commands/project/__tests__/installDeps.test.js +168 -0
  5. package/commands/project/__tests__/logs.test.js +305 -0
  6. package/commands/project/cloneApp.js +3 -16
  7. package/commands/project/deploy.js +4 -1
  8. package/commands/project/dev.js +15 -6
  9. package/commands/project/download.js +4 -1
  10. package/commands/project/installDeps.js +78 -0
  11. package/commands/project/logs.js +80 -242
  12. package/commands/project/migrateApp.js +3 -6
  13. package/commands/project/upload.js +5 -3
  14. package/commands/project/watch.js +3 -9
  15. package/commands/project.js +2 -0
  16. package/commands/sandbox/create.js +1 -0
  17. package/commands/sandbox.js +0 -2
  18. package/lang/en.lyaml +25 -70
  19. package/lib/LocalDevManager.js +1 -22
  20. package/lib/__tests__/dependencyManagement.test.js +245 -0
  21. package/lib/__tests__/projectLogsManager.test.js +210 -0
  22. package/lib/dependencyManagement.js +157 -0
  23. package/lib/errorHandlers/apiErrors.js +1 -3
  24. package/lib/errorHandlers/overrideErrors.js +57 -36
  25. package/lib/localDev.js +1 -7
  26. package/lib/projectLogsManager.js +144 -0
  27. package/lib/projects.js +3 -6
  28. package/lib/projectsWatch.js +2 -5
  29. package/lib/prompts/__tests__/projectsLogsPrompt.test.js +46 -0
  30. package/lib/prompts/createProjectPrompt.js +4 -0
  31. package/lib/prompts/projectDevTargetAccountPrompt.js +16 -25
  32. package/lib/prompts/projectsLogsPrompt.js +17 -108
  33. package/lib/sandboxSync.js +13 -15
  34. package/package.json +6 -6
  35. 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
- invalidAppName: "Could not find app with name \"{{ appName }}\" in project \"{{ projectName }}\""
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
- hubspotLogsLink: "View private apps in HubSpot"
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
- missingScopes: "Couldnt run the sync because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{#bold}}{{ accountName }}{{/bold}}, and generate a new one. Then run `hs auth` to update the CLI with the new key."
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."
@@ -63,7 +63,7 @@ class LocalDevManager {
63
63
  this.isGithubLinked = options.isGithubLinked;
64
64
  this.watcher = null;
65
65
  this.uploadWarnings = {};
66
- this.runnableComponents = this.getRunnableComponents(options.components);
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
+ });