webpack.config.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. /* eslint-disable quote-props */
  2. const dotenv = require("dotenv");
  3. const path = require("path");
  4. const webpack = require("webpack");
  5. const HtmlWebpackPlugin = require("html-webpack-plugin");
  6. const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  7. const TerserPlugin = require("terser-webpack-plugin");
  8. const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
  9. const HtmlWebpackInjectPreload = require("@principalstudio/html-webpack-inject-preload");
  10. const CopyWebpackPlugin = require("copy-webpack-plugin");
  11. // Environment variables
  12. // RIOT_OG_IMAGE_URL: specifies the URL to the image which should be used for the opengraph logo.
  13. // CSP_EXTRA_SOURCE: specifies a URL which should be appended to each CSP directive which uses 'self',
  14. // this can be helpful if your deployment has redirects for old bundles, such as develop.element.io.
  15. dotenv.config();
  16. let ogImageUrl = process.env.RIOT_OG_IMAGE_URL;
  17. if (!ogImageUrl) ogImageUrl = "https://app.element.io/themes/element/img/logos/opengraph.png";
  18. if (!process.env.VERSION) {
  19. console.warn("Unset VERSION variable - this may affect build output");
  20. process.env.VERSION = "!!UNSET!!";
  21. }
  22. const cssThemes = {
  23. // CSS themes
  24. "theme-legacy-light": "./node_modules/matrix-react-sdk/res/themes/legacy-light/css/legacy-light.pcss",
  25. "theme-legacy-dark": "./node_modules/matrix-react-sdk/res/themes/legacy-dark/css/legacy-dark.pcss",
  26. "theme-light": "./node_modules/matrix-react-sdk/res/themes/light/css/light.pcss",
  27. "theme-light-high-contrast":
  28. "./node_modules/matrix-react-sdk/res/themes/light-high-contrast/css/light-high-contrast.pcss",
  29. "theme-dark": "./node_modules/matrix-react-sdk/res/themes/dark/css/dark.pcss",
  30. "theme-light-custom": "./node_modules/matrix-react-sdk/res/themes/light-custom/css/light-custom.pcss",
  31. "theme-dark-custom": "./node_modules/matrix-react-sdk/res/themes/dark-custom/css/dark-custom.pcss",
  32. };
  33. function getActiveThemes() {
  34. // Default to `light` theme when the MATRIX_THEMES environment variable is not defined.
  35. const theme = process.env.MATRIX_THEMES ?? "light";
  36. return theme
  37. .split(",")
  38. .map((x) => x.trim())
  39. .filter(Boolean);
  40. }
  41. // See docs/customisations.md
  42. let fileOverrides = {
  43. /* {[file: string]: string} */
  44. };
  45. try {
  46. fileOverrides = require("./customisations.json");
  47. // stringify the output so it appears in logs correctly, as large files can sometimes get
  48. // represented as `<Object>` which is less than helpful.
  49. console.log("Using customisations.json : " + JSON.stringify(fileOverrides, null, 4));
  50. process.on("exit", () => {
  51. console.log(""); // blank line
  52. console.warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
  53. console.warn("!! Customisations have been deprecated and will be removed in a future release !!");
  54. console.warn("!! See https://github.com/element-hq/element-web/blob/develop/docs/customisations.md !!");
  55. console.warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
  56. console.log(""); // blank line
  57. });
  58. } catch (e) {
  59. // ignore - not important
  60. }
  61. function parseOverridesToReplacements(overrides) {
  62. return Object.entries(overrides).map(([oldPath, newPath]) => {
  63. return new webpack.NormalModuleReplacementPlugin(
  64. // because the input is effectively defined by the person running the build, we don't
  65. // need to do anything special to protect against regex overrunning, etc.
  66. new RegExp(oldPath.replace(/\//g, "[\\/\\\\]").replace(/\./g, "\\.")),
  67. function (resource) {
  68. resource.request = path.resolve(__dirname, newPath);
  69. resource.createData.resource = path.resolve(__dirname, newPath);
  70. // Starting with Webpack 5 we also need to set the context as otherwise replacing
  71. // files in e.g. matrix-react-sdk with files from element-web will try to resolve
  72. // them within matrix-react-sdk (https://github.com/webpack/webpack/issues/17716)
  73. resource.context = path.dirname(resource.request);
  74. resource.createData.context = path.dirname(resource.createData.resource);
  75. },
  76. );
  77. });
  78. }
  79. const moduleReplacementPlugins = [
  80. ...parseOverridesToReplacements(require("./components.json")),
  81. // Allow customisations to override the default components too
  82. ...parseOverridesToReplacements(fileOverrides),
  83. ];
  84. module.exports = (env, argv) => {
  85. // Establish settings based on the environment and args.
  86. //
  87. // argv.mode is always set to "production" by yarn build
  88. // (called to build prod, nightly and develop.element.io)
  89. // arg.mode is set to "development" by yarn start
  90. // (called by developers, runs the continuous reload script)
  91. // process.env.CI_PACKAGE is set when yarn build is called from scripts/ci_package.sh
  92. // (called to build nightly and develop.element.io)
  93. const nodeEnv = argv.mode;
  94. const devMode = nodeEnv !== "production";
  95. const useHMR = process.env.CSS_HOT_RELOAD === "1" && devMode;
  96. const enableMinification = !devMode && !process.env.CI_PACKAGE;
  97. const development = {};
  98. if (devMode) {
  99. // Embedded source maps for dev builds, can't use eval-source-map due to CSP
  100. development["devtool"] = "inline-source-map";
  101. } else {
  102. if (process.env.CI_PACKAGE) {
  103. // High quality source maps in separate .map files which include the source. This doesn't bulk up the .js
  104. // payload file size, which is nice for performance but also necessary to get the bundle to a small enough
  105. // size that sentry will accept the upload.
  106. development["devtool"] = "source-map";
  107. } else {
  108. // High quality source maps in separate .map files which don't include the source
  109. development["devtool"] = "nosources-source-map";
  110. }
  111. }
  112. // Resolve the directories for the react-sdk and js-sdk for later use. We resolve these early, so we
  113. // don't have to call them over and over. We also resolve to the package.json instead of the src
  114. // directory, so we don't have to rely on an index.js or similar file existing.
  115. const reactSdkSrcDir = path.resolve(require.resolve("matrix-react-sdk/package.json"), "..", "src");
  116. const jsSdkSrcDir = path.resolve(require.resolve("matrix-js-sdk/package.json"), "..", "src");
  117. const ACTIVE_THEMES = getActiveThemes();
  118. function getThemesImports() {
  119. const imports = ACTIVE_THEMES.map((t) => {
  120. return cssThemes[`theme-${t}`].replace("./node_modules/", ""); // theme import path
  121. });
  122. const s = JSON.stringify(ACTIVE_THEMES);
  123. return `
  124. window.MX_insertedThemeStylesCounter = 0;
  125. window.MX_DEV_ACTIVE_THEMES = (${s});
  126. ${imports.map((i) => `import("${i}")`).join("\n")};
  127. `;
  128. }
  129. return {
  130. ...development,
  131. bail: true,
  132. entry: {
  133. bundle: "./src/vector/index.ts",
  134. mobileguide: "./src/vector/mobile_guide/index.ts",
  135. jitsi: "./src/vector/jitsi/index.ts",
  136. usercontent: "./node_modules/matrix-react-sdk/src/usercontent/index.ts",
  137. ...(useHMR ? {} : cssThemes),
  138. },
  139. optimization: {
  140. // Put all of our CSS into one useful place - this is needed for MiniCssExtractPlugin.
  141. // Previously we used a different extraction plugin that did this magic for us, but
  142. // now we need to consider that the CSS needs to be bundled up together.
  143. splitChunks: {
  144. cacheGroups: {
  145. styles: {
  146. name: "styles",
  147. test: /\.css$/,
  148. enforce: true,
  149. // Do not add `chunks: 'all'` here because you'll break the app entry point.
  150. },
  151. // put the unhomoglyph data in its own file. It contains
  152. // magic characters which mess up line numbers in the
  153. // javascript debugger.
  154. unhomoglyph_data: {
  155. name: "unhomoglyph_data",
  156. test: /unhomoglyph\/data\.json$/,
  157. enforce: true,
  158. chunks: "all",
  159. },
  160. default: {
  161. reuseExistingChunk: true,
  162. },
  163. },
  164. },
  165. // Readable IDs for better debugging
  166. moduleIds: "named",
  167. // Minification is normally enabled by default for webpack in production mode, but
  168. // we use a CSS optimizer too and need to manage it ourselves.
  169. minimize: enableMinification,
  170. minimizer: enableMinification
  171. ? [
  172. new TerserPlugin({
  173. // Already minified and includes an auto-generated license comment
  174. // that the plugin would otherwise pointlessly extract into a separate
  175. // file. We add the actual license using CopyWebpackPlugin below.
  176. exclude: "jitsi_external_api.min.js",
  177. }),
  178. new CssMinimizerPlugin(),
  179. ]
  180. : [],
  181. // Set the value of `process.env.NODE_ENV` for libraries like React
  182. // See also https://v4.webpack.js.org/configuration/optimization/#optimizationnodeenv
  183. nodeEnv,
  184. },
  185. resolve: {
  186. // We define an alternative import path so we can safely use src/ across the react-sdk
  187. // and js-sdk. We already import from src/ where possible to ensure our source maps are
  188. // extremely accurate (and because we're capable of compiling the layers manually rather
  189. // than relying on partially-mangled output from babel), though we do need to fix the
  190. // package level import (stuff like `import {Thing} from "matrix-js-sdk"` for example).
  191. // We can't use the aliasing down below to point at src/ because that'll fail to resolve
  192. // the package.json for the dependency. Instead, we rely on the package.json of each
  193. // layer to have our custom alternate fields to load things in the right order. These are
  194. // the defaults of webpack prepended with `matrix_src_`.
  195. mainFields: ["matrix_src_browser", "matrix_src_main", "browser", "main"],
  196. aliasFields: ["matrix_src_browser", "browser"],
  197. // We need to specify that TS can be resolved without an extension
  198. extensions: [".js", ".json", ".ts", ".tsx"],
  199. alias: {
  200. // alias any requires to the react module to the one in our path,
  201. // otherwise we tend to get the react source included twice when
  202. // using `npm link` / `yarn link`.
  203. "react": path.resolve(__dirname, "node_modules/react"),
  204. "react-dom": path.resolve(__dirname, "node_modules/react-dom"),
  205. // Same goes for js/react-sdk - we don't need two copies.
  206. "matrix-js-sdk": path.resolve(__dirname, "node_modules/matrix-js-sdk"),
  207. "matrix-react-sdk": path.resolve(__dirname, "node_modules/matrix-react-sdk"),
  208. "@matrix-org/react-sdk-module-api": path.resolve(
  209. __dirname,
  210. "node_modules/@matrix-org/react-sdk-module-api",
  211. ),
  212. // and matrix-events-sdk & matrix-widget-api
  213. "matrix-events-sdk": path.resolve(__dirname, "node_modules/matrix-events-sdk"),
  214. "matrix-widget-api": path.resolve(__dirname, "node_modules/matrix-widget-api"),
  215. // Define a variable so the i18n stuff can load
  216. "$webapp": path.resolve(__dirname, "webapp"),
  217. },
  218. fallback: {
  219. // Mock out the NodeFS module: The opus decoder imports this wrongly.
  220. "fs": false,
  221. "net": false,
  222. "tls": false,
  223. "crypto": false,
  224. // Polyfill needed by counterpart
  225. "util": require.resolve("util/"),
  226. // Polyfill needed by matrix-js-sdk/src/crypto
  227. "buffer": require.resolve("buffer/"),
  228. // Polyfill needed by sentry
  229. "process/browser": require.resolve("process/browser"),
  230. },
  231. },
  232. module: {
  233. noParse: [
  234. // for cross platform compatibility use [\\\/] as the path separator
  235. // this ensures that the regex trips on both Windows and *nix
  236. // don't parse the languages within highlight.js. They cause stack
  237. // overflows (https://github.com/webpack/webpack/issues/1721), and
  238. // there is no need for webpack to parse them - they can just be
  239. // included as-is.
  240. /highlight\.js[\\/]lib[\\/]languages/,
  241. // olm takes ages for webpack to process, and it's already heavily
  242. // optimised, so there is little to gain by us uglifying it.
  243. /olm[\\/](javascript[\\/])?olm\.js$/,
  244. ],
  245. rules: [
  246. useHMR && {
  247. test: /devcss\.ts$/,
  248. loader: "string-replace-loader",
  249. options: {
  250. search: '"use theming";',
  251. replace: getThemesImports(),
  252. },
  253. },
  254. {
  255. test: /\.(ts|js)x?$/,
  256. include: (f) => {
  257. // our own source needs babel-ing
  258. if (f.startsWith(path.resolve(__dirname, "src"))) return true;
  259. // we use the original source files of react-sdk and js-sdk, so we need to
  260. // run them through babel. Because the path tested is the resolved, absolute
  261. // path, these could be anywhere thanks to yarn link. We must also not
  262. // include node modules inside these modules, so we add 'src'.
  263. if (f.startsWith(reactSdkSrcDir)) return true;
  264. if (f.startsWith(jsSdkSrcDir)) return true;
  265. // Some of the syntax in this package is not understood by
  266. // either webpack or our babel setup.
  267. // When we do get to upgrade our current setup, this should
  268. // probably be removed.
  269. if (f.includes(path.join("@vector-im", "compound-web"))) return true;
  270. // but we can't run all of our dependencies through babel (many of them still
  271. // use module.exports which breaks if babel injects an 'include' for its
  272. // polyfills: probably fixable but babeling all our dependencies is probably
  273. // not necessary anyway). So, for anything else, don't babel.
  274. return false;
  275. },
  276. loader: "babel-loader",
  277. options: {
  278. cacheDirectory: true,
  279. plugins: enableMinification ? ["babel-plugin-jsx-remove-data-test-id"] : [],
  280. },
  281. },
  282. {
  283. test: /\.css$/,
  284. use: [
  285. MiniCssExtractPlugin.loader,
  286. {
  287. loader: "css-loader",
  288. options: {
  289. importLoaders: 1,
  290. sourceMap: true,
  291. esModule: false,
  292. },
  293. },
  294. {
  295. loader: "postcss-loader",
  296. ident: "postcss",
  297. options: {
  298. sourceMap: true,
  299. postcssOptions: () => ({
  300. "plugins": [
  301. // Note that we use significantly fewer plugins on the plain
  302. // CSS parser. If we start to parse plain CSS, we end with all
  303. // kinds of nasty problems (like stylesheets not loading).
  304. //
  305. // You might have noticed that we're also sending regular CSS
  306. // through PostCSS. This looks weird, and in fact is probably
  307. // not what you'd expect, however in order for our CSS build
  308. // to work nicely we have to do this. Because down the line
  309. // our SCSS stylesheets reference plain CSS we have to load
  310. // the plain CSS through PostCSS so it can find it safely. This
  311. // also acts like a babel-for-css by transpiling our (S)CSS
  312. // down/up to the right browser support (prefixes, etc).
  313. // Further, if we don't do this then PostCSS assumes that our
  314. // plain CSS is SCSS and it really doesn't like that, even
  315. // though plain CSS should be compatible. The chunking options
  316. // at the top of this webpack config help group the SCSS and
  317. // plain CSS together for the bundler.
  318. require("postcss-simple-vars")(),
  319. require("postcss-hexrgba")(),
  320. // It's important that this plugin is last otherwise we end
  321. // up with broken CSS.
  322. require("postcss-preset-env")({ stage: 3, browsers: "last 2 versions" }),
  323. ],
  324. "parser": "postcss-scss",
  325. "local-plugins": true,
  326. }),
  327. },
  328. },
  329. ],
  330. },
  331. {
  332. test: /\.pcss$/,
  333. use: [
  334. /**
  335. * This code is hopeful that no .pcss outside of our themes will be directly imported in any
  336. * of the JS/TS files.
  337. * Should be MUCH better with webpack 5, but we're stuck to this solution for now.
  338. */
  339. useHMR
  340. ? {
  341. loader: "style-loader",
  342. /**
  343. * If we refactor the `theme.js` in `matrix-react-sdk` a little bit,
  344. * we could try using `lazyStyleTag` here to add and remove styles on demand,
  345. * that would nicely resolve issues of race conditions for themes,
  346. * at least for development purposes.
  347. */
  348. options: {
  349. insert: function insertBeforeAt(element) {
  350. const parent = document.querySelector("head");
  351. // We're in iframe
  352. if (!window.MX_DEV_ACTIVE_THEMES) {
  353. parent.appendChild(element);
  354. return;
  355. }
  356. // Properly disable all other instances of themes
  357. element.disabled = true;
  358. element.onload = () => {
  359. element.disabled = true;
  360. };
  361. const theme =
  362. window.MX_DEV_ACTIVE_THEMES[window.MX_insertedThemeStylesCounter];
  363. element.setAttribute("data-mx-theme", theme);
  364. window.MX_insertedThemeStylesCounter++;
  365. parent.appendChild(element);
  366. },
  367. },
  368. }
  369. : MiniCssExtractPlugin.loader,
  370. {
  371. loader: "css-loader",
  372. options: {
  373. importLoaders: 1,
  374. sourceMap: true,
  375. esModule: false,
  376. },
  377. },
  378. {
  379. loader: "postcss-loader",
  380. ident: "postcss",
  381. options: {
  382. sourceMap: true,
  383. postcssOptions: () => ({
  384. "plugins": [
  385. // Note that we use slightly different plugins for PostCSS.
  386. require("postcss-import")(),
  387. require("postcss-mixins")(),
  388. require("postcss-simple-vars")(),
  389. require("postcss-nested")(),
  390. require("postcss-easings")(),
  391. require("postcss-hexrgba")(),
  392. // It's important that this plugin is last otherwise we end
  393. // up with broken CSS.
  394. require("postcss-preset-env")({ stage: 3, browsers: "last 2 versions" }),
  395. ],
  396. "parser": "postcss-scss",
  397. "local-plugins": true,
  398. }),
  399. },
  400. },
  401. ],
  402. },
  403. {
  404. // the olm library wants to load its own wasm, rather than have webpack do it.
  405. // We therefore use the `file-loader` to tell webpack to dump the contents to
  406. // a separate file and return the name, and override the default `type` for `.wasm` files
  407. // (which is `webassembly/experimental` under webpack 4) to stop webpack trying to interpret
  408. // the filename as webassembly. (see also https://github.com/webpack/webpack/issues/6725)
  409. test: /olm\.wasm$/,
  410. loader: "file-loader",
  411. type: "javascript/auto",
  412. options: {
  413. name: "[name].[hash:7].[ext]",
  414. outputPath: ".",
  415. },
  416. },
  417. {
  418. // Fix up the name of the opus-recorder worker (react-sdk dependency).
  419. // We more or less just want it to be clear it's for opus and not something else.
  420. test: /encoderWorker\.min\.js$/,
  421. loader: "file-loader",
  422. type: "javascript/auto",
  423. options: {
  424. // We deliberately override the name so it makes sense in debugging
  425. name: "opus-encoderWorker.min.[hash:7].[ext]",
  426. outputPath: ".",
  427. },
  428. },
  429. {
  430. // Ideally we should use the built-in worklet support in Webpack 5 with the syntax
  431. // described in https://github.com/webpack/webpack.js.org/issues/6869. However, this
  432. // doesn't currently appear to work with our public path setup. So we handle this
  433. // with a custom loader instead.
  434. test: /RecorderWorklet\.ts$/,
  435. type: "javascript/auto",
  436. use: [
  437. {
  438. loader: path.resolve("./recorder-worklet-loader.js"),
  439. },
  440. {
  441. loader: "babel-loader",
  442. },
  443. ],
  444. },
  445. {
  446. // This is from the same place as the encoderWorker above, but only needed
  447. // for Safari support.
  448. test: /decoderWorker\.min\.js$/,
  449. loader: "file-loader",
  450. type: "javascript/auto", // https://github.com/webpack/webpack/issues/6725
  451. options: {
  452. // We deliberately override the name so it makes sense in debugging
  453. name: "opus-decoderWorker.min.[hash:7].[ext]",
  454. outputPath: ".",
  455. },
  456. },
  457. {
  458. // Same deal as olm.wasm: the decoderWorker wants to load the wasm artifact
  459. // itself.
  460. test: /decoderWorker\.min\.wasm$/,
  461. loader: "file-loader",
  462. type: "javascript/auto",
  463. options: {
  464. // We deliberately don't change the name because the decoderWorker has this
  465. // hardcoded. This is here to avoid the default wasm rule from adding a hash.
  466. name: "decoderWorker.min.wasm",
  467. outputPath: ".",
  468. },
  469. },
  470. {
  471. // This is from the same place as the encoderWorker above, but only needed
  472. // for Safari support.
  473. test: /waveWorker\.min\.js$/,
  474. loader: "file-loader",
  475. type: "javascript/auto", // https://github.com/webpack/webpack/issues/6725
  476. options: {
  477. // We deliberately override the name so it makes sense in debugging
  478. name: "wave-encoderWorker.min.[hash:7].[ext]",
  479. outputPath: ".",
  480. },
  481. },
  482. {
  483. // cache-bust languages.json file placed in
  484. // element-web/webapp/i18n during build by copy-res.ts
  485. test: /\.*languages.json$/,
  486. type: "javascript/auto",
  487. loader: "file-loader",
  488. options: {
  489. name: "i18n/[name].[hash:7].[ext]",
  490. },
  491. },
  492. {
  493. test: /\.svg$/,
  494. issuer: /\.(js|ts|jsx|tsx|html)$/,
  495. use: [
  496. {
  497. loader: "@svgr/webpack",
  498. options: {
  499. namedExport: "Icon",
  500. svgProps: {
  501. "role": "presentation",
  502. "aria-hidden": true,
  503. },
  504. // props set on the svg will override defaults
  505. expandProps: "end",
  506. svgoConfig: {
  507. plugins: [
  508. {
  509. name: "preset-default",
  510. params: {
  511. overrides: {
  512. removeViewBox: false,
  513. },
  514. },
  515. },
  516. // generates a viewbox if missing
  517. { name: "removeDimensions" },
  518. // https://github.com/facebook/docusaurus/issues/8297
  519. { name: "prefixIds" },
  520. ],
  521. },
  522. /**
  523. * Forwards the React ref to the root SVG element
  524. * Useful when using things like `asChild` in
  525. * radix-ui
  526. */
  527. ref: true,
  528. esModule: false,
  529. name: "[name].[hash:7].[ext]",
  530. outputPath: getAssetOutputPath,
  531. publicPath: function (url, resourcePath) {
  532. const outputPath = getAssetOutputPath(url, resourcePath);
  533. return toPublicPath(outputPath);
  534. },
  535. },
  536. },
  537. {
  538. loader: "file-loader",
  539. options: {
  540. esModule: false,
  541. name: "[name].[hash:7].[ext]",
  542. outputPath: getAssetOutputPath,
  543. publicPath: function (url, resourcePath) {
  544. const outputPath = getAssetOutputPath(url, resourcePath);
  545. return toPublicPath(outputPath);
  546. },
  547. },
  548. },
  549. ],
  550. },
  551. {
  552. test: /\.svg$/,
  553. issuer: /\.(pcss|scss|css)$/,
  554. use: [
  555. {
  556. loader: "file-loader",
  557. options: {
  558. esModule: false,
  559. name: "[name].[hash:7].[ext]",
  560. outputPath: getAssetOutputPath,
  561. publicPath: function (url, resourcePath) {
  562. // CSS image usages end up in the `bundles/[hash]` output
  563. // directory, so we adjust the final path to navigate up
  564. // twice.
  565. const outputPath = getAssetOutputPath(url, resourcePath);
  566. return toPublicPath(path.join("../..", outputPath));
  567. },
  568. },
  569. },
  570. ],
  571. },
  572. {
  573. test: /\.(gif|png|ttf|woff|woff2|xml|ico)$/,
  574. // Use a content-based hash in the name so that we can set a long cache
  575. // lifetime for assets while still delivering changes quickly.
  576. oneOf: [
  577. {
  578. // Assets referenced in CSS files
  579. issuer: /\.(pcss|scss|css)$/,
  580. loader: "file-loader",
  581. options: {
  582. esModule: false,
  583. name: "[name].[hash:7].[ext]",
  584. outputPath: getAssetOutputPath,
  585. publicPath: function (url, resourcePath) {
  586. // CSS image usages end up in the `bundles/[hash]` output
  587. // directory, so we adjust the final path to navigate up
  588. // twice.
  589. const outputPath = getAssetOutputPath(url, resourcePath);
  590. return toPublicPath(path.join("../..", outputPath));
  591. },
  592. },
  593. },
  594. {
  595. // Assets referenced in HTML and JS files
  596. loader: "file-loader",
  597. options: {
  598. esModule: false,
  599. name: "[name].[hash:7].[ext]",
  600. outputPath: getAssetOutputPath,
  601. publicPath: function (url, resourcePath) {
  602. const outputPath = getAssetOutputPath(url, resourcePath);
  603. return toPublicPath(outputPath);
  604. },
  605. },
  606. },
  607. ],
  608. },
  609. ].filter(Boolean),
  610. },
  611. plugins: [
  612. ...moduleReplacementPlugins,
  613. // This exports our CSS using the splitChunks and loaders above.
  614. new MiniCssExtractPlugin({
  615. filename: useHMR ? "bundles/[name].css" : "bundles/[hash]/[name].css",
  616. chunkFilename: useHMR ? "bundles/[name].css" : "bundles/[hash]/[name].css",
  617. ignoreOrder: false, // Enable to remove warnings about conflicting order
  618. }),
  619. // This is the app's main entry point.
  620. new HtmlWebpackPlugin({
  621. template: "./src/vector/index.html",
  622. // we inject the links ourselves via the template, because
  623. // HtmlWebpackPlugin will screw up our formatting like the names
  624. // of the themes and which chunks we actually care about.
  625. inject: false,
  626. excludeChunks: ["mobileguide", "usercontent", "jitsi"],
  627. minify: false,
  628. templateParameters: {
  629. og_image_url: ogImageUrl,
  630. csp_extra_source: process.env.CSP_EXTRA_SOURCE ?? "",
  631. },
  632. }),
  633. // This is the jitsi widget wrapper (embedded, so isolated stack)
  634. new HtmlWebpackPlugin({
  635. template: "./src/vector/jitsi/index.html",
  636. filename: "jitsi.html",
  637. minify: false,
  638. chunks: ["jitsi"],
  639. }),
  640. // This is the mobile guide's entry point (separate for faster mobile loading)
  641. new HtmlWebpackPlugin({
  642. template: "./src/vector/mobile_guide/index.html",
  643. filename: "mobile_guide/index.html",
  644. minify: false,
  645. chunks: ["mobileguide"],
  646. }),
  647. // These are the static error pages for when the javascript env is *really unsupported*
  648. new HtmlWebpackPlugin({
  649. template: "./src/vector/static/unable-to-load.html",
  650. filename: "static/unable-to-load.html",
  651. minify: false,
  652. chunks: [],
  653. }),
  654. new HtmlWebpackPlugin({
  655. template: "./src/vector/static/incompatible-browser.html",
  656. filename: "static/incompatible-browser.html",
  657. minify: false,
  658. chunks: [],
  659. }),
  660. // This is the usercontent sandbox's entry point (separate for iframing)
  661. new HtmlWebpackPlugin({
  662. template: "./node_modules/matrix-react-sdk/src/usercontent/index.html",
  663. filename: "usercontent/index.html",
  664. minify: false,
  665. chunks: ["usercontent"],
  666. }),
  667. new HtmlWebpackInjectPreload({
  668. files: [{ match: /.*Inter.*\.woff2$/ }],
  669. }),
  670. // Upload to sentry if sentry env is present
  671. // This plugin throws an error on import on some platforms like ppc64le & s390x even if the plugin isn't called,
  672. // so we require it conditionally.
  673. process.env.SENTRY_DSN &&
  674. require("@sentry/webpack-plugin").sentryWebpackPlugin({
  675. release: process.env.VERSION,
  676. sourcemaps: {
  677. paths: "./webapp/bundles/**",
  678. },
  679. errorHandler: (err, invokeErr, compilation) => {
  680. compilation.warnings.push("Sentry CLI Plugin: " + err.message);
  681. console.log(`::warning title=Sentry error::${err.message}`);
  682. },
  683. }),
  684. new webpack.EnvironmentPlugin(["VERSION"]),
  685. new CopyWebpackPlugin({
  686. patterns: [
  687. "res/apple-app-site-association",
  688. "res/jitsi_external_api.min.js",
  689. "res/jitsi_external_api.min.js.LICENSE.txt",
  690. "res/manifest.json",
  691. "res/sw.js",
  692. "res/welcome.html",
  693. { from: "welcome/**", context: path.resolve(__dirname, "res") },
  694. { from: "themes/**", context: path.resolve(__dirname, "res") },
  695. { from: "vector-icons/**", context: path.resolve(__dirname, "res") },
  696. { from: "decoder-ring/**", context: path.resolve(__dirname, "res") },
  697. { from: "media/**", context: path.resolve(__dirname, "node_modules/matrix-react-sdk/res/") },
  698. "node_modules/@matrix-org/olm/olm_legacy.js",
  699. { from: "config.json", noErrorOnMissing: true },
  700. "contribute.json",
  701. ],
  702. }),
  703. // Automatically load buffer & process modules as we use them without explicitly
  704. // importing them
  705. new webpack.ProvidePlugin({
  706. Buffer: ["buffer", "Buffer"],
  707. process: "process/browser",
  708. }),
  709. ].filter(Boolean),
  710. output: {
  711. path: path.join(__dirname, "webapp"),
  712. // The generated JS (and CSS, from the extraction plugin) are put in a
  713. // unique subdirectory for the build. There will only be one such
  714. // 'bundle' directory in the generated tarball; however, hosting
  715. // servers can collect 'bundles' from multiple versions into one
  716. // directory and symlink it into place - this allows users who loaded
  717. // an older version of the application to continue to access webpack
  718. // chunks even after the app is redeployed.
  719. filename: "bundles/[hash]/[name].js",
  720. chunkFilename: "bundles/[hash]/[name].js",
  721. webassemblyModuleFilename: "bundles/[hash]/[modulehash].wasm",
  722. },
  723. // configuration for the webpack-dev-server
  724. devServer: {
  725. client: {
  726. overlay: {
  727. // Only show overlay on build errors as anything more can get annoying quickly
  728. errors: true,
  729. warnings: false,
  730. runtimeErrors: false,
  731. },
  732. },
  733. static: {
  734. // Where to serve static assets from
  735. directory: "./webapp",
  736. },
  737. devMiddleware: {
  738. // Only output errors, warnings, or new compilations.
  739. // This hides the massive list of modules.
  740. stats: "minimal",
  741. },
  742. // Enable Hot Module Replacement without page refresh as a fallback in
  743. // case of build failures
  744. hot: "only",
  745. // Disable host check
  746. allowedHosts: "all",
  747. },
  748. };
  749. };
  750. /**
  751. * Merge assets found via CSS and imports into a single tree, while also preserving
  752. * directories under e.g. `res` or similar.
  753. *
  754. * @param {string} url The adjusted name of the file, such as `warning.1234567.svg`.
  755. * @param {string} resourcePath The absolute path to the source file with unmodified name.
  756. * @return {string} The returned paths will look like `img/warning.1234567.svg`.
  757. */
  758. function getAssetOutputPath(url, resourcePath) {
  759. const isKaTeX = resourcePath.includes("KaTeX");
  760. // `res` is the parent dir for our own assets in various layers
  761. // `dist` is the parent dir for KaTeX assets
  762. const prefix = /^.*[/\\](dist|res)[/\\]/;
  763. /**
  764. * Only needed for https://github.com/element-hq/element-web/pull/15939
  765. * If keeping this, we are not able to load external assets such as SVG
  766. * images coming from @vector-im/compound-web.
  767. */
  768. if (isKaTeX && !resourcePath.match(prefix)) {
  769. throw new Error(`Unexpected asset path: ${resourcePath}`);
  770. }
  771. let outputDir = path.dirname(resourcePath).replace(prefix, "");
  772. /**
  773. * Imports from Compound are "absolute", we need to strip out the prefix
  774. * coming before the npm package name.
  775. *
  776. * This logic is scoped to compound packages for now as they are the only
  777. * package that imports external assets. This might need to be made more
  778. * generic in the future
  779. */
  780. const compoundImportsPrefix = /@vector-im(?:\\|\/)compound-(.*?)(?:\\|\/)/;
  781. const compoundMatch = outputDir.match(compoundImportsPrefix);
  782. if (compoundMatch) {
  783. outputDir = outputDir.substring(compoundMatch.index + compoundMatch[0].length);
  784. }
  785. if (isKaTeX) {
  786. // Add a clearly named directory segment, rather than leaving the KaTeX
  787. // assets loose in each asset type directory.
  788. outputDir = path.join(outputDir, "KaTeX");
  789. }
  790. return path.join(outputDir, path.basename(url));
  791. }
  792. /**
  793. * Convert path to public path format, which always uses forward slashes, since it will
  794. * be placed directly into things like CSS files.
  795. *
  796. * @param {string} path Some path to a file.
  797. * @returns {string} converted path
  798. */
  799. function toPublicPath(path) {
  800. return path.replace(/\\/g, "/");
  801. }