// SPDX-FileCopyrightText: 2022 Gregor Kleen ,Sarah Vaupel ,Sarah Vaupel // // SPDX-License-Identifier: AGPL-3.0-or-later const webpack = require('webpack'); const path = require('path'); const tmp = require('tmp'); tmp.setGracefulCleanup(); const fs = require('fs-extra'); const glob = require('glob'); const { execSync } = require('child_process'); const request = require('request-promise'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const yaml = require('js-yaml'); const postcssPresetEnv = require('postcss-preset-env'); const RemovePlugin = require('remove-files-webpack-plugin'); const RealFaviconPlugin = require('real-favicon-webpack-plugin'); const crypto = require('crypto'); const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.'); const packageVersion = require('./package.json').version; async function webpackConfig() { const faviconNixDirectory = path.resolve(__dirname, '.nix-well-known'); let faviconApiVersion = undefined; if (!fs.existsSync(faviconNixDirectory)) { try { const faviconApiChangelog = await request({ method: 'GET', uri: 'https://realfavicongenerator.net/api/versions', headers: { 'Accept': '*/*' }, json: true }); faviconApiVersion = faviconApiChangelog.filter(vObj => vObj.relevance.automated_update).slice(-1)[0].version; } catch(e) { console.error(e); } } return { module: { rules: [ { loader: 'babel-loader', options: { plugins: ['syntax-dynamic-import'], presets: [ [ '@babel/preset-env', { modules: false, targets: { edge: "17", firefox: "50", chrome: "60", safari: "11.1", ie: "11", }, useBuiltIns: "usage", corejs: 3 } ] ] }, test: /\.js$/i, exclude: /node_modules/, }, { test: /\.css$/i, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true }}, { loader: 'postcss-loader', options: { sourceMap: true, postcssOptions: { plugins: [ 'postcss-preset-env' ] } }}, { loader: 'resolve-url-loader', options: { sourceMap: true }} ] }, { test: /\.s(c|a)ss$/i, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true }}, { loader: 'postcss-loader', options: { sourceMap: true, postcssOptions: { plugins: [ 'postcss-preset-env' ] } }}, { loader: 'resolve-url-loader', options: { sourceMap: true }}, { loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: true }} ] }, { test: /\.(woff(2)?|ttf|eot|svg)(\?.*)?$/i, type: 'asset' } ] }, entry: { main: [ path.resolve(__dirname, 'frontend/src', 'polyfill.js'), path.resolve(__dirname, 'frontend/src', 'main.js') ] }, plugins: [ new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // all options are optional filename: '[chunkhash].css', chunkFilename: '[chunkhash].css', ignoreOrder: false, // Enable to remove warnings about conflicting order }), new WebpackManifestPlugin({ fileName: path.resolve(__dirname, 'config', 'webpack.yml'), publicPath: `wp-${webpackVersion}`, generate: (seed, files, entrypoints) => Object.keys(entrypoints).reduce((acc, fs) => ({...acc, [fs]: files.filter(file => entrypoints[fs].filter(basename => !(/\.map$/.test(basename))).some(basename => file.path.endsWith(basename))).filter(file => file.isInitial).map(file => file.path)}), {}), serialize: yaml.dump }), new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [ path.resolve(__dirname, 'static'), path.resolve(__dirname, 'well-known'), ] }), new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), new webpack.DefinePlugin({ VERSION: JSON.stringify(packageVersion) }), ...(() => { const faviconJson = require('./config/favicon.json'); const langs = new Set(); function findLangs(json) { if (json && json._i18n) { Object.keys(json).forEach(key => { if (key !== '_i18n') { langs.add(key); } }) } else if (Array.isArray(json)) { json.forEach(elem => findLangs(elem)); } else if (typeof json === 'object') { Object.keys(json).forEach(key => findLangs(json[key])); } } findLangs(faviconJson); function selectLang(lang, json) { if (json && json._i18n) { return json[lang]; } else if (Array.isArray(json)) { return json.map(elem => selectLang(lang, elem)); } else if (typeof json === 'object') { return Object.fromEntries(Object.entries(json).map(([k, v]) => [k, selectLang(lang, v)])); } else { return json; } } const langJsons = {}; Array.from(langs).forEach(lang => { langJsons[lang] = selectLang(lang, faviconJson); }); const cacheHash = crypto.createHash('sha256'); cacheHash.update(JSON.stringify(langJsons)); const cacheFiles = new Set([ ...(Array.from(langs).map(lang => path.resolve(__dirname, langJsons[lang].masterPicture))), path.resolve(__dirname, 'config/robots.txt') ]); for (const cacheFile of cacheFiles) { cacheHash.update(fs.readFileSync(cacheFile)); } const cacheDigest = cacheHash.copy().digest('hex'); let cachedVersion = undefined; const versionFile = path.resolve(__dirname, '.well-known-cache', `${cacheDigest}.version`); try { if (fs.existsSync(versionFile)) { cachedVersion = fs.readFileSync(versionFile, 'utf8'); } } catch (e) { console.error(e); } if (faviconApiVersion) { cacheHash.update(faviconApiVersion); } const versionDigest = cacheHash.digest('hex'); return Array.from(langs).map(lang => { const faviconConfig = { versioning: { param_name: 'v', param_value: versionDigest.substr(0,10) }, ...langJsons[lang] }; const cacheDirectory = path.resolve(__dirname, '.well-known-cache', `${cacheDigest}-${lang}`); if (fs.existsSync(faviconNixDirectory)) { console.log("Using favicons generated by nix"); return [ new CopyPlugin({ patterns: [ { from: path.resolve(faviconNixDirectory, lang), to: path.resolve(__dirname, 'well-known', lang) } ] }) ]; } else if (fs.existsSync(cacheDirectory) && (!faviconApiVersion || faviconApiVersion === cachedVersion)) { console.log(`Using cached well-known from ${cacheDirectory} for ${lang}`); return [ new CopyPlugin({ patterns: [ { from: cacheDirectory, to: path.resolve(__dirname, 'well-known', lang) } ] }) ]; } else { const tmpobj = tmp.fileSync({ postfix: ".json" }); fs.writeSync(tmpobj.fd, JSON.stringify(faviconConfig)); fs.close(tmpobj.fd); return [ new RealFaviconPlugin({ faviconJson: path.relative(".", tmpobj.name), outputPath: path.resolve(__dirname, 'well-known', lang), inject: false }), new CopyPlugin({ patterns: [ { from: 'config/robots.txt', to: path.resolve(__dirname, 'well-known', lang, 'robots.txt') }, ] }), { apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => { const imgFiles = glob.sync(path.resolve(__dirname, 'well-known', lang, '*.@(png)')); const imgFilesArgs = Array.from(imgFiles).join(" "); execSync(`exiftool -overwrite_original -all= ${imgFilesArgs}`, { stdio: 'inherit' }); }) }, { apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => { fs.ensureDirSync(__dirname, '.well-known-cache'); fs.copySync(path.resolve(__dirname, 'well-known', lang), cacheDirectory); if (!fs.existsSync(versionFile)) { fs.writeFileSync(versionFile, faviconApiVersion, { encoding: 'utf8' }); } }) } ]; } }).flat(1); })() ], output: { chunkFilename: '[chunkhash].js', filename: '[chunkhash].js', path: path.resolve(__dirname, 'static', `wp-${webpackVersion}`), publicPath: `/static/res/wp-${webpackVersion}/`, hashFunction: 'shake256', hashDigestLength: 36 }, optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: true, terserOptions: { sourceMap: true } }), new OptimizeCSSAssetsPlugin({ cssProcessorOptions: { map: { inline: false } } }) ], moduleIds: 'named', chunkIds: 'named', runtimeChunk: 'single', realContentHash: false }, mode: 'production', recordsPath: path.join(__dirname, 'records.json'), performance: { assetFilter: (assetFilename) => !(/\.(map|svg|ttf|eot)$/.test(assetFilename)) }, devtool: 'source-map' }; } module.exports = webpackConfig;