webpack.conf.js 6.2 KB

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