const path = require('path'); const fs = require('fs'); const CopyPlugin = require('copy-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const sharp = require('sharp'); const { mergeWith } = require('lodash'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; console.log('Webpack started'); const package = require('./package.json'); const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'; const isProduction = mode === 'production'; const target = process.env.EXT_TARGET; const devtool = isProduction ? undefined : 'inline-source-map'; const isFastBuild = !isProduction && process.env.FAST_BUILD === 'on'; const isBundleAnalyzingEnabled = !isProduction || Boolean(process.env.DEBUG); const targetsList = ['firefox', 'firefox-standalone', 'chromium', 'chrome']; if (targetsList.indexOf(target) === -1) { throw new Error(`Invalid target "${target}" in EXT_TARGET`); } const devPrefix = isProduction ? '' : 'dev/'; const outDir = `build/${devPrefix}${target}`; const outputPath = path.join(__dirname, outDir); // https://lodash.com/docs/4.17.15#mergeWith function mergeCustomizer(objValue, srcValue) { if (Array.isArray(objValue) && Array.isArray(srcValue)) { return objValue.concat(srcValue); } } console.log('WebpackConfig', { mode, target, outputPath, isFastBuild, }); const offscreenDocuments = ['main', 'worker', 'translator']; const pages = ['popup', 'options', 'dictionary', 'history']; module.exports = { mode, devtool, target: 'web', stats: { colors: true, reasons: true, hash: true, version: true, timings: true, chunks: true, chunkModules: true, cached: true, cachedAssets: true, }, resolve: { extensions: ['.js', '.ts', '.tsx'], }, entry: { 'background-script': './src/background-script.ts', contentscript: './src/contentscript.tsx', ...Object.fromEntries( offscreenDocuments.map((name) => [ `offscreen-documents/${name}/${name}`, `./src/offscreen-documents/${name}/${name}.ts`, ]), ), ...Object.fromEntries( pages.map((name) => [ `pages/${name}/${name}`, `./src/pages/${name}/${name}.tsx`, ]), ), }, output: { path: outputPath, }, plugins: [ new MiniCssExtractPlugin({}), ...(isBundleAnalyzingEnabled ? [ new BundleAnalyzerPlugin({ analyzerPort: 8888 + 10 + targetsList.indexOf(target), }), ] : []), new CopyPlugin({ patterns: [ // Manifest { from: `./manifests/manifest.json`, to: path.join(outputPath, 'manifest.json'), transform(content) { const rawManifest = content.toString(); let manifest = JSON.parse(rawManifest); // Patch manifest with data from target manifest const targetManifestPath = path.join( __dirname, 'manifests', target + '.json', ); if ( target && fs.existsSync(targetManifestPath) && fs.lstatSync(targetManifestPath).isFile() ) { const rawTargetManifest = fs .readFileSync(targetManifestPath) .toString(); const targetManifest = JSON.parse(rawTargetManifest); manifest = mergeWith( manifest, targetManifest, mergeCustomizer, ); } // Patch manifest with production overrides const productionOverridesMap = { 'firefox-standalone': { // eslint-disable-next-line camelcase browser_specific_settings: { gecko: { id: '{e3fc2d33-09fc-4fe8-9331-d0a464698035}', }, }, }, }; if (isProduction && target in productionOverridesMap) { const productionOverrides = productionOverridesMap[target]; manifest = mergeWith( manifest, productionOverrides, mergeCustomizer, ); } // Set version manifest.version = package.version; // Stringify with prettifying and convert to buffer const modifiedManifest = JSON.stringify(manifest, null, '\t'); return Buffer.from(modifiedManifest); }, }, // HTML pages ...[ ...offscreenDocuments.map( (name) => `offscreen-documents/${name}/${name}.html`, ), ...pages.map((name) => `pages/${name}/${name}.html`), ].map((file) => ({ from: './src/' + file, to: path.join(outputPath, file), })), // Resources & locales { from: './src/_locales', to: path.join(outputPath, '_locales'), }, // Offline translator { from: 'thirdparty/bergamot/build/*.{js,wasm}', to: path.join(outputPath, 'thirdparty/bergamot/[name][ext]'), }, // Serve static files ...['logo.svg'].map((filename) => ({ from: './src/res/' + filename, to: path.join(outputPath, 'static', filename), })), // Convert svg to png files for use as addon logotypes (chromium is not support svg logotypes) ...['logo.svg'].map((file) => ({ from: './src/res/' + file, to: path.join(outputPath, 'static', file.replace(/\.svg$/, '.png')), transform(content) { return sharp(content).resize(128, 128).toBuffer(); }, })), ], }), ], module: { rules: [ { test: /\.tsx?$/, use: { loader: 'ts-loader', options: { onlyCompileBundledFiles: true, allowTsInNodeModules: true, transpileOnly: isFastBuild, }, }, }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], }, }, }, { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ [ 'postcss-rem-to-pixel', { rootValue: 16, unitPrecision: 5, propList: ['*'], selectorBlackList: [], replace: true, mediaQuery: false, minUnitValue: 0, }, ], ], }, }, }, ], }, { test: /\.ttf$/, loader: 'file-loader', options: { publicPath: '/', }, }, { test: /\.svg$/, use: ['@svgr/webpack'], }, { test: /\.txt$/i, type: 'asset/source', }, ], }, };