@hubspot/ui-extensions-dev-server 0.2.0 → 0.3.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/run.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { parseArgs, showHelp } = require('../cli/utils');
4
+ const {
5
+ buildAllExtensions,
6
+ buildSingleExtension,
7
+ startDevMode,
8
+ } = require('../index');
9
+
10
+ const { promptForExtensionToRun } = require('./userInput');
11
+ const OUTPUT_DIR = 'dist';
12
+ const logger = require('./logger');
13
+
14
+ // eslint-disable-next-line no-floating-promise/no-floating-promise
15
+ (async () => {
16
+ const { DEV_MODE, BUILD_MODE, extension, help } = parseArgs();
17
+
18
+ if (help || !(DEV_MODE || BUILD_MODE)) {
19
+ showHelp(OUTPUT_DIR);
20
+ } else if (DEV_MODE) {
21
+ startDevMode({
22
+ extension: extension || (await promptForExtensionToRun()),
23
+ outputDir: OUTPUT_DIR,
24
+ logger,
25
+ });
26
+ } else if (BUILD_MODE) {
27
+ if (extension) {
28
+ buildSingleExtension({
29
+ file: extension,
30
+ outputDir: OUTPUT_DIR,
31
+ });
32
+ } else {
33
+ buildAllExtensions({ outputDir: OUTPUT_DIR });
34
+ }
35
+ }
36
+ })();
@@ -0,0 +1,32 @@
1
+ const prompts = require('prompts');
2
+
3
+ const { getExtensionsList } = require('../lib/extensions');
4
+
5
+ async function promptForExtensionToRun() {
6
+ const extensionOptions = getExtensionsList();
7
+ const response = await prompts(
8
+ [
9
+ {
10
+ type: 'select',
11
+ name: 'extension',
12
+ message: 'Which extension would you like to run?',
13
+ choices: extensionOptions.map(option => {
14
+ return {
15
+ title: option,
16
+ value: option,
17
+ };
18
+ }),
19
+ },
20
+ ],
21
+ {
22
+ onCancel: () => {
23
+ process.exit(0); // When the user cancels interaction, exit the script
24
+ },
25
+ }
26
+ );
27
+ return response.extension;
28
+ }
29
+
30
+ module.exports = {
31
+ promptForExtensionToRun,
32
+ };
package/index.js CHANGED
@@ -1,34 +1,14 @@
1
- const { OUTPUT_DIR } = require('./constants');
2
- const { getUrlSafeFileName } = require('./utils');
3
- const { buildSingleExtension } = require('./build');
4
- const path = require('path');
5
- const manifestPlugin = require('./plugins/manifestPlugin');
6
-
7
- async function remoteBuild(root, entryPoint, outputDir) {
8
- const allowedExtensions = ['.js', '.ts', '.tsx', '.jsx'];
9
- const fileInfo = path.parse(entryPoint);
10
-
11
- if (!allowedExtensions.includes(fileInfo.ext)) {
12
- throw new Error(
13
- `The last argument should be the filename you wish to build. Supported file extensions are [${allowedExtensions.join(
14
- ', '
15
- )}]`
16
- );
17
- }
1
+ const {
2
+ remoteBuild,
3
+ buildAllExtensions,
4
+ buildSingleExtension,
5
+ } = require('./lib/build');
18
6
 
19
- const output = getUrlSafeFileName(entryPoint);
20
- await buildSingleExtension({
21
- file: entryPoint,
22
- outputFileName: getUrlSafeFileName(entryPoint),
23
- outputDir: outputDir || OUTPUT_DIR,
24
- plugins: {
25
- rollup: [manifestPlugin({ minify: true, output })],
26
- },
27
- minify: true,
28
- root,
29
- });
30
- }
7
+ const { startDevMode } = require('./lib/dev');
31
8
 
32
9
  module.exports = {
33
10
  remoteBuild,
11
+ buildAllExtensions,
12
+ buildSingleExtension,
13
+ startDevMode,
34
14
  };
