@hubspot/cli 7.6.0-beta.11 → 7.6.0-beta.13

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 (158) hide show
  1. package/commands/app/__tests__/migrate.test.js +1 -0
  2. package/commands/getStarted.js +7 -20
  3. package/commands/mcp/setup.d.ts +0 -1
  4. package/commands/mcp/setup.js +11 -11
  5. package/commands/project/__tests__/add.test.js +64 -0
  6. package/commands/project/__tests__/create.test.js +57 -0
  7. package/commands/project/__tests__/deploy.test.js +3 -2
  8. package/commands/project/add.d.ts +1 -1
  9. package/commands/project/add.js +3 -5
  10. package/commands/project/create.js +7 -2
  11. package/commands/project/deploy.js +9 -61
  12. package/commands/project/dev/index.js +1 -1
  13. package/commands/project/dev/unifiedFlow.js +4 -1
  14. package/commands/project/migrate.js +26 -7
  15. package/commands/project/upload.js +2 -2
  16. package/commands/project/validate.js +1 -1
  17. package/commands/project/watch.js +2 -2
  18. package/lang/en.d.ts +20 -5
  19. package/lang/en.js +38 -22
  20. package/lang/en.lyaml +12 -12
  21. package/lib/__tests__/hasFeature.test.js +145 -7
  22. package/lib/__tests__/importData.test.js +1 -1
  23. package/lib/app/__tests__/migrate.test.js +14 -51
  24. package/lib/app/migrate.d.ts +2 -8
  25. package/lib/app/migrate.js +5 -73
  26. package/lib/constants.d.ts +4 -0
  27. package/lib/constants.js +4 -0
  28. package/lib/errorHandlers/index.d.ts +4 -0
  29. package/lib/errorHandlers/index.js +1 -1
  30. package/lib/hasFeature.js +6 -0
  31. package/lib/importData.js +1 -1
  32. package/lib/links.d.ts +1 -0
  33. package/lib/links.js +10 -3
  34. package/lib/mcp/setup.d.ts +0 -2
  35. package/lib/mcp/setup.js +4 -29
  36. package/lib/projects/__tests__/AppDevModeInterface.test.js +71 -44
  37. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  38. package/lib/projects/__tests__/components.test.js +164 -7
  39. package/lib/projects/__tests__/deploy.test.js +164 -0
  40. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  41. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  42. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  43. package/lib/projects/add/__tests__/v3AddComponent.test.js +142 -8
  44. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  45. package/lib/projects/add/legacyAddComponent.js +5 -1
  46. package/lib/projects/add/v3AddComponent.d.ts +2 -1
  47. package/lib/projects/add/v3AddComponent.js +22 -5
  48. package/lib/projects/components.d.ts +1 -0
  49. package/lib/projects/components.js +27 -1
  50. package/lib/projects/create/__tests__/v3.test.js +97 -9
  51. package/lib/projects/create/index.js +2 -2
  52. package/lib/projects/create/legacy.js +1 -1
  53. package/lib/projects/create/v3.d.ts +2 -2
  54. package/lib/projects/create/v3.js +35 -12
  55. package/lib/projects/deploy.d.ts +13 -0
  56. package/lib/projects/deploy.js +63 -0
  57. package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -3
  58. package/lib/projects/localDev/AppDevModeInterface.js +110 -47
  59. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  60. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  61. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  62. package/lib/projects/localDev/LocalDevState.js +9 -1
  63. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  64. package/lib/projects/localDev/helpers/project.js +6 -8
  65. package/lib/projects/platformVersion.d.ts +1 -0
  66. package/lib/projects/platformVersion.js +10 -0
  67. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  68. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  69. package/lib/projects/upload.js +1 -1
  70. package/lib/projects/urls.d.ts +1 -0
  71. package/lib/projects/urls.js +3 -0
  72. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  73. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  74. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  75. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  76. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  77. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  78. package/lib/prompts/installAppPrompt.d.ts +6 -1
  79. package/lib/prompts/installAppPrompt.js +6 -1
  80. package/lib/prompts/projectAddPrompt.js +1 -1
  81. package/lib/prompts/promptUtils.d.ts +5 -0
  82. package/lib/prompts/promptUtils.js +9 -0
  83. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  84. package/lib/theme/__tests__/migrate.test.d.ts +1 -0
  85. package/lib/theme/__tests__/migrate.test.js +233 -0
  86. package/lib/theme/migrate.d.ts +13 -0
  87. package/lib/theme/migrate.js +90 -0
  88. package/lib/ui/index.js +3 -6
  89. package/lib/usageTracking.js +2 -2
  90. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  91. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  92. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  93. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  94. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  95. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  96. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  97. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  98. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  99. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  100. package/mcp-server/tools/cms/HsListTool.js +1 -1
  101. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  102. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  103. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  106. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  107. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  108. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  109. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  110. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  111. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  112. package/mcp-server/tools/index.js +13 -1
  113. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  114. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  115. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  116. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  117. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  118. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  119. package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
  120. package/mcp-server/tools/project/DocsSearchTool.js +7 -7
  121. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
  122. package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
  123. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  124. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  125. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  126. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  127. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  128. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  129. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  130. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
  131. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
  132. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  133. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  134. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  135. package/mcp-server/tools/project/constants.d.ts +1 -1
  136. package/mcp-server/tools/project/constants.js +14 -6
  137. package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
  138. package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
  139. package/mcp-server/utils/cliConfig.d.ts +1 -0
  140. package/mcp-server/utils/cliConfig.js +12 -0
  141. package/package.json +4 -3
  142. package/types/LocalDev.d.ts +2 -1
  143. package/types/Projects.d.ts +1 -0
  144. package/ui/components/BoxWithTitle.d.ts +8 -0
  145. package/ui/components/BoxWithTitle.js +9 -0
  146. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  147. package/ui/components/HorizontalSelectPrompt.js +30 -0
  148. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  149. package/ui/components/StatusMessageBoxes.js +31 -0
  150. package/ui/lib/ui-testing-utils.d.ts +9 -0
  151. package/ui/lib/ui-testing-utils.js +47 -0
  152. package/ui/lib/useTerminalSize.d.ts +13 -0
  153. package/ui/lib/useTerminalSize.js +31 -0
  154. package/ui/styles.d.ts +18 -0
  155. package/ui/styles.js +18 -0
  156. package/ui/views/UiSandbox.d.ts +5 -0
  157. package/ui/views/UiSandbox.js +25 -0
  158. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
