/// import path from "node:path"; import * as url from "node:url"; import type {ZopfliOptions} from "@gfx/zopfli"; import {gzip} from "@gfx/zopfli"; import CompressionPlugin from "compression-webpack-plugin"; import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import webpack from "webpack"; import BundleTracker from "webpack-bundle-tracker"; import DebugRequirePlugin from "./debug-require-webpack-plugin.ts"; import assets from "./webpack.assets.json" with {type: "json"}; import dev_assets from "./webpack.dev-assets.json" with {type: "json"}; const config = ( env: {minimize?: true; puppeteer_tests?: true; ZULIP_VERSION?: string} = {}, argv: {mode?: string}, ): webpack.Configuration[] => { const production: boolean = argv.mode === "production"; const baseConfig: webpack.Configuration = { mode: production ? "production" : "development", context: import.meta.dirname, cache: { type: "filesystem", buildDependencies: { config: [import.meta.filename], }, }, }; const plugins: webpack.WebpackPluginInstance[] = [ new webpack.DefinePlugin({ DEVELOPMENT: JSON.stringify(!production), ZULIP_VERSION: JSON.stringify(env.ZULIP_VERSION ?? "development"), }), new DebugRequirePlugin(), new BundleTracker({ path: path.join(import.meta.dirname, production ? ".." : "../var"), filename: production ? "webpack-stats-production.json" : "webpack-stats-dev.json", }), // Extract CSS from files new MiniCssExtractPlugin({ filename: production ? "[name].[contenthash].css" : "[name].css", chunkFilename: production ? "[contenthash].css" : "[id].css", }), new HtmlWebpackPlugin({ filename: "5xx.html", template: "html/5xx.html", chunks: ["error-styles"], publicPath: production ? "/static/webpack-bundles/" : "/webpack/", }), ]; if (production && !env.puppeteer_tests) { plugins.push( new CompressionPlugin({ // Use zopfli to write pre-compressed versions of text files test: /\.(js|css|html)$/, algorithm: gzip, }), ); } const frontendConfig: webpack.Configuration = { ...baseConfig, name: "frontend", entry: production ? assets : Object.fromEntries( Object.entries({...assets, ...dev_assets}).map(([name, paths]) => [ name, [...paths, "./src/debug.ts"], ]), ), module: { rules: [ { test: path.resolve(import.meta.dirname, "src/zulip_test.ts"), loader: "expose-loader", options: {exposes: "zulip_test"}, }, { test: path.resolve(import.meta.dirname, "debug-require.cjs"), loader: "expose-loader", options: {exposes: "require"}, }, { test: url.fileURLToPath(import.meta.resolve("jquery")), loader: "expose-loader", options: {exposes: ["$", "jQuery"]}, }, // Generate webfont { test: /\.font\.cjs$/, use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { url: false, // webfonts-loader generates public relative URLs }, }, { loader: "webfonts-loader", options: { fileName: production ? "files/[fontname].[chunkhash].[ext]" : "files/[fontname].[ext]", publicPath: "", }, }, ], type: "javascript/auto", }, // Transpile .js and .ts files with Babel { test: /\.[cm]?[jt]s$/, include: [ path.resolve(import.meta.dirname, "shared/src"), path.resolve(import.meta.dirname, "src"), ], loader: "babel-loader", }, // regular css files { test: /\.css$/, exclude: path.resolve(import.meta.dirname, "styles"), use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { sourceMap: true, }, }, ], }, // PostCSS loader { test: /\.css$/, include: path.resolve(import.meta.dirname, "styles"), use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { importLoaders: 1, sourceMap: true, }, }, { loader: "postcss-loader", options: { sourceMap: true, }, }, ], }, { test: /\.hbs$/, loader: "handlebars-loader", options: { ignoreHelpers: true, // Tell webpack not to explicitly require these. knownHelpers: [ // The ones below are defined in web/src/templates.js "eq", "and", "or", "not", "t", "tr", "rendered_markdown", "numberFormat", "tooltip_hotkey_hints", "popover_hotkey_hints", ], precompileOptions: { knownHelpersOnly: true, strict: true, explicitPartialContext: true, }, preventIndent: true, // This replaces relative image resources with // a computed require() path to them, so their // webpack-hashed URLs are used. inlineRequires: /^(\.\.\/)+(images|static)\//, }, }, // load fonts and files { test: /\.(eot|jpg|svg|ttf|otf|png|webp|woff2?)$/, type: "asset/resource", }, ], }, output: { path: path.resolve(import.meta.dirname, "../static/webpack-bundles"), publicPath: "auto", filename: production ? "[name].[contenthash].js" : "[name].js", assetModuleFilename: production ? "files/[name].[hash][ext][query]" : // Avoid directory traversal bug that upstream won't fix // (https://github.com/webpack/webpack/issues/11937) (pathData) => "files" + path.join("/", pathData.filename!), chunkFilename: production ? "[contenthash].js" : "[id].js", crossOriginLoading: "anonymous", }, // We prefer cheap-module-source-map over any eval-* options // because stacktrace-gps doesn't currently support extracting // the source snippets with the eval-* options. devtool: production ? "source-map" : "cheap-module-source-map", optimization: { minimize: env.minimize ?? production, minimizer: [ new CssMinimizerPlugin({ minify: CssMinimizerPlugin.cleanCssMinify, }), "...", ], splitChunks: { chunks: "all", // webpack/examples/many-pages suggests 20 requests for HTTP/2 maxAsyncRequests: 20, maxInitialRequests: 20, }, }, plugins, devServer: { client: { overlay: { runtimeErrors: false, }, }, devMiddleware: { publicPath: "/webpack/", stats: { // We want just errors and a clear, brief notice // whenever webpack compilation has finished. preset: "minimal", assets: false, modules: false, }, }, headers: { "Access-Control-Allow-Origin": "*", "Timing-Allow-Origin": "*", }, }, infrastructureLogging: { level: "warn", }, watchOptions: { ignored: [ "**/node_modules/**", // Prevent Emacs file locks from crashing webpack-dev-server // https://github.com/webpack/webpack-dev-server/issues/2821 "**/.#*", ], }, }; const serverConfig: webpack.Configuration = { ...baseConfig, name: "server", target: "node", entry: { katex_server: "babel-loader!./server/katex_server.ts", "katex-cli": "shebang-loader!katex/cli", }, output: { path: path.resolve(import.meta.dirname, "../static/webpack-bundles"), }, }; return [frontendConfig, serverConfig]; }; export default config;