@hubspot/ui-extensions-dev-server 0.0.1-prealpha.8 → 0.1.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/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,99 +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
- process.on('SIGINT', () => {
116
- logger.warn('\nSending shutdown signal to connected browser');
117
- broadcast({
118
- event: 'shutdown',
119
- appName: extensionConfig?.appName,
120
- extension: extensionConfig?.name,
121
- });
122
- process.exit(0);
123
- });
124
42
  }
125
43
 
126
44
  async function startDevMode(config, outputDir, port, extension) {
127
- const extensionConfig = await _devBuild(config, outputDir, extension);
128
- _startDevServer(outputDir, port, extensionConfig);
45
+ const extensionConfig = await getExtensionConfig(config, extension);
46
+ await _buildDevBundle(outputDir, extensionConfig);
47
+ startDevServer(outputDir, port, extensionConfig);
129
48
  }
130
49
 
131
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-prealpha.8",
3
+ "version": "0.1.0",
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": "b5968f431fe29ee9977e6aacdd9b9ab3aa08af0c"
61
+ "gitHead": "abac5960e6277af827714dcd4150ad7606e6a481"
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,7 +4,9 @@ const path = require('path');
4
4
  const assert = require('assert');
5
5
 
6
6
  function _testHelper(command, outputDirFiles) {
7
- execSync(command);
7
+ if (command) {
8
+ execSync(command);
9
+ }
8
10
 
9
11
  // Make sure the files are getting generated in the dist dir
10
12
  const distDir = path.join(process.cwd(), 'dist');
@@ -67,11 +69,11 @@ function testBuildWithExtensionFlag(logger) {
67
69
  }
68
70
 
69
71
  function testDefInfraBuildFileName(logger) {
70
- logger.warn('- Test build with entrypoint as arg 🤞');
71
- _testHelper('hs-ui-extensions-remote-build ProgressBarApp.tsx', [
72
- 'ProgressBarApp.js',
73
- 'manifest.json',
74
- ]);
72
+ const { remoteBuild } = require('../index');
73
+ logger.warn('- Test remoteBuild function 🤞');
74
+
75
+ remoteBuild(process.cwd(), 'ProgressBarApp.tsx', 'dist');
76
+ _testHelper(null, ['ProgressBarApp.js', 'manifest.json']);
75
77
  logger.info('- Test build with entrypoint as arg 🚀');
76
78
  }
77
79
 
@@ -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 🤞');
@@ -32,7 +38,10 @@ function testDevServer(logger, devServerProcess) {
32
38
  if (data.includes('built in')) {
33
39
  testBuild(testResults, logger);
34
40
  }
35
- if (data.includes('listening') && data.includes(`localhost:${port}`)) {
41
+ if (
42
+ data.includes('listening') &&
43
+ data.includes(`${host}:${port}/extensions`)
44
+ ) {
36
45
  setTimeout(() => {
37
46
  testExpressServer(testResults, logger);
38
47
  testWebSocketServer(testResults, logger);
@@ -71,10 +80,16 @@ function testDevServer(logger, devServerProcess) {
71
80
  function metConditions() {
72
81
  const {
73
82
  buildTestPassed,
74
- expressTestPassed,
83
+ expressStaticTestPassed,
84
+ extensionsEndpointPassed,
75
85
  webSocketTestPassed,
76
86
  } = testResults;
77
- return buildTestPassed && expressTestPassed && webSocketTestPassed;
87
+ return (
88
+ buildTestPassed &&
89
+ expressStaticTestPassed &&
90
+ extensionsEndpointPassed &&
91
+ webSocketTestPassed
92
+ );
78
93
  }
79
94
 
80
95
  // Test that the files were built in the proper location and spot
@@ -83,7 +98,7 @@ function testBuild(results, logger) {
83
98
  // // Make sure the files are getting generated in the dist dir
84
99
  const distDir = path.join(process.cwd(), 'dist');
85
100
  const filesInOutputDir = fs.readdirSync(distDir);
86
- assert.deepStrictEqual(filesInOutputDir, ['PhoneLines.js']);
101
+ assert.deepStrictEqual(filesInOutputDir, ['PhoneLines.js', 'manifest.json']);
87
102
  const fileContents = fs
88
103
  .readFileSync(path.join(distDir, filesInOutputDir[0]))
89
104
  .toString();
@@ -109,7 +124,7 @@ function testBuild(results, logger) {
109
124
  function testExpressServer(results, logger) {
110
125
  http.get(
111
126
  {
112
- host: 'localhost',
127
+ host,
113
128
  port,
114
129
  path: '/PhoneLines.js',
115
130
  },
@@ -118,7 +133,38 @@ function testExpressServer(results, logger) {
118
133
  throw Error('Error with express server');
119
134
  }
120
135
  logger.info('- Express server connected and serving files 🚀');
121
- 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
+ });
122
168
  }
123
169
  );
124
170
  }
@@ -126,19 +172,16 @@ function testExpressServer(results, logger) {
126
172
  // Test the the web socket server is running on the expected port and
127
173
  // that we are able to receive messages from it.
128
174
  function testWebSocketServer(results, logger) {
129
- const fileContents = fs
130
- .readFileSync(path.join('dist', 'PhoneLines.js'))
131
- .toString('base64');
132
-
133
175
  const ws = new WebSocket(`ws://localhost:${port}`);
134
176
  ws.on('message', messageBuffer => {
135
177
  const message = JSON.parse(messageBuffer.toString());
136
- assert(message.event === 'start' || message.event === 'update');
137
- assert(message.appName === 'example-app');
138
- assert.strictEqual(message.extension, 'Phone Lines');
139
- 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);
140
182
  logger.info('- WebSocket server connected and sending messages 🚀');
141
183
  results.webSocketTestPassed = true;
184
+ ws.close(); // The test passed, close the connection
142
185
  });
143
186
  }
144
187