@hubspot/ui-extensions-dev-server 0.4.0 → 0.5.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/cli/config.js CHANGED
@@ -3,21 +3,9 @@ const path = require('path');
3
3
  const { getUrlSafeFileName } = require('../lib/utils');
4
4
  const { MAIN_APP_CONFIG } = require('../lib/constants');
5
5
 
6
- function getAppConfigPath(projectFiles) {
7
- return projectFiles.find(filePath => filePath.endsWith(MAIN_APP_CONFIG));
8
- }
9
-
10
6
  function loadConfigByPath(configPath) {
11
- if (configPath) {
12
- try {
13
- const source = fs.readFileSync(configPath);
14
- const parsedConfig = JSON.parse(source);
15
- return parsedConfig;
16
- } catch (e) {
17
- console.log(e);
18
- }
19
- }
20
- return null;
7
+ const source = fs.readFileSync(configPath);
8
+ return JSON.parse(source);
21
9
  }
22
10
 
23
11
  function loadExtensionConfig(appConfig, appPath) {
@@ -32,23 +20,28 @@ function loadExtensionConfig(appConfig, appPath) {
32
20
  const cardConfig = loadConfigByPath(cardConfigPath);
33
21
 
34
22
  if (cardConfig && cardConfig.data) {
35
- const extensionsDir = path.join(appPath, 'extensions');
23
+ const cardConfigDir = path.parse(cardConfigPath).dir;
36
24
 
37
25
  const entryPointPath = path.join(
38
- extensionsDir,
26
+ cardConfigDir,
39
27
  cardConfig.data?.module?.file
40
28
  );
41
29
 
42
30
  cardConfig.data.module.file = entryPointPath;
43
31
 
44
- outputConfig[entryPointPath] = cardConfig;
45
- outputConfig[entryPointPath].output = getUrlSafeFileName(
46
- entryPointPath
47
- );
48
- outputConfig[entryPointPath].data.appName = appConfig.name;
32
+ outputConfig[entryPointPath] = {
33
+ ...cardConfig,
34
+ output: getUrlSafeFileName(entryPointPath),
35
+ path: appPath,
36
+ extensionPath: path.parse(entryPointPath).dir,
37
+ data: {
38
+ ...cardConfig.data,
39
+ appName: appConfig.name,
40
+ },
41
+ };
49
42
  }
50
43
  } catch (e) {
51
- console.log(e);
44
+ throw new Error(`Unable to load ${cardConfigPath}`);
52
45
  }
53
46
  });
54
47
 
