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 MonacoWebpackPlugin = require("monaco-editor-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 }), new MonacoWebpackPlugin({ // include only support for the JSON format in the Monaco editor languages: ["json"], }), ].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, the codicon font is added by the Monaco editor filter: (url) => url.includes("./fonts/") || url.includes("codicon.ttf"), }, }, }, { 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, };