@@ -10,6 +10,7 @@ vi.mock('@hubspot/local-dev-lib/config');
10
10
  vi.mock('@hubspot/local-dev-lib/logger');
11
11
  vi.mock('../../../lib/app/migrate');
12
12
  vi.mock('../../../lib/app/migrate_legacy');
13
+ vi.mock('../../../lib/projects/config.js');
13
14
  const mockYargs = yargs;
14
15
  const mockedGetAccountConfig = getAccountConfig;
15
16
  const mockedMigrateApp2023_2 = migrateApp2023_2;
@@ -16,7 +16,8 @@ import { handleProjectUpload } from '../lib/projects/upload.js';
16
16
  import { PROJECT_CONFIG_FILE, GET_STARTED_OPTIONS, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, } from '../lib/constants.js';
17
17
  import { writeProjectConfig, getProjectConfig, validateProjectConfig, } from '../lib/projects/config.js';
18
18
  import { getProjectPackageJsonLocations, installPackages, } from '../lib/dependencyManagement.js';
19
- import { pollProjectBuildAndDeploy, useV3Api, } from '../lib/projects/buildAndDeploy.js';
19
+ import { pollProjectBuildAndDeploy } from '../lib/projects/pollProjectBuildAndDeploy.js';
20
+ import { useV3Api } from '../lib/projects/platformVersion.js';
20
21
  import { openLink } from '../lib/links.js';
21
22
  import { getStaticAuthAppInstallUrl } from '../lib/app/urls.js';
22
23
  import { getEnv } from '@hubspot/local-dev-lib/config';