package/lib/build.js ADDED
@@ -0,0 +1,90 @@
1
+ const { build } = require('vite');
2
+ const { ROLLUP_OPTIONS, OUTPUT_DIR } = require('./constants');
3
+ const manifestPlugin = require('./plugins/manifestPlugin');
4
+ const path = require('path');
5
+ const { getUrlSafeFileName } = require('./utils');
6
+ const { loadConfig } = require('./config');
7
+
8
+ const allowedExtensions = ['.js', '.ts', '.tsx', '.jsx'];
9
+ const extensionErrorBaseMessage = `Supported file extensions are [${allowedExtensions.join(
10
+ ', '
11
+ )}], received:`;
12
+
13
+ async function buildAllExtensions({ outputDir }) {
14
+ const config = loadConfig();
15
+ const extensionKeys = Object.keys(config);
16
+ for (let i = 0; i < extensionKeys.length; ++i) {
17
+ const { data } = config[extensionKeys[i]];
18
+
19
+ await buildSingleExtension({
20
+ file: data.module.file,
21
+ outputDir,
22
+ emptyOutDir: i === 0,
23
+ plugins: {
24
+ rollup: [
25
+ manifestPlugin({ output: getUrlSafeFileName(data.module.file) }),
26
+ ],
27
+ },
28
+ });
29
+ }
30
+ }
31
+
32
+ async function buildSingleExtension({
33
+ file,
34
+ outputDir = OUTPUT_DIR,
35
+ emptyOutDir = true,
36
+ minify = false,
37
+ root = process.cwd(), // This is the vite default, so using that as our default
38
+ }) {
39
+ const output = getUrlSafeFileName(file);
40
+ await build({
41
+ root,
42
+ define: {
43
+ 'process.env.NODE_ENV': JSON.stringify(
44
+ process.env.NODE_ENV || 'production'
45
+ ),
46
+ },
47
+ build: {
48
+ lib: {
49
+ entry: file,
50
+ name: output,
51
+ formats: ['iife'],
52
+ fileName: () => output,
53
+ },
54
+ rollupOptions: {
55
+ ...ROLLUP_OPTIONS,
56
+ plugins: [manifestPlugin({ output })],
57
+ },
58
+ outDir: outputDir,
59
+ emptyOutDir,
60
+ minify,
61
+ },
62
+ });
63
+ }
64
+
65
+ async function remoteBuild(root, entryPoint, outputDir = OUTPUT_DIR) {
66
+ const fileInfo = path.parse(entryPoint);
67
+
68
+ if (!allowedExtensions.includes(fileInfo.ext)) {
69
+ throw new Error(`${extensionErrorBaseMessage} ${fileInfo.ext}`);
70
+ }
71
+
72
+ const output = getUrlSafeFileName(entryPoint);
73
+ await buildSingleExtension({
74
+ file: entryPoint,
75
+ outputFileName: output,
76
+ outputDir,
77
+ plugins: {
78
+ rollup: [manifestPlugin({ minify: true, output })],
79
+ },
80
+ minify: true,
81
+ root,
82
+ });
83
+ }
84
+
85
+ module.exports = {
86
+ buildAllExtensions,
87
+ buildSingleExtension,
88
+ remoteBuild,
89
+ extensionErrorBaseMessage,
90
+ };
package/lib/config.js ADDED
@@ -0,0 +1,81 @@
1
+ const path = require('path');
2
+ const { MAIN_APP_CONFIG } = require('./constants');
3
+
4
+ function _loadRequiredConfigFile(filePath) {
5
+ let config;
6
+ try {
7
+ config = require(filePath);
8
+ } catch (e) {
9
+ throw new Error(
10
+ `Unable to load ${filePath} file. Please make sure you are running the command from the src/app/extensions directory and that ${filePath} exists`
11
+ );
12
+ }
13
+ return config;
14
+ }
15
+
16
+ function loadConfig() {
17
+ // app.json is one level up from the extensions directory, which is where these commands
18
+ // will need to be ran from, the extensions directory
19
+ const configPath = path.join(process.cwd(), '..', MAIN_APP_CONFIG);
20
+
21
+ const mainAppConfig = _loadRequiredConfigFile(configPath);
22
+
23
+ const crmCardsSubConfigFiles = mainAppConfig?.extensions?.crm?.cards;
24
+ if (!crmCardsSubConfigFiles || crmCardsSubConfigFiles.length === 0) {
25
+ throw new Error(
26
+ `The "extensions.crm.cards" array in ${configPath} is missing or empty, it is a required configuration property`
27
+ );
28
+ }
29
+
30
+ const outputConfig = {};
31
+
32
+ crmCardsSubConfigFiles.forEach(card => {
33
+ const parsedFile = path.parse(card.file);
34
+ if (parsedFile.dir.startsWith('./extensions')) {
35
+ parsedFile.dir = parsedFile.dir.replace('./extensions', './'); // go up one level
36
+ }
37
+ // Get the path to the config file relative to the extensions directory
38
+ const configPathRelativeToExtensions = parsedFile.dir;
39
+ const extensionsPrefixRemoved = path.format(parsedFile);
40
+ const cardConfigPath = path.join(process.cwd(), extensionsPrefixRemoved);
41
+
42
+ try {
43
+ const cardConfig = require(cardConfigPath);
44
+ if (!cardConfig.data) {
45
+ throw new Error(
46
+ `Invalid config file at path ${cardConfigPath}, data is a required config property`
47
+ );
48
+ }
49
+
50
+ if (!cardConfig.data.module) {
51
+ throw new Error(
52
+ `Invalid config file at path ${cardConfigPath}, data.module is a require property`
53
+ );
54
+ }
55
+
56
+ // Join the two relative paths
57
+ const entryPointPath = path.join(
58
+ configPathRelativeToExtensions,
59
+ cardConfig.data.module.file
60
+ );
61
+
62
+ cardConfig.data.module.file = entryPointPath;
63
+
64
+ outputConfig[entryPointPath] = cardConfig;
65
+ outputConfig[entryPointPath].data.appName = mainAppConfig.name;
66
+ } catch (e) {
67
+ if (e?.code === 'MODULE_NOT_FOUND') {
68
+ throw new Error(
69
+ `Unable to load "${cardConfigPath}" file. \nPlease make sure you are running the command from the src/app/extensions directory and that your card JSON config exists within it.`
70
+ );
71
+ }
72
+ throw e;
73
+ }
74
+ });
75
+
76
+ return outputConfig;
77
+ }
78
+
79
+ module.exports = {
80
+ loadConfig,
81
+ };
@@ -1,10 +1,10 @@
1
- const VITE_DEFAULT_PORT = 5173;
2
- const WEBSOCKET_PORT = 5174;
3
- const MAIN_APP_CONFIG = 'app.json';
4
- const PROJECT_CONFIG = 'hsproject.json';
5
1
  const OUTPUT_DIR = 'dist';
