@hubspot/ui-extensions-dev-server 0.1.0 → 0.2.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/build.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { build } = require('vite');
2
2
  const { ROLLUP_OPTIONS } = require('./constants');
3
+ const manifestPlugin = require('./plugins/manifestPlugin');
3
4
 
4
5
  async function buildAllExtensions(config, outputDir) {
5
6
  const extensionKeys = Object.keys(config);
@@ -11,6 +12,9 @@ async function buildAllExtensions(config, outputDir) {
11
12
  outputFileName: data?.output,
12
13
  outputDir,
13
14
  emptyOutDir: i === 0,
15
+ plugins: {
16
+ rollup: [manifestPlugin({ output: data?.output })],
17
+ },
14
18
  });
15
19
  }
16
20
  }
package/constants.js CHANGED
@@ -1,7 +1,9 @@
1
1
  const VITE_DEFAULT_PORT = 5173;
2
+ const WEBSOCKET_PORT = 5174;
2
3
  const MAIN_APP_CONFIG = 'app.json';
3
4
  const PROJECT_CONFIG = 'hsproject.json';
4
5
  const OUTPUT_DIR = 'dist';
6
+ const MANIFEST_FILE = 'manifest.json';
5
7
 
6
8
  const ROLLUP_OPTIONS = {
7
9
  // Deps to exclude from the bundle
@@ -25,6 +27,8 @@ module.exports = {
25
27
  MAIN_APP_CONFIG,
26
28
  PROJECT_CONFIG,
27
29
  OUTPUT_DIR,
30
+ MANIFEST_FILE,
31
+ WEBSOCKET_PORT,
28
32
  EXTENSIONS_MESSAGE_VERSION,
29
33
  WEBSOCKET_MESSAGE_VERSION,
30
34
  };
package/dev.js CHANGED
@@ -1,50 +1,62 @@
1
- const { build } = require('vite');
1
+ const { createServer } = require('vite');
2
2
  const { getExtensionConfig } = require('./config');
3
- const { ROLLUP_OPTIONS } = require('./constants');
4
3
  const startDevServer = require('./server');
5
- const manifestPlugin = require('./plugins/manifestPlugin');
4
+ const devBuildPlugin = require('./plugins/devBuildPlugin');
6
5
 
7
- async function _buildDevBundle(outputDir, extensionConfig) {
8
- await build({
9
- define: {
10
- 'process.env.NODE_ENV': JSON.stringify(
11
- process.env.NODE_ENV || 'development'
12
- ),
13
- },
14
- build: {
15
- watch: {
16
- clearScreen: false,
17
- exclude: [
18
- 'node_modules',
19
- 'package.json',
20
- 'package-lock.json',
21
- 'app.json',
22
- ],
6
+ async function _createViteDevServer(
7
+ outputDir,
8
+ extensionConfig,
9
+ websocketPort,
10
+ baseMessage
11
+ ) {
12
+ return await createServer({
13
+ appType: 'custom',
14
+ mode: 'development',
15
+ server: {
16
+ middlewareMode: true,
17
+ hmr: {
18
+ port: websocketPort,
23
19
  },
24
- lib: {
25
- entry: extensionConfig?.file,
26
- name: extensionConfig?.output,
27
- formats: ['iife'],
28
- fileName: () => extensionConfig?.output,
20
+ watch: {
21
+ ignored: ['**/src/app/extensions/dist/**/*'],
29
22
  },
23
+ },
24
+ build: {
30
25
  rollupOptions: {
31
- ...ROLLUP_OPTIONS,
32
- plugins: [
33
- ...(ROLLUP_OPTIONS.plugins || []),
34
- manifestPlugin({ minify: false, extensionConfig }),
35
- ],
26
+ input: extensionConfig?.file,
27
+ output: extensionConfig.output,
36
28
  },
37
- outDir: outputDir,
38
- emptyOutDir: true,
39
- minify: false,
40
29
  },
30
+ plugins: [devBuildPlugin({ extensionConfig, outputDir, baseMessage })],
41
31
  });
42
32
  }
43
33
 
44
- async function startDevMode(config, outputDir, port, extension) {
34
+ async function startDevMode(
35
+ config,
36
+ outputDir,
37
+ expressPort,
38
+ webSocketPort,
39
+ extension
40
+ ) {
45
41
  const extensionConfig = await getExtensionConfig(config, extension);
46
- await _buildDevBundle(outputDir, extensionConfig);
47
- startDevServer(outputDir, port, extensionConfig);
42
+ const baseMessage = Object.freeze({
43
+ appName: extensionConfig?.appName,
44
+ title: extensionConfig?.name,
45
+ callback: `http://hslocal.net:${expressPort}/${extensionConfig?.output}`,
46
+ });
47
+ const viteDevServer = await _createViteDevServer(
48
+ outputDir,
49
+ extensionConfig,
50
+ webSocketPort,
51
+ baseMessage
52
+ );
53
+ startDevServer(
54
+ outputDir,
55
+ expressPort,
56
+ webSocketPort,
57
+ baseMessage,
58
+ viteDevServer
59
+ );
48
60
  }
49
61
 
50
62
  module.exports = {
package/index.js CHANGED
@@ -16,12 +16,13 @@ async function remoteBuild(root, entryPoint, outputDir) {
16
16
  );
17
17
  }
18
18
 
19
+ const output = getUrlSafeFileName(entryPoint);
19
20
  await buildSingleExtension({
20
21
  file: entryPoint,
21
22
  outputFileName: getUrlSafeFileName(entryPoint),
22
23
  outputDir: outputDir || OUTPUT_DIR,
23
24
  plugins: {
24
- rollup: [manifestPlugin({ minify: true })],
25
+ rollup: [manifestPlugin({ minify: true, output })],
25
26
  },
26
27
  minify: true,
27
28
  root,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,13 +22,14 @@
22
22
  "tests/runTests.js",
23
23
  "tests/testBuild.js",
24
24
  "tests/testDevServer.js",
25
- "plugins/manifestPlugin.js",
25
+ "tests/utils.js",
26
+ "plugins/*",
26
27
  "index.js",
27
28
  "server.js"
28
29
  ],
29
30
  "license": "MIT",
30
31
  "dependencies": {
31
- "chokidar": "^3.5.3",
32
+ "axios": "^1.4.0",
32
33
  "command-line-args": "^5.2.1",
33
34
  "command-line-usage": "^7.0.1",
34
35
  "console-log-colors": "^0.4.0",
@@ -58,5 +59,5 @@
58
59
  "optional": true
59
60
  }
60
61
  },
61
- "gitHead": "abac5960e6277af827714dcd4150ad7606e6a481"
62
+ "gitHead": "ab7ad192e9d98163d2613d77f68677e3b48898aa"
62
63
  }
@@ -0,0 +1,98 @@
1
+ const { ROLLUP_OPTIONS, WEBSOCKET_MESSAGE_VERSION } = require('../constants');
2
+ const { build } = require('vite');
3
+ const manifestPlugin = require('./manifestPlugin');
4
+ const logger = require('../logger');
5
+
6
+ function devBuildPlugin(options = {}) {
7
+ const { extensionConfig, outputDir, baseMessage } = options;
8
+ const versionedBaseMessage = {
9
+ ...baseMessage,
10
+ version: WEBSOCKET_MESSAGE_VERSION,
11
+ };
12
+
13
+ const devBuild = async () => {
14
+ try {
15
+ await build({
16
+ mode: 'development',
17
+ define: {
18
+ 'process.env.NODE_ENV': JSON.stringify(
19
+ process.env.NODE_ENV || 'development'
20
+ ),
21
+ },
22
+ build: {
23
+ lib: {
24
+ entry: extensionConfig?.file,
25
+ name: extensionConfig?.output,
26
+ formats: ['iife'],
27
+ fileName: () => extensionConfig?.output,
28
+ },
29
+ rollupOptions: {
30
+ ...ROLLUP_OPTIONS,
31
+ plugins: [
32
+ ...(ROLLUP_OPTIONS.plugins || []),
33
+ manifestPlugin({
34
+ minify: false,
35
+ output: extensionConfig?.output,
36
+ }),
37
+ ],
38
+ output: {
39
+ ...ROLLUP_OPTIONS.output,
40
+ sourcemap: 'inline',
41
+ },
42
+ },
43
+ outDir: outputDir,
44
+ emptyOutDir: true,
45
+ minify: false,
46
+ },
47
+ });
48
+ } catch (e) {
49
+ logger.error(e.message);
50
+ }
51
+ };
52
+
53
+ let localServer;
54
+ return {
55
+ name: 'ui-extensibility-dev-build-plugin',
56
+ enforce: 'pre',
57
+ configureServer: async server => {
58
+ // Store a reference to the server to be used in hooks that don't get the server injected
59
+ // See https://vitejs.dev/guide/api-plugin.html#configureserver for information on this pattern
60
+ localServer = server;
61
+ localServer.ws.on('connection', () => {
62
+ logger.info('Browser connected and listening for bundle updates');
63
+ localServer.ws.send({
64
+ ...versionedBaseMessage,
65
+ event: 'start',
66
+ });
67
+ });
68
+ await devBuild();
69
+ },
70
+ handleHotUpdate: async ({ server }) => {
71
+ await devBuild();
72
+ if (server.ws.clients.size) {
73
+ logger.info('Bundle updated, notifying connected browsers');
74
+ } else {
75
+ logger.warn('Bundle updated, no browsers connected to notify');
76
+ }
77
+ server.ws.send({
78
+ ...versionedBaseMessage,
79
+ event: 'update',
80
+ });
81
+ return [];
82
+ },
83
+ buildEnd(error) {
84
+ if (error) {
85
+ logger.error(error);
86
+ }
87
+ logger.warn('Sending shutdown message to connected browsers');
88
+ if (localServer && localServer.ws) {
89
+ localServer.ws.send({
90
+ ...versionedBaseMessage,
91
+ event: 'shutdown',
92
+ });
93
+ }
94
+ },
95
+ };
96
+ }
97
+
98
+ module.exports = devBuildPlugin;
@@ -1,8 +1,9 @@
1
1
  const { readFileSync } = require('fs');
2
2
  const { normalize } = require('path');
3
+ const { MANIFEST_FILE } = require('../constants');
3
4
  const logger = require('../logger');
5
+ const path = require('path');
4
6
 
5
- const DEFAULT_MANIFEST_NAME = 'manifest.json';
6
7
  const PACKAGE_LOCK_FILE = 'package-lock.json';
7
8
  const PACKAGE_FILE = 'package.json';
8
9
  const EXTENSIONS_PATH = 'src/app/extensions/';
@@ -12,19 +13,16 @@ function plugin(options = {}) {
12
13
  name: 'ui-extensions-manifest-generation-plugin',
13
14
  enforce: 'post', // run after default rollup plugins
14
15
  generateBundle(_rollupOptions, bundle) {
15
- const {
16
- output = DEFAULT_MANIFEST_NAME,
17
- minify = false,
18
- extensionConfig,
19
- } = options;
16
+ const { output, minify = false } = options;
20
17
  try {
21
- const manifest = _generateManifestContents(bundle, extensionConfig);
18
+ const filename = path.parse(output).name;
19
+ const manifest = _generateManifestContents(bundle);
22
20
  this.emitFile({
23
21
  type: 'asset',
24
22
  source: minify
25
23
  ? JSON.stringify(manifest)
26
24
  : JSON.stringify(manifest, null, 2),
27
- fileName: normalize(output),
25
+ fileName: normalize(`${filename}-${MANIFEST_FILE}`),
28
26
  });
29
27
  } catch (e) {
30
28
  logger.warn(`\nUnable to write manifest file in ${output}, ${e}`);
@@ -33,14 +31,13 @@ function plugin(options = {}) {
33
31
  };
34
32
  }
35
33
 
36
- function _generateManifestContents(bundle, extension) {
34
+ function _generateManifestContents(bundle) {
37
35
  const baseManifest = {
38
36
  package: _loadPackageFile(),
39
- extension,
40
37
  };
41
38
 
42
39
  // The keys to bundle are the filename without any path information
43
- const bundles = Object.keys(bundle);
40
+ const bundles = Object.keys(bundle).filter(cur => cur.endsWith('.js'));
44
41
 
45
42
  if (bundles.length === 1) {
46
43
  return {
@@ -86,7 +83,7 @@ function _loadPackageFile() {
86
83
  }
87
84
 
88
85
  function _stripPathPriorToExtDir(filepath) {
89
- return filepath.split(EXTENSIONS_PATH).pop();
86
+ return filepath?.split(EXTENSIONS_PATH).pop();
90
87
  }
91
88
 
92
89
  function _buildModulesInfo(moduleIds, modules) {
package/run.js CHANGED
@@ -3,27 +3,37 @@
3
3
  const { startDevMode } = require('./dev');
4
4
  const { loadConfig } = require('./config');
5
5
  const { buildAllExtensions, buildSingleExtension } = require('./build');
6
- const { VITE_DEFAULT_PORT, OUTPUT_DIR } = require('./constants');
6
+ const {
7
+ VITE_DEFAULT_PORT,
8
+ OUTPUT_DIR,
9
+ WEBSOCKET_PORT,
10
+ } = require('./constants');
7
11
  const { parseArgs, showHelp } = require('./cli');
8
12
  const { getUrlSafeFileName } = require('./utils');
9
13
  const manifestPlugin = require('./plugins/manifestPlugin');
10
14
 
11
- const { DEV_MODE, BUILD_MODE, port, extension, help } = parseArgs();
12
- const PORT = port || VITE_DEFAULT_PORT;
15
+ const { DEV_MODE, BUILD_MODE, extension, help } = parseArgs();
13
16
 
14
17
  if (help || !(DEV_MODE || BUILD_MODE)) {
15
18
  showHelp(OUTPUT_DIR);
16
19
  }
17
20
 
18
21
  if (DEV_MODE) {
19
- startDevMode(loadConfig(), OUTPUT_DIR, PORT, extension);
22
+ startDevMode(
23
+ loadConfig(),
24
+ OUTPUT_DIR,
25
+ VITE_DEFAULT_PORT,
26
+ WEBSOCKET_PORT,
27
+ extension
28
+ );
20
29
  } else if (BUILD_MODE) {
21
30
  if (extension) {
31
+ const output = getUrlSafeFileName(extension);
22
32
  buildSingleExtension({
23
33
  file: extension,
24
- outputFileName: getUrlSafeFileName(extension),
34
+ outputFileName: output,
25
35
  outputDir: OUTPUT_DIR,
26
- plugins: { rollup: [manifestPlugin()] },
36
+ plugins: { rollup: [manifestPlugin({ output })] },
27
37
  });
28
38
  } else {
29
39
  buildAllExtensions(loadConfig(), OUTPUT_DIR);
package/server.js CHANGED
@@ -1,120 +1,64 @@
1
1
  const express = require('express');
2
- const chokidar = require('chokidar');
3
- const { WebSocketServer } = require('ws');
4
- const http = require('http');
5
2
  const logger = require('./logger');
6
3
  const path = require('path');
7
4
  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
-
5
+ const fs = require('fs');
6
+ const { EXTENSIONS_MESSAGE_VERSION, MANIFEST_FILE } = require('./constants');
7
+
8
+ function startDevServer(
9
+ outputDir,
10
+ expressPort,
11
+ webSocketPort,
12
+ baseMessage,
13
+ viteDevServer
14
+ ) {
16
15
  const app = express();
17
16
 
18
- // Using http.createServer so WebSocket server can be attached before initializing express.
19
- const server = http.createServer(app);
20
-
21
17
  // Setup middleware
22
18
  app.use(cors());
23
19
  app.use(express.static(outputDir));
24
-
25
- _addExtensionsEndpoint(app, port);
26
-
27
- const { broadcast, wss } = _setupWebSocketServer(
28
- server,
29
- extensionConfig,
30
- callback
20
+ _addExtensionsEndpoint(
21
+ app,
22
+ expressPort,
23
+ webSocketPort,
24
+ outputDir,
25
+ baseMessage
31
26
  );
32
- _setupChokidarWatcher(broadcast, outputDir);
33
-
34
- server.listen({ port }, () => {
35
- logger.warn(`Listening at ${callback}`);
36
- });
37
-
38
- _configureShutDownHandlers(server, wss, broadcast);
39
- }
40
27
 
41
- function _setupWebSocketServer(server, extensionConfig, callback) {
42
- const wss = new WebSocketServer({ server });
28
+ // Vite middlewares needs to go last because it's greedy and will block other middleware
29
+ app.use(viteDevServer.middlewares);
43
30
 
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
- );
31
+ const server = app.listen({ port: expressPort }, () => {
32
+ logger.warn(`Listening at ${baseMessage.callback}`);
59
33
  });
60
34
 
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
- });
35
+ _configureShutDownHandlers(server, viteDevServer);
95
36
  }
96
37
 
97
- function _addExtensionsEndpoint(server, port) {
38
+ function _addExtensionsEndpoint(
39
+ server,
40
+ expressPort,
41
+ webSocketPort,
42
+ outputDir,
43
+ baseMessage
44
+ ) {
98
45
  const endpoint = '/extensions';
99
46
  server.get(endpoint, (_req, res) => {
100
47
  try {
101
- const manifest = require(path.join(process.cwd(), 'dist/manifest.json'));
102
- const {
103
- extension: { appName, name, output },
104
- } = manifest;
48
+ const output = path.parse(baseMessage.callback).name;
49
+ const manifest = JSON.parse(
50
+ fs.readFileSync(
51
+ path.join(process.cwd(), `${outputDir}/${output}-${MANIFEST_FILE}`)
52
+ )
53
+ );
105
54
 
106
55
  const response = {
107
- websocket: `ws://localhost:${port}`,
56
+ websocket: `ws://localhost:${webSocketPort}`,
108
57
  version: EXTENSIONS_MESSAGE_VERSION,
109
58
  extensions: [
110
59
  {
111
- appName,
112
- title: name,
113
- callback: `http://hslocal.net:${port}/${output}`,
114
- manifest: {
115
- ...manifest,
116
- extension: undefined,
117
- },
60
+ ...baseMessage,
61
+ manifest,
118
62
  },
119
63
  ],
120
64
  };
@@ -125,19 +69,16 @@ function _addExtensionsEndpoint(server, port) {
125
69
  });
126
70
  }
127
71
  });
128
- logger.warn(`Listening at http://hslocal.net:${port}${endpoint}`);
72
+ logger.warn(`Listening at http://hslocal.net:${expressPort}${endpoint}`);
129
73
  }
130
74
 
131
- function _configureShutDownHandlers(server, wss, broadcast) {
132
- function shutdown() {
75
+ function _configureShutDownHandlers(server, viteDevServer) {
76
+ async function shutdown() {
133
77
  logger.warn('\nCleaning up after ourselves...');
78
+ await viteDevServer.pluginContainer.close();
134
79
  // Stop new connections to express server
135
- broadcast({
136
- event: 'shutdown',
137
- });
138
- wss.close(() => {});
139
80
  server.close(() => {});
140
- logger.warn('Clean up done');
81
+ logger.info('Clean up done, exiting.');
141
82
  process.exit(0);
142
83
  }
143
84
 
package/tests/runTests.js CHANGED
@@ -3,30 +3,48 @@
3
3
  const { testBuild } = require('./testBuild');
4
4
  const { testDevServer } = require('./testDevServer');
5
5
  const logger = require('../logger');
6
+ const { generateSpec } = require('./utils');
6
7
 
7
8
  let devServerProcess;
8
9
 
10
+ // Overwrite console.debug to only log when in DEBUG mode
11
+ console.debug = (...args) => {
12
+ if (process.env.DEBUG) {
13
+ console.log(...args);
14
+ }
15
+ };
16
+
9
17
  function handleFailure() {
10
18
  if (devServerProcess) {
11
19
  devServerProcess.kill();
12
20
  }
13
21
  }
14
22
 
15
- process.on('SIGINT', () => {
16
- handleFailure();
17
- });
23
+ process.on('SIGINT', handleFailure);
24
+ process.on('SIGTERM', handleFailure);
18
25
 
19
26
  process.on('uncaughtException', e => {
20
27
  console.error(e);
21
28
  handleFailure();
22
29
  });
23
30
 
24
- try {
25
- testBuild(logger);
26
- testDevServer(logger, devServerProcess);
27
- } catch (e) {
28
- console.error(e.message);
29
- logger.error('Tests failed 😭');
30
- handleFailure();
31
- process.exit(1);
32
- }
31
+ // eslint-disable-next-line no-floating-promise/no-floating-promise
32
+ (async () => {
33
+ try {
34
+ const spec = generateSpec();
35
+
36
+ logger.warn(
37
+ `Running tests for the following entrypoints: [\n\t${spec.build.entrypoints.join(
38
+ ',\n\t'
39
+ )}\n]`
40
+ );
41
+
42
+ await testBuild(logger, spec.build);
43
+ testDevServer(logger, devServerProcess, spec.dev);
44
+ } catch (e) {
45
+ console.error(e.message);
46
+ logger.error('Tests failed 😭');
47
+ handleFailure();
48
+ process.exit(1);
49
+ }
50
+ })();
@@ -2,6 +2,7 @@ const { execSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const assert = require('assert');
5
+ const { generateManifestOutputPair, verifyFileContents } = require('./utils');
5
6
 
6
7
  function _testHelper(command, outputDirFiles) {
7
8
  if (command) {
@@ -16,76 +17,64 @@ function _testHelper(command, outputDirFiles) {
16
17
  // Spot check the file contents to make sure they seem ok
17
18
  filesInOutputDir.forEach(file => {
18
19
  const fileContents = fs.readFileSync(path.join(distDir, file)).toString();
19
- if (file === 'manifest.json') {
20
- const manifest = JSON.parse(fileContents);
21
- assert(manifest.entry);
22
- assert(manifest.modules);
23
- assert(manifest.modules.internal);
24
- manifest.modules.internal.forEach(mod => {
25
- assert(mod.module);
26
- assert(mod.renderedExports);
27
- });
28
- assert(manifest.modules.external);
29
- manifest.modules.external.forEach(mod => {
30
- assert(mod.module);
31
- assert(mod.renderedExports);
32
- });
33
- assert(manifest.package);
34
- assert(manifest.package.packages);
35
- } else {
36
- const stringsToSpotCheck = [
37
- '.createRemoteReactComponent',
38
- '.createElement',
39
- 'hubspot.extend',
40
- 'React',
41
- 'RemoteUI',
42
- ];
43
- stringsToSpotCheck.forEach(stringToCheck => {
44
- assert(
45
- fileContents.includes(stringToCheck),
46
- `File ${file} contents should contain: "${stringToCheck}"`
47
- );
48
- });
49
- }
20
+ verifyFileContents(file, fileContents);
50
21
  });
51
22
  }
52
23
 
53
- function testDefaultBuildPath(logger) {
54
- logger.warn('- Test default build path started 🤞');
55
- _testHelper('hs-ui-extensions-dev-server build', [
56
- 'PhoneLines.js',
57
- 'ProgressBarApp.js',
58
- ]);
59
- logger.info('- Test default build path passed 🚀');
24
+ function testBuildAll(logger, entrypoints) {
25
+ const expected = entrypoints
26
+ .reduce((acc, cur) => {
27
+ return acc.concat(generateManifestOutputPair(cur));
28
+ }, [])
29
+ .sort();
30
+ logger.warn('- Test build all extensions started 🤞');
31
+ _testHelper('hs-ui-extensions-dev-server build', expected);
32
+ logger.info('- Test build all extensions passed 🚀');
60
33
  }
61
34
 
62
- function testBuildWithExtensionFlag(logger) {
63
- logger.warn('- Test build with flags started 🤞');
64
- _testHelper(
65
- 'hs-ui-extensions-dev-server build --extension ProgressBarApp.tsx',
66
- ['ProgressBarApp.js', 'manifest.json']
67
- );
68
- logger.info('- Test build with flags passed 🚀');
35
+ function testBuildSingle(logger, entrypoints) {
36
+ entrypoints.forEach(entrypoint => {
37
+ logger.warn(
38
+ `- Test build single extension started for entrypoint ${entrypoint} 🤞`
39
+ );
40
+ _testHelper(
41
+ `hs-ui-extensions-dev-server build --extension ${entrypoint}`,
42
+ generateManifestOutputPair(entrypoint)
43
+ );
44
+ logger.info(
45
+ `- Test build single extension passed for entrypoint ${entrypoint} 🚀`
46
+ );
47
+ });
69
48
  }
70
49
 
71
- function testDefInfraBuildFileName(logger) {
50
+ async function testBuildRemote(logger, entrypoints) {
72
51
  const { remoteBuild } = require('../index');
73
- logger.warn('- Test remoteBuild function 🤞');
52
+ for (let i = 0; i < entrypoints.length; ++i) {
53
+ const entrypoint = entrypoints[i];
54
+ logger.warn(
55
+ `- Test remoteBuild function started for entrypoint ${entrypoint} 🤞`
56
+ );
57
+
58
+ await remoteBuild(process.cwd(), entrypoint, 'dist');
59
+ _testHelper(null, generateManifestOutputPair(entrypoint));
74
60
 
75
- remoteBuild(process.cwd(), 'ProgressBarApp.tsx', 'dist');
76
- _testHelper(null, ['ProgressBarApp.js', 'manifest.json']);
77
- logger.info('- Test build with entrypoint as arg 🚀');
61
+ logger.info(
62
+ `- Test remoteBuild function passed for entrypoint ${entrypoint} 🚀`
63
+ );
64
+ }
78
65
  }
79
66
 
80
- function testBuild(logger) {
67
+ async function testBuild(logger, buildSpec) {
68
+ const { entrypoints } = buildSpec;
69
+
81
70
  logger.warn('\nBuild Tests started - External Devs 🤞');
82
- testDefaultBuildPath(logger);
83
- testBuildWithExtensionFlag(logger);
71
+ testBuildAll(logger, entrypoints);
72
+ testBuildSingle(logger, entrypoints);
84
73
  logger.info('Build Tests passed - External Devs🚀');
85
74
 
86
- logger.warn('\nBuild Tests started - Dev Infra 🤞');
87
- testDefInfraBuildFileName(logger);
88
- logger.info('Build Tests passed - Dev Infra 🚀');
75
+ logger.warn('\nBuild Tests started - Remote 🤞');
76
+ await testBuildRemote(logger, entrypoints);
77
+ logger.info('Build Tests passed - Remote 🚀');
89
78
  }
90
79
 
91
80
  module.exports = {
@@ -2,49 +2,92 @@ const { spawn } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const assert = require('assert');
5
- const http = require('http');
5
+ const axios = require('axios');
6
6
  const WebSocket = require('ws');
7
7
  const {
8
+ VITE_DEFAULT_PORT,
9
+ WEBSOCKET_PORT,
8
10
  WEBSOCKET_MESSAGE_VERSION,
9
11
  EXTENSIONS_MESSAGE_VERSION,
10
12
  } = require('../constants');
13
+ const { verifyFileContents, generateManifestOutputPair } = require('./utils');
14
+ const { getUrlSafeFileName } = require('../utils');
11
15
 
12
- const testResults = {
16
+ const preShutdownResults = {
13
17
  buildTestPassed: false,
14
18
  expressStaticTestPassed: false,
15
19
  extensionsEndpointPassed: false,
16
20
  webSocketTestPassed: false,
17
21
  };
18
22
 
19
- const port = 5172;
20
- const host = 'hslocal.net';
23
+ const postShutdownResults = {
24
+ websocketShutdownReceived: false,
25
+ expressServerShutdown: false,
26
+ };
27
+
28
+ const host = 'http://hslocal.net';
29
+
30
+ let running = false;
21
31
 
22
- function testDevServer(logger, devServerProcess) {
23
- logger.warn('\nDev Server Tests started 🤞');
32
+ function resetResults() {
33
+ Object.keys(preShutdownResults).forEach(result => {
34
+ preShutdownResults[result] = false;
35
+ });
36
+ Object.keys(postShutdownResults).forEach(result => {
37
+ postShutdownResults[result] = false;
38
+ });
39
+ }
24
40
 
41
+ function testDevServer(logger, devServerProcess, specs) {
42
+ const devServerInterval = setInterval(runDevServer, 1000);
43
+ let runCount = 0;
44
+
45
+ function runDevServer() {
46
+ if (running === false && !devServerProcess) {
47
+ resetResults();
48
+ _testDevServer(logger, devServerProcess, specs[runCount]);
49
+ runCount += 1;
50
+ }
51
+ if (runCount === specs.length) {
52
+ clearInterval(devServerInterval);
53
+ }
54
+ }
55
+ }
56
+
57
+ function _testDevServer(logger, devServerProcess, spec) {
58
+ running = true;
59
+ const { entrypoint, expected } = spec;
60
+
61
+ const filename = getUrlSafeFileName(entrypoint);
62
+ const localExpectations = {
63
+ callback: `${host}:${VITE_DEFAULT_PORT}/${filename}`,
64
+ websocketUrl: `ws://localhost:${WEBSOCKET_PORT}`,
65
+ ...expected,
66
+ };
67
+
68
+ logger.warn(`\nDev Server Tests started - ${entrypoint} 🤞`);
25
69
  // We need to use spawn here because it will put the process into the background,
26
70
  // which is required because dev mode is a blocking process and we want to test that
27
71
  // the express server and websocket server are starting properly
28
72
  devServerProcess = spawn('hs-ui-extensions-dev-server', [
29
73
  'dev',
30
74
  '--extension',
31
- 'PhoneLines.tsx',
32
- '--port',
33
- `${port}`,
75
+ entrypoint,
34
76
  ]);
35
77
 
36
78
  devServerProcess.stdout.on('data', buffer => {
37
79
  const data = buffer.toString().toLowerCase();
80
+ console.debug('[Dev Server]:', data);
38
81
  if (data.includes('built in')) {
39
- testBuild(testResults, logger);
82
+ testBuild(logger, entrypoint);
40
83
  }
41
84
  if (
42
85
  data.includes('listening') &&
43
- data.includes(`${host}:${port}/extensions`)
86
+ data.includes(`${host}:${VITE_DEFAULT_PORT}/extensions`)
44
87
  ) {
45
88
  setTimeout(() => {
46
- testExpressServer(testResults, logger);
47
- testWebSocketServer(testResults, logger);
89
+ testExpressServer(logger, localExpectations, filename);
90
+ testWebSocketServer(logger, localExpectations);
48
91
  }, 1000);
49
92
  }
50
93
  });
@@ -58,32 +101,33 @@ function testDevServer(logger, devServerProcess) {
58
101
 
59
102
  // When the process closes make sure we met all the success conditions
60
103
  devServerProcess.on('close', () => {
61
- if (metConditions()) {
62
- logger.info('Dev Server Tests passed 🚀');
104
+ if (metPreShutdownConditions()) {
105
+ logger.info(`- Dev Server pre-shutdown tests passed - ${entrypoint} 🚀`);
106
+ testPostShutdown(logger, filename);
63
107
  } else {
64
- console.log(testResults);
108
+ console.log(preShutdownResults);
65
109
  logger.error('Tests failed 😭');
66
110
  }
67
111
  });
68
112
 
69
- const interval = setInterval(callback, 1000);
70
- let count = 0;
71
- function callback() {
72
- count += 1;
73
- if (metConditions() || count === 5) {
113
+ const preShutdownInterval = setInterval(preShutdownCallback, 1000);
114
+ let preShutdownCheckCount = 0;
115
+ function preShutdownCallback() {
116
+ preShutdownCheckCount += 1;
117
+ if (metPreShutdownConditions() || preShutdownCheckCount === 5) {
118
+ clearInterval(preShutdownInterval);
74
119
  devServerProcess.kill();
75
- clearInterval(interval);
76
120
  }
77
121
  }
78
122
  }
79
123
 
80
- function metConditions() {
124
+ function metPreShutdownConditions() {
81
125
  const {
82
126
  buildTestPassed,
83
127
  expressStaticTestPassed,
84
128
  extensionsEndpointPassed,
85
129
  webSocketTestPassed,
86
- } = testResults;
130
+ } = preShutdownResults;
87
131
  return (
88
132
  buildTestPassed &&
89
133
  expressStaticTestPassed &&
@@ -92,99 +136,152 @@ function metConditions() {
92
136
  );
93
137
  }
94
138
 
139
+ function metPostShutdownConditions() {
140
+ const {
141
+ websocketShutdownReceived,
142
+ expressServerShutdown,
143
+ } = postShutdownResults;
144
+
145
+ return websocketShutdownReceived && expressServerShutdown;
146
+ }
147
+
95
148
  // Test that the files were built in the proper location and spot
96
149
  // check the contents of the files.
97
- function testBuild(results, logger) {
150
+ function testBuild(logger, entryPoint) {
98
151
  // // Make sure the files are getting generated in the dist dir
99
152
  const distDir = path.join(process.cwd(), 'dist');
100
153
  const filesInOutputDir = fs.readdirSync(distDir);
101
- assert.deepStrictEqual(filesInOutputDir, ['PhoneLines.js', 'manifest.json']);
154
+ const filename = path.parse(entryPoint).name;
155
+ assert.deepStrictEqual(
156
+ filesInOutputDir,
157
+ generateManifestOutputPair(entryPoint)
158
+ );
102
159
  const fileContents = fs
103
- .readFileSync(path.join(distDir, filesInOutputDir[0]))
160
+ .readFileSync(path.join(distDir, `${filename}.js`))
104
161
  .toString();
105
- const stringsToSpotCheck = [
106
- '.createRemoteReactComponent',
107
- '.createElement',
108
- 'hubspot.extend',
109
- 'React',
110
- 'RemoteUI',
111
- ];
112
- stringsToSpotCheck.forEach(stringToCheck => {
113
- assert(
114
- fileContents.includes(stringToCheck),
115
- `File ${filesInOutputDir[0]} contents should contain: "${stringToCheck}"`
116
- );
117
- });
162
+ verifyFileContents(`${filename}.js`, fileContents);
163
+
164
+ // Check for the inlined source map
165
+ assert(
166
+ fileContents.includes(
167
+ '//# sourceMappingURL=data:application/json;charset=utf-8;base64,'
168
+ )
169
+ );
118
170
  logger.info('- Build succeeded 🚀');
119
- results.buildTestPassed = true;
171
+ preShutdownResults.buildTestPassed = true;
120
172
  }
121
173
 
122
174
  // Test that the express server is running on the expected port
123
175
  // and that it is serving the files as expected.
124
- function testExpressServer(results, logger) {
125
- http.get(
126
- {
127
- host,
128
- port,
129
- path: '/PhoneLines.js',
130
- },
131
- response => {
132
- if (response.statusCode !== 200) {
133
- throw Error('Error with express server');
134
- }
176
+ function testExpressServer(logger, expected, filename) {
177
+ axios
178
+ .get(`${host}:${VITE_DEFAULT_PORT}/${filename}`)
179
+ .then(response => {
180
+ assert.strictEqual(response.status, 200, 'Bundle status code');
135
181
  logger.info('- Express server connected and serving files 🚀');
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
- });
168
- }
169
- );
182
+ preShutdownResults.expressStaticTestPassed = true;
183
+ })
184
+ .catch(logger.error);
185
+ axios
186
+ .get(`${host}:${VITE_DEFAULT_PORT}/extensions`)
187
+ .then(response => {
188
+ const { data, status } = response;
189
+ assert.strictEqual(status, 200, '/extensions status code');
190
+
191
+ // Top level response assertions
192
+ assert.strictEqual(data.websocket, expected.websocketUrl);
193
+ assert.strictEqual(data.version, EXTENSIONS_MESSAGE_VERSION);
194
+ assert(data.extensions);
195
+
196
+ // Extension level response assertions
197
+ assert.equal(data.extensions.length, 1, '/extensions body length');
198
+ const extension = data.extensions.pop();
199
+ assert(extension.manifest);
200
+ assert.strictEqual(
201
+ extension.appName,
202
+ expected.appName,
203
+ '/extensions appName'
204
+ );
205
+ assert.strictEqual(extension.title, expected.title, '/extensions title');
206
+ assert.strictEqual(
207
+ extension.callback,
208
+ expected.callback,
209
+ '/extensions callback'
210
+ );
211
+ logger.info('- Express serving extension data 🚀');
212
+ preShutdownResults.extensionsEndpointPassed = true;
213
+ })
214
+ .catch(logger.error);
170
215
  }
171
216
 
172
217
  // Test the the web socket server is running on the expected port and
173
218
  // that we are able to receive messages from it.
174
- function testWebSocketServer(results, logger) {
175
- const ws = new WebSocket(`ws://localhost:${port}`);
219
+ function testWebSocketServer(logger, expected) {
220
+ const ws = new WebSocket(expected.websocketUrl);
176
221
  ws.on('message', messageBuffer => {
177
222
  const message = JSON.parse(messageBuffer.toString());
178
- assert(message.event === 'start');
179
- assert.strictEqual(message.appName, 'Example App React UI');
180
- assert.strictEqual(message.title, 'Phone Lines');
223
+ console.debug('[WebSocket Message]:', message);
224
+ // Vite sends { type: 'connected'} as a greeting on connect
225
+ if (message.type === 'connected') {
226
+ return;
227
+ }
228
+
229
+ assert(['start', 'shutdown'].includes(message.event), 'Message Event');
230
+ assert.strictEqual(message.appName, expected.appName, 'Message appName');
231
+ assert.strictEqual(message.title, expected.title, 'Message title');
181
232
  assert.strictEqual(message.version, WEBSOCKET_MESSAGE_VERSION);
182
- logger.info('- WebSocket server connected and sending messages 🚀');
183
- results.webSocketTestPassed = true;
184
- ws.close(); // The test passed, close the connection
233
+ logger.info(
234
+ `- WebSocket server connected and sending '${message.event}' messages 🚀`
235
+ );
236
+ if (message.event === 'start') {
237
+ preShutdownResults.webSocketTestPassed = true;
238
+ } else if (message.event === 'shutdown') {
239
+ postShutdownResults.websocketShutdownReceived = true;
240
+ }
185
241
  });
186
242
  }
187
243
 
244
+ function testPostShutdown(logger, filename) {
245
+ testExpressHasShutdown(filename);
246
+
247
+ const shutdownInterval = setInterval(shutdownCallback, 1000);
248
+ let shutdownCheckCount = 0;
249
+ function shutdownCallback() {
250
+ shutdownCheckCount += 1;
251
+ if (metPostShutdownConditions()) {
252
+ clearInterval(shutdownInterval);
253
+ logger.info('- Dev Server post-shutdown tests passed 🚀');
254
+ logger.info('Dev Server tests passed 🚀');
255
+ running = false;
256
+ } else if (shutdownCheckCount === 5) {
257
+ clearInterval(shutdownInterval);
258
+ console.log(postShutdownResults);
259
+ logger.error('Tests failed 😭');
260
+ }
261
+ }
262
+ }
263
+
264
+ function testExpressHasShutdown(filename) {
265
+ axios
266
+ .get(`${host}:${VITE_DEFAULT_PORT}/${filename}`)
267
+ .then(response => {
268
+ console.debug(response);
269
+ })
270
+ .catch(err => {
271
+ assert.strictEqual(
272
+ err.syscall,
273
+ 'connect',
274
+ 'The connect call should fail'
275
+ );
276
+ assert.strictEqual(
277
+ err.code,
278
+ 'ECONNREFUSED',
279
+ 'Express should refuse connection post shutdown'
280
+ );
281
+ postShutdownResults.expressServerShutdown = true;
282
+ });
283
+ }
284
+
188
285
  module.exports = {
189
286
  testDevServer,
190
287
  };
package/tests/utils.js ADDED
@@ -0,0 +1,76 @@
1
+ const path = require('path');
2
+ const { MANIFEST_FILE } = require('../constants');
3
+ const { getUrlSafeFileName } = require('../utils');
4
+ const assert = require('assert');
5
+ const { loadConfig } = require('../config');
6
+
7
+ function generateManifestOutputPair(entrypoint) {
8
+ const fileName = path.parse(entrypoint).name;
9
+ return [`${fileName}-${MANIFEST_FILE}`, getUrlSafeFileName(entrypoint)];
10
+ }
11
+
12
+ function verifyFileContents(file, contents) {
13
+ if (file.endsWith(MANIFEST_FILE)) {
14
+ const manifest = JSON.parse(contents);
15
+ assert(manifest.entry);
16
+ assert(manifest.modules);
17
+ assert(manifest.modules.internal);
18
+ manifest.modules.internal.forEach(mod => {
19
+ assert(mod.module);
20
+ assert(mod.renderedExports);
21
+ });
22
+ assert(manifest.modules.external);
23
+ manifest.modules.external.forEach(mod => {
24
+ assert(mod.module);
25
+ assert(mod.renderedExports);
26
+ });
27
+ assert(manifest.package);
28
+ assert(manifest.package.packages);
29
+ } else {
30
+ const stringsToSpotCheck = [
31
+ '.createRemoteReactComponent',
32
+ '.createElement',
33
+ 'hubspot.extend',
34
+ 'React',
35
+ 'RemoteUI',
36
+ ];
37
+ stringsToSpotCheck.forEach(stringToCheck => {
38
+ assert(
39
+ contents.includes(stringToCheck),
40
+ `File ${file} contents should contain: "${stringToCheck}"`
41
+ );
42
+ });
43
+ }
44
+ }
45
+
46
+ function generateSpec() {
47
+ const config = loadConfig();
48
+ const entrypoints = Object.keys(config);
49
+ if (entrypoints.length === 0) {
50
+ throw new Error(
51
+ 'Unable to determine testable entrypoints from config files'
52
+ );
53
+ }
54
+
55
+ const dev = entrypoints.map(entrypoint => {
56
+ const { data } = config[entrypoint];
57
+ return {
58
+ entrypoint,
59
+ expected: {
60
+ ...data,
61
+ },
62
+ };
63
+ });
64
+ return {
65
+ build: {
66
+ entrypoints,
67
+ },
68
+ dev,
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ generateManifestOutputPair,
74
+ verifyFileContents,
75
+ generateSpec,
76
+ };