@hubspot/ui-extensions-dev-server 0.0.1-prealpha.0 β†’ 0.0.1-prealpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,27 @@
1
1
  # ui-extensions-dev-server
2
- This package contains the build script for bundling and serving remote extensions locally. It checks for a config file named `extension-config.json` in directory the dev server command is ran from. This config file is optional, and the below example contains what the default values are.
3
-
4
- Example config file:
5
-
6
- ```json
7
- {
8
- "version": "1.0",
9
- "extensions": {
10
- "example-app": {
11
- "file": "App.tsx"
12
- }
13
- }
14
- }
15
- ```
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 CHANGED
@@ -1,32 +1,43 @@
1
- /* eslint-disable hubspot-dev/no-async-await */
2
- /*global module*/
3
- 'use es6';
4
-
5
1
  const { build } = require('vite');
6
2
  const { ROLLUP_OPTIONS } = require('./constants');
7
3
 
8
- async function buildExtensions(config, outputDir) {
4
+ async function buildAllExtensions(config, outputDir) {
9
5
  const extensionKeys = Object.keys(config);
10
-
11
6
  for (let i = 0; i < extensionKeys.length; ++i) {
12
7
  const { data } = config[extensionKeys[i]];
13
- await build({
14
- build: {
15
- lib: {
16
- entry: data?.module?.file,
17
- name: data.output,
18
- formats: ['iife'],
19
- fileName: () => data.output,
20
- },
21
- rollupOptions: ROLLUP_OPTIONS,
22
- outDir: outputDir,
23
- emptyOutDir: i === 0,
24
- minify: false,
25
- },
26
- });
8
+
9
+ await buildSingleExtension(
10
+ data?.module.file,
11
+ data?.output,
12
+ outputDir,
13
+ i === 0
14
+ );
27
15
  }
28
16
  }
29
17
 
18
+ async function buildSingleExtension(
19
+ file,
20
+ outputFileName,
21
+ outputDir,
22
+ emptyOutDir = true
23
+ ) {
24
+ await build({
25
+ build: {
26
+ lib: {
27
+ entry: file,
28
+ name: outputFileName,
29
+ formats: ['iife'],
30
+ fileName: () => outputFileName,
31
+ },
32
+ rollupOptions: ROLLUP_OPTIONS,
33
+ outDir: outputDir,
34
+ emptyOutDir,
35
+ minify: false,
36
+ },
37
+ });
38
+ }
39
+
30
40
  module.exports = {
31
- buildExtensions,
41
+ buildAllExtensions,
42
+ buildSingleExtension,
32
43
  };
package/cli.js ADDED
@@ -0,0 +1,65 @@
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
+ typeLabel: '{underline file}',
47
+ description:
48
+ 'The extension entrypoint file build or start local development for',
49
+ },
50
+ {
51
+ name: 'help',
52
+ description: 'Print this usage guide.',
53
+ },
54
+ ],
55
+ },
56
+ ];
57
+ const usage = commandLineUsage(sections);
58
+ logger.info(usage);
59
+ process.exit(0);
60
+ }
61
+
62
+ module.exports = {
63
+ parseArgs,
64
+ showHelp,
65
+ };
package/config.js CHANGED
@@ -1,17 +1,14 @@
1
- /* eslint-disable hubspot-dev/no-async-await */
2
- /*global process, module */
3
- 'use es6';
4
1
  const prompts = require('prompts');
5
2
  const logger = require('./logger');
6
3
  const path = require('path');
7
4
  const { MAIN_APP_CONFIG, PROJECT_CONFIG } = require('./constants');
8
5
  const { getUrlSafeFileName } = require('./utils');
9
6
 