@@ -28,10 +29,10 @@ async function handler(args) {
28
29
  const { derivedAccountId } = args;
29
30
  const env = getEnv(derivedAccountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD;
30
31
  await trackCommandUsage('get-started', {}, derivedAccountId);
31
- // TODO: Put this in constants.ts once we have a defined place for the template before INBOUND
32
- const templateSource = 'robrown-hubspot/hubspot-project-components-ua-app-objects-beta';
32
+ const accountName = uiAccountDescription(derivedAccountId);
33
33
  uiInfoSection(commands.getStarted.startTitle, () => {
34
34
  uiLogger.log(commands.getStarted.startDescription);
35
+ uiLogger.log(commands.getStarted.guideOverview(accountName));
35
36
  });
36
37
  const { default: selectedOption } = await promptUser([
37
38
  {
@@ -80,16 +81,7 @@ async function handler(args) {
80
81
  else {
81
82
  uiLogger.log(' ');
82
83
  uiLogger.log(commands.getStarted.logs.appSelected);
83
- // 1. Fetch project templates
84
- let latestRepoReleaseTag;
85
84
  const { dest, name } = await projectNameAndDestPrompt(args);
86
- // Specific template for get-started command
87
- const projectTemplate = {
88
- name: 'private-app-get-started-template',
89
- label: 'CRM getting started project with private apps',
90
- path: 'projects/private-app-get-started-template',
91
- };
92
- // 3. Create the project files
93
85
  const projectDest = path.resolve(getCwd(), dest);
94
86
  const { projectConfig: existingProjectConfig, projectDir: existingProjectDir, } = await getProjectConfig(projectDest);
95
87
  if (existingProjectConfig &&
@@ -104,13 +96,10 @@ async function handler(args) {
104
96
  uiLogger.error(commands.project.create.errors.cannotNestProjects(existingProjectDir));
105
97
  process.exit(EXIT_CODES.ERROR);
106
98
  }
107
- const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
108
99
  // 4. Clone the project template from GitHub
109
- // This is temporary until we have the UA template in the main repo
110
100
  try {
111
- await cloneGithubRepo(repo, projectDest, {
112
- sourceDir: projectTemplate.path,
113
- tag: latestRepoReleaseTag,
101
+ await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, projectDest, {
102
+ sourceDir: '2025.2/private-app-get-started-template',
114
103
  hideLogs: true,
115
104
  });
116
105
  await trackCommandMetadataUsage('get-started', {
@@ -163,7 +152,6 @@ async function handler(args) {
163
152
  uiLogger.log(' ');
164
153
  }
165
154
  // 6. Ask user if they want to upload the project
166
- const accountName = uiAccountDescription(derivedAccountId);
167
155
  const { shouldUpload } = await promptUser([
168
156
  {
169
157
  type: 'confirm',
@@ -192,12 +180,11 @@ async function handler(args) {
192
180
  process.exit(EXIT_CODES.ERROR);
193
181
  }
194
182
  validateProjectConfig(newProjectConfig, newProjectDir);
195
- const targetAccountId = derivedAccountId;
196
183
  uiLogger.log(' ');
197
184
  uiLogger.log(commands.getStarted.logs.uploadingProject);
198
185
  uiLogger.log(' ');
199
186
  const { result, uploadError } = await handleProjectUpload({
200
- accountId: targetAccountId,
187
+ accountId: derivedAccountId,
201
188
  projectConfig: newProjectConfig,
202
189
  projectDir: newProjectDir,
203
190
  callbackFunc: pollProjectBuildAndDeploy,
@@ -1,7 +1,6 @@
1
1
  import { CommonArgs, YargsCommandModule } from '../../types/Yargs.js';
2
2
  interface MCPSetupArgs extends CommonArgs {
3
3
  client?: string[];
4
- addDocsSearch?: boolean;
5
4
  }
6
5
  declare const mcpSetupCommand: YargsCommandModule<unknown, MCPSetupArgs>;
7
6
  export default mcpSetupCommand;
@@ -2,11 +2,19 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
2
2
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
3
3
  import { commands } from '../../lang/en.js';
4
4
  import { uiLogger } from '../../lib/ui/logger.js';
5
- import { addMcpServerToConfig, addMintlifyMcpServer, supportedTools, } from '../../lib/mcp/setup.js';
5
+ import { addMcpServerToConfig, supportedTools } from '../../lib/mcp/setup.js';
6
6
  import { trackCommandUsage } from '../../lib/usageTracking.js';
7
+ import { hasFeature } from '../../lib/hasFeature.js';
8
+ import { FEATURES } from '../../lib/constants.js';
7
9
  const command = ['setup', 'update'];
8
10
  const describe = undefined; // Leave hidden for now
9
11
  async function handler(args) {
12
+ const { derivedAccountId } = args;
13
+ const hasMcpAccess = await hasFeature(derivedAccountId, FEATURES.MCP_ACCESS);
14
+ if (!hasMcpAccess) {
15
+ uiLogger.error(commands.mcp.setup.errors.needsMcpAccess(derivedAccountId));
16
+ process.exit(EXIT_CODES.ERROR);
17
+ }
10
18
  try {
11
19
  await import('@modelcontextprotocol/sdk/server/mcp.js');
12
20
  }
@@ -16,10 +24,7 @@ async function handler(args) {
16
24
  }
17
25
  trackCommandUsage('mcp-setup', {}, args.derivedAccountId);
18
26
  try {
19
- const derivedTargets = await addMcpServerToConfig(args.client);
20
- if (args.addDocsSearch) {
21
- await addMintlifyMcpServer(derivedTargets);
22
- }
27
+ await addMcpServerToConfig(args.client);
23
28
  }
24
29
  catch (e) {
25
30
  process.exit(EXIT_CODES.ERROR);
@@ -27,15 +32,10 @@ async function handler(args) {
27
32
  process.exit(EXIT_CODES.SUCCESS);
28
33
  }
29
34
  function setupBuilder(yargs) {
30
- yargs
31
- .option('client', {
35
+ yargs.option('client', {
32
36
  describe: commands.mcp.setup.args.client,
33
37
  type: 'array',
34
38
  choices: [...supportedTools.map(tool => tool.value)],
35
- })
36
- .option('add-docs-search', {
37
- type: 'boolean',
38
- hidden: true,
39
39
  });
40
40
  return yargs;
41
41
  }
@@ -1,7 +1,22 @@
1
1
  import yargs from 'yargs';
2
2
  import projectAddCommand from '../add.js';
3
3
  import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../../lib/constants.js';
4
+ import { v3AddComponent } from '../../../lib/projects/add/v3AddComponent.js';
5
+ import { legacyAddComponent } from '../../../lib/projects/add/legacyAddComponent.js';
6
+ import { getProjectConfig } from '../../../lib/projects/config.js';
7
+ import { useV3Api } from '../../../lib/projects/platformVersion.js';
8
+ import { trackCommandUsage } from '../../../lib/usageTracking.js';
4
9
  vi.mock('../../../lib/commonOpts');
10
+ vi.mock('../../../lib/projects/add/v3AddComponent');
11
+ vi.mock('../../../lib/projects/add/legacyAddComponent');
12
+ vi.mock('../../../lib/projects/config');
13
+ vi.mock('../../../lib/projects/platformVersion');
14
+ vi.mock('../../../lib/usageTracking');
15
+ const mockedV3AddComponent = vi.mocked(v3AddComponent);
16
+ const mockedLegacyAddComponent = vi.mocked(legacyAddComponent);
17
+ const mockedGetProjectConfig = vi.mocked(getProjectConfig);
18
+ const mockedUseV3Api = vi.mocked(useV3Api);
19
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
5
20
  describe('commands/project/add', () => {
6
21
  const yargsMock = yargs;
7
22
  describe('command', () => {
@@ -40,4 +55,53 @@ describe('commands/project/add', () => {
40
55
  });
41
56
  });
42
57
  });
58
+ describe('handler', () => {
59
+ const mockProjectConfig = {
60
+ name: 'test-project',
61
+ srcDir: 'src',
62
+ platformVersion: 'v3',
63
+ };
64
+ const mockProjectDir = '/path/to/project';
65
+ const mockArgs = {
66
+ derivedAccountId: 123,
67
+ name: 'test-component',
68
+ type: 'module',
69
+ };
70
+ beforeEach(() => {
71
+ mockedGetProjectConfig.mockResolvedValue({
72
+ projectConfig: mockProjectConfig,
73
+ projectDir: mockProjectDir,
74
+ });
75
+ mockedTrackCommandUsage.mockResolvedValue();
76
+ mockedV3AddComponent.mockResolvedValue();
77
+ mockedLegacyAddComponent.mockResolvedValue();
78
+ vi.spyOn(process, 'exit').mockImplementation(() => {
79
+ throw new Error('process.exit called');
80
+ });
81
+ });
82
+ it('should call v3AddComponent with accountId for v3 projects', async () => {
83
+ mockedUseV3Api.mockReturnValue(true);
84
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
85
+ expect(mockedV3AddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
86
+ expect(mockedLegacyAddComponent).not.toHaveBeenCalled();
87
+ });
88
+ it('should call legacyAddComponent for non-v3 projects', async () => {
89
+ mockedUseV3Api.mockReturnValue(false);
90
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
91
+ expect(mockedLegacyAddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
92
+ expect(mockedV3AddComponent).not.toHaveBeenCalled();
93
+ });
94
+ it('should exit with error when project config is not found', async () => {
95
+ mockedGetProjectConfig.mockResolvedValue({
96
+ projectConfig: null,
97
+ projectDir: null,
98
+ });
99
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
100
+ throw new Error('process.exit called');
101
+ });
102
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
103
+ expect(mockExit).toHaveBeenCalledWith(1);
104
+ mockExit.mockRestore();
105
+ });
106
+ });
43
107
  });
@@ -27,14 +27,71 @@ describe('commands/project/create', () => {
27
27
  it('should define project creation options', () => {
28
28
  const optionsSpy = vi.spyOn(yargsMock, 'options');
29
29
  const exampleSpy = vi.spyOn(yargsMock, 'example');
30
+ const conflictsSpy = vi.spyOn(yargsMock, 'conflicts');
30
31
  projectCreateCommand.builder(yargsMock);
31
32
  expect(optionsSpy).toHaveBeenCalledWith(expect.objectContaining({
32
33
  name: expect.any(Object),
33
34
  dest: expect.any(Object),
34
35
  template: expect.any(Object),
35
36
  'template-source': expect.any(Object),
37
+ 'platform-version': expect.any(Object),
38
+ 'project-base': expect.any(Object),
39
+ distribution: expect.any(Object),
40
+ auth: expect.any(Object),
41
+ features: expect.any(Object),
36
42
  }));
43
+ expect(conflictsSpy).toHaveBeenCalledWith('template', 'features');
37
44
  expect(exampleSpy).toHaveBeenCalled();
38
45
  });
46
+ it('should define platform version option with correct choices', () => {
47
+ const optionsSpy = vi.spyOn(yargsMock, 'options');
48
+ projectCreateCommand.builder(yargsMock);
49
+ const optionsCall = optionsSpy.mock.calls[0][0];
50
+ expect(optionsCall['platform-version']).toEqual(expect.objectContaining({
51
+ hidden: true,
52
+ type: 'string',
53
+ choices: ['2023.2', '2025.1', '2025.2'],
54
+ default: '2023.2',
55
+ }));
56
+ });
57
+ it('should define project base option with correct choices', () => {
58
+ const optionsSpy = vi.spyOn(yargsMock, 'options');
59
+ projectCreateCommand.builder(yargsMock);
60
+ const optionsCall = optionsSpy.mock.calls[0][0];
61
+ expect(optionsCall['project-base']).toEqual(expect.objectContaining({
62
+ hidden: true,
63
+ type: 'string',
64
+ choices: ['empty', 'app'],
65
+ }));
66
+ });
67
+ it('should define distribution option with correct choices', () => {
68
+ const optionsSpy = vi.spyOn(yargsMock, 'options');
69
+ projectCreateCommand.builder(yargsMock);
70
+ const optionsCall = optionsSpy.mock.calls[0][0];
71
+ expect(optionsCall.distribution).toEqual(expect.objectContaining({
72
+ hidden: true,
73
+ type: 'string',
74
+ choices: ['private', 'marketplace'],
75
+ }));
76
+ });
77
+ it('should define auth option with correct choices', () => {
78
+ const optionsSpy = vi.spyOn(yargsMock, 'options');
79
+ projectCreateCommand.builder(yargsMock);
80
+ const optionsCall = optionsSpy.mock.calls[0][0];
81
+ expect(optionsCall.auth).toEqual(expect.objectContaining({
82
+ hidden: true,
83
+ type: 'string',
84
+ choices: ['oauth', 'static'],
85
+ }));
86
+ });
87
+ it('should define features option as array', () => {
88
+ const optionsSpy = vi.spyOn(yargsMock, 'options');
89
+ projectCreateCommand.builder(yargsMock);
90
+ const optionsCall = optionsSpy.mock.calls[0][0];
91
+ expect(optionsCall.features).toEqual(expect.objectContaining({
92
+ hidden: true,
93
+ type: 'array',
94
+ }));
95
+ });
39
96
  });
40
97
  });
@@ -8,7 +8,7 @@ import * as ui from '../../../lib/ui/index.js';
8
8
  import { addAccountOptions, addConfigOptions, addJSONOutputOptions, addUseEnvironmentOptions, } from '../../../lib/commonOpts.js';
9
9
  import * as projectUtils from '../../../lib/projects/config.js';
10
10
  import * as projectUrlUtils from '../../../lib/projects/urls.js';
11
- import { pollDeployStatus } from '../../../lib/projects/buildAndDeploy.js';
11
+ import { pollDeployStatus } from '../../../lib/projects/pollProjectBuildAndDeploy.js';
12
12
  import * as projectNamePrompt from '../../../lib/prompts/projectNamePrompt.js';
13
13
  import * as promptUtils from '../../../lib/prompts/promptUtils.js';
14
14
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
@@ -23,7 +23,8 @@ vi.mock('../../../lib/commonOpts');
23
23
  vi.mock('../../../lib/validation');
24
24
  vi.mock('../../../lib/projects/config');
25
25
  vi.mock('../../../lib/projects/urls');
26
- vi.mock('../../../lib/projects/buildAndDeploy');
26
+ vi.mock('../../../lib/projects/pollProjectBuildAndDeploy');
27
+ vi.mock('../../../lib/projects/platformVersion');
27
28
  vi.mock('../../../lib/prompts/projectNamePrompt');
28
29
  vi.mock('../../../lib/prompts/promptUtils');
29
30
  vi.mock('../../../lib/usageTracking');
@@ -1,5 +1,5 @@
1
1
  import { YargsCommandModule, CommonArgs } from '../../types/Yargs.js';
2
- type ProjectAddArgs = CommonArgs & {
2
+ export type ProjectAddArgs = CommonArgs & {
3
3
  type?: string;
4
4
  name?: string;
5
5
  features?: string[];
@@ -1,11 +1,10 @@
1
1
  import { logError } from '../../lib/errorHandlers/index.js';
2
- import { trackCommandUsage } from '../../lib/usageTracking.js';
3
2
  import { getProjectConfig } from '../../lib/projects/config.js';
4
3
  import { uiBetaTag } from '../../lib/ui/index.js';
5
4
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
6
5
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
7
6
  import { commands } from '../../lang/en.js';
8
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
7
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
9
8
  import { legacyAddComponent } from '../../lib/projects/add/legacyAddComponent.js';
10
9
  import { v3AddComponent } from '../../lib/projects/add/v3AddComponent.js';
11
10
  import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../lib/constants.js';
@@ -15,7 +14,6 @@ const describe = uiBetaTag(commands.project.add.describe, false);
15
14
  async function handler(args) {
16
15
  try {
17
16
  const { derivedAccountId } = args;
18
- trackCommandUsage('project-add', undefined, derivedAccountId);
19
17
  const { projectConfig, projectDir } = await getProjectConfig();
20
18
  if (!projectDir || !projectConfig) {
21
19
  uiLogger.error(commands.project.add.error.locationInProject);
@@ -23,10 +21,10 @@ async function handler(args) {
23
21
  }
24
22
  const isV3ProjectCreate = useV3Api(projectConfig.platformVersion);
25
23
  if (isV3ProjectCreate) {
26
- await v3AddComponent(args, projectDir, projectConfig);
24
+ await v3AddComponent(args, projectDir, projectConfig, derivedAccountId);
27
25
  }
28
26
  else {
29
- await legacyAddComponent(args, projectDir, projectConfig);
27
+ await legacyAddComponent(args, projectDir, projectConfig, derivedAccountId);
30
28
  }
31
29
  }
32
30
  catch (e) {
@@ -16,6 +16,8 @@ import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
16
16
  import { commands } from '../../lang/en.js';
17
17
  import { uiLogger } from '../../lib/ui/logger.js';
18
18
  import { handleProjectCreationFlow, } from '../../lib/projects/create/index.js';
19
+ import { getProjectMetadata, } from '@hubspot/project-parsing-lib/src/lib/project.js';
20
+ import { updateHsMetaFilesWithAutoGeneratedFields } from '../../lib/projects/components.js';
19
21
  const command = ['create', 'init'];
20
22
  const describe = uiBetaTag(commands.project.create.describe, false);
21
23
  const { v2023_2, v2025_1, v2025_2 } = PLATFORM_VERSIONS;
@@ -39,7 +41,7 @@ async function handler(args) {
39
41
  type: selectProjectTemplatePromptResponse.projectTemplate?.name ||
40
42
  (selectProjectTemplatePromptResponse.componentTemplates || [])
41
43
  // @ts-expect-error
42
- .map((item) => item.label)
44
+ .map((item) => item.type)
43
45
  .join(','),
44
46
  }, derivedAccountId);
45
47
  const projectDest = path.resolve(getCwd(), projectNameAndDestPromptResponse.dest);
@@ -73,10 +75,13 @@ async function handler(args) {
73
75
  }
74
76
  const projectConfigPath = path.join(projectDest, PROJECT_CONFIG_FILE);
75
77
  const parsedConfigFile = JSON.parse(fs.readFileSync(projectConfigPath).toString());
78
+ const projectName = projectNameAndDestPromptResponse.name;
76
79
  writeProjectConfig(projectConfigPath, {
77
80
  ...parsedConfigFile,
78
- name: projectNameAndDestPromptResponse.name,
81
+ name: projectName,
79
82
  });
83
+ const projectMetadata = await getProjectMetadata(path.join(projectDest, parsedConfigFile.srcDir));
84
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, projectMetadata.hsMetaFiles);
80
85
  // If the template is 'no-template', we need to manually create a src directory
81
86
  if (selectProjectTemplatePromptResponse.projectTemplate?.name ===
82
87
  EMPTY_PROJECT_TEMPLATE_NAME ||
@@ -1,11 +1,10 @@
1
- import { deployProject, fetchProject, } from '@hubspot/local-dev-lib/api/projects';
1
+ import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
2
2
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
3
3
  import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
4
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
4
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
5
5
  import { trackCommandUsage } from '../../lib/usageTracking.js';
6
6
  import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
7
7
  import { getProjectConfig } from '../../lib/projects/config.js';
8
- import { pollDeployStatus } from '../../lib/projects/buildAndDeploy.js';
9
8
  import { projectNamePrompt } from '../../lib/prompts/projectNamePrompt.js';
10
9
  import { promptUser } from '../../lib/prompts/promptUtils.js';
11
10
  import { uiBetaTag, uiLine } from '../../lib/ui/index.js';
@@ -13,54 +12,11 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
13
12
  import { uiLogger } from '../../lib/ui/logger.js';
14
13
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
15
14
  import { loadProfile, logProfileFooter, logProfileHeader, exitIfUsingProfiles, } from '../../lib/projectProfiles.js';
16
- import { PROJECT_ERROR_TYPES, PROJECT_DEPLOY_TEXT, } from '../../lib/constants.js';
15
+ import { PROJECT_DEPLOY_TEXT } from '../../lib/constants.js';
17
16
  import { commands } from '../../lang/en.js';
17
+ import { handleProjectDeploy, validateBuildIdForDeploy, logDeployErrors, } from '../../lib/projects/deploy.js';
18
18
  const command = 'deploy';
19
19
  const describe = uiBetaTag(commands.project.deploy.describe, false);
20
- function validateBuildId(buildId, deployedBuildId, latestBuildId, projectName, accountId) {
21
- if (Number(buildId) > latestBuildId) {
22
- return commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName);
23
- }
24
- if (Number(buildId) === deployedBuildId) {
25
- return commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName);
26
- }
27
- return true;
28
- }
29
- function logDeployErrors(errorData) {
30
- uiLogger.error(errorData.message);
31
- errorData.errors.forEach(err => {
32
- // This is how the pre-deploy check manifests itself in < 2025.2 projects
33
- if (err.subCategory === PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS) {
34
- uiLogger.log(commands.project.deploy.errors.deployContainsRemovals(err.context.COMPONENT_NAME));
35
- }
36
- else {
37
- uiLogger.log(err.message);
38
- }
39
- });
40
- }
41
- function handleBlockedDeploy(deployResp) {
42
- const deployCanBeForced = deployResp.issues.every(issue => issue.blockingMessages.every(message => message.isWarning));
43
- uiLogger.log('');
44
- if (deployCanBeForced) {
45
- uiLogger.warn(commands.project.deploy.errors.deployWarningsHeader);
46
- uiLogger.log('');
47
- }
48
- else {
49
- uiLogger.error(commands.project.deploy.errors.deployBlockedHeader);
50
- uiLogger.log('');
51
- }
52
- deployResp.issues.forEach(issue => {
53
- if (issue.blockingMessages.length > 0) {
54
- issue.blockingMessages.forEach(message => {
55
- uiLogger.log(commands.project.deploy.errors.deployIssueComponentWarning(issue.uid, issue.componentTypeName, message.message));
56
- });
57
- }
58
- else {
59
- uiLogger.log(commands.project.deploy.errors.deployIssueComponentGeneric(issue.uid, issue.componentTypeName));
60
- }
61
- uiLogger.log('');
62
- });
63
- }
64
20
  async function handler(args) {
65
21
  const { derivedAccountId, project: projectOption, buildId: buildIdOption, force: forceOption, deployLatestBuild: deployLatestBuildOption, json: formatOutputAsJson, } = args;
66
22
  const accountConfig = getAccountConfig(derivedAccountId);
@@ -105,7 +61,7 @@ async function handler(args) {
105
61
  return process.exit(EXIT_CODES.ERROR);
106
62
  }
107
63
  if (buildIdToDeploy) {
108
- const validationResult = validateBuildId(buildIdToDeploy, deployedBuildId, latestBuild.buildId, projectName, targetAccountId);
64
+ const validationResult = validateBuildIdForDeploy(buildIdToDeploy, deployedBuildId, latestBuild.buildId, projectName, targetAccountId);
109
65
  if (validationResult !== true) {
110
66
  uiLogger.error(validationResult.toString());
111
67
  return process.exit(EXIT_CODES.ERROR);
@@ -122,7 +78,7 @@ async function handler(args) {
122
78
  default: latestBuild.buildId === deployedBuildId
123
79
  ? undefined
124
80
  : latestBuild.buildId,
125
- validate: buildId => validateBuildId(buildId, deployedBuildId, latestBuild.buildId, projectName, targetAccountId),
81
+ validate: buildId => validateBuildIdForDeploy(buildId, deployedBuildId, latestBuild.buildId, projectName, targetAccountId),
126
82
  });
127
83
  buildIdToDeploy = deployBuildIdPromptResponse.buildId;
128
84
  }
@@ -131,21 +87,13 @@ async function handler(args) {
131
87
  uiLogger.error(commands.project.deploy.errors.noBuildId);
132
88
  return process.exit(EXIT_CODES.ERROR);
133
89
  }
134
- const { data: deployResp } = await deployProject(targetAccountId, projectName, buildIdToDeploy, useV3Api(projectConfig?.platformVersion), forceOption);
135
- if (!deployResp || deployResp.buildResultType !== 'DEPLOY_QUEUED') {
136
- if (deployResp?.buildResultType === 'DEPLOY_BLOCKED') {
137
- handleBlockedDeploy(deployResp);
138
- process.exit(EXIT_CODES.ERROR);
139
- }
140
- else {
141
- uiLogger.error(commands.project.deploy.errors.deploy);
142
- }
90
+ const deployResult = await handleProjectDeploy(targetAccountId, projectName, buildIdToDeploy, useV3Api(projectConfig?.platformVersion), forceOption);
91
+ if (!deployResult) {
143
92
  return process.exit(EXIT_CODES.ERROR);
144
93
  }
145
94
  else if (formatOutputAsJson) {
146
- jsonOutput.deployId = Number(deployResp.id);
95
+ jsonOutput.deployId = deployResult.deployId;
147
96
  }
148
- const deployResult = await pollDeployStatus(targetAccountId, projectName, Number(deployResp.id), buildIdToDeploy);
149
97
  if (deployResult.status === PROJECT_DEPLOY_TEXT.STATES.SUCCESS) {
150
98
  deploySuccess = true;
151
99
  }
@@ -5,7 +5,7 @@ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
5
5
  import { uiBetaTag, uiLine } from '../../../lib/ui/index.js';
6
6
  import { deprecatedProjectDevFlow } from './deprecatedFlow.js';
7
7
  import { unifiedProjectDevFlow } from './unifiedFlow.js';
8
- import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
8
+ import { useV3Api } from '../../../lib/projects/platformVersion.js';
9
9
  import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
10
10
  import { loadProfile, exitIfUsingProfiles, } from '../../../lib/projectProfiles.js';
11
11
  import { commands } from '../../../lang/en.js';
@@ -22,6 +22,7 @@ import LocalDevWebsocketServer from '../../../lib/projects/localDev/LocalDevWebs
22
22
  export async function unifiedProjectDevFlow({ args, targetProjectAccountId, providedTargetTestingAccountId, projectConfig, projectDir, }) {
23
23
  const env = getValidEnv(getEnv(targetProjectAccountId));
24
24
  let projectNodes;
25
+ let projectProfileData;
25
26
  // Get IR
26
27
  try {
27
28
  const intermediateRepresentation = await translateForLocalDev({
@@ -30,6 +31,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
30
31
  accountId: targetProjectAccountId,
31
32
  }, { profile: args.profile });
32
33
  projectNodes = intermediateRepresentation.intermediateNodesIndexedByUid;
34
+ projectProfileData = intermediateRepresentation.profileData;
33
35
  uiLogger.debug(util.inspect(projectNodes, false, null, true));
34
36
  }
35
37
  catch (e) {
@@ -107,7 +109,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
107
109
  let project = uploadedProject;
108
110
  SpinniesManager.init();
109
111
  if (projectExists && project) {
110
- await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes);
112
+ await compareLocalProjectToDeployed(projectConfig, targetProjectAccountId, project.deployedBuild?.buildId, projectNodes, args.profile);
111
113
  }
112
114
  else {
113
115
  project = await createNewProjectForLocalDev(projectConfig, targetProjectAccountId, false, false);
@@ -116,6 +118,7 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
116
118
  // End setup, start local dev process
117
119
  const localDevProcess = new LocalDevProcess({
118
120
  initialProjectNodes: projectNodes,
121
+ initialProjectProfileData: projectProfileData,
119
122
  debug: args.debug,
120
123
  profile: args.profile,
121
124
  targetProjectAccountId,
@@ -8,6 +8,9 @@ import { uiCommandReference } from '../../lib/ui/index.js';
8
8
  import { commands, lib } from '../../lang/en.js';
9
9
  import { uiLogger } from '../../lib/ui/logger.js';
10
10
  import { logInBox } from '../../lib/ui/boxen.js';
11
+ import { getHasMigratableThemes, migrateThemes2025_2, } from '../../lib/theme/migrate.js';
12
+ import { hasFeature } from '../../lib/hasFeature.js';
13
+ import { FEATURES } from '../../lib/constants.js';
11
14
  const { v2025_2 } = PLATFORM_VERSIONS;
12
15
  const command = 'migrate';
13
16
  const describe = undefined; // commands.project.migrate.describe
@@ -26,13 +29,29 @@ async function handler(args) {
26
29
  }
27
30
  const { derivedAccountId } = args;
28
31
  try {
29
- await migrateApp2025_2(derivedAccountId, {
30
- ...args,
31
- name: projectConfig?.projectConfig?.name,
32
- platformVersion: unstable
33
- ? PLATFORM_VERSIONS.unstable
34
- : platformVersion,
35
- }, projectConfig);
32
+ const { hasMigratableThemes, migratableThemesCount } = await getHasMigratableThemes(projectConfig);
33
+ if (hasMigratableThemes) {
34
+ const hasThemeMigrationAccess = await hasFeature(derivedAccountId, FEATURES.THEME_MIGRATION_2025_2);
35
+ if (!hasThemeMigrationAccess) {
36
+ uiLogger.error(commands.project.migrate.errors.noThemeMigrationAccess(derivedAccountId));
37
+ return process.exit(EXIT_CODES.ERROR);
38
+ }
39
+ await migrateThemes2025_2(derivedAccountId, {
40
+ ...args,
41
+ platformVersion: unstable
42
+ ? PLATFORM_VERSIONS.unstable
43
+ : platformVersion,
44
+ }, migratableThemesCount, projectConfig);
45
+ }
46
+ else {
47
+ await migrateApp2025_2(derivedAccountId, {
48
+ ...args,
49
+ name: projectConfig?.projectConfig?.name,
50
+ platformVersion: unstable
51
+ ? PLATFORM_VERSIONS.unstable
52
+ : platformVersion,
53
+ }, projectConfig);
54
+ }
36
55
  }
37
56
  catch (error) {
38
57
  logError(error);
@@ -2,14 +2,14 @@ import chalk from 'chalk';
2
2
  import { logger } from '@hubspot/local-dev-lib/logger';
3
3
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
4
4
  import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
5
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
5
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
6
6
  import { uiBetaTag, uiCommandReference } from '../../lib/ui/index.js';
7
7
  import { trackCommandUsage } from '../../lib/usageTracking.js';
8
8
  import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/config.js';
9
9
  import { logFeedbackMessage } from '../../lib/projects/ui.js';
10
10
  import { handleProjectUpload } from '../../lib/projects/upload.js';
11
11
  import { loadAndValidateProfile } from '../../lib/projectProfiles.js';
12
- import { displayWarnLogs, pollProjectBuildAndDeploy, } from '../../lib/projects/buildAndDeploy.js';
12
+ import { displayWarnLogs, pollProjectBuildAndDeploy, } from '../../lib/projects/pollProjectBuildAndDeploy.js';
13
13
  import { i18n } from '../../lib/lang.js';
14
14
  import { PROJECT_ERROR_TYPES } from '../../lib/constants.js';
15
15
  import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
3
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
3
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
4
4
  import { trackCommandUsage } from '../../lib/usageTracking.js';
5
5
  import { uiLogger } from '../../lib/ui/logger.js';
6
6
  import { getProjectConfig, validateProjectConfig as validateProjectConfig, } from '../../lib/projects/config.js';
@@ -1,6 +1,6 @@
1
1
  import { cancelStagedBuild, fetchProjectBuilds, } from '@hubspot/local-dev-lib/api/projects';
2
2
  import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
3
- import { useV3Api } from '../../lib/projects/buildAndDeploy.js';
3
+ import { useV3Api } from '../../lib/projects/platformVersion.js';
4
4
  import { uiCommandReference, uiLink, uiBetaTag } from '../../lib/ui/index.js';
5
5
  import { i18n } from '../../lib/lang.js';
6
6
  import { createWatcher } from '../../lib/projects/watch.js';
@@ -11,7 +11,7 @@ import { trackCommandUsage } from '../../lib/usageTracking.js';
11
11
  import { getProjectConfig, validateProjectConfig, } from '../../lib/projects/config.js';
12
12
  import { logFeedbackMessage } from '../../lib/projects/ui.js';
13
13
  import { handleProjectUpload } from '../../lib/projects/upload.js';
14
- import { pollBuildStatus, pollDeployStatus, } from '../../lib/projects/buildAndDeploy.js';
14
+ import { pollBuildStatus, pollDeployStatus, } from '../../lib/projects/pollProjectBuildAndDeploy.js';
15
15
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
16
16
  import { handleKeypress, handleExit } from '../../lib/process.js';
17
17
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';