webpack.conf.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. const { modifyWebpackConfig, shallowMerge, defaultOptions } = require('@gera2ld/plaid');
  2. const { isProd } = require('@gera2ld/plaid/util');
  3. const fs = require('fs');
  4. const webpack = require('webpack');
  5. const WrapperWebpackPlugin = require('wrapper-webpack-plugin');
  6. const HTMLInlineCSSWebpackPlugin = isProd && require('html-inline-css-webpack-plugin').default;
  7. const TerserPlugin = isProd && require('terser-webpack-plugin');
  8. const deepmerge = isProd && require('deepmerge');
  9. const { ListBackgroundScriptsPlugin } = require('./manifest-helper');
  10. const ProtectWebpackBootstrapPlugin = require('./webpack-protect-bootstrap-plugin');
  11. const projectConfig = require('./plaid.conf');
  12. const mergedConfig = shallowMerge(defaultOptions, projectConfig);
  13. // Avoiding collisions with globals of a content-mode userscript
  14. const INIT_FUNC_NAME = `Violentmonkey:${
  15. Buffer.from(
  16. new Uint32Array(2)
  17. .map(() => Math.random() * (2 ** 32))
  18. .buffer,
  19. ).toString('base64')
  20. }`;
  21. const VAULT_ID = '__VAULT_ID__';
  22. const HANDSHAKE_ID = '__HANDSHAKE_ID__';
  23. // eslint-disable-next-line import/no-dynamic-require
  24. const VM_VER = require(`${defaultOptions.distDir}/manifest.json`).version;
  25. const WEBPACK_OPTS = {
  26. node: {
  27. global: false,
  28. process: false,
  29. setImmediate: false,
  30. },
  31. performance: {
  32. maxEntrypointSize: 1e6,
  33. maxAssetSize: 0.5e6,
  34. },
  35. };
  36. const MIN_OPTS = {
  37. cache: true,
  38. parallel: true,
  39. sourceMap: true,
  40. terserOptions: {
  41. compress: {
  42. // `terser` often inlines big one-time functions inside a small "hot" function
  43. reduce_funcs: false,
  44. reduce_vars: false,
  45. },
  46. output: {
  47. ascii_only: true,
  48. },
  49. },
  50. };
  51. const MIN_OPTS_PUBLIC = isProd && {
  52. chunkFilter: ({ name }) => name.startsWith('public/'),
  53. ...MIN_OPTS,
  54. };
  55. const MIN_OPTS_MAIN = isProd && deepmerge.all([{}, MIN_OPTS, {
  56. chunkFilter: ({ name }) => !name.startsWith('public/'),
  57. terserOptions: {
  58. compress: {
  59. ecma: 8, // ES2017 Object.entries and so on
  60. passes: 2, // necessary now since we removed plaid's minimizer
  61. unsafe_arrows: true, // it's 'safe' since we don't rely on function prototypes
  62. },
  63. },
  64. }]);
  65. const pickEnvs = (items) => {
  66. return Object.assign({}, ...items.map(x => ({
  67. [`process.env.${x.key}`]: JSON.stringify(
  68. 'val' in x ? x.val
  69. : process.env[x.key] ?? x.def,
  70. ),
  71. })));
  72. };
  73. const defsObj = {
  74. ...pickEnvs([
  75. { key: 'DEBUG', def: false },
  76. { key: 'VM_VER', val: VM_VER },
  77. { key: 'SYNC_GOOGLE_CLIENT_ID' },
  78. { key: 'SYNC_GOOGLE_CLIENT_SECRET' },
  79. { key: 'SYNC_ONEDRIVE_CLIENT_ID' },
  80. { key: 'SYNC_ONEDRIVE_CLIENT_SECRET' },
  81. ]),
  82. 'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
  83. 'process.env.VAULT_ID_NAME': JSON.stringify(VAULT_ID),
  84. 'process.env.VAULT_ID': VAULT_ID,
  85. 'process.env.HANDSHAKE_ID': HANDSHAKE_ID,
  86. 'process.env.HANDSHAKE_ACK': '1',
  87. };
  88. const defsRe = new RegExp(`\\b(${Object.keys(defsObj).join('|').replace(/\./g, '\\.')})\\b`, 'g');
  89. const definitions = new webpack.DefinePlugin(defsObj);
  90. // avoid running webpack bootstrap in a potentially hacked environment
  91. // after documentElement was replaced which triggered reinjection of content scripts
  92. const skipReinjectionHeader = `if (window['${INIT_FUNC_NAME}'] !== 1)`;
  93. // {entryName: path}
  94. const entryGlobals = {
  95. common: [
  96. './src/common/safe-globals.js',
  97. ],
  98. 'injected/content': [
  99. './src/injected/safe-globals-injected.js',
  100. './src/injected/content/safe-globals-content.js',
  101. ],
  102. 'injected/web': [
  103. './src/injected/safe-globals-injected.js',
  104. './src/injected/web/safe-globals-web.js',
  105. ],
  106. };
  107. /**
  108. * Adds a watcher for files in entryGlobals to properly recompile the project on changes.
  109. */
  110. const addWrapper = (config, name, callback) => {
  111. config.module.rules.push({
  112. test: new RegExp(`/${name}/.*?\\.js$`.replace(/\//g, /[/\\]/.source)),
  113. use: [{
  114. loader: './scripts/fake-dep-loader.js',
  115. options: { files: entryGlobals[name] },
  116. }],
  117. });
  118. const reader = () => (
  119. entryGlobals[name]
  120. .map(path => fs.readFileSync(path, { encoding: 'utf8' }))
  121. .join('\n')
  122. .replace(/export\s+(?=(const|let)\s)/g, '')
  123. .replace(defsRe, s => defsObj[s])
  124. );
  125. config.plugins.push(new WrapperWebpackPlugin(callback(reader)));
  126. };
  127. const modify = (page, entry, init) => modifyWebpackConfig(
  128. (config) => {
  129. Object.assign(config, WEBPACK_OPTS);
  130. config.plugins.push(definitions);
  131. config.optimization.minimizer.find((m, i, arr) => (
  132. m.constructor.name === 'TerserPlugin' && arr.splice(i, 1)
  133. ));
  134. config.optimization.minimizer.push(...!isProd ? [] : [
  135. new TerserPlugin(MIN_OPTS_PUBLIC),
  136. new TerserPlugin(MIN_OPTS_MAIN),
  137. ]);
  138. if (!entry) init = page;
  139. if (init) init(config);
  140. return config;
  141. }, {
  142. projectConfig: {
  143. ...mergedConfig,
  144. ...entry && { pages: { [page]: { entry } } },
  145. },
  146. },
  147. );
  148. module.exports = Promise.all([
  149. modify((config) => {
  150. addWrapper(config, 'common', getGlobals => ({
  151. header: () => `{ ${getGlobals()}`,
  152. footer: '}',
  153. test: /^(?!injected|public).*\.js$/,
  154. }));
  155. /* Embedding as <style> to ensure uiTheme option doesn't cause FOUC.
  156. * Note that in production build there's no <head> in html but document.head is still
  157. * auto-created per the specification so our styles will be placed correctly anyway. */
  158. if (isProd) {
  159. config.plugins.push(new HTMLInlineCSSWebpackPlugin({
  160. replace: {
  161. target: '<body>',
  162. position: 'before',
  163. },
  164. }));
  165. config.plugins.find(p => (
  166. p.constructor.name === 'MiniCssExtractPlugin'
  167. && Object.assign(p.options, { ignoreOrder: true })
  168. ));
  169. }
  170. config.plugins.push(new ListBackgroundScriptsPlugin({
  171. minify: false, // keeping readable
  172. }));
  173. }),
  174. modify('injected', './src/injected', (config) => {
  175. config.plugins.push(new ProtectWebpackBootstrapPlugin());
  176. addWrapper(config, 'injected/content', getGlobals => ({
  177. header: () => `${skipReinjectionHeader} { ${getGlobals()}`,
  178. footer: '}',
  179. }));
  180. }),
  181. modify('injected-web', './src/injected/web', (config) => {
  182. // TODO: replace WebPack's Object.*, .call(), .apply() with safe calls
  183. config.output.libraryTarget = 'commonjs2';
  184. config.plugins.push(new ProtectWebpackBootstrapPlugin());
  185. addWrapper(config, 'injected/web', getGlobals => ({
  186. header: () => `${skipReinjectionHeader}
  187. window['${INIT_FUNC_NAME}'] = function (IS_FIREFOX,${HANDSHAKE_ID},${VAULT_ID}) {
  188. const module = { __proto__: null };
  189. ${getGlobals()}`,
  190. footer: `
  191. const { exports } = module;
  192. return exports.__esModule ? exports.default : exports;
  193. };0;`,
  194. }));
  195. }),
  196. ]);