@hubspot/ui-extensions-dev-server 0.0.1-beta.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 +27 -0
- package/build.js +56 -0
- package/cli.js +67 -0
- package/config.js +128 -0
- package/constants.js +25 -0
- package/dev.js +165 -0
- package/index.js +33 -0
- package/logger.js +19 -0
- package/package.json +60 -0
- package/plugins/manifestPlugin.js +108 -0
- package/run.js +31 -0
- package/tests/runTests.js +31 -0
- package/tests/testBuild.js +93 -0
- package/tests/testDevServer.js +149 -0
- package/utils.js +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ui-extensions-dev-server
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
This package contains the cli for running HubSpot UI extensions locally, as well as running a production build for debugging purposes.
|
|
5
|
+
|
|
6
|
+
### Help
|
|
7
|
+
If for any reason you need help with the `ui-extensions-dev-server` you can access the help menu in a few ways:
|
|
8
|
+
|
|
9
|
+
- `hs-ui-extensions-dev-server`
|
|
10
|
+
- `hs-ui-extensions-dev-server -h`
|
|
11
|
+
- `hs-ui-extensions-dev-server --help`
|
|
12
|
+
|
|
13
|
+
### Local Development
|
|
14
|
+
Prompts you which of your extensions you would like to run in local dev mode. Once you have made your selection, it sets up a watcher on the entry point file for that project as well as any child files. Any changes to those files will trigger a local build which will bundle your code and output the bundle in a `dist/` directory inside of your current working directory. To initiate local dev mode in the browser, you just need to navigate to the CRM where your the deployed version of your card lives. The browser will detect that you have the dev server running, and make the connection on your behalf.
|
|
15
|
+
|
|
16
|
+
| Action | Command |
|
|
17
|
+
| - | - |
|
|
18
|
+
| Standard Local Dev | `hs-ui-extensions-dev-server dev` |
|
|
19
|
+
| Bypass Prompt | `hs-ui-extensions-dev-server dev --extension MyEntryPointFile.jsx` <br /> `hs-ui-extensions-dev-server dev -e MyEntryPointFile.jsx`|
|
|
20
|
+
|
|
21
|
+
### Production Build
|
|
22
|
+
This should not be required in the standard development workflow. It is there to aid debugging the output of an extension. This command will run a build similar to what we will run when these extensions are uploaded. The output will be written into a `dist/` directory in the current working directory.
|
|
23
|
+
|
|
24
|
+
| Action | Command |
|
|
25
|
+
| - | - |
|
|
26
|
+
| Build All Extensions | `hs-ui-extensions-dev-server build` |
|
|
27
|
+
| Build a single extension | `hs-ui-extensions-dev-server build --extension MyEntryPointFile.jsx` <br /> `hs-ui-extensions-dev-server build -e MyEntryPointFile.jsx`|
|
package/build.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { build } = require('vite');
|
|
2
|
+
const { ROLLUP_OPTIONS } = require('./constants');
|
|
3
|
+
|
|
4
|
+
async function buildAllExtensions(config, outputDir) {
|
|
5
|
+
const extensionKeys = Object.keys(config);
|
|
6
|
+
for (let i = 0; i < extensionKeys.length; ++i) {
|
|
7
|
+
const { data } = config[extensionKeys[i]];
|
|
8
|
+
|
|
9
|
+
await buildSingleExtension({
|
|
10
|
+
file: data?.module.file,
|
|
11
|
+
outputFileName: data?.output,
|
|
12
|
+
outputDir,
|
|
13
|
+
emptyOutDir: i === 0,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function buildSingleExtension({
|
|
19
|
+
file,
|
|
20
|
+
outputFileName,
|
|
21
|
+
outputDir,
|
|
22
|
+
emptyOutDir = true,
|
|
23
|
+
plugins = { rollup: [], vite: [] },
|
|
24
|
+
minify = false,
|
|
25
|
+
root = process.cwd(), // This is the vite default, so using that as our default
|
|
26
|
+
}) {
|
|
27
|
+
await build({
|
|
28
|
+
root,
|
|
29
|
+
define: {
|
|
30
|
+
'process.env.NODE_ENV': JSON.stringify(
|
|
31
|
+
process.env.NODE_ENV || 'production'
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
build: {
|
|
35
|
+
lib: {
|
|
36
|
+
entry: file,
|
|
37
|
+
name: outputFileName,
|
|
38
|
+
formats: ['iife'],
|
|
39
|
+
fileName: () => outputFileName,
|
|
40
|
+
},
|
|
41
|
+
rollupOptions: {
|
|
42
|
+
...ROLLUP_OPTIONS,
|
|
43
|
+
plugins: [...(ROLLUP_OPTIONS.plugins || []), ...plugins?.rollup],
|
|
44
|
+
},
|
|
45
|
+
outDir: outputDir,
|
|
46
|
+
emptyOutDir,
|
|
47
|
+
minify,
|
|
48
|
+
plugins: plugins?.vite,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
buildAllExtensions,
|
|
55
|
+
buildSingleExtension,
|
|
56
|
+
};
|
package/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const logger = require('./logger');
|
|
2
|
+
const commandLineArgs = require('command-line-args');
|
|
3
|
+
const commandLineUsage = require('command-line-usage');
|
|
4
|
+
|
|
5
|
+
function parseArgs() {
|
|
6
|
+
const mainDefinitions = [{ name: 'command', defaultOption: true }];
|
|
7
|
+
const mainOptions = commandLineArgs(mainDefinitions, {
|
|
8
|
+
stopAtFirstUnknown: true,
|
|
9
|
+
});
|
|
10
|
+
const argv = mainOptions._unknown || [];
|
|
11
|
+
const DEV_MODE = mainOptions.command === 'dev';
|
|
12
|
+
const BUILD_MODE = mainOptions.command === 'build';
|
|
13
|
+
|
|
14
|
+
const optionDefinitions = [
|
|
15
|
+
{ name: 'port', alias: 'p', type: Number },
|
|
16
|
+
{ name: 'extension', alias: 'e', type: String },
|
|
17
|
+
{ name: 'help', alias: 'h', type: Boolean },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const options = commandLineArgs(
|
|
21
|
+
optionDefinitions,
|
|
22
|
+
DEV_MODE || BUILD_MODE ? { argv } : {}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return { DEV_MODE, BUILD_MODE, ...options };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function showHelp(OUTPUT_DIR) {
|
|
29
|
+
const sections = [
|
|
30
|
+
{
|
|
31
|
+
header: 'HubSpot UI Extensions Local Dev Server',
|
|
32
|
+
content: `Used for local development of HubSpot extensions. Built assets can be found in the ${OUTPUT_DIR} directory`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
header: 'Available Commands',
|
|
36
|
+
content: [
|
|
37
|
+
{ name: 'dev', summary: 'starts the local development server' },
|
|
38
|
+
{ name: 'build', summary: 'runs a build of your extensions' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
header: 'Options',
|
|
43
|
+
optionList: [
|
|
44
|
+
{
|
|
45
|
+
name: 'extension',
|
|
46
|
+
alias: 'e',
|
|
47
|
+
typeLabel: '{underline file}',
|
|
48
|
+
description:
|
|
49
|
+
'The extension entrypoint file to build or start local development for',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'help',
|
|
53
|
+
alias: 'h',
|
|
54
|
+
description: 'Print this usage guide.',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
const usage = commandLineUsage(sections);
|
|
60
|
+
logger.info(usage);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
parseArgs,
|
|
66
|
+
showHelp,
|
|
67
|
+
};
|
package/config.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const prompts = require('prompts');
|
|
2
|
+
const logger = require('./logger');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { MAIN_APP_CONFIG, PROJECT_CONFIG } = require('./constants');
|
|
5
|
+
const { getUrlSafeFileName } = require('./utils');
|
|
6
|
+
|
|
7
|
+
async function getExtensionConfig(configuration, extension) {
|
|
8
|
+
if (extension && configuration[extension]) {
|
|
9
|
+
const { data } = configuration[extension];
|
|
10
|
+
return {
|
|
11
|
+
key: extension,
|
|
12
|
+
name: data.title,
|
|
13
|
+
file: data?.module?.file,
|
|
14
|
+
output: data?.output,
|
|
15
|
+
appName: data?.appName,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const extensionOptions = Object.keys(configuration);
|
|
20
|
+
const response = await prompts(
|
|
21
|
+
[
|
|
22
|
+
{
|
|
23
|
+
type: 'select',
|
|
24
|
+
name: 'extension',
|
|
25
|
+
message: 'Which extension would you like to run?',
|
|
26
|
+
choices: extensionOptions.map(option => {
|
|
27
|
+
const { data } = configuration[option];
|
|
28
|
+
return {
|
|
29
|
+
title: option,
|
|
30
|
+
value: {
|
|
31
|
+
key: option,
|
|
32
|
+
name: data?.title,
|
|
33
|
+
file: data?.module?.file,
|
|
34
|
+
output: data?.output,
|
|
35
|
+
appName: data?.appName,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
{
|
|
42
|
+
onCancel: () => {
|
|
43
|
+
process.exit(0); // When the user cancels interaction, exit the script
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
return response.extension;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _loadRequiredConfigFile(filePath) {
|
|
51
|
+
let config;
|
|
52
|
+
try {
|
|
53
|
+
config = require(filePath);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
logger.error(
|
|
56
|
+
`Unable to load ${filePath} file. Please make sure you are running the command from the src/app/extensions directory and that ${filePath} exists`
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadConfig() {
|
|
64
|
+
// app.json is one level up from the extensions directory, which is where these commands
|
|
65
|
+
// will need to be ran from, the extensions directory
|
|
66
|
+
const configPath = path.join(process.cwd(), `../${MAIN_APP_CONFIG}`);
|
|
67
|
+
|
|
68
|
+
const projectConfig = _loadRequiredConfigFile(
|
|
69
|
+
path.join(process.cwd(), `../../../${PROJECT_CONFIG}`)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const mainAppConfig = _loadRequiredConfigFile(configPath);
|
|
73
|
+
|
|
74
|
+
const crmCardsSubConfigFiles = mainAppConfig?.extensions?.crm?.cards;
|
|
75
|
+
if (!crmCardsSubConfigFiles) {
|
|
76
|
+
logger.error(
|
|
77
|
+
`The "extensions.crm.cards" array in ${configPath} is missing, it is a required configuration property`
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
} else if (crmCardsSubConfigFiles.length === 0) {
|
|
81
|
+
logger.error(
|
|
82
|
+
`The "extensions.crm.cards" array in ${configPath} is empty, it is a required configuration property.`
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const outputConfig = {};
|
|
88
|
+
|
|
89
|
+
crmCardsSubConfigFiles.forEach(card => {
|
|
90
|
+
const extensionsRemoved = card.file.replace('extensions/', '');
|
|
91
|
+
const cardConfigPath = path.join(process.cwd(), extensionsRemoved);
|
|
92
|
+
// Get the path to the config file relative to the extensions directory
|
|
93
|
+
const configPathRelativeToExtensions = path.parse(extensionsRemoved)?.dir;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const cardConfig = require(cardConfigPath);
|
|
97
|
+
|
|
98
|
+
// Join the two relative paths
|
|
99
|
+
const entryPointPath = path.join(
|
|
100
|
+
configPathRelativeToExtensions,
|
|
101
|
+
cardConfig.data?.module?.file
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
cardConfig.data.module.file = entryPointPath;
|
|
105
|
+
|
|
106
|
+
outputConfig[entryPointPath] = cardConfig;
|
|
107
|
+
outputConfig[entryPointPath].data.output = getUrlSafeFileName(
|
|
108
|
+
entryPointPath
|
|
109
|
+
);
|
|
110
|
+
outputConfig[entryPointPath].data.appName = projectConfig.name;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
let errorMessage = e?.message;
|
|
113
|
+
if (e?.code === 'MODULE_NOT_FOUND') {
|
|
114
|
+
errorMessage = `Unable to load "${cardConfigPath}" file. \nPlease make sure you are running the command from the src/app/extensions directory and that your card JSON config exists within it.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.error(errorMessage);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return outputConfig;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
loadConfig,
|
|
127
|
+
getExtensionConfig,
|
|
128
|
+
};
|
package/constants.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const VITE_DEFAULT_PORT = 5173;
|
|
2
|
+
const MAIN_APP_CONFIG = 'app.json';
|
|
3
|
+
const PROJECT_CONFIG = 'hsproject.json';
|
|
4
|
+
const OUTPUT_DIR = 'dist';
|
|
5
|
+
|
|
6
|
+
const ROLLUP_OPTIONS = {
|
|
7
|
+
// Deps to exclude from the bundle
|
|
8
|
+
external: ['react', 'react-dom', '@remote-ui/react'],
|
|
9
|
+
output: {
|
|
10
|
+
// Maps libs to the variables to be injected via the window
|
|
11
|
+
globals: {
|
|
12
|
+
react: 'React',
|
|
13
|
+
'@remote-ui/react': 'RemoteUI',
|
|
14
|
+
},
|
|
15
|
+
extend: true,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
VITE_DEFAULT_PORT,
|
|
21
|
+
ROLLUP_OPTIONS,
|
|
22
|
+
MAIN_APP_CONFIG,
|
|
23
|
+
PROJECT_CONFIG,
|
|
24
|
+
OUTPUT_DIR,
|
|
25
|
+
};
|
package/dev.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
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 { build } = require('vite');
|
|
7
|
+
const { getExtensionConfig } = require('./config');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { ROLLUP_OPTIONS } = require('./constants');
|
|
11
|
+
|
|
12
|
+
async function _devBuild(config, outputDir, extension) {
|
|
13
|
+
const extensionConfig = await getExtensionConfig(config, extension);
|
|
14
|
+
build({
|
|
15
|
+
define: {
|
|
16
|
+
'process.env.NODE_ENV': JSON.stringify(
|
|
17
|
+
process.env.NODE_ENV || 'development'
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
build: {
|
|
21
|
+
watch: {
|
|
22
|
+
clearScreen: false,
|
|
23
|
+
exclude: [
|
|
24
|
+
'node_modules',
|
|
25
|
+
'package.json',
|
|
26
|
+
'package-lock.json',
|
|
27
|
+
'app.json',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
lib: {
|
|
31
|
+
entry: extensionConfig?.file,
|
|
32
|
+
name: extensionConfig?.output,
|
|
33
|
+
formats: ['iife'],
|
|
34
|
+
fileName: () => extensionConfig?.output,
|
|
35
|
+
},
|
|
36
|
+
rollupOptions: ROLLUP_OPTIONS,
|
|
37
|
+
outDir: outputDir,
|
|
38
|
+
emptyOutDir: true,
|
|
39
|
+
minify: false,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return extensionConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _startDevServer(outputDir, port, extensionConfig) {
|
|
46
|
+
const app = express();
|
|
47
|
+
const server = http.createServer(app);
|
|
48
|
+
|
|
49
|
+
// Host the OUTPUT_DIR
|
|
50
|
+
app.use(express.static(outputDir));
|
|
51
|
+
|
|
52
|
+
// Setup websocket server to send messages to browser on bundle update
|
|
53
|
+
const wss = new WebSocketServer({ server });
|
|
54
|
+
|
|
55
|
+
const callback = `http://localhost:${port}/${extensionConfig?.output}`;
|
|
56
|
+
|
|
57
|
+
function broadcast(message) {
|
|
58
|
+
if (wss.clients.size === 0) {
|
|
59
|
+
logger.warn('No browsers connected to update');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
wss.clients.forEach(client => {
|
|
63
|
+
client.send(JSON.stringify(message));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
wss.on('connection', client => {
|
|
68
|
+
let base64Callback;
|
|
69
|
+
try {
|
|
70
|
+
base64Callback = fs
|
|
71
|
+
.readFileSync(
|
|
72
|
+
path.join(process.cwd(), outputDir, extensionConfig?.output)
|
|
73
|
+
)
|
|
74
|
+
.toString('base64');
|
|
75
|
+
} catch (e) {
|
|
76
|
+
logger.warn(
|
|
77
|
+
'File not found:',
|
|
78
|
+
path.join(process.cwd(), outputDir, extensionConfig?.output)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logger.info('Browser connected and listening for bundle updates');
|
|
83
|
+
client.send(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
event: 'start',
|
|
86
|
+
appName: extensionConfig?.appName,
|
|
87
|
+
extension: extensionConfig?.name,
|
|
88
|
+
callback: base64Callback
|
|
89
|
+
? `data:text/javascript;base64,${base64Callback}`
|
|
90
|
+
: undefined,
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Start the express and websocket servers
|
|
96
|
+
server.listen({ port }, () => {
|
|
97
|
+
logger.warn(`Listening at ${callback}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Setup a watcher on the dist directory and update broadcast
|
|
101
|
+
//to all clients when an event is observed
|
|
102
|
+
chokidar.watch(outputDir).on('change', file => {
|
|
103
|
+
const base64Callback = fs
|
|
104
|
+
.readFileSync(path.join(process.cwd(), file))
|
|
105
|
+
.toString('base64');
|
|
106
|
+
logger.debug(`${file} updated, reloading extension`);
|
|
107
|
+
broadcast({
|
|
108
|
+
event: 'update',
|
|
109
|
+
appName: extensionConfig?.appName,
|
|
110
|
+
extension: extensionConfig?.name,
|
|
111
|
+
callback: `data:text/javascript;base64,${base64Callback}`,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
_configureShutDownHandlers(wss, broadcast, extensionConfig, server);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _configureShutDownHandlers(wss, broadcast, extensionConfig, server) {
|
|
119
|
+
function shutdown() {
|
|
120
|
+
logger.warn('\nSending shutdown signal to connected browser');
|
|
121
|
+
broadcast({
|
|
122
|
+
event: 'shutdown',
|
|
123
|
+
appName: extensionConfig?.appName,
|
|
124
|
+
extension: extensionConfig?.name,
|
|
125
|
+
});
|
|
126
|
+
logger.warn('\nCleaning up after ourselves...\n');
|
|
127
|
+
|
|
128
|
+
// Terminate all active connections, close seems to hang otherwise
|
|
129
|
+
wss.clients.forEach(client => {
|
|
130
|
+
client.terminate();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Shut down the WebSocket server first since it is connected to the express server
|
|
134
|
+
wss.close(webSocketError => {
|
|
135
|
+
if (webSocketError) {
|
|
136
|
+
logger.error(
|
|
137
|
+
`WebSocket Server unable to shutdown correctly, ${webSocketError}`
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
logger.warn('WebSocket Server stopped');
|
|
141
|
+
}
|
|
142
|
+
// Shutdown the express server
|
|
143
|
+
server.close(error => {
|
|
144
|
+
if (error) {
|
|
145
|
+
logger.error(`Express server unable to shutdown correctly, ${error}`);
|
|
146
|
+
} else {
|
|
147
|
+
logger.warn('Express server stopped');
|
|
148
|
+
}
|
|
149
|
+
process.exit(webSocketError || error ? 1 : 0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
process.on('SIGINT', shutdown);
|
|
155
|
+
process.on('SIGTERM', shutdown);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function startDevMode(config, outputDir, port, extension) {
|
|
159
|
+
const extensionConfig = await _devBuild(config, outputDir, extension);
|
|
160
|
+
_startDevServer(outputDir, port, extensionConfig);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
startDevMode,
|
|
165
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { OUTPUT_DIR } = require('./constants');
|
|
2
|
+
const { getUrlSafeFileName } = require('./utils');
|
|
3
|
+
const { buildSingleExtension } = require('./build');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const manifestPlugin = require('./plugins/manifestPlugin');
|
|
6
|
+
|
|
7
|
+
async function remoteBuild(root, entryPoint, outputDir) {
|
|
8
|
+
const allowedExtensions = ['.js', '.ts', '.tsx', '.jsx'];
|
|
9
|
+
const fileInfo = path.parse(entryPoint);
|
|
10
|
+
|
|
11
|
+
if (!allowedExtensions.includes(fileInfo.ext)) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`The last argument should be the filename you wish to build. Supported file extensions are [${allowedExtensions.join(
|
|
14
|
+
', '
|
|
15
|
+
)}]`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await buildSingleExtension({
|
|
20
|
+
file: entryPoint,
|
|
21
|
+
outputFileName: getUrlSafeFileName(entryPoint),
|
|
22
|
+
outputDir: outputDir || OUTPUT_DIR,
|
|
23
|
+
plugins: {
|
|
24
|
+
rollup: [manifestPlugin({ minify: true })],
|
|
25
|
+
},
|
|
26
|
+
minify: true,
|
|
27
|
+
root,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
remoteBuild,
|
|
33
|
+
};
|
package/logger.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { cyan, red, yellow, magenta } = require('console-log-colors');
|
|
2
|
+
|
|
3
|
+
function info(message) {
|
|
4
|
+
console.log(cyan(message));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function error(message) {
|
|
8
|
+
console.error(red(message));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function warn(message) {
|
|
12
|
+
console.info(yellow(message));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function debug(message) {
|
|
16
|
+
console.debug(magenta(message));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { info, error, warn, debug };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hubspot/ui-extensions-dev-server",
|
|
3
|
+
"version": "0.0.1-beta.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo 'test'"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"README.md",
|
|
14
|
+
"build.js",
|
|
15
|
+
"cli.js",
|
|
16
|
+
"config.js",
|
|
17
|
+
"constants.js",
|
|
18
|
+
"dev.js",
|
|
19
|
+
"logger.js",
|
|
20
|
+
"run.js",
|
|
21
|
+
"utils.js",
|
|
22
|
+
"tests/runTests.js",
|
|
23
|
+
"tests/testBuild.js",
|
|
24
|
+
"tests/testDevServer.js",
|
|
25
|
+
"plugins/manifestPlugin.js",
|
|
26
|
+
"index.js"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chokidar": "^3.5.3",
|
|
31
|
+
"command-line-args": "^5.2.1",
|
|
32
|
+
"command-line-usage": "^7.0.1",
|
|
33
|
+
"console-log-colors": "^0.4.0",
|
|
34
|
+
"express": "^4.18.2",
|
|
35
|
+
"process": "^0.11.10",
|
|
36
|
+
"prompts": "^2.4.2",
|
|
37
|
+
"vite": "^4.0.4",
|
|
38
|
+
"ws": "^8.12.1"
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"hs-ui-extensions-dev-server": "run.js"
|
|
42
|
+
},
|
|
43
|
+
"eslintConfig": {
|
|
44
|
+
"env": {
|
|
45
|
+
"node": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=16"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"typescript": "^5.0.4"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"typescript": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"gitHead": "0bfb66cacc456a73f05a40841938e6b42f87aaba"
|
|
60
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const { readFileSync } = require('fs');
|
|
2
|
+
const { normalize } = require('path');
|
|
3
|
+
const logger = require('../logger');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MANIFEST_NAME = 'manifest.json';
|
|
6
|
+
const PACKAGE_LOCK_FILE = 'package-lock.json';
|
|
7
|
+
const PACKAGE_FILE = 'package.json';
|
|
8
|
+
const EXTENSIONS_PATH = 'src/app/extensions/';
|
|
9
|
+
|
|
10
|
+
function plugin(options = {}) {
|
|
11
|
+
return {
|
|
12
|
+
name: 'ui-extensions-manifest-generation-plugin',
|
|
13
|
+
enforce: 'post', // run after default rollup plugins
|
|
14
|
+
generateBundle(_rollupOptions, bundle) {
|
|
15
|
+
const { output = DEFAULT_MANIFEST_NAME, minify = false } = options;
|
|
16
|
+
try {
|
|
17
|
+
const manifest = _generateManifestContents(bundle);
|
|
18
|
+
this.emitFile({
|
|
19
|
+
type: 'asset',
|
|
20
|
+
source: minify
|
|
21
|
+
? JSON.stringify(manifest)
|
|
22
|
+
: JSON.stringify(manifest, null, 2),
|
|
23
|
+
fileName: normalize(output),
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
logger.warn(`\nUnable to write manifest file in ${output}, ${e}`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _generateManifestContents(bundle) {
|
|
33
|
+
const baseManifest = {
|
|
34
|
+
package: _loadPackageFile(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// The keys to bundle are the filename without any path information
|
|
38
|
+
const bundles = Object.keys(bundle);
|
|
39
|
+
|
|
40
|
+
if (bundles.length === 1) {
|
|
41
|
+
return {
|
|
42
|
+
..._generateManifestEntry(bundle[bundles[0]]),
|
|
43
|
+
...baseManifest,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const manifest = bundles.reduce((acc, current) => {
|
|
48
|
+
return {
|
|
49
|
+
...acc,
|
|
50
|
+
[current]: _generateManifestEntry(bundle[current], false),
|
|
51
|
+
};
|
|
52
|
+
}, {});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...manifest,
|
|
56
|
+
...baseManifest,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _generateManifestEntry(subBundle) {
|
|
61
|
+
const { facadeModuleId, moduleIds, modules } = subBundle;
|
|
62
|
+
return {
|
|
63
|
+
entry: _stripPathPriorToExtDir(facadeModuleId),
|
|
64
|
+
modules: _buildModulesInfo(moduleIds, modules),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _loadJsonFileSafely(filename) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(filename).toString());
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _loadPackageFile() {
|
|
77
|
+
// Look for package-lock.json then fallback to package.json
|
|
78
|
+
return (
|
|
79
|
+
_loadJsonFileSafely(PACKAGE_LOCK_FILE) || _loadJsonFileSafely(PACKAGE_FILE)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _stripPathPriorToExtDir(filepath) {
|
|
84
|
+
return filepath.split(EXTENSIONS_PATH).pop();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _buildModulesInfo(moduleIds, modules) {
|
|
88
|
+
return moduleIds.reduce(
|
|
89
|
+
(acc, mod) => {
|
|
90
|
+
const { renderedExports } = modules[mod];
|
|
91
|
+
|
|
92
|
+
const moduleData = {
|
|
93
|
+
module: _stripPathPriorToExtDir(mod),
|
|
94
|
+
renderedExports,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (moduleData.module.includes('node_modules')) {
|
|
98
|
+
acc.external.push(moduleData);
|
|
99
|
+
} else {
|
|
100
|
+
acc.internal.push(moduleData);
|
|
101
|
+
}
|
|
102
|
+
return acc;
|
|
103
|
+
},
|
|
104
|
+
{ internal: [], external: [] }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = plugin;
|
package/run.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { startDevMode } = require('./dev');
|
|
4
|
+
const { loadConfig } = require('./config');
|
|
5
|
+
const { buildAllExtensions, buildSingleExtension } = require('./build');
|
|
6
|
+
const { VITE_DEFAULT_PORT, OUTPUT_DIR } = require('./constants');
|
|
7
|
+
const { parseArgs, showHelp } = require('./cli');
|
|
8
|
+
const { getUrlSafeFileName } = require('./utils');
|
|
9
|
+
const manifestPlugin = require('./plugins/manifestPlugin');
|
|
10
|
+
|
|
11
|
+
const { DEV_MODE, BUILD_MODE, port, extension, help } = parseArgs();
|
|
12
|
+
const PORT = port || VITE_DEFAULT_PORT;
|
|
13
|
+
|
|
14
|
+
if (help || !(DEV_MODE || BUILD_MODE)) {
|
|
15
|
+
showHelp(OUTPUT_DIR);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (DEV_MODE) {
|
|
19
|
+
startDevMode(loadConfig(), OUTPUT_DIR, PORT, extension);
|
|
20
|
+
} else if (BUILD_MODE) {
|
|
21
|
+
if (extension) {
|
|
22
|
+
buildSingleExtension({
|
|
23
|
+
file: extension,
|
|
24
|
+
outputFileName: getUrlSafeFileName(extension),
|
|
25
|
+
outputDir: OUTPUT_DIR,
|
|
26
|
+
plugins: { rollup: [manifestPlugin()] },
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
buildAllExtensions(loadConfig(), OUTPUT_DIR);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { testBuild } = require('./testBuild');
|
|
4
|
+
const { testDevServer } = require('./testDevServer');
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
|
|
7
|
+
let devServerProcess;
|
|
8
|
+
|
|
9
|
+
function handleFailure() {
|
|
10
|
+
if (devServerProcess) {
|
|
11
|
+
devServerProcess.kill();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process.on('SIGINT', () => {
|
|
16
|
+
handleFailure();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.on('uncaughtException', () => {
|
|
20
|
+
handleFailure();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
testBuild(logger);
|
|
25
|
+
testDevServer(logger, devServerProcess);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error(e.message);
|
|
28
|
+
logger.error('Tests failed ðŸ˜');
|
|
29
|
+
handleFailure();
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
|
|
6
|
+
function _testHelper(command, outputDirFiles) {
|
|
7
|
+
if (command) {
|
|
8
|
+
execSync(command);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Make sure the files are getting generated in the dist dir
|
|
12
|
+
const distDir = path.join(process.cwd(), 'dist');
|
|
13
|
+
const filesInOutputDir = fs.readdirSync(distDir);
|
|
14
|
+
assert.deepStrictEqual(filesInOutputDir, outputDirFiles);
|
|
15
|
+
|
|
16
|
+
// Spot check the file contents to make sure they seem ok
|
|
17
|
+
filesInOutputDir.forEach(file => {
|
|
18
|
+
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
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
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 🚀');
|
|
60
|
+
}
|
|
61
|
+
|
|
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 🚀');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function testDefInfraBuildFileName(logger) {
|
|
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']);
|
|
77
|
+
logger.info('- Test build with entrypoint as arg 🚀');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function testBuild(logger) {
|
|
81
|
+
logger.warn('\nBuild Tests started - External Devs 🤞');
|
|
82
|
+
testDefaultBuildPath(logger);
|
|
83
|
+
testBuildWithExtensionFlag(logger);
|
|
84
|
+
logger.info('Build Tests passed - External Devs🚀');
|
|
85
|
+
|
|
86
|
+
logger.warn('\nBuild Tests started - Dev Infra 🤞');
|
|
87
|
+
testDefInfraBuildFileName(logger);
|
|
88
|
+
logger.info('Build Tests passed - Dev Infra 🚀');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
testBuild,
|
|
93
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const WebSocket = require('ws');
|
|
7
|
+
|
|
8
|
+
const testResults = {
|
|
9
|
+
buildTestPassed: false,
|
|
10
|
+
expressTestPassed: false,
|
|
11
|
+
webSocketTestPassed: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const port = 5172;
|
|
15
|
+
|
|
16
|
+
function testDevServer(logger, devServerProcess) {
|
|
17
|
+
logger.warn('\nDev Server Tests started 🤞');
|
|
18
|
+
|
|
19
|
+
// We need to use spawn here because it will put the process into the background,
|
|
20
|
+
// which is required because dev mode is a blocking process and we want to test that
|
|
21
|
+
// the express server and websocket server are starting properly
|
|
22
|
+
devServerProcess = spawn('hs-ui-extensions-dev-server', [
|
|
23
|
+
'dev',
|
|
24
|
+
'--extension',
|
|
25
|
+
'PhoneLines.tsx',
|
|
26
|
+
'--port',
|
|
27
|
+
`${port}`,
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
devServerProcess.stdout.on('data', buffer => {
|
|
31
|
+
const data = buffer.toString().toLowerCase();
|
|
32
|
+
console.log('[Dev Server Log]:', data);
|
|
33
|
+
if (data.includes('built in')) {
|
|
34
|
+
testBuild(testResults, logger);
|
|
35
|
+
}
|
|
36
|
+
if (data.includes('listening') && data.includes(`localhost:${port}`)) {
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
testExpressServer(testResults, logger);
|
|
39
|
+
testWebSocketServer(testResults, logger);
|
|
40
|
+
}, 1000);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// If the dev server writes to stderr, log the error and throw a new error
|
|
45
|
+
devServerProcess.stderr.on('data', buffer => {
|
|
46
|
+
const data = buffer.toString();
|
|
47
|
+
logger.error(data.toString());
|
|
48
|
+
throw new Error(data);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// When the process closes make sure we met all the success conditions
|
|
52
|
+
devServerProcess.on('close', () => {
|
|
53
|
+
if (metConditions()) {
|
|
54
|
+
logger.info('Dev Server Tests passed 🚀');
|
|
55
|
+
} else {
|
|
56
|
+
console.log(testResults);
|
|
57
|
+
logger.error('Tests failed ðŸ˜');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const interval = setInterval(callback, 1000);
|
|
62
|
+
let count = 0;
|
|
63
|
+
function callback() {
|
|
64
|
+
count += 1;
|
|
65
|
+
if (metConditions() || count === 5) {
|
|
66
|
+
devServerProcess.kill();
|
|
67
|
+
clearInterval(interval);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function metConditions() {
|
|
73
|
+
const {
|
|
74
|
+
buildTestPassed,
|
|
75
|
+
expressTestPassed,
|
|
76
|
+
webSocketTestPassed,
|
|
77
|
+
} = testResults;
|
|
78
|
+
return buildTestPassed && expressTestPassed && webSocketTestPassed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Test that the files were built in the proper location and spot
|
|
82
|
+
// check the contents of the files.
|
|
83
|
+
function testBuild(results, logger) {
|
|
84
|
+
// // Make sure the files are getting generated in the dist dir
|
|
85
|
+
const distDir = path.join(process.cwd(), 'dist');
|
|
86
|
+
const filesInOutputDir = fs.readdirSync(distDir);
|
|
87
|
+
assert.deepStrictEqual(filesInOutputDir, ['PhoneLines.js']);
|
|
88
|
+
const fileContents = fs
|
|
89
|
+
.readFileSync(path.join(distDir, filesInOutputDir[0]))
|
|
90
|
+
.toString();
|
|
91
|
+
const stringsToSpotCheck = [
|
|
92
|
+
'.createRemoteReactComponent',
|
|
93
|
+
'.createElement',
|
|
94
|
+
'hubspot.extend',
|
|
95
|
+
'React',
|
|
96
|
+
'RemoteUI',
|
|
97
|
+
];
|
|
98
|
+
stringsToSpotCheck.forEach(stringToCheck => {
|
|
99
|
+
assert(
|
|
100
|
+
fileContents.includes(stringToCheck),
|
|
101
|
+
`File ${filesInOutputDir[0]} contents should contain: "${stringToCheck}"`
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
logger.info('- Build succeeded 🚀');
|
|
105
|
+
results.buildTestPassed = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Test that the express server is running on the expected port
|
|
109
|
+
// and that it is serving the files as expected.
|
|
110
|
+
function testExpressServer(results, logger) {
|
|
111
|
+
http.get(
|
|
112
|
+
{
|
|
113
|
+
host: 'localhost',
|
|
114
|
+
port,
|
|
115
|
+
path: '/PhoneLines.js',
|
|
116
|
+
},
|
|
117
|
+
response => {
|
|
118
|
+
if (response.statusCode !== 200) {
|
|
119
|
+
throw Error('Error with express server');
|
|
120
|
+
}
|
|
121
|
+
logger.info('- Express server connected and serving files 🚀');
|
|
122
|
+
results.expressTestPassed = true;
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Test the the web socket server is running on the expected port and
|
|
128
|
+
// that we are able to receive messages from it.
|
|
129
|
+
function testWebSocketServer(results, logger) {
|
|
130
|
+
const fileContents = fs
|
|
131
|
+
.readFileSync(path.join('dist', 'PhoneLines.js'))
|
|
132
|
+
.toString('base64');
|
|
133
|
+
|
|
134
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
135
|
+
ws.on('message', messageBuffer => {
|
|
136
|
+
const message = JSON.parse(messageBuffer.toString());
|
|
137
|
+
console.log('[WebSocket Message]:', message);
|
|
138
|
+
assert(message.event === 'start' || message.event === 'update');
|
|
139
|
+
assert(message.appName === 'example-app-remote-ui');
|
|
140
|
+
assert.strictEqual(message.extension, 'Phone Lines');
|
|
141
|
+
assert(message.callback, `data:text/javascript;base64,${fileContents}`);
|
|
142
|
+
logger.info('- WebSocket server connected and sending messages 🚀');
|
|
143
|
+
results.webSocketTestPassed = true;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
testDevServer,
|
|
149
|
+
};
|