agama/web/webpack.config.js
David Díaz González 144474cb9e
refactor(web): keep product appearance CSS in src/assets/products
Co-locate a product's appearance overrides with its other assets:
<product-id>.css now lives in src/assets/products (alongside the logos)
instead of a separate assets/appearance directory. Webpack still copies it
to assets/appearance/ in dist, so the runtime loader path is unchanged.
2026-06-09 08:18:35 +01:00

232 lines
7.4 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const Copy = require("copy-webpack-plugin");
const Extract = require("mini-css-extract-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const StylelintPlugin = require("stylelint-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const ReactRefreshTypeScript = require("react-refresh-typescript");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const webpack = require("webpack");
/* A standard nodejs and webpack pattern */
const production = process.env.NODE_ENV === "production";
const development = !production;
/* development options for faster iteration */
const eslint = process.env.ESLINT !== "0";
/* Default to disable csslint for faster production builds */
const stylelint = process.env.STYLELINT ? process.env.STYLELINT !== "0" : development;
// Agama API server. By default it connects to a local development server.
let agamaServer = process.env.AGAMA_SERVER || "localhost";
if (!agamaServer.startsWith("http")) {
agamaServer = "http://" + agamaServer;
}
// Non-JS files which are copied verbatim to dist/
const copy_files = [
"./src/index.html",
// TODO: consider using something more complete like https://github.com/jantimon/favicons-webpack-plugin
"./src/assets/favicon.svg",
"./src/languages.json",
{ from: "./src/assets/products/*.svg", to: "assets/logos/[name][ext]" },
// Per-product appearance overrides, loaded at runtime by product id.
{ from: "./src/assets/products/*.css", to: "assets/appearance/[name][ext]" },
];
const plugins = [
new Copy({ patterns: copy_files }),
new Extract({ filename: "[name].css" }),
development && new ReactRefreshWebpackPlugin({ overlay: false }),
// replace the "process.env.WEBPACK_SERVE" text in the source code by
// the current value of the environment variable, that variable is set to
// "true" when running the development server ("npm run server")
// https://webpack.js.org/plugins/environment-plugin/
new webpack.EnvironmentPlugin({ WEBPACK_SERVE: null, LOCAL_CONNECTION: null }),
new webpack.SourceMapDevToolPlugin({
filename: "[file].map",
// skip the source maps for the translation files, they are twice (!) big as the JS files
// themselves and do not provide any value because there are basically just arrays of texts
exclude: /po-.*\.js$/,
}),
// run the TypeScript type checks in a parallel process (report the problems to the dev server in
// the development mode)
new ForkTsCheckerWebpackPlugin({ devServer: development }),
].filter(Boolean);
if (eslint) {
plugins.push(
new ESLintPlugin({
configType: "flat",
extensions: ["js", "jsx", "ts", "tsx"],
failOnError: production,
failOnWarning: production,
}),
);
}
if (stylelint) {
plugins.push(
new StylelintPlugin({
context: "src/",
}),
);
}
/* Only minimize when in production mode */
if (production) {
plugins.unshift(
new CompressionPlugin({
test: /\.(js|html|css)$/,
deleteOriginalAssets: false,
}),
);
}
module.exports = {
mode: production ? "production" : "development",
resolve: {
modules: ["node_modules", path.resolve(__dirname, "src/lib")],
plugins: [new TsconfigPathsPlugin({ extensions: [".ts", ".tsx", ".js", ".jsx", ".json"] })],
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
},
resolveLoader: {
modules: ["node_modules", path.resolve(__dirname, "src/lib")],
},
watchOptions: {
ignored: /node_modules/,
},
entry: {
index: ["./src/index.tsx"],
},
devServer: {
hot: true,
proxy: [
{
context: ["/api/ws"],
target: agamaServer.replace(/^http/, "ws"),
ws: true,
secure: false,
},
{
context: ["/api"],
target: agamaServer,
secure: false,
},
],
},
// source maps are configured using the SourceMapDevToolPlugin above
devtool: false,
stats: "errors-warnings",
// always regenerate dist/, so make rules work
output: { clean: true, compareBeforeEmit: false },
optimization: {
minimize: production,
minimizer: [
new TerserJSPlugin({
// src/components/core/Page.jsx is using a type?.name.endsWith("PageMenu") for extracting page menus.
// Thus, it's needed not mangling function names ending in PageMenu to keep it working in production
// until adopting a better solution, if any.
terserOptions: {
keep_fnames: /PageMenu$/,
},
extractComments: {
condition: true,
filename: `[file].LICENSE.txt?query=[query]&filebase=[base]`,
banner(licenseFile) {
return `License information can be found in ${licenseFile}`;
},
},
}),
// remove also the spaces between the tags
new HtmlMinimizerPlugin({ minimizerOptions: { conservativeCollapse: false } }),
new CssMinimizerPlugin(),
],
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
plugins: [development && require.resolve("react-refresh/babel")].filter(Boolean),
},
},
{
loader: require.resolve("ts-loader"),
options: {
getCustomTransformers: () => ({
before: [development && ReactRefreshTypeScript.default()].filter(Boolean),
}),
// skip the TypeScript type checks in the TS loader, the types are checked by the
// "fork-ts-checker-webpack-plugin" which runs the checks in parallel in a separate
// process to not block the main webpack build process
transpileOnly: true,
},
},
],
},
{
test: /\.s?css$/,
use: [
Extract.loader,
{
loader: "css-loader",
options: {
sourceMap: true,
url: {
// Only follow the Agama fonts links to be processed by the next rule and place
// them in dist/fonts
filter: (url) => url.includes("./fonts/"),
},
},
},
{
loader: "sass-loader",
options: {
sourceMap: development,
sassOptions: {
includePaths: ["node_modules"],
outputStyle: production ? "compressed" : undefined,
silenceDeprecations: ["global-builtin", "if-function"],
},
},
},
],
},
{
test: /\.(eot|ttf|woff|woff2)$/,
type: "asset/resource",
generator: {
filename: "fonts/[name][ext]",
},
},
// Load SVG files
{
test: /\.svg$/i,
type: "asset",
resourceQuery: { not: [/component/] }, // exclude file import includes ""?component"
},
{
test: /\.svg$/i,
issuer: /\.(j|t)sx?$/,
resourceQuery: /component/, // *.svg?component
use: ["@svgr/webpack"],
},
],
},
plugins: plugins,
};