@elliemae/pui-cli 9.0.0-next.52 → 9.0.0-next.61

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.
Files changed (169) hide show
  1. package/app.tsconfig.json +6 -3
  2. package/dist/cjs/babel.config.cjs +1 -1
  3. package/dist/cjs/build/vite.config.js +45 -0
  4. package/dist/cjs/cli.js +3 -1
  5. package/dist/cjs/commands/buildcdn.js +74 -0
  6. package/dist/cjs/commands/lint.js +1 -1
  7. package/dist/cjs/commands/test.js +0 -1
  8. package/dist/cjs/commands/tscheck.js +8 -1
  9. package/dist/cjs/commands/utils.js +8 -8
  10. package/dist/cjs/commands/vitest.js +0 -1
  11. package/dist/cjs/index.js +5 -7
  12. package/dist/cjs/lint-config/eslint.config.js +259 -0
  13. package/dist/cjs/lint-config/stylelint.config.js +1 -1
  14. package/dist/cjs/monorepo/utils.cjs +2 -2
  15. package/dist/cjs/monorepo/utils.js +1 -1
  16. package/dist/cjs/server/cert.js +45 -0
  17. package/dist/cjs/server/certs/cert.crt +25 -0
  18. package/dist/cjs/server/certs/cert.csr +18 -0
  19. package/dist/cjs/server/certs/cert.ext +7 -0
  20. package/dist/cjs/server/certs/cert.key +30 -0
  21. package/dist/cjs/server/certs/cert.pfx +0 -0
  22. package/dist/cjs/server/certs/rootCA.crt +24 -0
  23. package/dist/cjs/server/certs/rootCA.key +30 -0
  24. package/dist/cjs/server/certs/rootCA.srl +1 -0
  25. package/dist/cjs/server/csp.js +40 -29
  26. package/dist/cjs/server/index.js +8 -3
  27. package/dist/cjs/server/logger.js +5 -2
  28. package/dist/cjs/server/middlewares.js +2 -2
  29. package/dist/cjs/server/wsServer.js +7 -4
  30. package/dist/cjs/testing/jest.config.cjs +1 -2
  31. package/dist/cjs/testing/jest.polyfills.cjs +1 -1
  32. package/dist/cjs/testing/mocks/iframe.js +24 -0
  33. package/dist/cjs/testing/mocks/svg.js +0 -11
  34. package/dist/cjs/testing/resolver.cjs +0 -1
  35. package/dist/cjs/testing/vitest.config.js +1 -5
  36. package/dist/cjs/transpile/.swcrc +1 -0
  37. package/dist/cjs/utils.cjs +1 -1
  38. package/dist/cjs/utils.js +3 -1
  39. package/dist/cjs/webpack/csp-plugin.js +79 -0
  40. package/dist/cjs/webpack/csp.js +158 -0
  41. package/dist/cjs/webpack/helpers.js +14 -14
  42. package/dist/cjs/webpack/interceptor-middleware.js +125 -0
  43. package/dist/cjs/webpack/webpack.dev.babel.js +16 -4
  44. package/dist/cjs/webpack/webpack.lib.base.babel.js +0 -2
  45. package/dist/cjs/webpack/webpack.lib.prod.babel.js +3 -1
  46. package/dist/cjs/webpack/webpack.prod.babel.js +12 -5
  47. package/dist/cjs/webpack/webpack.storybook.js +1 -1
  48. package/dist/esm/babel.config.cjs +1 -1
  49. package/dist/esm/build/vite.config.js +25 -0
  50. package/dist/esm/cli.js +3 -1
  51. package/dist/esm/commands/buildcdn.js +43 -0
  52. package/dist/esm/commands/lint.js +1 -1
  53. package/dist/esm/commands/test.js +0 -1
  54. package/dist/esm/commands/tscheck.js +8 -1
  55. package/dist/esm/commands/utils.js +8 -8
  56. package/dist/esm/commands/vitest.js +0 -1
  57. package/dist/esm/index.js +2 -4
  58. package/dist/esm/lint-config/eslint.config.js +228 -0
  59. package/dist/esm/lint-config/stylelint.config.js +1 -1
  60. package/dist/esm/monorepo/utils.cjs +2 -2
  61. package/dist/esm/monorepo/utils.js +1 -1
  62. package/dist/esm/server/cert.js +14 -0
  63. package/dist/esm/server/certs/cert.crt +25 -0
  64. package/dist/esm/server/certs/cert.csr +18 -0
  65. package/dist/esm/server/certs/cert.ext +7 -0
  66. package/dist/esm/server/certs/cert.key +30 -0
  67. package/dist/esm/server/certs/cert.pfx +0 -0
  68. package/dist/esm/server/certs/rootCA.crt +24 -0
  69. package/dist/esm/server/certs/rootCA.key +30 -0
  70. package/dist/esm/server/certs/rootCA.srl +1 -0
  71. package/dist/esm/server/csp.js +40 -29
  72. package/dist/esm/server/index.js +8 -3
  73. package/dist/esm/server/logger.js +5 -2
  74. package/dist/esm/server/middlewares.js +2 -2
  75. package/dist/esm/server/wsServer.js +7 -4
  76. package/dist/esm/testing/jest.config.cjs +1 -2
  77. package/dist/esm/testing/jest.polyfills.cjs +1 -1
  78. package/dist/esm/testing/mocks/iframe.js +4 -0
  79. package/dist/esm/testing/mocks/svg.js +0 -1
  80. package/dist/esm/testing/resolver.cjs +0 -1
  81. package/dist/esm/testing/vitest.config.js +1 -4
  82. package/dist/esm/transpile/.swcrc +1 -0
  83. package/dist/esm/utils.cjs +1 -1
  84. package/dist/esm/utils.js +3 -1
  85. package/dist/esm/webpack/csp-plugin.js +49 -0
  86. package/dist/esm/webpack/csp.js +128 -0
  87. package/dist/esm/webpack/helpers.js +14 -14
  88. package/dist/esm/webpack/interceptor-middleware.js +105 -0
  89. package/dist/esm/webpack/webpack.dev.babel.js +16 -4
  90. package/dist/esm/webpack/webpack.lib.base.babel.js +0 -2
  91. package/dist/esm/webpack/webpack.lib.prod.babel.js +3 -1
  92. package/dist/esm/webpack/webpack.prod.babel.js +12 -5
  93. package/dist/esm/webpack/webpack.storybook.js +1 -1
  94. package/dist/types/eslint.config.d.ts +3 -0
  95. package/dist/types/eslint.config.d.ts.map +1 -0
  96. package/dist/types/lib/build/vite.config.d.ts +3 -0
  97. package/dist/types/lib/build/vite.config.d.ts.map +1 -0
  98. package/dist/types/lib/commands/build.d.ts.map +1 -1
  99. package/dist/types/lib/commands/buildcdn.d.ts +3 -0
  100. package/dist/types/lib/commands/buildcdn.d.ts.map +1 -0
  101. package/dist/types/lib/commands/gendoc.d.ts.map +1 -1
  102. package/dist/types/lib/commands/pack.d.ts.map +1 -1
  103. package/dist/types/lib/commands/start.d.ts.map +1 -1
  104. package/dist/types/lib/commands/test.d.ts.map +1 -1
  105. package/dist/types/lib/commands/tscheck.d.ts.map +1 -1
  106. package/dist/types/lib/commands/utils.d.ts +2 -1
  107. package/dist/types/lib/commands/utils.d.ts.map +1 -1
  108. package/dist/types/lib/commands/vitest.d.ts.map +1 -1
  109. package/dist/types/lib/index.d.ts +1 -2
  110. package/dist/types/lib/index.d.ts.map +1 -1
  111. package/dist/types/lib/lint-config/eslint.config.d.ts +3 -0
  112. package/dist/types/lib/lint-config/eslint.config.d.ts.map +1 -0
  113. package/dist/types/lib/server/cert.d.ts +5 -0
  114. package/dist/types/lib/server/cert.d.ts.map +1 -0
  115. package/dist/types/lib/server/csp.d.ts +0 -1
  116. package/dist/types/lib/server/csp.d.ts.map +1 -1
  117. package/dist/types/lib/server/logger.d.ts.map +1 -1
  118. package/dist/types/lib/server/wsServer.d.ts +2 -2
  119. package/dist/types/lib/server/wsServer.d.ts.map +1 -1
  120. package/dist/types/lib/testing/jest.config.d.cts +2 -1
  121. package/dist/types/lib/testing/jest.node.config.d.cts +2 -1
  122. package/dist/types/lib/testing/mocks/cssModule.d.ts.map +1 -1
  123. package/dist/types/lib/testing/mocks/html.d.ts.map +1 -1
  124. package/dist/types/lib/testing/mocks/iframe.d.ts +3 -0
  125. package/dist/types/lib/testing/mocks/iframe.d.ts.map +1 -0
  126. package/dist/types/lib/testing/mocks/image.d.ts.map +1 -1
  127. package/dist/types/lib/testing/mocks/svg.d.ts.map +1 -1
  128. package/dist/types/lib/testing/resolver.d.cts.map +1 -1
  129. package/dist/types/lib/testing/vitest.config.d.ts +1 -1
  130. package/dist/types/lib/testing/vitest.config.d.ts.map +1 -1
  131. package/dist/types/lib/transpile/esbuild.d.ts.map +1 -1
  132. package/dist/types/lib/utils.d.cts +1 -1
  133. package/dist/types/lib/utils.d.cts.map +1 -1
  134. package/dist/types/lib/utils.d.ts +2 -2
  135. package/dist/types/lib/utils.d.ts.map +1 -1
  136. package/dist/types/lib/webpack/csp-plugin.d.ts +33 -0
  137. package/dist/types/lib/webpack/csp-plugin.d.ts.map +1 -0
  138. package/dist/types/lib/webpack/csp.d.ts +66 -0
  139. package/dist/types/lib/webpack/csp.d.ts.map +1 -0
  140. package/dist/types/lib/webpack/helpers.d.ts +0 -1
  141. package/dist/types/lib/webpack/helpers.d.ts.map +1 -1
  142. package/dist/types/lib/webpack/interceptor-middleware.d.ts +11 -0
  143. package/dist/types/lib/webpack/interceptor-middleware.d.ts.map +1 -0
  144. package/dist/types/lib/webpack/webpack.base.babel.d.ts.map +1 -1
  145. package/dist/types/lib/webpack/webpack.dev.babel.d.ts.map +1 -1
  146. package/dist/types/lib/webpack/webpack.lib.base.babel.d.ts.map +1 -1
  147. package/dist/types/lib/webpack/webpack.lib.prod.babel.d.ts.map +1 -1
  148. package/dist/types/lib/webpack/webpack.prod.babel.d.ts.map +1 -1
  149. package/dist/types/tsconfig.tsbuildinfo +1 -1
  150. package/library.tsconfig.json +8 -5
  151. package/package.json +146 -142
  152. package/dist/cjs/lint-config/eslint/config.js +0 -186
  153. package/dist/cjs/lint-config/eslint/react.js +0 -107
  154. package/dist/cjs/lint-config/eslint/typescript/config.js +0 -97
  155. package/dist/cjs/testing/setup-test-env.js +0 -6
  156. package/dist/esm/lint-config/eslint/config.js +0 -155
  157. package/dist/esm/lint-config/eslint/react.js +0 -76
  158. package/dist/esm/lint-config/eslint/typescript/config.js +0 -67
  159. package/dist/esm/testing/setup-test-env.js +0 -5
  160. package/dist/types/lib/lint-config/eslint/config.d.ts +0 -2
  161. package/dist/types/lib/lint-config/eslint/config.d.ts.map +0 -1
  162. package/dist/types/lib/lint-config/eslint/react.d.ts +0 -2
  163. package/dist/types/lib/lint-config/eslint/react.d.ts.map +0 -1
  164. package/dist/types/lib/lint-config/eslint/typescript/config.d.ts +0 -2
  165. package/dist/types/lib/lint-config/eslint/typescript/config.d.ts.map +0 -1
  166. package/dist/types/lib/testing/setup-test-env.d.ts +0 -2
  167. package/dist/types/lib/testing/setup-test-env.d.ts.map +0 -1
  168. package/dist/types/lib/tests/basic.test.d.ts +0 -1
  169. package/dist/types/lib/tests/basic.test.d.ts.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import ip from "ip";
