// SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel , David Mosbach , Gregor Kleen , Sarah Vaupel , Sarah Vaupel // // SPDX-License-Identifier: AGPL-3.0-or-later import webpack from 'webpack'; import { resolve, join, relative } from 'node:path'; import { execSync } from 'node:child_process'; import tmp from 'tmp'; tmp.setGracefulCleanup(); import fs from 'fs-extra'; import { glob, globSync } from 'glob'; import axios from 'axios'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CopyPlugin from 'copy-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; import yaml from 'js-yaml'; import postcssPresetEnv from 'postcss-preset-env'; import RemovePlugin from 'remove-files-webpack-plugin'; import RealFaviconPlugin from 'real-favicon-webpack-plugin'; import crypto from 'crypto'; // import { version as webpackVersion } from 'webpack/package.json' assert { type: 'json' }; // version.split('.').slice(0, 2).join('.'); // import { version as packageVersion } from './package.json' assert { type: 'json' }; import webpackJson from 'webpack/package.json' assert { type: "json" }; const webpackVersion = webpackJson.version.split('.').slice(0, 2).join('.'); import packageJson from './package.json' assert { type: "json" }; const packageVersion = packageJson.version; import faviconJson from './config/favicon.json' assert { type: 'json' }; async function webpackConfig() { const wellKnownCacheDir = resolve('.cache/well-known'); const assetsDirectory = resolve('assets'); let faviconApiVersion = undefined; if (!fs.existsSync(wellKnownCacheDir)) { try { const faviconApiChangelog = await axios.get('https://realfavicongenerator.net/api/versions'); faviconApiVersion = faviconApiChangelog.data.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: import('sass'), sourceMap: true }} ] }, { test: /\.(woff(2)?|ttf|eot|svg)(\?.*)?$/i, type: 'asset' } ] }, entry: { main: [ resolve('frontend/src/polyfill.js'), resolve('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: resolve('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: [ resolve('static'), resolve('well-known'), ] }), new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), new webpack.DefinePlugin({ VERSION: JSON.stringify(packageVersion) }), ...(() => { 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 => resolve(langJsons[lang].masterPicture))), resolve('config/robots.txt'), ]); for (const cacheFile of cacheFiles) { cacheHash.update(fs.readFileSync(cacheFile)); } const cacheDigest = cacheHash.copy().digest('hex'); let cachedVersion = undefined; const versionFile = resolve('.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 = resolve('.well-known-cache', `${cacheDigest}-${lang}`); if (fs.existsSync(wellKnownCacheDir)) { console.log("Using favicons generated by nix"); return [ new CopyPlugin({ patterns: [ { from: resolve(wellKnownCacheDir, lang), to: resolve('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: resolve('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: relative(".", tmpobj.name), outputPath: resolve('well-known', lang), inject: false }), new CopyPlugin({ patterns: [ { from: 'config/robots.txt', to: resolve('well-known', lang, 'robots.txt') }, ] }), { apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => { const imgFiles = globSync(resolve('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('.well-known-cache'); fs.copySync(resolve('well-known', lang), cacheDirectory); if (!fs.existsSync(versionFile)) { fs.writeFileSync(versionFile, faviconApiVersion, { encoding: 'utf8' }); } }) } ]; } }).flat(1); })() ], output: { chunkFilename: '[chunkhash].js', filename: '[chunkhash].js', path: resolve('static', `wp-${webpackVersion}`), publicPath: `/static/res/wp-${webpackVersion}/`, hashFunction: 'shake256', hashDigestLength: 36 }, optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: true, terserOptions: { sourceMap: true } }), new MiniCssExtractPlugin(), ], moduleIds: 'named', chunkIds: 'named', runtimeChunk: 'single', realContentHash: false }, mode: 'production', recordsPath: join(resolve('records.json')), performance: { assetFilter: (assetFilename) => !(/\.(map|svg|ttf|eot)$/.test(assetFilename)) }, devtool: 'source-map' }; } export default webpackConfig;