webpack.conf.js 6.2 KB

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