3
+ import { isHttps } from "../utils.js";
3
4
  const divider = chalk.gray("\n-----------------------------------");
4
5
  const logger = {
5
6
  // Called whenever there's an error on the server we want to print
@@ -15,10 +16,12 @@ const logger = {
15
16
  const accessUrls = `${chalk.bold("Access URLs:")}${divider}
16
17
  `;
17
18
  const localHostUrl = `Localhost: ${chalk.magenta(
18
- `http://${host}:${port}`
19
+ `${isHttps() ? "https" : "http"}://${host}:${port}`
19
20
  )}
20
21
  `;
21
- const lanUrl = `LAN: ${chalk.magenta(`http://${ip.address()}:${port}`)}
22
+ const lanUrl = `LAN: ${chalk.magenta(
23
+ `${isHttps() ? "https" : "http"}://${ip.address()}:${port}`
24
+ )}
22
25
  `;
23
26
  const proxy = tunnelStarted ? `
24
27
  Proxy: ${chalk.magenta(tunnelStarted)}` : "";
@@ -27,7 +27,7 @@ const setupDefaultMiddlewares = (app) => {
27
27
  const setupAdditionalMiddlewars = (app, options = {}) => {
28
28
  const { buildPath = paths.buildPath, basePath = paths.basePath } = options;
29
29
  app.use(compression());
30
- app.get(basePath, (req, res) => {
30
+ app.get(basePath, (_req, res) => {
31
31
  sendFileWithCSPNonce({ buildPath, res });
32
32
  });
33
33
  app.use(
@@ -41,7 +41,7 @@ const setupAdditionalMiddlewars = (app, options = {}) => {
41
41
  app.use(expressStaticGzip("cdn", {}));
42
42
  app.get(
43
43
  "*",
44
- (req, res) => sendFileWithCSPNonce({ buildPath, res })
44
+ (_req, res) => sendFileWithCSPNonce({ buildPath, res })
45
45
  );
46
46
  return app;
47
47
  };
@@ -1,5 +1,8 @@
1
+ import https from "node:https";
1
2
  import http from "node:http";
2
3
  import * as wsLib from "ws";
4
+ import { getCertOptions } from "./cert.js";
5
+ import { isHttps } from "../utils.js";
3
6
  const PING_INTERVAL = 3e4;
4
7
  const DEFAULT_PORT = 5001;
5
8
  const onSocketError = (err) => {
@@ -25,9 +28,9 @@ const createWSServer = ({
25
28
  const heartbeat = () => {
26
29
  isAlive = true;
27
30
  };
28
- const httpServer = http.createServer();
31
+ const server = isHttps() ? https.createServer(getCertOptions()) : http.createServer();
29
32
  const wsServer = new wsLib.WebSocketServer({ noServer: true });
30
- httpServer.on("upgrade", (req, socket, head) => {
33
+ server.on("upgrade", (req, socket, head) => {
31
34
  socket.on("error", onSocketError);
32
35
  wsServer.handleUpgrade(req, socket, head, (ws) => {
33
36
  const protocols = req.headers["sec-websocket-protocol"]?.split(",");
@@ -84,9 +87,9 @@ const createWSServer = ({
84
87
  clearInterval(interval);
85
88
  });
86
89
  return new Promise((resolve) => {
87
- httpServer.listen(port, () => {
90
+ server.listen(port, () => {
88
91
  console.log(`Websocket server listening on port ${port}`);
89
- return resolve({ httpServer, wsServer });
92
+ return resolve({ server, wsServer });
90
93
  });
91
94
  });
92
95
  };
@@ -6,10 +6,8 @@ const { findMonoRepoRoot } = require('../monorepo/utils.cjs');
6
6
 
7
7
  let isReactModule = true;
8
8
  try {
9
- /* eslint-disable global-require, import/no-unresolved */
10
9
  require('react');
11
10
  require('styled-components');
12
- /* eslint-enable global-require, import/no-unresolved */
13
11
  } catch (err) {
14
12
  isReactModule = false;
15
13
  }
@@ -64,6 +62,7 @@ const jestConfig = {
64
62
  '.*\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$':
65
63
  getMockFilePath('image.js'),
66
64
  '.*\\.svg(?:\\?[a-zA-Z]+)?$': getMockFilePath('svg.js'),
65
+ '.*iframe\\.html(?:\\?[a-zA-Z]+)?$': getMockFilePath('iframe.js'),
67
66
  '.*\\.html(?:\\?[a-zA-Z]+)?$': getMockFilePath('html.js'),
68
67
  '@elliemae/pui-user-monitoring': getMockFilePath('pui-user-monitoring.js'),
69
68
  '@elliemae/pui-app-loader': getMockFilePath('pui-app-loader.js'),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @note The block below contains polyfills for Node.js globals
2
+ * The block below contains polyfills for Node.js globals
3
3
  * required for Jest to function when running JSDOM tests.
4
4
  * These HAVE to be require's and HAVE to be in this exact
5
5
  * order, since "undici" depends on the "TextEncoder" global API.
@@ -0,0 +1,4 @@
1
+ var iframe_default = "./iframe.html";
2
+ export {
3
+ iframe_default as default
4
+ };
@@ -1,4 +1,3 @@
1
- import * as React from "react";
2
1
  var svg_default = "SvgrURL";
3
2
  const ReactComponent = "div";
4
3
  export {
@@ -32,7 +32,6 @@ module.exports = (request, options) => {
32
32
  }
33
33
  const resolution = resolutions.find(({ matcher }) => matcher.test(request));
34
34
  if (resolution) {
35
- // eslint-disable-next-line no-restricted-syntax
36
35
  for (const extension of resolution.extensions) {
37
36
  try {
38
37
  return resolver(
@@ -1,16 +1,13 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
1
  import { defineConfig, configDefaults } from "vitest/config";
4
2
  import react from "@vitejs/plugin-react";
5
3
  import tsconfigPaths from "vite-tsconfig-paths";
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
4
  const vitestConfig = defineConfig({
5
+ // @ts-expect-error - `react` and `tsconfigPaths` are not part of the default config
8
6
  plugins: [react(), tsconfigPaths()],
9
7
  test: {
10
8
  globals: true,
11
9
  root: process.cwd(),
12
10
  environment: "happy-dom",
13
- setupFiles: [path.resolve(__dirname, "./setup-test-env.js")],
14
11
  include: ["./{app,lib}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
15
12
  exclude: [...configDefaults.exclude, ".idea", ".git", ".cache", "e2e"],
16
13
  coverage: {
@@ -4,6 +4,7 @@
4
4
  "syntax": "typescript",
5
5
  "jsx": true,
6
6
  "tsx": true,
7
+ "decorators": true,
7
8
  "dynamicImport": true
8
9
  },
9
10
  "target": "es2022"
@@ -14,7 +14,7 @@ exports.getAppConfig = () => {
14
14
 
15
15
  if (!fs.existsSync(appConfigPath)) return '{}';
16
16
  const appConfig = fs.readFileSync(appConfigPath);
17
- return appConfig || '{}';
17
+ return appConfig ?? '{}';
18
18
  };
19
19
 
20
20
  exports.isApp = isApp;
package/dist/esm/utils.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  const basePath = (process.env.BASE_PATH ?? "/").replace(/\/?$/, "/");
4
4
  const isApp = () => fs.existsSync(path.join(process.cwd(), "app"));
5
+ const isHttps = () => process.env.HTTPS === "true";
5
6
  const getAppConfig = () => {
6
7
  const appConfigPath = path.join(
7
8
  process.cwd(),
@@ -15,5 +16,6 @@ const getAppConfig = () => {
15
16
  export {
16
17
  basePath,
17
18
  getAppConfig,
18
- isApp
19
+ isApp,
20
+ isHttps
19
21
  };
@@ -0,0 +1,49 @@
1
+ import HtmlWebpackPlugin from "html-webpack-plugin";
2
+ import { CSP } from "./csp.js";
3
+ const defaultOptions = {
4
+ enableUnsafeEval: false
5
+ };
6
+ class CspPlugin {
7
+ #options = defaultOptions;
8
+ /**
9
+ * @param {object} options Additional options for this module.
10
+ */
11
+ constructor(options) {
12
+ this.#options = { ...this.#options, ...options ?? {} };
13
+ }
14
+ /**
15
+ * Processes HtmlWebpackPlugin's html data by adding the CSP
16
+ * @param _compilation
17
+ * @param htmlPluginData
18
+ * @param htmlPluginData.html
19
+ * @param compileCb
20
+ * @returns {*}
21
+ */
22
+ processCsp(_compilation, htmlPluginData, compileCb) {
23
+ const cspModule = new CSP(htmlPluginData.html);
24
+ cspModule.refactorSourcedScriptsForHashBasedCsp();
25
+ const scriptHashes = cspModule.hashAllInlineScripts();
26
+ const { enableUnsafeEval } = this.#options;
27
+ const strictCsp = CSP.getStrictCsp(scriptHashes, {
28
+ enableUnsafeEval
29
+ });
30
+ cspModule.addMetaTag(strictCsp);
31
+ htmlPluginData.html = cspModule.serializeDom();
32
+ return compileCb(null, htmlPluginData);
33
+ }
34
+ /**
35
+ * Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
36
+ * @param compiler
37
+ */
38
+ apply(compiler) {
39
+ compiler.hooks.compilation.tap("CspPlugin", (compilation) => {
40
+ HtmlWebpackPlugin.getCompilationHooks(compilation).beforeEmit.tapAsync(
41
+ "CspPlugin",
42
+ this.processCsp.bind(this, compilation)
43
+ );
44
+ });
45
+ }
46
+ }
47
+ export {
48
+ CspPlugin
49
+ };
@@ -0,0 +1,128 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as cheerio from "cheerio";
3
+ class CSP {
4
+ static HASH_FUNCTION = "sha256";
5
+ static INLINE_SCRIPT_SELECTOR = "script:not([src])";
6
+ static SOURCED_SCRIPT_SELECTOR = "script[src]";
7
+ $;
8
+ constructor(html) {
9
+ this.$ = cheerio.load(html);
10
+ }
11
+ serializeDom() {
12
+ return this.$.root().html() ?? "";
13
+ }
14
+ /**
15
+ * Returns a strict Content Security Policy for mittigating XSS.
16
+ * For more details read csp.withgoogle.com.
17
+ * If you modify this CSP, make sure it has not become trivially bypassable by
18
+ * checking the policy using csp-evaluator.withgoogle.com.
19
+ * @param hashes A list of sha-256 hashes of trusted inline scripts.
20
+ * @param cspOptions
21
+ * @param enableTrustedTypes If Trusted Types should be enabled for scripts.
22
+ * @param enableBrowserFallbacks If fallbacks for older browsers should be
23
+ * added. This is will not weaken the policy as modern browsers will ignore
24
+ * the fallbacks.
25
+ * @param enableUnsafeEval If you cannot remove all uses of eval(), you can
26
+ * still set a strict CSP, but you will have to use the 'unsafe-eval'
27
+ * keyword which will make your policy slightly less secure.
28
+ * @param cspOptions.enableBrowserFallbacks
29
+ * @param cspOptions.enableTrustedTypes
30
+ * @param cspOptions.enableUnsafeEval
31
+ * @returns A strict Content Security Policy string.
32
+ */
33
+ static getStrictCsp(hashes, cspOptions = {
34
+ enableUnsafeEval: false
35
+ }) {
36
+ const strictCspTemplate = {
37
+ // 'strict-dynamic' allows hashed scripts to create new scripts.
38
+ "script-src": [`'strict-dynamic'`, ...hashes ?? []],
39
+ // Restricts `object-src` to disable dangerous plugins like Flash.
40
+ "object-src": [`'none'`],
41
+ // Restricts `base-uri` to block the injection of `<base>` tags. This
42
+ // prevents attackers from changing the locations of scripts loaded from
43
+ // relative URLs.
44
+ "base-uri": [`'self'`]
45
+ };
46
+ if (cspOptions.enableUnsafeEval) {
47
+ strictCspTemplate["script-src"].push(`'unsafe-eval'`);
48
+ }
49
+ return Object.entries(strictCspTemplate).map(([directive, values]) => `${directive} ${values.join(" ")};`).join("");
50
+ }
51
+ /**
52
+ * Enables a CSP via a meta tag at the beginning of the document.
53
+ * Warning: It's recommended to set CSP as HTTP response header instead of
54
+ * using a meta tag. Injections before the meta tag will not be covered by CSP
55
+ * and meta tags don't support CSP in report-only mode.
56
+ * @param csp A Content Security Policy string.
57
+ */
58
+ addMetaTag(csp) {
59
+ let metaTag = this.$('meta[http-equiv="Content-Security-Policy"]');
60
+ if (!metaTag.length) {
61
+ metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
62
+ "meta"
63
+ );
64
+ metaTag.prependTo(this.$("head"));
65
+ }
66
+ metaTag.attr("content", csp);
67
+ }
68
+ /**
69
+ * Replaces all sourced scripts with a single inline script that can be hashed
70
+ */
71
+ refactorSourcedScriptsForHashBasedCsp() {
72
+ const scriptInfoList = this.$(CSP.SOURCED_SCRIPT_SELECTOR).map((_i, script) => {
73
+ const src = this.$(script).attr("src") ?? "";
74
+ const type = this.$(script).attr("type") ?? "";
75
+ this.$(script).remove();
76
+ return { src, type };
77
+ }).toArray().filter((info) => info.src);
78
+ const loaderScript = CSP.createLoaderScript(scriptInfoList);
79
+ if (!loaderScript) {
80
+ return;
81
+ }
82
+ const newScript = cheerio.load("<script>")("script");
83
+ newScript.text(loaderScript);
84
+ newScript.appendTo(this.$("body"));
85
+ }
86
+ /**
87
+ * Returns a list of hashes of all inline scripts found in the HTML document.
88
+ * @returns A list of sha-256 hashes of inline scripts.
89
+ */
90
+ hashAllInlineScripts() {
91
+ return this.$(CSP.INLINE_SCRIPT_SELECTOR).map((_i, elem) => CSP.hashInlineScript(this.$(elem).html() ?? "")).get();
92
+ }
93
+ /**
94
+ * Returns JS code for dynamically loading sourced (external) scripts.
95
+ * @param scriptInfoList A list of objects containing src and type for scripts that should be loaded
96
+ * @returns JS code for loading scripts.
97
+ */
98
+ static createLoaderScript(scriptInfoList) {
99
+ if (!scriptInfoList.length) {
100
+ return void 0;
101
+ }
102
+ return `
103
+ var scripts = ${JSON.stringify(scriptInfoList)};
104
+ scripts.forEach(function(scriptInfo) {
105
+ var s = document.createElement('script');
106
+ s.src = scriptInfo.src;
107
+ if (scriptInfo.type) {
108
+ s.type = scriptInfo.type;
109
+ }
110
+ s.async = false; // preserve execution order.
111
+ document.body.appendChild(s);
112
+ });
113
+ `;
114
+ }
115
+ /**
116
+ * Calculates a CSP compatible hash of an inline script.
117
+ * @param scriptText Text between opening and closing script tag. Has to
118
+ * include whitespaces and newlines!
119
+ * @returns A sha-256 hash of the script.
120
+ */
121
+ static hashInlineScript(scriptText) {
122
+ const hash = crypto.createHash(CSP.HASH_FUNCTION).update(scriptText, "utf-8").digest("base64");
123
+ return `'${CSP.HASH_FUNCTION}-${hash}'`;
124
+ }
125
+ }
126
+ export {
127
+ CSP
128
+ };
@@ -15,7 +15,7 @@ const getNodeModulesRegEx = (modules) => modules.map(
15
15
  const excludeNodeModulesExcept = (modules) => {
16
16
  const moduleRegExps = getNodeModulesRegEx(modules);
17
17
  return function(modulePath) {
18
- if (/node_modules/.test(modulePath)) {
18
+ if (modulePath.includes("node_modules")) {
19
19
  for (const moduleRegExp of moduleRegExps)
20
20
  if (moduleRegExp.test(modulePath)) return false;
21
21
  return true;
@@ -87,9 +87,9 @@ const getUserMonitoringFileName = () => {
87
87
  );
88
88
  if (!fs.existsSync(userMonLibPath)) return `${libName}.js`;
89
89
  const distJSFiles = fs.readdirSync(userMonLibPath);
90
- return distJSFiles.filter(
91
- (fName) => fName.match(/emuiUserMonitoring+((?!chunk).)*\.js$/)
92
- )[0];
90
+ return distJSFiles.find(
91
+ (fName) => /emuiUserMonitoring+((?!chunk).)*\.js$/.exec(fName)
92
+ );
93
93
  };
94
94
  const getAppLoaderFileName = () => {
95
95
  const libName = "emuiAppLoader";
@@ -99,9 +99,9 @@ const getAppLoaderFileName = () => {
99
99
  );
100
100
  if (!fs.existsSync(appLoaderLibPath)) return `${libName}.js`;
101
101
  const distJSFiles = fs.readdirSync(appLoaderLibPath);
102
- return distJSFiles.filter(
103
- (fName) => fName.match(/emuiAppLoader+((?!chunk).)*\.js$/)
104
- )[0];
102
+ return distJSFiles.find(
103
+ (fName) => /emuiAppLoader+((?!chunk).)*\.js$/.exec(fName)
104
+ );
105
105
  };
106
106
  const getDiagnosticsFileName = () => {
107
107
  const libName = "emuiDiagnostics";
@@ -111,9 +111,9 @@ const getDiagnosticsFileName = () => {
111
111
  );
112
112
  if (!fs.existsSync(libPath)) return `${libName}.js`;
113
113
  const distJSFiles = fs.readdirSync(libPath);
114
- return distJSFiles.filter(
115
- (fName) => fName.match(/emuiDiagnostics+((?!chunk).)*\.js$/)
116
- )[0];
114
+ return distJSFiles.find(
115
+ (fName) => /emuiDiagnostics+((?!chunk).)*\.js$/.exec(fName)
116
+ );
117
117
  };
118
118
  const getENCWLoaderFileName = () => {
119
119
  const libName = "emuiEncwLoader";
@@ -123,13 +123,13 @@ const getENCWLoaderFileName = () => {
123
123
  );
124
124
  if (!fs.existsSync(appLoaderLibPath)) return `${libName}.js`;
125
125
  const distJSFiles = fs.readdirSync(appLoaderLibPath);
126
- return distJSFiles.filter(
127
- (fName) => fName.match(/emuiEncwLoader+((?!chunk).)*\.js$/)
128
- )[0];
126
+ return distJSFiles.find(
127
+ (fName) => /emuiEncwLoader+((?!chunk).)*\.js$/.exec(fName)
128
+ );
129
129
  };
130
130
  const getAppVersion = () => {
131
131
  if (!process.env.APP_VERSION) return LATEST_VERSION;
132
- const match = process.env.APP_VERSION.match(/^v?(\d+\.\d+)\..*$/);
132
+ const match = /^v?(\d+\.\d+)\..*$/.exec(process.env.APP_VERSION);
133
133
  return match?.[1] ?? LATEST_VERSION;
134
134
  };
135
135
  const getPaths = (latestVersion = true) => {
@@ -0,0 +1,105 @@
1
+ const VALID_PARAMS = ["isInterceptable", "intercept", "afterSend"];
2
+ const validateParams = (methods) => {
3
+ Object.keys(methods).forEach((k) => {
4
+ if (!VALID_PARAMS.includes(k)) {
5
+ throw new Error(`${k} isn't a valid param (${VALID_PARAMS.join(", ")})`);
6
+ }
7
+ });
8
+ if (!("isInterceptable" in methods)) {
9
+ throw new Error("isInterceptable is a required param (function)");
10
+ }
11
+ };
12
+ const interceptorMiddleware = (fn) => (req, res, next) => {
13
+ const methods = fn(req, res);
14
+ validateParams(methods);
15
+ const originalEnd = res.end;
16
+ const originalWrite = res.write;
17
+ const chunks = [];
18
+ let isIntercepting;
19
+ let isFirstWrite = true;
20
+ function intercept(rawChunk, encoding) {
21
+ if (isFirstWrite) {
22
+ isFirstWrite = false;
23
+ isIntercepting = methods.isInterceptable();
24
+ }
25
+ if (isIntercepting) {
26
+ if (rawChunk) {
27
+ let tempChunk = rawChunk;
28
+ if (rawChunk !== null && !Buffer.isBuffer(tempChunk)) {
29
+ if (!encoding) {
30
+ tempChunk = Buffer.from(rawChunk);
31
+ } else {
32
+ tempChunk = Buffer.from(
33
+ rawChunk,
34
+ encoding
35
+ );
36
+ }
37
+ }
38
+ chunks.push(tempChunk);
39
+ }
40
+ }
41
+ return isIntercepting;
42
+ }
43
+ const newResWrite = (...args) => {
44
+ if (!intercept(args[0], args[1])) {
45
+ return originalWrite.apply(res, args);
46
+ }
47
+ return true;
48
+ };
49
+ res.write = newResWrite;
50
+ const afterSend = (oldBody, newBody) => {
51
+ if (typeof methods.afterSend === "function") {
52
+ process.nextTick(() => {
53
+ methods.afterSend?.(oldBody, newBody);
54
+ });
55
+ }
56
+ };
57
+ const newResEnd = (...args) => {
58
+ if (intercept(args[0], args[1])) {
59
+ isIntercepting = false;
60
+ const oldBody = Buffer.concat(chunks).toString("utf-8");
61
+ if (methods.intercept) {
62
+ if (typeof methods.intercept !== "function") {
63
+ throw new Error(
64
+ "`send` must be a function with the body to be sent as the only param"
65
+ );
66
+ }
67
+ res.removeHeader("Content-Length");
68
+ methods.intercept(oldBody, (newBody) => {
69
+ args[0] = newBody;
70
+ originalEnd.apply(res, args);
71
+ afterSend(oldBody, newBody);
72
+ });
73
+ } else {
74
+ afterSend(oldBody, oldBody);
75
+ originalEnd.apply(res, args);
76
+ }
77
+ } else {
78
+ originalEnd.apply(res, args);
79
+ }
80
+ };
81
+ res.end = newResEnd;
82
+ next();
83
+ };
84
+ const addCSPNonceMiddleware = (middlewares) => {
85
+ const index = middlewares.findIndex(
86
+ (middleware) => middleware.name === "webpack-dev-middleware"
87
+ );
88
+ middlewares.splice(index, 0, {
89
+ name: "csp-nonce-middleware",
90
+ middleware: interceptorMiddleware((_req, res) => ({
91
+ // Only HTML responses will be intercepted
92
+ isInterceptable() {
93
+ return (res.get("Content-Type") ?? "").includes("text/html");
94
+ },
95
+ // Appends a paragraph at the end of the response body
96
+ intercept(body, send) {
97
+ send(body.replace(/__CSP_NONCE__/g, res.locals.cspNonce));
98
+ }
99
+ }))
100
+ });
101
+ };
102
+ export {
103
+ addCSPNonceMiddleware,
104
+ interceptorMiddleware
105
+ };
@@ -6,6 +6,7 @@ import CircularDependencyPlugin from "circular-dependency-plugin";
6
6
  import MiniCssExtractPlugin from "mini-css-extract-plugin";
