@hubspot/ui-extensions-dev-server 0.0.1-beta.0 → 0.0.1-beta.2

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/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # ui-extensions-dev-server
1
+ # UI Extensions – Dev Server
2
+
3
+ Development server to run and test your HubSpot UI Extensions.
2
4
 
3
5
  ## Overview
4
6
  This package contains the cli for running HubSpot UI extensions locally, as well as running a production build for debugging purposes.
package/config.js CHANGED
@@ -1,14 +1,20 @@
1
1
  const prompts = require('prompts');
2
2
  const logger = require('./logger');
3
3
  const path = require('path');
4
- const { MAIN_APP_CONFIG, PROJECT_CONFIG } = require('./constants');
4
+ const { MAIN_APP_CONFIG } = require('./constants');
5
5
  const { getUrlSafeFileName } = require('./utils');
6
6
 
7
7
  async function getExtensionConfig(configuration, extension) {
8
- if (extension && configuration[extension]) {
9
- const { data } = configuration[extension];
8
+ const extensionOptions = Object.keys(configuration);
9
+
10
+ if (
11
+ (extension && configuration[extension]) ||
12
+ extensionOptions.length === 1
13
+ ) {
14
+ const extensionToRun = extension || extensionOptions[0];
15
+ const { data } = configuration[extensionToRun];
10
16
  return {
11
- key: extension,
17
+ key: extensionToRun,
12
18
  name: data.title,
13
19
  file: data?.module?.file,
14
20
  output: data?.output,
@@ -16,7 +22,6 @@ async function getExtensionConfig(configuration, extension) {
16
22
  };
17
23
  }
18
24
 
19
- const extensionOptions = Object.keys(configuration);
20
25
  const response = await prompts(
21
26
  [
22
27
  {
@@ -65,10 +70,6 @@ function loadConfig() {
65
70
  // will need to be ran from, the extensions directory
66
71
  const configPath = path.join(process.cwd(), `../${MAIN_APP_CONFIG}`);
67
72
 
68
- const projectConfig = _loadRequiredConfigFile(
69
- path.join(process.cwd(), `../../../${PROJECT_CONFIG}`)
70
- );
71
-
72
73
  const mainAppConfig = _loadRequiredConfigFile(configPath);
73
74
 
74
75
  const crmCardsSubConfigFiles = mainAppConfig?.extensions?.crm?.cards;
@@ -107,7 +108,7 @@ function loadConfig() {
107
108
  outputConfig[entryPointPath].data.output = getUrlSafeFileName(
108
109
  entryPointPath
109
110
  );
110
- outputConfig[entryPointPath].data.appName = projectConfig.name;
111
+ outputConfig[entryPointPath].data.appName = mainAppConfig.name;
111
112
  } catch (e) {
112
113
  let errorMessage = e?.message;
113
114
  if (e?.code === 'MODULE_NOT_FOUND') {
package/constants.js CHANGED
@@ -16,10 +16,15 @@ const ROLLUP_OPTIONS = {
16
16
  },
17
17
  };
18
18
 
19
+ const EXTENSIONS_MESSAGE_VERSION = 0;
20
+ const WEBSOCKET_MESSAGE_VERSION = 0;
21
+
19
22
  module.exports = {
20
23
  VITE_DEFAULT_PORT,
21
24
  ROLLUP_OPTIONS,
22
25
  MAIN_APP_CONFIG,
23
26
  PROJECT_CONFIG,
24
27
  OUTPUT_DIR,
28
+ EXTENSIONS_MESSAGE_VERSION,
29
+ WEBSOCKET_MESSAGE_VERSION,
25
30
  };
package/dev.js CHANGED
@@ -1,17 +1,11 @@
1
- const express = require('express');
2
- const chokidar = require('chokidar');
3
- const { WebSocketServer } = require('ws');
4
- const http = require('http');
5
- const logger = require('./logger');
6
1
  const { build } = require('vite');
7
2
  const { getExtensionConfig } = require('./config');
8
- const fs = require('fs');
9
- const path = require('path');
10
3
  const { ROLLUP_OPTIONS } = require('./constants');
4
+ const startDevServer = require('./server');
5
+ const manifestPlugin = require('./plugins/manifestPlugin');
11
6
 
12
- async function _devBuild(config, outputDir, extension) {
13
- const extensionConfig = await getExtensionConfig(config, extension);
14
- build({
7
+ async function _buildDevBundle(outputDir, extensionConfig) {
8
+ await build({
15
9
  define: {
16
10
  'process.env.NODE_ENV': JSON.stringify(
17
11
  process.env.NODE_ENV || 'development'
@@ -33,131 +27,24 @@ async function _devBuild(config, outputDir, extension) {
33
27
  formats: ['iife'],
34
28
  fileName: () => extensionConfig?.output,
35
29
  },
36
- rollupOptions: ROLLUP_OPTIONS,
30
+ rollupOptions: {
31
+ ...ROLLUP_OPTIONS,
32
+ plugins: [
33
+ ...(ROLLUP_OPTIONS.plugins || []),
34
+ manifestPlugin({ minify: false, extensionConfig }),
35
+ ],
36
+ },
37
37
  outDir: outputDir,
38
38
  emptyOutDir: true,
39
39
  minify: false,
40
40
  },
41
41
  });
42
- return extensionConfig;
43
- }
44
-
45
- function _startDevServer(outputDir, port, extensionConfig) {
46
- const app = express();
47
- const server = http.createServer(app);
48
-
49
- // Host the OUTPUT_DIR
50
- app.use(express.static(outputDir));
51
-
52
- // Setup websocket server to send messages to browser on bundle update
53
- const wss = new WebSocketServer({ server });
54
-
55
- const callback = `http://localhost:${port}/${extensionConfig?.output}`;
56
-
57
- function broadcast(message) {
58
- if (wss.clients.size === 0) {
59
- logger.warn('No browsers connected to update');
60
- return;
61
- }
62
- wss.clients.forEach(client => {
63
- client.send(JSON.stringify(message));
64
- });
65
- }
66
-
67
- wss.on('connection', client => {
68
- let base64Callback;
69
- try {
70
- base64Callback = fs
71
- .readFileSync(
72
- path.join(process.cwd(), outputDir, extensionConfig?.output)
73
- )
74
- .toString('base64');
75
- } catch (e) {
76
- logger.warn(
77
- 'File not found:',
78
- path.join(process.cwd(), outputDir, extensionConfig?.output)
79
- );
80
- }
81
-
82
- logger.info('Browser connected and listening for bundle updates');
83
- client.send(
84
- JSON.stringify({
85
- event: 'start',
86
- appName: extensionConfig?.appName,
87
- extension: extensionConfig?.name,
88
- callback: base64Callback
89
- ? `data:text/javascript;base64,${base64Callback}`
90
- : undefined,
91
- })
92
- );
93
- });
94
-
95
- // Start the express and websocket servers
96
- server.listen({ port }, () => {
97
- logger.warn(`Listening at ${callback}`);
98
- });
99
-
100
- // Setup a watcher on the dist directory and update broadcast
101
- //to all clients when an event is observed
102
- chokidar.watch(outputDir).on('change', file => {
103
- const base64Callback = fs
104
- .readFileSync(path.join(process.cwd(), file))
105
- .toString('base64');
106
- logger.debug(`${file} updated, reloading extension`);
107
- broadcast({
108
- event: 'update',
109
- appName: extensionConfig?.appName,
110
- extension: extensionConfig?.name,
111
- callback: `data:text/javascript;base64,${base64Callback}`,
112
- });
113
- });
114
-
115
- _configureShutDownHandlers(wss, broadcast, extensionConfig, server);
116
- }
117
-
118
- function _configureShutDownHandlers(wss, broadcast, extensionConfig, server) {
119
- function shutdown() {
120
- logger.warn('\nSending shutdown signal to connected browser');
121
- broadcast({
122
- event: 'shutdown',
123
- appName: extensionConfig?.appName,
124
- extension: extensionConfig?.name,
125
- });
126
- logger.warn('\nCleaning up after ourselves...\n');
127
-
128
- // Terminate all active connections, close seems to hang otherwise
129
- wss.clients.forEach(client => {
130
- client.terminate();
131
- });
132
-
133
- // Shut down the WebSocket server first since it is connected to the express server
134
- wss.close(webSocketError => {
135
- if (webSocketError) {
136
- logger.error(
137
- `WebSocket Server unable to shutdown correctly, ${webSocketError}`
138
- );
139
- } else {
140
- logger.warn('WebSocket Server stopped');
141
- }
142
- // Shutdown the express server
143
- server.close(error => {
144
- if (error) {
145
- logger.error(`Express server unable to shutdown correctly, ${error}`);
146
- } else {
147
- logger.warn('Express server stopped');
148
- }
149
- process.exit(webSocketError || error ? 1 : 0);
150
- });
151
- });
152
- }
153
-
154
- process.on('SIGINT', shutdown);
155
- process.on('SIGTERM', shutdown);
156
42
  }
157
43
 
158
44
  async function startDevMode(config, outputDir, port, extension) {
159
- const extensionConfig = await _devBuild(config, outputDir, extension);
160
- _startDevServer(outputDir, port, extensionConfig);
45
+ const extensionConfig = await getExtensionConfig(config, extension);
46
+ await _buildDevBundle(outputDir, extensionConfig);
47
+ startDevServer(outputDir, port, extensionConfig);
161
48
  }
162
49
 
163
50
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.0.1-beta.0",
3
+ "version": "0.0.1-beta.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -23,7 +23,8 @@
23
23
  "tests/testBuild.js",
24
24
  "tests/testDevServer.js",
25
25
  "plugins/manifestPlugin.js",
26
- "index.js"
26
+ "index.js",
27
+ "server.js"
27
28
  ],
28
29
  "license": "MIT",
29
30
  "dependencies": {
@@ -31,11 +32,12 @@
31
32
  "command-line-args": "^5.2.1",
32
33
  "command-line-usage": "^7.0.1",
33
34
  "console-log-colors": "^0.4.0",
35
+ "cors": "^2.8.5",
34
36
  "express": "^4.18.2",
35
37
  "process": "^0.11.10",
36
38
  "prompts": "^2.4.2",
37
39
  "vite": "^4.0.4",
38
- "ws": "^8.12.1"
40
+ "ws": "^8.13.0"
39
41
  },
40
42
  "bin": {
41
43
  "hs-ui-extensions-dev-server": "run.js"
@@ -56,5 +58,5 @@
56
58
  "optional": true
57
59
  }
58
60
  },
59
- "gitHead": "0bfb66cacc456a73f05a40841938e6b42f87aaba"
61
+ "gitHead": "3bad0a556e00ca7c628916da199bb6f6b787ad6f"
60
62
  }
@@ -12,9 +12,13 @@ function plugin(options = {}) {
12
12
  name: 'ui-extensions-manifest-generation-plugin',
13
13
  enforce: 'post', // run after default rollup plugins
14
14
  generateBundle(_rollupOptions, bundle) {
15
- const { output = DEFAULT_MANIFEST_NAME, minify = false } = options;
15
+ const {
16
+ output = DEFAULT_MANIFEST_NAME,
17
+ minify = false,
18
+ extensionConfig,
19
+ } = options;
16
20
  try {
17
- const manifest = _generateManifestContents(bundle);
21
+ const manifest = _generateManifestContents(bundle, extensionConfig);
18
22
  this.emitFile({
19
23
  type: 'asset',
20
24
  source: minify
@@ -29,9 +33,10 @@ function plugin(options = {}) {
29
33
  };
30
34
  }
31
35
 
32
- function _generateManifestContents(bundle) {
36
+ function _generateManifestContents(bundle, extension) {
33
37
  const baseManifest = {
34
38
  package: _loadPackageFile(),
39
+ extension,
35
40
  };
36
41
 
37
42
  // The keys to bundle are the filename without any path information
package/server.js ADDED
@@ -0,0 +1,148 @@
1
+ const express = require('express');
2
+ const chokidar = require('chokidar');
3
+ const { WebSocketServer } = require('ws');
4
+ const http = require('http');
5
+ const logger = require('./logger');
6
+ const path = require('path');
7
+ const cors = require('cors');
8
+ const {
9
+ EXTENSIONS_MESSAGE_VERSION,
10
+ WEBSOCKET_MESSAGE_VERSION,
11
+ } = require('./constants');
12
+
13
+ function startDevServer(outputDir, port, extensionConfig) {
14
+ const callback = `http://hslocal.net:${port}/${extensionConfig?.output}`;
15
+
16
+ const app = express();
17
+
18
+ // Using http.createServer so WebSocket server can be attached before initializing express.
19
+ const server = http.createServer(app);
20
+
21
+ // Setup middleware
22
+ app.use(cors());
23
+ app.use(express.static(outputDir));
24
+
25
+ _addExtensionsEndpoint(app, port);
26
+
27
+ const { broadcast, wss } = _setupWebSocketServer(
28
+ server,
29
+ extensionConfig,
30
+ callback
31
+ );
32
+ _setupChokidarWatcher(broadcast, outputDir);
33
+
34
+ server.listen({ port }, () => {
35
+ logger.warn(`Listening at ${callback}`);
36
+ });
37
+
38
+ _configureShutDownHandlers(server, wss, broadcast);
39
+ }
40
+
41
+ function _setupWebSocketServer(server, extensionConfig, callback) {
42
+ const wss = new WebSocketServer({ server });
43
+
44
+ const baseMessage = {
45
+ appName: extensionConfig?.appName,
46
+ title: extensionConfig?.name,
47
+ callback,
48
+ version: WEBSOCKET_MESSAGE_VERSION,
49
+ };
50
+
51
+ wss.on('connection', client => {
52
+ logger.info('Browser connected and listening for bundle updates');
53
+ client.send(
54
+ JSON.stringify({
55
+ event: 'start',
56
+ ...baseMessage,
57
+ })
58
+ );
59
+ });
60
+
61
+ return {
62
+ wss,
63
+ broadcast: message => {
64
+ if (wss.clients.size === 0) {
65
+ logger.warn('No browsers connected to notify');
66
+ return;
67
+ }
68
+ wss.clients.forEach(client => {
69
+ client.send(
70
+ JSON.stringify({
71
+ ...message,
72
+ ...baseMessage,
73
+ })
74
+ );
75
+ });
76
+ },
77
+ };
78
+ }
79
+
80
+ // Setup a watcher on the dist directory and update broadcast
81
+ //to all clients when an event is observed
82
+ function _setupChokidarWatcher(broadcast, outputDir) {
83
+ chokidar.watch(outputDir).on('all', (event, file) => {
84
+ if (event !== 'change' || event !== 'add' || file === 'manifest.json') {
85
+ // We need to listen to 'change' and 'add' because sometimes chokidar
86
+ // sees it as an 'unlink' and 'add' instead of just a 'change'
87
+ // Since we are adding the manifest.json to the build, we want to ignore changes to that so we don't double send messages
88
+ return;
89
+ }
90
+ logger.info('Bundle updated notifying browser');
91
+ broadcast({
92
+ event: 'update',
93
+ });
94
+ });
95
+ }
96
+
97
+ function _addExtensionsEndpoint(server, port) {
98
+ const endpoint = '/extensions';
99
+ server.get(endpoint, (_req, res) => {
100
+ try {
101
+ const manifest = require(path.join(process.cwd(), 'dist/manifest.json'));
102
+ const {
103
+ extension: { appName, name, output },
104
+ } = manifest;
105
+
106
+ const response = {
107
+ websocket: `ws://localhost:${port}`,
108
+ version: EXTENSIONS_MESSAGE_VERSION,
109
+ extensions: [
110
+ {
111
+ appName,
112
+ title: name,
113
+ callback: `http://hslocal.net:${port}/${output}`,
114
+ manifest: {
115
+ ...manifest,
116
+ extension: undefined,
117
+ },
118
+ },
119
+ ],
120
+ };
121
+ res.status(200).json(response);
122
+ } catch (e) {
123
+ res.status(500).json({
124
+ message: 'Unable to load manifest file',
125
+ });
126
+ }
127
+ });
128
+ logger.warn(`Listening at http://hslocal.net:${port}${endpoint}`);
129
+ }
130
+
131
+ function _configureShutDownHandlers(server, wss, broadcast) {
132
+ function shutdown() {
133
+ logger.warn('\nCleaning up after ourselves...');
134
+ // Stop new connections to express server
135
+ broadcast({
136
+ event: 'shutdown',
137
+ });
138
+ wss.close(() => {});
139
+ server.close(() => {});
140
+ logger.warn('Clean up done');
141
+ process.exit(0);
142
+ }
143
+
144
+ process.on('SIGINT', shutdown);
145
+ process.on('SIGTERM', shutdown);
146
+ }
147
+
148
+ module.exports = startDevServer;
package/tests/runTests.js CHANGED
@@ -16,7 +16,8 @@ process.on('SIGINT', () => {
16
16
  handleFailure();
17
17
  });
18
18
 
19
- process.on('uncaughtException', () => {
19
+ process.on('uncaughtException', e => {
20
+ console.error(e);
20
21
  handleFailure();
21
22
  });
22
23
 
@@ -4,14 +4,20 @@ const path = require('path');
4
4
  const assert = require('assert');
5
5
  const http = require('http');
6
6
  const WebSocket = require('ws');
7
+ const {
8
+ WEBSOCKET_MESSAGE_VERSION,
9
+ EXTENSIONS_MESSAGE_VERSION,
10
+ } = require('../constants');
7
11
 
8
12
  const testResults = {
9
13
  buildTestPassed: false,
10
- expressTestPassed: false,
14
+ expressStaticTestPassed: false,
15
+ extensionsEndpointPassed: false,
11
16
  webSocketTestPassed: false,
12
17
  };
13
18
 
14
19
  const port = 5172;
20
+ const host = 'hslocal.net';
15
21
 
16
22
  function testDevServer(logger, devServerProcess) {
17
23
  logger.warn('\nDev Server Tests started 🤞');
@@ -29,11 +35,13 @@ function testDevServer(logger, devServerProcess) {
29
35
 
30
36
  devServerProcess.stdout.on('data', buffer => {
31
37
  const data = buffer.toString().toLowerCase();
32
- console.log('[Dev Server Log]:', data);
33
38
  if (data.includes('built in')) {
34
39
  testBuild(testResults, logger);
35
40
  }
36
- if (data.includes('listening') && data.includes(`localhost:${port}`)) {
41
+ if (
42
+ data.includes('listening') &&
43
+ data.includes(`${host}:${port}/extensions`)
44
+ ) {
37
45
  setTimeout(() => {
38
46
  testExpressServer(testResults, logger);
39
47
  testWebSocketServer(testResults, logger);
@@ -72,10 +80,16 @@ function testDevServer(logger, devServerProcess) {
72
80
  function metConditions() {
73
81
  const {
74
82
  buildTestPassed,
75
- expressTestPassed,
83
+ expressStaticTestPassed,
84
+ extensionsEndpointPassed,
76
85
  webSocketTestPassed,
77
86
  } = testResults;
78
- return buildTestPassed && expressTestPassed && webSocketTestPassed;
87
+ return (
88
+ buildTestPassed &&
89
+ expressStaticTestPassed &&
90
+ extensionsEndpointPassed &&
91
+ webSocketTestPassed
92
+ );
79
93
  }
80
94
 
81
95
  // Test that the files were built in the proper location and spot
@@ -84,7 +98,7 @@ function testBuild(results, logger) {
84
98
  // // Make sure the files are getting generated in the dist dir
85
99
  const distDir = path.join(process.cwd(), 'dist');
86
100
  const filesInOutputDir = fs.readdirSync(distDir);
87
- assert.deepStrictEqual(filesInOutputDir, ['PhoneLines.js']);
101
+ assert.deepStrictEqual(filesInOutputDir, ['PhoneLines.js', 'manifest.json']);
88
102
  const fileContents = fs
89
103
  .readFileSync(path.join(distDir, filesInOutputDir[0]))
90
104
  .toString();
@@ -110,7 +124,7 @@ function testBuild(results, logger) {
110
124
  function testExpressServer(results, logger) {
111
125
  http.get(
112
126
  {
113
- host: 'localhost',
127
+ host,
114
128
  port,
115
129
  path: '/PhoneLines.js',
116
130
  },
@@ -119,7 +133,38 @@ function testExpressServer(results, logger) {
119
133
  throw Error('Error with express server');
120
134
  }
121
135
  logger.info('- Express server connected and serving files 🚀');
122
- results.expressTestPassed = true;
136
+ results.expressStaticTestPassed = true;
137
+ }
138
+ );
139
+ http.get(
140
+ {
141
+ host,
142
+ port,
143
+ path: '/extensions',
144
+ },
145
+ response => {
146
+ let body = '';
147
+ response.on('data', chunk => {
148
+ body += chunk.toString();
149
+ });
150
+ response.on('end', () => {
151
+ assert(response.statusCode === 200);
152
+ body = JSON.parse(body);
153
+ assert(body.extensions);
154
+ assert.equal(body.extensions.length, 1);
155
+ const extension = body.extensions.pop();
156
+ assert(extension.manifest);
157
+ assert.equal(extension.appName, 'Example App React UI');
158
+ assert.equal(extension.title, 'Phone Lines');
159
+ assert.equal(
160
+ extension.callback,
161
+ `http://${host}:${port}/PhoneLines.js`
162
+ );
163
+ assert.equal(body.websocket, `ws://localhost:${port}`);
164
+ assert.strictEquals(body.version, EXTENSIONS_MESSAGE_VERSION);
165
+ logger.info('- Express serving extension data 🚀');
166
+ results.extensionsEndpointPassed = true;
167
+ });
123
168
  }
124
169
  );
125
170
  }
@@ -127,20 +172,16 @@ function testExpressServer(results, logger) {
127
172
  // Test the the web socket server is running on the expected port and
128
173
  // that we are able to receive messages from it.
129
174
  function testWebSocketServer(results, logger) {
130
- const fileContents = fs
131
- .readFileSync(path.join('dist', 'PhoneLines.js'))
132
- .toString('base64');
133
-
134
175
  const ws = new WebSocket(`ws://localhost:${port}`);
135
176
  ws.on('message', messageBuffer => {
136
177
  const message = JSON.parse(messageBuffer.toString());
137
- console.log('[WebSocket Message]:', message);
138
- assert(message.event === 'start' || message.event === 'update');
139
- assert(message.appName === 'example-app-remote-ui');
140
- assert.strictEqual(message.extension, 'Phone Lines');
141
- assert(message.callback, `data:text/javascript;base64,${fileContents}`);
178
+ assert(message.event === 'start');
179
+ assert.strictEqual(message.appName, 'Example App React UI');
180
+ assert.strictEqual(message.title, 'Phone Lines');
181
+ assert.strictEqual(message.version, WEBSOCKET_MESSAGE_VERSION);
142
182
  logger.info('- WebSocket server connected and sending messages 🚀');
143
183
  results.webSocketTestPassed = true;
184
+ ws.close(); // The test passed, close the connection
144
185
  });
145
186
  }
146
187