@hubspot/ui-extensions-dev-server 0.1.0 → 0.3.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/lib/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ const path = require('path');
2
+
3
+ function getUrlSafeFileName(filePath) {
4
+ const { name } = path.parse(filePath);
5
+ return encodeURIComponent(`${name}.js`);
6
+ }
7
+
8
+ // Strips ANSI color codes out of strings because we don't want to pass them to the browser
9
+ function stripAnsiColorCodes(string) {
10
+ return string.replace(
11
+ // eslint-disable-next-line no-control-regex
12
+ /[\u001b][[]*([0-9]{1,4};?)*[m]/g,
13
+ ''
14
+ );
15
+ }
16
+
17
+ module.exports = {
18
+ getUrlSafeFileName,
19
+ stripAnsiColorCodes,
20
+ };
package/package.json CHANGED
@@ -1,50 +1,53 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo 'test'"
7
+ "test": "jest",
8
+ "jest": "jest --watch"
8
9
  },
9
10
  "publishConfig": {
10
11
  "access": "public"
11
12
  },
12
13
  "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",
14
+ "cli/logger.js",
15
+ "cli/run.js",
16
+ "cli/userInput.js",
17
+ "cli/utils.js",
18
+ "lib/plugins/*",
19
+ "lib/build.js",
20
+ "lib/config.js",
21
+ "lib/constants.js",
22
+ "lib/dev.js",
23
+ "lib/extensions.js",
24
+ "lib/server.js",
25
+ "lib/utils.js",
26
26
  "index.js",
27
- "server.js"
27
+ "README.md"
28
28
  ],
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "chokidar": "^3.5.3",
32
31
  "command-line-args": "^5.2.1",
33
32
  "command-line-usage": "^7.0.1",
34
33
  "console-log-colors": "^0.4.0",
35
34
  "cors": "^2.8.5",
36
35
  "express": "^4.18.2",
37
- "process": "^0.11.10",
38
36
  "prompts": "^2.4.2",
39
- "vite": "^4.0.4",
37
+ "vite": "^4.0.4"
38
+ },
39
+ "devDependencies": {
40
+ "axios": "^1.4.0",
41
+ "jest": "^29.5.0",
40
42
  "ws": "^8.13.0"
41
43
  },
42
44
  "bin": {
43
- "hs-ui-extensions-dev-server": "run.js"
45
+ "hs-ui-extensions-dev-server": "./cli/run.js"
44
46
  },
45
47
  "eslintConfig": {
46
48
  "env": {
47
- "node": true
49
+ "node": true,
50
+ "jest": true
48
51
  }
49
52
  },
50
53
  "engines": {
@@ -58,5 +61,5 @@
58
61
  "optional": true
59
62
  }
60
63
  },
61
- "gitHead": "abac5960e6277af827714dcd4150ad7606e6a481"
64
+ "gitHead": "0b20c005a9564bbde58c22ce21959c713f354607"
62
65
  }