7
7
  import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
8
8
  import expressStaticGzip from "express-static-gzip";
9
+ import { addCSPNonceMiddleware } from "./interceptor-middleware.js";
9
10
  import { setupDefaultMiddlewares } from "../server/middlewares.js";
10
11
  import { loadRoutes } from "../server/appRoutes.js";
11
12
  import {
@@ -16,6 +17,8 @@ import {
16
17
  import { baseConfig } from "./webpack.base.babel.js";
17
18
  import { wsPort } from "../server/utils.js";
18
19
  import { createWSServer } from "../server/wsServer.js";
20
+ import { getCertOptions } from "../server/cert.js";
21
+ import { isHttps } from "../utils.js";
19
22
  const __filename = fileURLToPath(import.meta.url);
20
23
  const {
21
24
  appVersion,
@@ -27,6 +30,10 @@ const {
27
30
  diagnosticsScriptPath,
28
31
  encwLoaderScriptPath
29
32
  } = getPaths();
33
+ const serverOptions = {
34
+ type: "https",
35
+ options: getCertOptions()
36
+ };
30
37
  const devConfig = {
31
38
  mode: "development",
32
39
  cache: {
@@ -70,7 +77,7 @@ const devConfig = {
70
77
  // Add development plugins
71
78
  plugins: [
72
79
  new HtmlWebpackPlugin({
73
- inject: !isAppLoaderEnabled() ? "head" : false,
80
+ inject: !isAppLoaderEnabled() && process.env.CSP !== "true",
74
81
  // Inject all files that are generated by webpack, e.g. bundle.js
75
82
  template: !isAppLoaderEnabled() ? "app/index.html" : "app/index-app-loader.html",
76
83
  emui: {
@@ -81,10 +88,14 @@ const devConfig = {
81
88
  appLoaderScriptPath,
82
89
  diagnosticsScriptPath,
83
90
  encwLoaderScriptPath,
84
- googleTagManager: isGoogleTagManagerEnabled()
91
+ googleTagManager: isGoogleTagManagerEnabled(),
92
+ csp: process.env.CSP === "true"
85
93
  },
86
94
  scriptLoading: "defer"
87
95
  }),
96
+ // new CspPlugin(HtmlWebpackPlugin, {
97
+ // enableUnsafeEval: true,
98
+ // }),
88
99
  new CircularDependencyPlugin({
89
100
  exclude: /a\.(js|ts|jsx|tsx)|node_modules/,
90
101
  // exclude node_modules
@@ -117,11 +128,12 @@ const devConfig = {
117
128
  hot: true,
118
129
  open: [basePath],
119
130
  port: process.env.PORT ?? "auto",
131
+ server: isHttps() ? serverOptions : {},
120
132
  setupMiddlewares: (middlewares, devServer) => {
133
+ addCSPNonceMiddleware(middlewares);
121
134
  if (devServer.app) {
122
135
  setupDefaultMiddlewares(devServer.app);
123
- loadRoutes(devServer.app).then(() => {
124
- }).catch((err) => {
136
+ loadRoutes(devServer.app).catch((err) => {
125
137
  console.error(err);
126
138
  });
127
139
  const cdnRouter = expressStaticGzip("cdn", {});
@@ -39,13 +39,11 @@ const plugins = [
39
39
  new webpack.DefinePlugin({
40
40
  APP_CONFIG: getAppConfig()
41
41
  }),
42
- /* eslint-disable indent */
43
42
  ...copyPluginPatterns.length > 0 ? [
44
43
  new CopyWebpackPlugin({
45
44
  patterns: copyPluginPatterns
46
45
  })
47
46
  ] : [],
48
- /* eslint-enable indent */
49
47
  new webpack.optimize.LimitChunkCountPlugin({
50
48
  maxChunks: 1
51
49
  }),
@@ -45,7 +45,9 @@ const prodConfig = {
45
45
  minimizer: [
46
46
  new EsbuildPlugin({
47
47
  target: browserslistToEsbuild(),
48
- css: true
48
+ css: true,
49
+ pure: ["console.log", "console.warn"],
50
+ drop: ["debugger"]
49
51
  })
50
52
  ],
51
53
  nodeEnv: "production",
@@ -29,10 +29,13 @@ const getProdConfig = ({ latestVersion = true } = {}) => {
29
29
  },
30
30
  optimization: {
31
31
  moduleIds: "deterministic",
32
+ // realContentHash: false,
32
33
  minimizer: [
33
34
  new EsbuildPlugin({
34
35
  target: browserslistToEsbuild(),
35
- css: true
36
+ css: true,
37
+ pure: ["console.log", "console.warn"],
38
+ drop: ["debugger"]
36
39
  })
37
40
  ],
38
41
  runtimeChunk: true,
@@ -46,7 +49,8 @@ const getProdConfig = ({ latestVersion = true } = {}) => {
46
49
  },
47
50
  vendors: {
48
51
  name: "vendors",
49
- test: /[\\/]node_modules[\\/]/
52
+ test: /[\\/]node_modules[\\/]/,
53
+ enforce: true
50
54
  }
51
55
  }
52
56
  }
@@ -83,7 +87,7 @@ const {
83
87
  basePath
84
88
  } = getPaths();
85
89
  const htmlWebpackPlugin = new HtmlWebpackPlugin({
86
- inject: !isAppLoaderEnabled() ? "head" : false,
90
+ inject: !isAppLoaderEnabled() && process.env.CSP !== "true",
87
91
  template: !isAppLoaderEnabled() ? "app/index.html" : "app/index-app-loader.html",
88
92
  minify: {
89
93
  removeComments: true,
@@ -105,12 +109,15 @@ const htmlWebpackPlugin = new HtmlWebpackPlugin({
105
109
  appLoaderScriptPath,
106
110
  diagnosticsScriptPath,
107
111
  encwLoaderScriptPath,
108
- googleTagManager: isGoogleTagManagerEnabled()
112
+ googleTagManager: isGoogleTagManagerEnabled(),
113
+ csp: process.env.CSP === "true"
109
114
  },
110
115
  scriptLoading: "defer"
111
116
  });
112
117
  const config = baseConfig(getProdConfig());
113
- if (config.plugins) config.plugins.push(htmlWebpackPlugin);
118
+ if (config.plugins) {
119
+ config.plugins.push(htmlWebpackPlugin);
120
+ }
114
121
  var webpack_prod_babel_default = config;
115
122
  export {
116
123
  webpack_prod_babel_default as default