10
- async function getExtensionConfig(configuration) {
11
- if (process.env.EXTENSION && configuration[process.env.EXTENSION]) {
12
- const { data } = configuration[process.env.EXTENSION];
7
+ async function getExtensionConfig(configuration, extension) {
8
+ if (extension && configuration[extension]) {
9
+ const { data } = configuration[extension];
13
10
  return {
14
- key: process.env.EXTENSION,
11
+ key: extension,
15
12
  name: data.title,
16
13
  file: data?.module?.file,
17
14
  output: data?.output,
@@ -82,21 +79,28 @@ function loadConfig() {
82
79
  }
83
80
 
84
81
  const outputConfig = {};
85
- try {
86
- mainAppConfig.extensions.crm.cards.forEach(card => {
87
- const extensionsRemoved = card.file.replace('extensions/', '');
88
- const cardConfig = require(path.join(process.cwd(), extensionsRemoved));
82
+
83
+ mainAppConfig.extensions.crm.cards.forEach(card => {
84
+ const extensionsRemoved = card.file.replace('extensions/', '');
85
+ const cardPath = path.join(process.cwd(), extensionsRemoved);
86
+ try {
87
+ const cardConfig = require(cardPath);
89
88
  const extensionFileName = cardConfig.data?.module?.file;
90
89
  outputConfig[extensionFileName] = cardConfig;
91
90
  outputConfig[extensionFileName].data.output = getUrlSafeFileName(
92
91
  cardConfig.data?.module?.file
93
92
  );
94
93
  outputConfig[extensionFileName].data.appName = projectConfig.name;
95
- });
96
- } catch (e) {
97
- logger.error(e.message);
98
- process.exit(1);
99
- }
94
+ } catch (e) {
95
+ let errorMessage = e?.message;
96
+ if (e?.code === 'MODULE_NOT_FOUND') {
97
+ errorMessage = `Unable to load "${cardPath}" file. \nPlease make sure you are running the command from the src/app/extensions directory and that your card JSON config exists within it.`;
98
+ }
99
+
100
+ logger.error(errorMessage);
101
+ process.exit(1);
102
+ }
103
+ });
100
104
 
101
105
  return outputConfig;
102
106
  }
package/constants.js CHANGED
@@ -1,9 +1,7 @@
1
- /*global module */
2
- 'use es6';
3
-
4
1
  const VITE_DEFAULT_PORT = 5173;
5
2
  const MAIN_APP_CONFIG = 'app.json';
6
3
  const PROJECT_CONFIG = 'hsproject.json';
4
+ const OUTPUT_DIR = 'dist';
7
5
 
8
6
  const ROLLUP_OPTIONS = {
9
7
  // Deps to exclude from the bundle
@@ -23,4 +21,5 @@ module.exports = {
23
21
  ROLLUP_OPTIONS,
24
22
  MAIN_APP_CONFIG,
25
23
  PROJECT_CONFIG,
24
+ OUTPUT_DIR,
26
25
  };
package/dev.js CHANGED
@@ -1,6 +1,3 @@
1
- /* eslint-disable hubspot-dev/no-async-await */
2
- /*global module, process*/
3
- 'use es6';
4
1
  const express = require('express');
5
2
  const chokidar = require('chokidar');
6
3
  const { WebSocketServer } = require('ws');
@@ -12,8 +9,8 @@ const fs = require('fs');
12
9
  const path = require('path');
13
10
  const { ROLLUP_OPTIONS } = require('./constants');
14
11
 
15
- async function _devBuild(config, outputDir) {
16
- const extensionConfig = await getExtensionConfig(config);
12
+ async function _devBuild(config, outputDir, extension) {
13
+ const extensionConfig = await getExtensionConfig(config, extension);
17
14
  build({
18
15
  build: {
19
16
  watch: {
@@ -121,8 +118,8 @@ function _startDevServer(outputDir, port, extensionConfig) {
121
118
  });
122
119
  }
123
120
 
124
- async function startDevMode(config, outputDir, port) {
125
- const extensionConfig = await _devBuild(config, outputDir);
121
+ async function startDevMode(config, outputDir, port, extension) {
122
+ const extensionConfig = await _devBuild(config, outputDir, extension);
126
123
  _startDevServer(outputDir, port, extensionConfig);
127
124
  }
128
125
 
package/logger.js CHANGED
@@ -1,6 +1,3 @@
1
- /*global module*/
2
- 'use es6';
3
-
4
1
  const { cyan, red, yellow, magenta } = require('console-log-colors');
5
2
 
6
3
  function info(message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.0.1-prealpha.0",
3
+ "version": "0.0.1-prealpha.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -9,9 +9,26 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
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
+ "remoteBuild.js",
21
+ "run.js",
22
+ "utils.js",
23
+ "tests/runTests.js",
24
+ "tests/testBuild.js",
25
+ "tests/testDevServer.js"
26
+ ],
12
27
  "license": "MIT",
13
28
  "dependencies": {
14
29
  "chokidar": "^3.5.3",
30
+ "command-line-args": "^5.2.1",
31
+ "command-line-usage": "^7.0.1",
15
32
  "console-log-colors": "^0.4.0",
16
33
  "express": "^4.18.2",
17
34
  "process": "^0.11.10",
@@ -20,7 +37,13 @@
20
37
  "ws": "^8.12.1"
21
38
  },
22
39
  "bin": {
23
- "hs-ui-extensions-dev-server": "run.js"
40
+ "hs-ui-extensions-dev-server": "run.js",
41
+ "hs-ui-extensions-remote-build": "remoteBuild.js"
24
42
  },
25
- "gitHead": "eb6b25c4f33ce83c1727cdddb7024267f0b63a70"
43
+ "eslintConfig": {
44
+ "env": {
45
+ "node": true
46
+ }
47
+ },
48
+ "gitHead": "74dbffa7fe6e4ad7d5029897801c11dc5d5edae1"
26
49
  }
package/remoteBuild.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ const { OUTPUT_DIR } = require('./constants');
3
+ const { getUrlSafeFileName } = require('./utils');
4
+ const { buildSingleExtension } = require('./build');
5
+
6
+ // Regular expressions to match files ending in ts, js, tsx, and jsx
7
+ const entryPointRegex = new RegExp(/.*\.(ts|js)[x]?$/);
8
+
9
+ let extension = process.argv[process.argv.length - 1];
10
+
11
+ if (!entryPointRegex.test(extension)) {
12
+ throw new Error(
13
+ 'The last argument should be the filename you wish to build. Supported file extensions are [ts, tsx, js, jsx]'
14
+ );
15
+ }
16
+
17
+ // The incoming filename may be the full path the the file, so split the string
18
+ // to remove any directory prefix up until the extensions directory
19
+ extension = extension.split('extensions/').pop();
20
+
21
+ buildSingleExtension(extension, getUrlSafeFileName(extension), OUTPUT_DIR);
package/run.js CHANGED
@@ -1,21 +1,25 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable hubspot-dev/no-async-await */
3
- /*global process*/
4
- 'use es6';
5
2
 
6
3
  const { startDevMode } = require('./dev');
7
4
  const { loadConfig } = require('./config');
8
- const { buildExtensions } = require('./build');
9
- const { VITE_DEFAULT_PORT } = require('./constants');
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');
10
9
 
11
- const DEV_MODE = process.env.DEV_MODE || false;
12
- const OUTPUT_DIR = 'dist';
13
- const PORT = process.env.PORT || VITE_DEFAULT_PORT;
10
+ const { DEV_MODE, BUILD_MODE, port, extension, help } = parseArgs();
11
+ const PORT = port || VITE_DEFAULT_PORT;
14
12
 
15
- const config = loadConfig();
13
+ if (help || !(DEV_MODE || BUILD_MODE)) {
14
+ showHelp(OUTPUT_DIR);
15
+ }
16
16
 
17
17
  if (DEV_MODE) {
18
- startDevMode(config, OUTPUT_DIR, PORT);
19
- } else {
20
- buildExtensions(config, OUTPUT_DIR);
18
+ startDevMode(loadConfig(), OUTPUT_DIR, PORT, extension);
19
+ } else if (BUILD_MODE) {
20
+ if (extension) {
21
+ buildSingleExtension(extension, getUrlSafeFileName(extension), OUTPUT_DIR);
22
+ } else {
23
+ buildAllExtensions(loadConfig(), OUTPUT_DIR);
24
+ }
21
25
  }
package/tests/runTests.js CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /*global process */
3
- 'use es6';
4
2
 
5
3
  const { testBuild } = require('./testBuild');
6
4
  const { testDevServer } = require('./testDevServer');
@@ -1,22 +1,15 @@
1
- /*global module, process */
2
- 'use es6';
3
-
4
1
  const { execSync } = require('child_process');
5
2
  const fs = require('fs');
6
3
  const path = require('path');
7
4
  const assert = require('assert');
8
5
 
9
- function testBuild(logger) {
10
- logger.info('Build Tests started 🀞');
11
- execSync('hs-ui-extensions-dev-server');
6
+ function _testHelper(command, outputDirFiles) {
7
+ execSync(command);
12
8
 
13
9
  // Make sure the files are getting generated in the dist dir
14
10
  const distDir = path.join(process.cwd(), 'dist');
15
11
  const filesInOutputDir = fs.readdirSync(distDir);
16
- assert.deepStrictEqual(filesInOutputDir, [
17
- 'PhoneLines.js',
18
- 'ProgressBarApp.js',
19
- ]);
12
+ assert.deepStrictEqual(filesInOutputDir, outputDirFiles);
20
13
 
21
14
  // Spot check the file contents to make sure they seem ok
22
15
  filesInOutputDir.forEach(file => {
@@ -35,8 +28,53 @@ function testBuild(logger) {
35
28
  );
36
29
  });
37
30
  });
31
+ }
32
+
33
+ function testDefaultBuildPath(logger) {
34
+ logger.warn('- Test default build path started 🀞');
35
+ _testHelper('hs-ui-extensions-dev-server build', [
36
+ 'PhoneLines.js',
37
+ 'ProgressBarApp.js',
38
+ ]);
39
+ logger.info('- Test default build path passed πŸš€');
40
+ }
41
+
42
+ function testBuildWithExtensionFlag(logger) {
43
+ logger.warn('- Test build with flags started 🀞');
44
+ _testHelper(
45
+ 'hs-ui-extensions-dev-server build --extension ProgressBarApp.tsx',
46
+ ['ProgressBarApp.js']
47
+ );
48
+ logger.info('- Test build with flags passed πŸš€');
49
+ }
50
+
51
+ function testDefInfraBuildFileName(logger) {
52
+ logger.warn('- Test build with entrypoint as arg 🀞');
53
+ _testHelper('hs-ui-extensions-remote-build ProgressBarApp.tsx', [
54
+ 'ProgressBarApp.js',
55
+ ]);
56
+ logger.info('- Test build with entrypoint as arg πŸš€');
57
+ }
58
+
59
+ function testDevInfraBuildFileNameWithPathPrefix(logger) {
60
+ logger.warn('- Test build with config file as arg 🀞');
61
+ _testHelper(
62
+ 'hs-ui-extensions-remote-build some/super/long/file/path/to/the/extensions/PhoneLines.tsx',
63
+ ['PhoneLines.js']
64
+ );
65
+ logger.info('- Test build with config file as arg πŸš€');
66
+ }
67
+
68
+ function testBuild(logger) {
69
+ logger.warn('\nBuild Tests started - External Devs 🀞');
70
+ testDefaultBuildPath(logger);
71
+ testBuildWithExtensionFlag(logger);
72
+ logger.info('Build Tests passed - External DevsπŸš€');
38
73
 
39
- logger.info('Build Tests passed πŸš€');
74
+ logger.warn('\nBuild Tests started - Dev Infra 🀞');
75
+ testDefInfraBuildFileName(logger);
76
+ testDevInfraBuildFileNameWithPathPrefix(logger);
77
+ logger.info('Build Tests passed - Dev Infra πŸš€');
40
78
  }
41
79
 
42
80
  module.exports = {
@@ -1,6 +1,3 @@
1
- /*global module, process */
2
- 'use es6';
3
-
4
1
  const { spawn } = require('child_process');
5
2
  const fs = require('fs');
6
3
  const path = require('path');
@@ -14,24 +11,32 @@ const testResults = {
14
11
  webSocketTestPassed: false,
15
12
  };
16
13
 
14
+ const port = 5172;
15
+
17
16
  function testDevServer(logger, devServerProcess) {
18
- logger.info('Dev Server Tests started 🀞');
17
+ logger.warn('\nDev Server Tests started 🀞');
19
18
 
20
19
  // We need to use spawn here because it will put the process into the background,
21
20
  // which is required because dev mode is a blocking process and we want to test that
22
21
  // the express server and websocket server are starting properly
23
- devServerProcess = spawn('hs-ui-extensions-dev-server', {
24
- env: { ...process.env, DEV_MODE: 'true', EXTENSION: 'PhoneLines.tsx' },
25
- });
22
+ devServerProcess = spawn('hs-ui-extensions-dev-server', [
23
+ 'dev',
24
+ '--extension',
25
+ 'PhoneLines.tsx',
26
+ '--port',
27
+ `${port}`,
28
+ ]);
26
29
 
27
30
  devServerProcess.stdout.on('data', buffer => {
28
31
  const data = buffer.toString().toLowerCase();
29
32
  if (data.includes('built in')) {
30
- testBuild(testResults);
33
+ testBuild(testResults, logger);
31
34
  }
32
- if (data.includes('listening') && data.includes('localhost:5173')) {
33
- testExpressServer(testResults);
34
- testWebSocketServer(testResults);
35
+ if (data.includes('listening') && data.includes(`localhost:${port}`)) {
36
+ setTimeout(() => {
37
+ testExpressServer(testResults, logger);
38
+ testWebSocketServer(testResults, logger);
39
+ }, 1000);
35
40
  }
36
41
  });
37
42
 
@@ -74,7 +79,7 @@ function metConditions() {
74
79
 
75
80
  // Test that the files were built in the proper location and spot
76
81
  // check the contents of the files.
77
- function testBuild(results) {
82
+ function testBuild(results, logger) {
78
83
  // // Make sure the files are getting generated in the dist dir
79
84
  const distDir = path.join(process.cwd(), 'dist');
80
85
  const filesInOutputDir = fs.readdirSync(distDir);
@@ -95,41 +100,44 @@ function testBuild(results) {
95
100
  `File ${filesInOutputDir[0]} contents should contain: "${stringToCheck}"`
96
101
  );
97
102
  });
103
+ logger.info('- Build succeeded πŸš€');
98
104
  results.buildTestPassed = true;
99
105
  }
100
106
 
101
107
  // Test that the express server is running on the expected port
102
108
  // and that it is serving the files as expected.
103
- function testExpressServer(results) {
109
+ function testExpressServer(results, logger) {
104
110
  http.get(
105
111
  {
106
112
  host: 'localhost',
107
- port: 5173,
113
+ port,
108
114
  path: '/PhoneLines.js',
109
115
  },
110
116
  response => {
111
117
  if (response.statusCode !== 200) {
112
118
  throw Error('Error with express server');
113
119
  }
120
+ logger.info('- Express server connected and serving files πŸš€');
114
121
  results.expressTestPassed = true;
115
122
  }
116
123
  );
117
124
  }
118
125
 
119
126
  // Test the the web socket server is running on the expected port and
120
- // that we are able to recieve messages from it.
121
- function testWebSocketServer(results) {
127
+ // that we are able to receive messages from it.
128
+ function testWebSocketServer(results, logger) {
122
129
  const fileContents = fs
123
130
  .readFileSync(path.join('dist', 'PhoneLines.js'))
124
131
  .toString('base64');
125
132
 
126
- const ws = new WebSocket('ws://localhost:5173');
133
+ const ws = new WebSocket(`ws://localhost:${port}`);
127
134
  ws.on('message', messageBuffer => {
128
135
  const message = JSON.parse(messageBuffer.toString());
129
136
  assert(message.event === 'start' || message.event === 'update');
130
137
  assert(message.appName === 'example-app');
131
138
  assert.strictEqual(message.extension, 'Phone Lines');
132
139
  assert(message.callback, `data:text/javascript;base64,${fileContents}`);
140
+ logger.info('- WebSocket server connected and sending messages πŸš€');
133
141
  results.webSocketTestPassed = true;
134
142
  });
135
143
  }
package/utils.js CHANGED
@@ -1,6 +1,3 @@
1
- /*global module*/
2
- 'use es6';
3
-
4
1
  function getUrlSafeFileName(filePath) {
5
2
  const fileName = filePath.split('/').pop();
6
3
  const fileNameWithJsExtension = fileName.replace(/\.ts[x]?$|\.jsx$/g, '.js');