package/build.js DELETED
@@ -1,56 +0,0 @@
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/config.js DELETED
@@ -1,129 +0,0 @@
1
- const prompts = require('prompts');
2
- const logger = require('./logger');
3
- const path = require('path');
4
- const { MAIN_APP_CONFIG } = require('./constants');
5
- const { getUrlSafeFileName } = require('./utils');
6
-
7
- async function getExtensionConfig(configuration, extension) {
8
- const extensionOptions = Object.keys(configuration);
9
-
10
- if (
11
- (extension && configuration[extension]) ||
12
- extensionOptions.length === 1
13
- ) {
14
- const extensionToRun = extension || extensionOptions[0];
15
- const { data } = configuration[extensionToRun];
16
- return {
17
- key: extensionToRun,
18
- name: data.title,
19
- file: data?.module?.file,
20
- output: data?.output,
21
- appName: data?.appName,
22
- };
23
- }
24
-
25
- const response = await prompts(
26
- [
27
- {
28
- type: 'select',
29
- name: 'extension',
30
- message: 'Which extension would you like to run?',
31
- choices: extensionOptions.map(option => {
32
- const { data } = configuration[option];
33
- return {
34
- title: option,
35
- value: {
36
- key: option,
37
- name: data?.title,
38
- file: data?.module?.file,
39
- output: data?.output,
40
- appName: data?.appName,
41
- },
42
- };
43
- }),
44
- },
45
- ],
46
- {
47
- onCancel: () => {
48
- process.exit(0); // When the user cancels interaction, exit the script
49
- },
50
- }
51
- );
52
- return response.extension;
53
- }
54
-
55
- function _loadRequiredConfigFile(filePath) {
56
- let config;
57
- try {
58
- config = require(filePath);
59
- } catch (e) {
60
- logger.error(
61
- `Unable to load ${filePath} file. Please make sure you are running the command from the src/app/extensions directory and that ${filePath} exists`
62
- );
63
- process.exit(1);
64
- }
65
- return config;
66
- }
67
-
68
- function loadConfig() {
69
- // app.json is one level up from the extensions directory, which is where these commands
70
- // will need to be ran from, the extensions directory
71
- const configPath = path.join(process.cwd(), `../${MAIN_APP_CONFIG}`);
72
-
73
- const mainAppConfig = _loadRequiredConfigFile(configPath);
74
-
75
- const crmCardsSubConfigFiles = mainAppConfig?.extensions?.crm?.cards;
76
- if (!crmCardsSubConfigFiles) {
77
- logger.error(
78
- `The "extensions.crm.cards" array in ${configPath} is missing, it is a required configuration property`
79
- );
80
- process.exit(1);
81
- } else if (crmCardsSubConfigFiles.length === 0) {
82
- logger.error(
83
- `The "extensions.crm.cards" array in ${configPath} is empty, it is a required configuration property.`
84
- );
85
- process.exit(1);
86
- }
87
-
88
- const outputConfig = {};
89
-
90
- crmCardsSubConfigFiles.forEach(card => {
91
- const extensionsRemoved = card.file.replace('extensions/', '');
92
- const cardConfigPath = path.join(process.cwd(), extensionsRemoved);
93
- // Get the path to the config file relative to the extensions directory
94
- const configPathRelativeToExtensions = path.parse(extensionsRemoved)?.dir;
95
-
96
- try {
97
- const cardConfig = require(cardConfigPath);
98
-
99
- // Join the two relative paths
100
- const entryPointPath = path.join(
101
- configPathRelativeToExtensions,
102
- cardConfig.data?.module?.file
103
- );
104
-
105
- cardConfig.data.module.file = entryPointPath;
106
-
107
- outputConfig[entryPointPath] = cardConfig;
108
- outputConfig[entryPointPath].data.output = getUrlSafeFileName(
109
- entryPointPath
110
- );
111
- outputConfig[entryPointPath].data.appName = mainAppConfig.name;
112
- } catch (e) {
113
- let errorMessage = e?.message;
114
- if (e?.code === 'MODULE_NOT_FOUND') {
115
- 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.`;
116
- }
117
-
118
- logger.error(errorMessage);
119
- process.exit(1);
120
- }
121
- });
122
-
123
- return outputConfig;
124
- }
125
-
126
- module.exports = {
127
- loadConfig,
128
- getExtensionConfig,
129
- };
package/dev.js DELETED
@@ -1,52 +0,0 @@
1
- const { build } = require('vite');
2
- const { getExtensionConfig } = require('./config');
3
- const { ROLLUP_OPTIONS } = require('./constants');
4
- const startDevServer = require('./server');
5
- const manifestPlugin = require('./plugins/manifestPlugin');
6
-
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
- ],
23
- },
24
- lib: {
25
- entry: extensionConfig?.file,
26
- name: extensionConfig?.output,
27
- formats: ['iife'],
28
- fileName: () => extensionConfig?.output,
29
- },
30
- rollupOptions: {
31
- ...ROLLUP_OPTIONS,
32
- plugins: [
33
- ...(ROLLUP_OPTIONS.plugins || []),
34
- manifestPlugin({ minify: false, extensionConfig }),
35
- ],
36
- },
37
- outDir: outputDir,
38
- emptyOutDir: true,
39
- minify: false,
40
- },
41
- });
42
- }
43
-
44
- async function startDevMode(config, outputDir, port, extension) {
45
- const extensionConfig = await getExtensionConfig(config, extension);
46
- await _buildDevBundle(outputDir, extensionConfig);
47
- startDevServer(outputDir, port, extensionConfig);
48
- }
49
-
50
- module.exports = {
51
- startDevMode,
52
- };
package/run.js DELETED
@@ -1,31 +0,0 @@
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
- }
package/server.js DELETED
@@ -1,148 +0,0 @@
1
- const express = require('express');
2
- const chokidar = require('chokidar');
3
- const { WebSocketServer } = require('ws');
4
- const http = require('http');
5
- const logger = require('./logger');
6
- const path = require('path');
7
- const cors = require('cors');
8
- const {
9
- EXTENSIONS_MESSAGE_VERSION,
10
- WEBSOCKET_MESSAGE_VERSION,
11
- } = require('./constants');
12
-
13
- function startDevServer(outputDir, port, extensionConfig) {
14
- const callback = `http://hslocal.net:${port}/${extensionConfig?.output}`;
15
-
16
- const app = express();
17
-
18
- // Using http.createServer so WebSocket server can be attached before initializing express.
19
- const server = http.createServer(app);
20
-
21
- // Setup middleware
22
- app.use(cors());
23
- app.use(express.static(outputDir));
24
-
25
- _addExtensionsEndpoint(app, port);
26
-
27
- const { broadcast, wss } = _setupWebSocketServer(
28
- server,
29
- extensionConfig,
30
- callback
31
- );
32
- _setupChokidarWatcher(broadcast, outputDir);
33
-
34
- server.listen({ port }, () => {
35
- logger.warn(`Listening at ${callback}`);
36
- });
37
-
38
- _configureShutDownHandlers(server, wss, broadcast);
39
- }
40
-
41
- function _setupWebSocketServer(server, extensionConfig, callback) {
42
- const wss = new WebSocketServer({ server });
43
-
44
- const baseMessage = {
45
- appName: extensionConfig?.appName,
46
- title: extensionConfig?.name,
47
- callback,
48
- version: WEBSOCKET_MESSAGE_VERSION,
49
- };
50
-
51
- wss.on('connection', client => {
52
- logger.info('Browser connected and listening for bundle updates');
53
- client.send(
54
- JSON.stringify({
55
- event: 'start',
56
- ...baseMessage,
57
- })
58
- );
59
- });
60
-
61
- return {
62
- wss,
63
- broadcast: message => {
64
- if (wss.clients.size === 0) {
65
- logger.warn('No browsers connected to notify');
66
- return;
67
- }
68
- wss.clients.forEach(client => {
69
- client.send(
70
- JSON.stringify({
71
- ...message,
72
- ...baseMessage,
73
- })
74
- );
75
- });
76
- },
77
- };
78
- }
79
-
80
- // Setup a watcher on the dist directory and update broadcast
81
- //to all clients when an event is observed
82
- function _setupChokidarWatcher(broadcast, outputDir) {
83
- chokidar.watch(outputDir).on('all', (event, file) => {
84
- if (event !== 'change' || event !== 'add' || file === 'manifest.json') {
85
- // We need to listen to 'change' and 'add' because sometimes chokidar
86
- // sees it as an 'unlink' and 'add' instead of just a 'change'
87
- // Since we are adding the manifest.json to the build, we want to ignore changes to that so we don't double send messages
88
- return;
89
- }
90
- logger.info('Bundle updated notifying browser');
91
- broadcast({
92
- event: 'update',
93
- });
94
- });
95
- }
96
-
97
- function _addExtensionsEndpoint(server, port) {
98
- const endpoint = '/extensions';
99
- server.get(endpoint, (_req, res) => {
100
- try {
101
- const manifest = require(path.join(process.cwd(), 'dist/manifest.json'));
102
- const {
103
- extension: { appName, name, output },
104
- } = manifest;
105
-
106
- const response = {
107
- websocket: `ws://localhost:${port}`,
108
- version: EXTENSIONS_MESSAGE_VERSION,
109
- extensions: [
110
- {
111
- appName,
112
- title: name,
113
- callback: `http://hslocal.net:${port}/${output}`,
114
- manifest: {
115
- ...manifest,
116
- extension: undefined,
117
- },
118
- },
119
- ],
120
- };
121
- res.status(200).json(response);
122
- } catch (e) {
123
- res.status(500).json({
124
- message: 'Unable to load manifest file',
125
- });
126
- }
127
- });
128
- logger.warn(`Listening at http://hslocal.net:${port}${endpoint}`);
129
- }
130
-
131
- function _configureShutDownHandlers(server, wss, broadcast) {
132
- function shutdown() {
133
- logger.warn('\nCleaning up after ourselves...');
134
- // Stop new connections to express server
135
- broadcast({
136
- event: 'shutdown',
137
- });
138
- wss.close(() => {});
139
- server.close(() => {});
140
- logger.warn('Clean up done');
141
- process.exit(0);
142
- }
143
-
144
- process.on('SIGINT', shutdown);
145
- process.on('SIGTERM', shutdown);
146
- }
147
-
148
- module.exports = startDevServer;
package/tests/runTests.js DELETED
@@ -1,32 +0,0 @@
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', e => {
20
- console.error(e);
21
- handleFailure();
22
- });
23
-
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
- }
@@ -1,93 +0,0 @@
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
- };