2
+ const MAIN_APP_CONFIG = 'app.json';
6
3
  const MANIFEST_FILE = 'manifest.json';
7
4
 
5
+ const VITE_DEFAULT_PORT = 5173;
6
+ const WEBSOCKET_PORT = 5174;
7
+
8
8
  const ROLLUP_OPTIONS = {
9
9
  // Deps to exclude from the bundle
10
10
  external: ['react', 'react-dom', '@remote-ui/react'],
@@ -22,13 +22,12 @@ const EXTENSIONS_MESSAGE_VERSION = 0;
22
22
  const WEBSOCKET_MESSAGE_VERSION = 0;
23
23
 
24
24
  module.exports = {
25
- VITE_DEFAULT_PORT,
26
25
  ROLLUP_OPTIONS,
27
- MAIN_APP_CONFIG,
28
- PROJECT_CONFIG,
29
26
  OUTPUT_DIR,
27
+ MAIN_APP_CONFIG,
30
28
  MANIFEST_FILE,
31
- WEBSOCKET_PORT,
32
29
  EXTENSIONS_MESSAGE_VERSION,
30
+ VITE_DEFAULT_PORT,
33
31
  WEBSOCKET_MESSAGE_VERSION,
32
+ WEBSOCKET_PORT,
34
33
  };
package/lib/dev.js ADDED
@@ -0,0 +1,89 @@
1
+ const { createServer } = require('vite');
2
+ const startDevServer = require('./server');
3
+ const devBuildPlugin = require('./plugins/devBuildPlugin');
4
+ const { getUrlSafeFileName } = require('./utils');
5
+ const {
6
+ VITE_DEFAULT_PORT,
7
+ WEBSOCKET_PORT,
8
+ OUTPUT_DIR,
9
+ } = require('./constants');
10
+ const { loadConfig } = require('./config');
11
+
12
+ async function _createViteDevServer(
13
+ outputDir,
14
+ extensionConfig,
15
+ websocketPort,
16
+ baseMessage,
17
+ logger
18
+ ) {
19
+ return await createServer({
20
+ appType: 'custom',
21
+ mode: 'development',
22
+ server: {
23
+ middlewareMode: true,
24
+ hmr: {
25
+ port: websocketPort,
26
+ },
27
+ watch: {
28
+ ignored: [`${process.cwd()}/${outputDir}/**/*`],
29
+ },
30
+ },
31
+ build: {
32
+ rollupOptions: {
33
+ input: extensionConfig.data.module.file,
34
+ output: getUrlSafeFileName(extensionConfig.data.module.file),
35
+ },
36
+ },
37
+ plugins: [
38
+ devBuildPlugin({ extensionConfig, outputDir, baseMessage, logger }),
39
+ ],
40
+ });
41
+ }
42
+
43
+ async function startDevMode({
44
+ extension,
45
+ logger,
46
+ outputDir = OUTPUT_DIR,
47
+ expressPort = VITE_DEFAULT_PORT,
48
+ webSocketPort = WEBSOCKET_PORT,
49
+ }) {
50
+ if (!extension) {
51
+ throw new Error('Unable to determine which extension to run');
52
+ }
53
+
54
+ const config = loadConfig();
55
+ const extensionConfig = config[extension];
56
+ if (!extensionConfig) {
57
+ throw new Error(
58
+ `Unable to locate a configuration file for the specified extension ${extension}`
59
+ );
60
+ }
61
+
62
+ extensionConfig.output = getUrlSafeFileName(extensionConfig.data.module.file);
63
+
64
+ const baseMessage = Object.freeze({
65
+ appName: extensionConfig.data.appName,
66
+ title: extensionConfig.data.title,
67
+ callback: `http://hslocal.net:${expressPort}/${extensionConfig.output}`,
68
+ });
69
+
70
+ const viteDevServer = await _createViteDevServer(
71
+ outputDir,
72
+ extensionConfig,
73
+ webSocketPort,
74
+ baseMessage,
75
+ logger
76
+ );
77
+ startDevServer(
78
+ outputDir,
79
+ expressPort,
80
+ webSocketPort,
81
+ baseMessage,
82
+ viteDevServer,
83
+ logger
84
+ );
85
+ }
86
+
87
+ module.exports = {
88
+ startDevMode,
89
+ };
@@ -0,0 +1,9 @@
1
+ const { loadConfig } = require('./config');
2
+
3
+ function getExtensionsList() {
4
+ return Object.keys(loadConfig());
5
+ }
6
+
7
+ module.exports = {
8
+ getExtensionsList,
9
+ };
@@ -1,16 +1,39 @@
1
1
  const { ROLLUP_OPTIONS, WEBSOCKET_MESSAGE_VERSION } = require('../constants');
2
2
  const { build } = require('vite');
3
3
  const manifestPlugin = require('./manifestPlugin');
4
- const logger = require('../logger');
4
+ const { stripAnsiColorCodes } = require('../utils');
5
5
 
6
6
  function devBuildPlugin(options = {}) {
7
- const { extensionConfig, outputDir, baseMessage } = options;
7
+ const { extensionConfig, outputDir, baseMessage, logger } = options;
8
8
  const versionedBaseMessage = {
9
9
  ...baseMessage,
10
10
  version: WEBSOCKET_MESSAGE_VERSION,
11
11
  };
12
12
 
13
- const devBuild = async () => {
13
+ const handleBuildError = (error, server) => {
14
+ const { plugin, hook, code, errors, frame, loc, id } = error;
15
+ if (
16
+ plugin === 'vite:esbuild' &&
17
+ hook === 'transform' &&
18
+ code === 'PLUGIN_ERROR'
19
+ ) {
20
+ server.ws.send({
21
+ ...versionedBaseMessage,
22
+ event: 'error',
23
+ error: {
24
+ type: 'transformation',
25
+ details: {
26
+ errors,
27
+ formattedError: stripAnsiColorCodes(frame),
28
+ location: loc,
29
+ file: id,
30
+ },
31
+ },
32
+ });
33
+ }
34
+ };
35
+
36
+ const devBuild = async server => {
14
37
  try {
15
38
  await build({
16
39
  mode: 'development',
@@ -21,10 +44,10 @@ function devBuildPlugin(options = {}) {
21
44
  },
22
45
  build: {
23
46
  lib: {
24
- entry: extensionConfig?.file,
25
- name: extensionConfig?.output,
47
+ entry: extensionConfig.data.module.file,
48
+ name: extensionConfig.output,
26
49
  formats: ['iife'],
27
- fileName: () => extensionConfig?.output,
50
+ fileName: () => extensionConfig.output,
28
51
  },
29
52
  rollupOptions: {
30
53
  ...ROLLUP_OPTIONS,
@@ -32,7 +55,7 @@ function devBuildPlugin(options = {}) {
32
55
  ...(ROLLUP_OPTIONS.plugins || []),
33
56
  manifestPlugin({
34
57
  minify: false,
35
- output: extensionConfig?.output,
58
+ output: extensionConfig.output,
36
59
  }),
37
60
  ],
38
61
  output: {
@@ -45,8 +68,10 @@ function devBuildPlugin(options = {}) {
45
68
  minify: false,
46
69
  },
47
70
  });
48
- } catch (e) {
49
- logger.error(e.message);
71
+ return true;
72
+ } catch (error) {
73
+ handleBuildError(error, server);
74
+ return false;
50
75
  }
51
76
  };
52
77
 
@@ -65,15 +90,31 @@ function devBuildPlugin(options = {}) {
65
90
  event: 'start',
66
91
  });
67
92
  });
68
- await devBuild();
93
+ localServer.ws.on('build', async () => {
94
+ logger.info('Browser has requested a build, rebuilding');
95
+ const successful = await devBuild(localServer);
96
+ if (successful) {
97
+ server.ws.send({
98
+ ...versionedBaseMessage,
99
+ event: 'update',
100
+ });
101
+ }
102
+ });
103
+ await devBuild(localServer);
69
104
  },
70
105
  handleHotUpdate: async ({ server }) => {
71
- await devBuild();
72
- if (server.ws.clients.size) {
73
- logger.info('Bundle updated, notifying connected browsers');
74
- } else {
106
+ const successful = await devBuild(server);
107
+
108
+ if (!successful) {
109
+ return [];
110
+ }
111
+
112
+ if (server.ws.clients.size === 0) {
75
113
  logger.warn('Bundle updated, no browsers connected to notify');
114
+ return [];
76
115
  }
116
+
117
+ logger.info('Bundle updated, notifying connected browsers');
77
118
  server.ws.send({
78
119
  ...versionedBaseMessage,
79
120
  event: 'update',
@@ -1,7 +1,6 @@
1
1
  const { readFileSync } = require('fs');
2
2
  const { normalize } = require('path');
3
3
  const { MANIFEST_FILE } = require('../constants');
4
- const logger = require('../logger');
5
4
  const path = require('path');
6
5
 
7
6
  const PACKAGE_LOCK_FILE = 'package-lock.json';
@@ -13,7 +12,7 @@ function plugin(options = {}) {
13
12
  name: 'ui-extensions-manifest-generation-plugin',
14
13
  enforce: 'post', // run after default rollup plugins
15
14
  generateBundle(_rollupOptions, bundle) {
16
- const { output, minify = false } = options;
15
+ const { output, minify = false, logger } = options;
17
16
  try {
18
17
  const filename = path.parse(output).name;
19
18
  const manifest = _generateManifestContents(bundle);
@@ -1,5 +1,4 @@
1
1
  const express = require('express');
2
- const logger = require('./logger');
3
2
  const path = require('path');
4
3
  const cors = require('cors');
5
4
  const fs = require('fs');
@@ -10,7 +9,8 @@ function startDevServer(
10
9
  expressPort,
11
10
  webSocketPort,
12
11
  baseMessage,
13
- viteDevServer
12
+ viteDevServer,
13
+ logger
14
14
  ) {
15
15
  const app = express();
16
16
 
@@ -22,7 +22,8 @@ function startDevServer(
22
22
  expressPort,
23
23
  webSocketPort,
24
24
  outputDir,
25
- baseMessage
25
+ baseMessage,
26
+ logger
26
27
  );
27
28
 
28
29
  // Vite middlewares needs to go last because it's greedy and will block other middleware
@@ -32,7 +33,7 @@ function startDevServer(
32
33
  logger.warn(`Listening at ${baseMessage.callback}`);
33
34
  });
34
35
 
35
- _configureShutDownHandlers(server, viteDevServer);
36
+ _configureShutDownHandlers(server, viteDevServer, logger);
36
37
  }
37
38
 
38
39
  function _addExtensionsEndpoint(
@@ -40,7 +41,8 @@ function _addExtensionsEndpoint(
40
41
  expressPort,
41
42
  webSocketPort,
42
43
  outputDir,
43
- baseMessage
44
+ baseMessage,
45
+ logger
44
46
  ) {
45
47
  const endpoint = '/extensions';
46
48
  server.get(endpoint, (_req, res) => {
@@ -72,7 +74,7 @@ function _addExtensionsEndpoint(
72
74
  logger.warn(`Listening at http://hslocal.net:${expressPort}${endpoint}`);
73
75
  }
74
76
 
75
- function _configureShutDownHandlers(server, viteDevServer) {
77
+ function _configureShutDownHandlers(server, viteDevServer, logger) {
76
78
  async function shutdown() {
77
79
  logger.warn('\nCleaning up after ourselves...');
78
80
  await viteDevServer.pluginContainer.close();
package/lib/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ const path = require('path');
2
+
3
+ function getUrlSafeFileName(filePath) {
4
+ const { name } = path.parse(filePath);
5
+ return encodeURIComponent(`${name}.js`);
6
+ }
7
+
8
+ // Strips ANSI color codes out of strings because we don't want to pass them to the browser
9
+ function stripAnsiColorCodes(string) {
10
+ return string.replace(
11
+ // eslint-disable-next-line no-control-regex
12
+ /[\u001b][[]*([0-9]{1,4};?)*[m]/g,
13
+ ''
14
+ );
15
+ }
16
+
17
+ module.exports = {
18
+ getUrlSafeFileName,
19
+ stripAnsiColorCodes,
20
+ };