webpack.conf.js 5.2 KB

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