@@ -78,7 +71,7 @@ function loadConfig() {
78
71
  crmCardsSubConfigFiles.forEach(card => {
79
72
  const cardConfigPath = path.join(process.cwd(), '..', card.file);
80
73
  try {
81
- const cardConfig = require(cardConfigPath);
74
+ const cardConfig = loadConfigByPath(cardConfigPath);
82
75
  if (!cardConfig.data) {
83
76
  throw new Error(
84
77
  `Invalid config file at path ${cardConfigPath}, data is a required config property`
@@ -112,7 +105,6 @@ function loadConfig() {
112
105
  }
113
106
 
114
107
  module.exports = {
115
- getAppConfigPath,
116
108
  loadConfigByPath,
117
109
  loadExtensionConfig,
118
110
  loadConfig,
package/cli/run.js CHANGED
@@ -7,11 +7,11 @@ const {
7
7
  DevModeInterface,
8
8
  } = require('../index');
9
9
 
10
- const { promptForExtensionToRun } = require('./userInput');
11
- const OUTPUT_DIR = 'dist';
12
10
  const logger = require('./logger');
13
11
  const path = require('path');
14
- const { MAIN_APP_CONFIG } = require('../lib/constants');
12
+ const { MAIN_APP_CONFIG, OUTPUT_DIR } = require('../lib/constants');
13
+ const inquirer = require('inquirer');
14
+ const { loadConfigByPath, loadExtensionConfig } = require('./config');
15
15
 
16
16
  // eslint-disable-next-line no-floating-promise/no-floating-promise
17
17
  (async () => {
@@ -20,13 +20,29 @@ const { MAIN_APP_CONFIG } = require('../lib/constants');
20
20
  if (help || !(DEV_MODE || BUILD_MODE)) {
21
21
  showHelp(OUTPUT_DIR);
22
22
  } else if (DEV_MODE) {
23
- DevModeInterface.start({
24
- extension: extension
25
- ? path.parse(extension).name
26
- : await promptForExtensionToRun(),
23
+ const extensionPath = process.cwd(); // Assumed to be /path/to/src/app/extensions
24
+ const appPath = path.join(extensionPath, '..');
25
+ const appConfig = loadConfigByPath(path.join(appPath, MAIN_APP_CONFIG));
26
+
27
+ let extensionConfig;
28
+ if (extension) {
29
+ const allExtensionsConfig = loadExtensionConfig(appConfig, appPath);
30
+ extensionConfig =
31
+ allExtensionsConfig[path.join(extensionPath, extension)];
32
+ }
33
+ await DevModeInterface.setup({
34
+ promptUser: inquirer.createPromptModule(),
35
+ components: {
36
+ [appConfig.name]: {
37
+ config: appConfig,
38
+ path: path.join(extensionPath, '..'),
39
+ },
40
+ },
41
+ extensionConfig,
27
42
  logger,
28
- projectFiles: [path.join(process.cwd(), '..', MAIN_APP_CONFIG)],
29
43
  });
44
+
45
+ await DevModeInterface.start({});
30
46
  } else if (BUILD_MODE) {
31
47
  if (extension) {
32
48
  buildSingleExtension({
@@ -1,66 +1,123 @@
1
1
  const { startDevMode } = require('./dev');
2
2
  const path = require('path');
3
3
  const { OUTPUT_DIR } = require('./constants');
4
- const {
5
- getAppConfigPath,
6
- loadConfigByPath,
7
- loadExtensionConfig,
8
- } = require('../cli/config');
4
+ const { loadExtensionConfig } = require('../cli/config');
9
5
 
10
6
  class DevModeInterface {
11
7
  constructor() {
12
- this.shutdown = null;
8
+ this._setupLogger(console);
13
9
  }
14
10
 
15
- async start({ extension, logger = console, projectFiles }) {
16
- this.logger = logger;
17
- this.logger.info('UIE Dev server initializing...');
11
+ _setupLogger(_logger) {
12
+ this.logger = {
13
+ ..._logger,
14
+ debug: (...args) => {
15
+ if (this.debug) {
16
+ _logger.debug(...args);
17
+ }
18
+ },
19
+ };
20
+ }
18
21
 
19
- const appConfigPath = getAppConfigPath(projectFiles);
20
- if (!appConfigPath) {
21
- const message = 'Unable to determine the location of the app.json file';
22
- this.logger.error(message);
23
- throw new Error(message);
24
- }
22
+ _setDataFromExtensionConfig(extensionConfig) {
23
+ this.config = extensionConfig;
24
+ this.appName = extensionConfig.data.appName;
25
+ this.title = extensionConfig.data.title;
26
+ }
25
27
 
26
- const appPath = path.dirname(appConfigPath);
27
- const appConfig = loadConfigByPath(appConfigPath);
28
- if (!appConfig) {
29
- const message = 'Unable to load app.json';
30
- this.logger.error(message);
31
- throw new Error(message);
32
- }
28
+ _generateAppExtensionMappings(components) {
29
+ // Loop over all of the app configs that are passed in
30
+ const allComponentNames = Object.keys(components);
33
31
 
34
- const extensionsConfigs = loadExtensionConfig(appConfig, appPath);
32
+ return allComponentNames.reduce((appExtensionMappings, componentName) => {
33
+ const component = components[componentName];
34
+ if (!component.config.extensions?.crm?.cards) {
35
+ return appExtensionMappings; // It's not an app
36
+ }
37
+ // Load all of the extension configs for a particular app.json file
38
+ const extensionsConfigForApp = loadExtensionConfig(
39
+ component.config,
40
+ component.path
41
+ );
35
42
 
36
- if (!extension || !extensionsConfigs) {
37
- const message =
38
- 'Unable to load the extension config, was an extension specified?';
39
- this.logger.error(message);
40
- throw new Error(message);
43
+ const extensionFilePaths = Object.keys(extensionsConfigForApp);
44
+ // Loop over the loaded extension configs and generate the list of choices to use to prompt the user for input
45
+ extensionFilePaths.forEach(extensionPath => {
46
+ const extensionConfig = extensionsConfigForApp[extensionPath];
47
+ appExtensionMappings.push({
48
+ name: `${componentName}/${extensionConfig.data.title}`,
49
+ value: extensionConfig,
50
+ });
51
+ });
52
+
53
+ return appExtensionMappings;
54
+ }, []);
55
+ }
56
+
57
+ async setup({
58
+ debug = false,
59
+ accountId,
60
+ httpClient,
61
+ promptUser,
62
+ components,
63
+ extensionConfig,
64
+ logger,
65
+ }) {
66
+ this.debug = debug;
67
+ this.accountId = accountId;
68
+ this.httpClient = httpClient;
69
+ if (logger) {
70
+ this._setupLogger(logger);
71
+ }
72
+ if (extensionConfig) {
73
+ this._setDataFromExtensionConfig(extensionConfig);
74
+ return;
41
75
  }
42
76
 
43
- const extensionConfigKey = Object.keys(extensionsConfigs).find(
44
- extensionPath => {
45
- return (
46
- extensionPath.endsWith(`${extension}.jsx`) ||
47
- extensionPath.endsWith(`${extension}.tsx`)
48
- );
49
- }
50
- );
77
+ const choices = this._generateAppExtensionMappings(components);
78
+
79
+ if (choices.length === 0) {
80
+ throw new Error('No extensions to run');
81
+ } else if (choices.length === 1) {
82
+ this._setDataFromExtensionConfig(choices[0].value);
83
+ } else {
84
+ const answers = await promptUser({
85
+ type: 'list',
86
+ name: 'extension',
87
+ message: 'Which extension would you like to run?',
88
+ choices,
89
+ });
90
+ this._setDataFromExtensionConfig(answers.extension);
91
+ }
92
+ }
51
93
 
52
- if (!extensionConfigKey) {
53
- const message = `Unable to find config for extension: ${extension}`;
54
- this.logger.error(message);
55
- throw new Error(message);
94
+ async start({ debug }) {
95
+ this.debug = debug;
96
+ if (!this.config || !this.config.path) {
97
+ throw new Error(
98
+ 'Unable to load the required extension configuration files'
99
+ );
56
100
  }
101
+ const appPath = this.config.path;
102
+
103
+ // Pass options from the CLI for running app functions locally
104
+ const functionsConfig = {
105
+ app: { path: appPath },
106
+ accountId: this.accountId,
107
+ httpClient: this.httpClient,
108
+ };
57
109
 
58
110
  this.shutdown = await startDevMode({
59
- extensionConfig: extensionsConfigs[extensionConfigKey],
60
- outputDir: path.join(appPath, 'extensions', OUTPUT_DIR),
111
+ extensionConfig: this.config,
112
+ outputDir: path.join(this.config.extensionPath, OUTPUT_DIR),
113
+ functionsConfig,
61
114
  logger: this.logger,
115
+ root: appPath,
62
116
  });
63
- this.logger.info('UIE Dev server done initializing...');
117
+
118
+ this.logger.info(
119
+ `Running extension '${this.title}' from app '${this.appName}'`
120
+ );
64
121
  }
65
122
 
66
123
  async cleanup() {
package/lib/constants.js CHANGED
@@ -18,9 +18,14 @@ const ROLLUP_OPTIONS = {
18
18
  },
19
19
  };
20
20
 
21
- const EXTENSIONS_MESSAGE_VERSION = 0;
21
+ const EXTENSIONS_MESSAGE_VERSION = 1;
22
22
  const WEBSOCKET_MESSAGE_VERSION = 0;
23
23
 
24
+ const SERVER_CAPABILITIES = [
25
+ // Supports running app functions locally
26
+ 'app-functions-local-dev',
27
+ ];
28
+
24
29
  module.exports = {
25
30
  ROLLUP_OPTIONS,
26
31
  OUTPUT_DIR,
@@ -30,4 +35,5 @@ module.exports = {
30
35
  VITE_DEFAULT_PORT,
31
36
  WEBSOCKET_MESSAGE_VERSION,
32
37
  WEBSOCKET_PORT,
38
+ SERVER_CAPABILITIES,
33
39
  };
package/lib/dev.js CHANGED
@@ -14,9 +14,11 @@ async function _createViteDevServer(
14
14
  extensionConfig,
15
15
  websocketPort,
16
16
  baseMessage,
17
- logger
17
+ logger,
18
+ root
18
19
  ) {
19
20
  return await createServer({
21
+ root,
20
22
  appType: 'custom',
21
23
  mode: 'development',
22
24
  server: {
@@ -48,10 +50,12 @@ async function _createViteDevServer(
48
50
 
49
51
  async function startDevMode({
50
52
  extensionConfig,
53
+ functionsConfig,
51
54
  logger,
52
55
  outputDir = OUTPUT_DIR,
53
56
  expressPort = VITE_DEFAULT_PORT,
54
57
  webSocketPort = WEBSOCKET_PORT,
58
+ root = process.cwd(),
55
59
  }) {
56
60
  if (!extensionConfig) {
57
61
  throw new Error('Unable to determine which extension to run');
@@ -68,7 +72,8 @@ async function startDevMode({
68
72
  extensionConfig,
69
73
  webSocketPort,
70
74
  baseMessage,
71
- logger
75
+ logger,
76
+ root
72
77
  );
73
78
  return startDevServer(
74
79
  outputDir,
@@ -76,6 +81,7 @@ async function startDevMode({
76
81
  webSocketPort,
77
82
  baseMessage,
78
83
  viteDevServer,
84
+ functionsConfig,
79
85
  logger
80
86
  );
81
87
  }
@@ -1,5 +1,8 @@
1
1
  const path = require('path');
2
- const { EXTENSIONS_MESSAGE_VERSION } = require('./constants');
2
+ const {
3
+ EXTENSIONS_MESSAGE_VERSION,
4
+ SERVER_CAPABILITIES,
5
+ } = require('./constants');
3
6
  const { loadManifest } = require('./utils');
4
7
 
5
8
  class ExtensionsService {
@@ -23,6 +26,7 @@ class ExtensionsService {
23
26
  const response = {
24
27
  websocket: `ws://localhost:${webSocketPort}`,
25
28
  version: EXTENSIONS_MESSAGE_VERSION,
29
+ capabilities: SERVER_CAPABILITIES,
26
30
  extensions: [
27
31
  {
28
32
  ...baseMessage,
@@ -36,6 +36,7 @@ function devBuildPlugin(options = {}) {
36
36
  const devBuild = async server => {
37
37
  try {
38
38
  await build({
39
+ logLevel: 'warn',
39
40
  mode: 'development',
40
41
  define: {
41
42
  'process.env.NODE_ENV': JSON.stringify(
@@ -111,7 +112,7 @@ function devBuildPlugin(options = {}) {
111
112
  }
112
113
 
113
114
  if (server.ws.clients.size === 0) {
114
- logger.info('Bundle updated, no browsers connected to notify');
115
+ logger.warn('Bundle updated, no browsers connected to notify');
115
116
  return [];
116
117
  }
117
118
 
package/lib/server.js CHANGED
@@ -1,6 +1,9 @@
1
1
  const express = require('express');
2
2
  const cors = require('cors');
3
3
  const extensionsService = require('./extensionsService');
4
+ const {
5
+ AppFunctionExecutionService,
6
+ } = require('@hubspot/app-functions-dev-server');
4
7
 
5
8
  function startDevServer(
6
9
  outputDir,
@@ -8,6 +11,7 @@ function startDevServer(
8
11
  webSocketPort,
9
12
  baseMessage,
10
13
  viteDevServer,
14
+ functionsConfig,
11
15
  logger
12
16
  ) {
13
17
  const app = express();
@@ -15,6 +19,13 @@ function startDevServer(
15
19
  // Setup middleware
16
20
  app.use(cors());
17
21
  app.use(express.static(outputDir));
22
+
23
+ app.use(
24
+ '/api/crm-extensibility/execution/internal/v3',
25
+ AppFunctionExecutionService(functionsConfig)
26
+ );
27
+ logger.info('Serving app functions locally');
28
+
18
29
  const endpointsAdded = extensionsService.add(
19
30
  app,
20
31
  webSocketPort,
@@ -23,14 +34,14 @@ function startDevServer(
23
34
  );
24
35
 
25
36
  endpointsAdded.forEach(endpoint => {
26
- logger.info(`Listening at http://hslocal.net:${expressPort}${endpoint}`);
37
+ logger.debug(`Listening at http://hslocal.net:${expressPort}${endpoint}`);
27
38
  });
28
39
 
29
40
  // Vite middlewares needs to go last because it's greedy and will block other middleware
30
41
  app.use(viteDevServer.middlewares);
31
42
 
32
43
  const server = app.listen({ port: expressPort }, () => {
33
- logger.info(`Listening at ${baseMessage.callback}`);
44
+ logger.debug(`Listening at ${baseMessage.callback}`);
34
45
  });
35
46
 
36
47
  return async function shutdown() {
@@ -38,7 +49,6 @@ function startDevServer(
38
49
  // Stop new connections to express server
39
50
  server.close(() => {});
40
51
  logger.info('Clean up done, exiting.');
41
- process.exit(0);
42
52
  };
43
53
  }
44
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,10 +12,8 @@
12
12
  },
13
13
  "files": [
14
14
  "cli/config.js",
15
- "cli/extensions.js",
16
15
  "cli/logger.js",
17
16
  "cli/run.js",
18
- "cli/userInput.js",
19
17
  "cli/utils.js",
20
18
  "lib/plugins/*",
21
19
  "lib/build.js",
@@ -30,12 +28,13 @@
30
28
  ],
31
29
  "license": "MIT",
32
30
  "dependencies": {
31
+ "@hubspot/app-functions-dev-server": "^0.5.0",
33
32
  "command-line-args": "^5.2.1",
34
33
  "command-line-usage": "^7.0.1",
35
34
  "console-log-colors": "^0.4.0",
36
35
  "cors": "^2.8.5",
37
36
  "express": "^4.18.2",
38
- "prompts": "^2.4.2",
37
+ "inquirer": "8.2.0",
39
38
  "vite": "^4.0.4"
40
39
  },
41
40
  "devDependencies": {
@@ -63,5 +62,5 @@
63
62
  "optional": true
64
63
  }
65
64
  },
66
- "gitHead": "1269fb86282dd5f6b9686d064b6c652f00cb863f"
65
+ "gitHead": "d11a7bb73cf91ed44279b041f17778833948839f"
67
66
  }
package/cli/extensions.js DELETED
@@ -1,9 +0,0 @@
1
- const { loadConfig } = require('./config');
2
-
3
- function getExtensionsList() {
4
- return Object.keys(loadConfig());
5
- }
6
-
7
- module.exports = {
8
- getExtensionsList,
9
- };
package/cli/userInput.js DELETED
@@ -1,33 +0,0 @@
1
- const prompts = require('prompts');
2
- const path = require('path');
3
-
4
- const { getExtensionsList } = require('./extensions');
5
-
6
- async function promptForExtensionToRun() {
7
- const extensionOptions = getExtensionsList();
8
- const response = await prompts(
9
- [
10
- {
11
- type: 'select',
12
- name: 'extension',
13
- message: 'Which extension would you like to run?',
14
- choices: extensionOptions.map(option => {
15
- return {
16
- title: option,
17
- value: option,
18
- };
19
- }),
20
- },
21
- ],
22
- {
23
- onCancel: () => {
24
- process.exit(0); // When the user cancels interaction, exit the script
25
- },
26
- }
27
- );
28
- return path.parse(response.extension).name;
29
- }
30
-
31
- module.exports = {
32
- promptForExtensionToRun,
33
- };