@hubspot/ui-extensions-dev-server 0.0.1-prealpha.8 → 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/README.md +3 -1
- package/build.js +4 -0
- package/config.js +11 -10
- package/constants.js +9 -0
- package/dev.js +49 -118
- package/index.js +2 -1
- package/package.json +9 -6
- package/plugins/devBuildPlugin.js +98 -0
- package/plugins/manifestPlugin.js +7 -5
- package/run.js +16 -6
- package/server.js +89 -0
- package/tests/runTests.js +32 -13
- package/tests/testBuild.js +50 -59
- package/tests/testDevServer.js +209 -69
- package/tests/utils.js +76 -0
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
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/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/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
|
|
4
|
+
const { MAIN_APP_CONFIG } = require('./constants');
|
|
5
5
|
const { getUrlSafeFileName } = require('./utils');
|
|
6
6
|
|
|
7
7
|
async function getExtensionConfig(configuration, extension) {
|
|
8
|
-
|
|
9
|
-
|
|
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:
|
|
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 =
|
|
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
|
@@ -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
|
|
@@ -16,10 +18,17 @@ const ROLLUP_OPTIONS = {
|
|
|
16
18
|
},
|
|
17
19
|
};
|
|
18
20
|
|
|
21
|
+
const EXTENSIONS_MESSAGE_VERSION = 0;
|
|
22
|
+
const WEBSOCKET_MESSAGE_VERSION = 0;
|
|
23
|
+
|
|
19
24
|
module.exports = {
|
|
20
25
|
VITE_DEFAULT_PORT,
|
|
21
26
|
ROLLUP_OPTIONS,
|
|
22
27
|
MAIN_APP_CONFIG,
|
|
23
28
|
PROJECT_CONFIG,
|
|
24
29
|
OUTPUT_DIR,
|
|
30
|
+
MANIFEST_FILE,
|
|
31
|
+
WEBSOCKET_PORT,
|
|
32
|
+
EXTENSIONS_MESSAGE_VERSION,
|
|
33
|
+
WEBSOCKET_MESSAGE_VERSION,
|
|
25
34
|
};
|
package/dev.js
CHANGED
|
@@ -1,131 +1,62 @@
|
|
|
1
|
-
const
|
|
2
|
-
const chokidar = require('chokidar');
|
|
3
|
-
const { WebSocketServer } = require('ws');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const logger = require('./logger');
|
|
6
|
-
const { build } = require('vite');
|
|
1
|
+
const { createServer } = require('vite');
|
|
7
2
|
const { getExtensionConfig } = require('./config');
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const { ROLLUP_OPTIONS } = require('./constants');
|
|
3
|
+
const startDevServer = require('./server');
|
|
4
|
+
const devBuildPlugin = require('./plugins/devBuildPlugin');
|
|
11
5
|
|
|
12
|
-
async function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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,
|
|
19
|
+
},
|
|
21
20
|
watch: {
|
|
22
|
-
|
|
23
|
-
exclude: [
|
|
24
|
-
'node_modules',
|
|
25
|
-
'package.json',
|
|
26
|
-
'package-lock.json',
|
|
27
|
-
'app.json',
|
|
28
|
-
],
|
|
21
|
+
ignored: ['**/src/app/extensions/dist/**/*'],
|
|
29
22
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
},
|
|
24
|
+
build: {
|
|
25
|
+
rollupOptions: {
|
|
26
|
+
input: extensionConfig?.file,
|
|
27
|
+
output: extensionConfig.output,
|
|
35
28
|
},
|
|
36
|
-
rollupOptions: ROLLUP_OPTIONS,
|
|
37
|
-
outDir: outputDir,
|
|
38
|
-
emptyOutDir: true,
|
|
39
|
-
minify: false,
|
|
40
29
|
},
|
|
30
|
+
plugins: [devBuildPlugin({ extensionConfig, outputDir, baseMessage })],
|
|
41
31
|
});
|
|
42
|
-
return extensionConfig;
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
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);
|
|
34
|
+
async function startDevMode(
|
|
35
|
+
config,
|
|
36
|
+
outputDir,
|
|
37
|
+
expressPort,
|
|
38
|
+
webSocketPort,
|
|
39
|
+
extension
|
|
40
|
+
) {
|
|
41
|
+
const extensionConfig = await getExtensionConfig(config, extension);
|
|
42
|
+
const baseMessage = Object.freeze({
|
|
43
|
+
appName: extensionConfig?.appName,
|
|
44
|
+
title: extensionConfig?.name,
|
|
45
|
+
callback: `http://hslocal.net:${expressPort}/${extensionConfig?.output}`,
|
|
123
46
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
);
|
|
129
60
|
}
|
|
130
61
|
|
|
131
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.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,20 +22,23 @@
|
|
|
22
22
|
"tests/runTests.js",
|
|
23
23
|
"tests/testBuild.js",
|
|
24
24
|
"tests/testDevServer.js",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"tests/utils.js",
|
|
26
|
+
"plugins/*",
|
|
27
|
+
"index.js",
|
|
28
|
+
"server.js"
|
|
27
29
|
],
|
|
28
30
|
"license": "MIT",
|
|
29
31
|
"dependencies": {
|
|
30
|
-
"
|
|
32
|
+
"axios": "^1.4.0",
|
|
31
33
|
"command-line-args": "^5.2.1",
|
|
32
34
|
"command-line-usage": "^7.0.1",
|
|
33
35
|
"console-log-colors": "^0.4.0",
|
|
36
|
+
"cors": "^2.8.5",
|
|
34
37
|
"express": "^4.18.2",
|
|
35
38
|
"process": "^0.11.10",
|
|
36
39
|
"prompts": "^2.4.2",
|
|
37
40
|
"vite": "^4.0.4",
|
|
38
|
-
"ws": "^8.
|
|
41
|
+
"ws": "^8.13.0"
|
|
39
42
|
},
|
|
40
43
|
"bin": {
|
|
41
44
|
"hs-ui-extensions-dev-server": "run.js"
|
|
@@ -56,5 +59,5 @@
|
|
|
56
59
|
"optional": true
|
|
57
60
|
}
|
|
58
61
|
},
|
|
59
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "ab7ad192e9d98163d2613d77f68677e3b48898aa"
|
|
60
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,15 +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 { output
|
|
16
|
+
const { output, minify = false } = options;
|
|
16
17
|
try {
|
|
18
|
+
const filename = path.parse(output).name;
|
|
17
19
|
const manifest = _generateManifestContents(bundle);
|
|
18
20
|
this.emitFile({
|
|
19
21
|
type: 'asset',
|
|
20
22
|
source: minify
|
|
21
23
|
? JSON.stringify(manifest)
|
|
22
24
|
: JSON.stringify(manifest, null, 2),
|
|
23
|
-
fileName: normalize(
|
|
25
|
+
fileName: normalize(`${filename}-${MANIFEST_FILE}`),
|
|
24
26
|
});
|
|
25
27
|
} catch (e) {
|
|
26
28
|
logger.warn(`\nUnable to write manifest file in ${output}, ${e}`);
|
|
@@ -35,7 +37,7 @@ function _generateManifestContents(bundle) {
|
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
// The keys to bundle are the filename without any path information
|
|
38
|
-
const bundles = Object.keys(bundle);
|
|
40
|
+
const bundles = Object.keys(bundle).filter(cur => cur.endsWith('.js'));
|
|
39
41
|
|
|
40
42
|
if (bundles.length === 1) {
|
|
41
43
|
return {
|
|
@@ -81,7 +83,7 @@ function _loadPackageFile() {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
function _stripPathPriorToExtDir(filepath) {
|
|
84
|
-
return filepath
|
|
86
|
+
return filepath?.split(EXTENSIONS_PATH).pop();
|
|
85
87
|
}
|
|
86
88
|
|
|
87
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 {
|
|
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,
|
|
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(
|
|
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:
|
|
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
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const logger = require('./logger');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const cors = require('cors');
|
|
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
|
+
) {
|
|
15
|
+
const app = express();
|
|
16
|
+
|
|
17
|
+
// Setup middleware
|
|
18
|
+
app.use(cors());
|
|
19
|
+
app.use(express.static(outputDir));
|
|
20
|
+
_addExtensionsEndpoint(
|
|
21
|
+
app,
|
|
22
|
+
expressPort,
|
|
23
|
+
webSocketPort,
|
|
24
|
+
outputDir,
|
|
25
|
+
baseMessage
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Vite middlewares needs to go last because it's greedy and will block other middleware
|
|
29
|
+
app.use(viteDevServer.middlewares);
|
|
30
|
+
|
|
31
|
+
const server = app.listen({ port: expressPort }, () => {
|
|
32
|
+
logger.warn(`Listening at ${baseMessage.callback}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
_configureShutDownHandlers(server, viteDevServer);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _addExtensionsEndpoint(
|
|
39
|
+
server,
|
|
40
|
+
expressPort,
|
|
41
|
+
webSocketPort,
|
|
42
|
+
outputDir,
|
|
43
|
+
baseMessage
|
|
44
|
+
) {
|
|
45
|
+
const endpoint = '/extensions';
|
|
46
|
+
server.get(endpoint, (_req, res) => {
|
|
47
|
+
try {
|
|
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
|
+
);
|
|
54
|
+
|
|
55
|
+
const response = {
|
|
56
|
+
websocket: `ws://localhost:${webSocketPort}`,
|
|
57
|
+
version: EXTENSIONS_MESSAGE_VERSION,
|
|
58
|
+
extensions: [
|
|
59
|
+
{
|
|
60
|
+
...baseMessage,
|
|
61
|
+
manifest,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
res.status(200).json(response);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
res.status(500).json({
|
|
68
|
+
message: 'Unable to load manifest file',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
logger.warn(`Listening at http://hslocal.net:${expressPort}${endpoint}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _configureShutDownHandlers(server, viteDevServer) {
|
|
76
|
+
async function shutdown() {
|
|
77
|
+
logger.warn('\nCleaning up after ourselves...');
|
|
78
|
+
await viteDevServer.pluginContainer.close();
|
|
79
|
+
// Stop new connections to express server
|
|
80
|
+
server.close(() => {});
|
|
81
|
+
logger.info('Clean up done, exiting.');
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.on('SIGINT', shutdown);
|
|
86
|
+
process.on('SIGTERM', shutdown);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = startDevServer;
|
package/tests/runTests.js
CHANGED
|
@@ -3,29 +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
|
-
|
|
17
|
-
});
|
|
23
|
+
process.on('SIGINT', handleFailure);
|
|
24
|
+
process.on('SIGTERM', handleFailure);
|
|
18
25
|
|
|
19
|
-
process.on('uncaughtException',
|
|
26
|
+
process.on('uncaughtException', e => {
|
|
27
|
+
console.error(e);
|
|
20
28
|
handleFailure();
|
|
21
29
|
});
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
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
|
+
})();
|
package/tests/testBuild.js
CHANGED
|
@@ -2,9 +2,12 @@ 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) {
|
|
9
|
+
execSync(command);
|
|
10
|
+
}
|
|
8
11
|
|
|
9
12
|
// Make sure the files are getting generated in the dist dir
|
|
10
13
|
const distDir = path.join(process.cwd(), 'dist');
|
|
@@ -14,76 +17,64 @@ function _testHelper(command, outputDirFiles) {
|
|
|
14
17
|
// Spot check the file contents to make sure they seem ok
|
|
15
18
|
filesInOutputDir.forEach(file => {
|
|
16
19
|
const fileContents = fs.readFileSync(path.join(distDir, file)).toString();
|
|
17
|
-
|
|
18
|
-
const manifest = JSON.parse(fileContents);
|
|
19
|
-
assert(manifest.entry);
|
|
20
|
-
assert(manifest.modules);
|
|
21
|
-
assert(manifest.modules.internal);
|
|
22
|
-
manifest.modules.internal.forEach(mod => {
|
|
23
|
-
assert(mod.module);
|
|
24
|
-
assert(mod.renderedExports);
|
|
25
|
-
});
|
|
26
|
-
assert(manifest.modules.external);
|
|
27
|
-
manifest.modules.external.forEach(mod => {
|
|
28
|
-
assert(mod.module);
|
|
29
|
-
assert(mod.renderedExports);
|
|
30
|
-
});
|
|
31
|
-
assert(manifest.package);
|
|
32
|
-
assert(manifest.package.packages);
|
|
33
|
-
} else {
|
|
34
|
-
const stringsToSpotCheck = [
|
|
35
|
-
'.createRemoteReactComponent',
|
|
36
|
-
'.createElement',
|
|
37
|
-
'hubspot.extend',
|
|
38
|
-
'React',
|
|
39
|
-
'RemoteUI',
|
|
40
|
-
];
|
|
41
|
-
stringsToSpotCheck.forEach(stringToCheck => {
|
|
42
|
-
assert(
|
|
43
|
-
fileContents.includes(stringToCheck),
|
|
44
|
-
`File ${file} contents should contain: "${stringToCheck}"`
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
20
|
+
verifyFileContents(file, fileContents);
|
|
48
21
|
});
|
|
49
22
|
}
|
|
50
23
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
logger.
|
|
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 🚀');
|
|
58
33
|
}
|
|
59
34
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
});
|
|
67
48
|
}
|
|
68
49
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
async function testBuildRemote(logger, entrypoints) {
|
|
51
|
+
const { remoteBuild } = require('../index');
|
|
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));
|
|
60
|
+
|
|
61
|
+
logger.info(
|
|
62
|
+
`- Test remoteBuild function passed for entrypoint ${entrypoint} 🚀`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
function testBuild(logger) {
|
|
67
|
+
async function testBuild(logger, buildSpec) {
|
|
68
|
+
const { entrypoints } = buildSpec;
|
|
69
|
+
|
|
79
70
|
logger.warn('\nBuild Tests started - External Devs 🤞');
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
testBuildAll(logger, entrypoints);
|
|
72
|
+
testBuildSingle(logger, entrypoints);
|
|
82
73
|
logger.info('Build Tests passed - External Devs🚀');
|
|
83
74
|
|
|
84
|
-
logger.warn('\nBuild Tests started -
|
|
85
|
-
|
|
86
|
-
logger.info('Build Tests passed -
|
|
75
|
+
logger.warn('\nBuild Tests started - Remote 🤞');
|
|
76
|
+
await testBuildRemote(logger, entrypoints);
|
|
77
|
+
logger.info('Build Tests passed - Remote 🚀');
|
|
87
78
|
}
|
|
88
79
|
|
|
89
80
|
module.exports = {
|
package/tests/testDevServer.js
CHANGED
|
@@ -2,40 +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
|
|
5
|
+
const axios = require('axios');
|
|
6
6
|
const WebSocket = require('ws');
|
|
7
|
+
const {
|
|
8
|
+
VITE_DEFAULT_PORT,
|
|
9
|
+
WEBSOCKET_PORT,
|
|
10
|
+
WEBSOCKET_MESSAGE_VERSION,
|
|
11
|
+
EXTENSIONS_MESSAGE_VERSION,
|
|
12
|
+
} = require('../constants');
|
|
13
|
+
const { verifyFileContents, generateManifestOutputPair } = require('./utils');
|
|
14
|
+
const { getUrlSafeFileName } = require('../utils');
|
|
7
15
|
|
|
8
|
-
const
|
|
16
|
+
const preShutdownResults = {
|
|
9
17
|
buildTestPassed: false,
|
|
10
|
-
|
|
18
|
+
expressStaticTestPassed: false,
|
|
19
|
+
extensionsEndpointPassed: false,
|
|
11
20
|
webSocketTestPassed: false,
|
|
12
21
|
};
|
|
13
22
|
|
|
14
|
-
const
|
|
23
|
+
const postShutdownResults = {
|
|
24
|
+
websocketShutdownReceived: false,
|
|
25
|
+
expressServerShutdown: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const host = 'http://hslocal.net';
|
|
29
|
+
|
|
30
|
+
let running = false;
|
|
15
31
|
|
|
16
|
-
function
|
|
17
|
-
|
|
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
|
+
}
|
|
18
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} 🤞`);
|
|
19
69
|
// We need to use spawn here because it will put the process into the background,
|
|
20
70
|
// which is required because dev mode is a blocking process and we want to test that
|
|
21
71
|
// the express server and websocket server are starting properly
|
|
22
72
|
devServerProcess = spawn('hs-ui-extensions-dev-server', [
|
|
23
73
|
'dev',
|
|
24
74
|
'--extension',
|
|
25
|
-
|
|
26
|
-
'--port',
|
|
27
|
-
`${port}`,
|
|
75
|
+
entrypoint,
|
|
28
76
|
]);
|
|
29
77
|
|
|
30
78
|
devServerProcess.stdout.on('data', buffer => {
|
|
31
79
|
const data = buffer.toString().toLowerCase();
|
|
80
|
+
console.debug('[Dev Server]:', data);
|
|
32
81
|
if (data.includes('built in')) {
|
|
33
|
-
testBuild(
|
|
82
|
+
testBuild(logger, entrypoint);
|
|
34
83
|
}
|
|
35
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
data.includes('listening') &&
|
|
86
|
+
data.includes(`${host}:${VITE_DEFAULT_PORT}/extensions`)
|
|
87
|
+
) {
|
|
36
88
|
setTimeout(() => {
|
|
37
|
-
testExpressServer(
|
|
38
|
-
testWebSocketServer(
|
|
89
|
+
testExpressServer(logger, localExpectations, filename);
|
|
90
|
+
testWebSocketServer(logger, localExpectations);
|
|
39
91
|
}, 1000);
|
|
40
92
|
}
|
|
41
93
|
});
|
|
@@ -49,99 +101,187 @@ function testDevServer(logger, devServerProcess) {
|
|
|
49
101
|
|
|
50
102
|
// When the process closes make sure we met all the success conditions
|
|
51
103
|
devServerProcess.on('close', () => {
|
|
52
|
-
if (
|
|
53
|
-
logger.info(
|
|
104
|
+
if (metPreShutdownConditions()) {
|
|
105
|
+
logger.info(`- Dev Server pre-shutdown tests passed - ${entrypoint} 🚀`);
|
|
106
|
+
testPostShutdown(logger, filename);
|
|
54
107
|
} else {
|
|
55
|
-
console.log(
|
|
108
|
+
console.log(preShutdownResults);
|
|
56
109
|
logger.error('Tests failed 😭');
|
|
57
110
|
}
|
|
58
111
|
});
|
|
59
112
|
|
|
60
|
-
const
|
|
61
|
-
let
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
if (
|
|
113
|
+
const preShutdownInterval = setInterval(preShutdownCallback, 1000);
|
|
114
|
+
let preShutdownCheckCount = 0;
|
|
115
|
+
function preShutdownCallback() {
|
|
116
|
+
preShutdownCheckCount += 1;
|
|
117
|
+
if (metPreShutdownConditions() || preShutdownCheckCount === 5) {
|
|
118
|
+
clearInterval(preShutdownInterval);
|
|
65
119
|
devServerProcess.kill();
|
|
66
|
-
clearInterval(interval);
|
|
67
120
|
}
|
|
68
121
|
}
|
|
69
122
|
}
|
|
70
123
|
|
|
71
|
-
function
|
|
124
|
+
function metPreShutdownConditions() {
|
|
72
125
|
const {
|
|
73
126
|
buildTestPassed,
|
|
74
|
-
|
|
127
|
+
expressStaticTestPassed,
|
|
128
|
+
extensionsEndpointPassed,
|
|
75
129
|
webSocketTestPassed,
|
|
76
|
-
} =
|
|
77
|
-
return
|
|
130
|
+
} = preShutdownResults;
|
|
131
|
+
return (
|
|
132
|
+
buildTestPassed &&
|
|
133
|
+
expressStaticTestPassed &&
|
|
134
|
+
extensionsEndpointPassed &&
|
|
135
|
+
webSocketTestPassed
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function metPostShutdownConditions() {
|
|
140
|
+
const {
|
|
141
|
+
websocketShutdownReceived,
|
|
142
|
+
expressServerShutdown,
|
|
143
|
+
} = postShutdownResults;
|
|
144
|
+
|
|
145
|
+
return websocketShutdownReceived && expressServerShutdown;
|
|
78
146
|
}
|
|
79
147
|
|
|
80
148
|
// Test that the files were built in the proper location and spot
|
|
81
149
|
// check the contents of the files.
|
|
82
|
-
function testBuild(
|
|
150
|
+
function testBuild(logger, entryPoint) {
|
|
83
151
|
// // Make sure the files are getting generated in the dist dir
|
|
84
152
|
const distDir = path.join(process.cwd(), 'dist');
|
|
85
153
|
const filesInOutputDir = fs.readdirSync(distDir);
|
|
86
|
-
|
|
154
|
+
const filename = path.parse(entryPoint).name;
|
|
155
|
+
assert.deepStrictEqual(
|
|
156
|
+
filesInOutputDir,
|
|
157
|
+
generateManifestOutputPair(entryPoint)
|
|
158
|
+
);
|
|
87
159
|
const fileContents = fs
|
|
88
|
-
.readFileSync(path.join(distDir,
|
|
160
|
+
.readFileSync(path.join(distDir, `${filename}.js`))
|
|
89
161
|
.toString();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
assert(
|
|
99
|
-
fileContents.includes(stringToCheck),
|
|
100
|
-
`File ${filesInOutputDir[0]} contents should contain: "${stringToCheck}"`
|
|
101
|
-
);
|
|
102
|
-
});
|
|
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
|
+
);
|
|
103
170
|
logger.info('- Build succeeded 🚀');
|
|
104
|
-
|
|
171
|
+
preShutdownResults.buildTestPassed = true;
|
|
105
172
|
}
|
|
106
173
|
|
|
107
174
|
// Test that the express server is running on the expected port
|
|
108
175
|
// and that it is serving the files as expected.
|
|
109
|
-
function testExpressServer(
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
path: '/PhoneLines.js',
|
|
115
|
-
},
|
|
116
|
-
response => {
|
|
117
|
-
if (response.statusCode !== 200) {
|
|
118
|
-
throw Error('Error with express server');
|
|
119
|
-
}
|
|
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');
|
|
120
181
|
logger.info('- Express server connected and serving files 🚀');
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
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);
|
|
124
215
|
}
|
|
125
216
|
|
|
126
217
|
// Test the the web socket server is running on the expected port and
|
|
127
218
|
// that we are able to receive messages from it.
|
|
128
|
-
function testWebSocketServer(
|
|
129
|
-
const
|
|
130
|
-
.readFileSync(path.join('dist', 'PhoneLines.js'))
|
|
131
|
-
.toString('base64');
|
|
132
|
-
|
|
133
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
219
|
+
function testWebSocketServer(logger, expected) {
|
|
220
|
+
const ws = new WebSocket(expected.websocketUrl);
|
|
134
221
|
ws.on('message', messageBuffer => {
|
|
135
222
|
const message = JSON.parse(messageBuffer.toString());
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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');
|
|
232
|
+
assert.strictEqual(message.version, WEBSOCKET_MESSAGE_VERSION);
|
|
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
|
+
}
|
|
142
241
|
});
|
|
143
242
|
}
|
|
144
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
|
+
|
|
145
285
|
module.exports = {
|
|
146
286
|
testDevServer,
|
|
147
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
|
+
};
|