1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-27 06:18:27 -04:00
jarv.is/webpack.config.js

243 lines
7.9 KiB
JavaScript

import path from "path";
import { fileURLToPath } from "url";
import webpack from "webpack";
import AssetsManifestPlugin from "webpack-assets-manifest";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
import { LicenseWebpackPlugin as LicensePlugin } from "license-webpack-plugin";
import CopyPlugin from "copy-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
import TerserPlugin from "terser-webpack-plugin";
// PostCSS stuff:
import autoprefixer from "autoprefixer";
import postcssSvgo from "postcss-svgo";
import postcssFocus from "postcss-focus";
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#what-do-i-use-instead-of-__dirname-and-__filename
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default (env, argv) => {
const isProd = argv.mode === "production";
const globalBannerText = `Licensed under MIT. Copyright (c) 2015-${new Date().getFullYear()} Jake Jarvis <https://jarv.is/>.`;
return {
entry: [path.resolve(__dirname, "assets/js/main.js"), path.resolve(__dirname, "assets/sass/main.scss")],
mode: isProd ? "production" : "development",
devtool: isProd ? "source-map" : "inline-source-map",
output: {
filename: isProd ? "js/[name]-[contenthash:6].js" : "js/[name].js",
path: path.resolve(__dirname, "static/assets/"),
publicPath: "/assets/",
clean: true,
environment: {
// https://github.com/babel/babel-loader#top-level-function-iife-is-still-arrow-on-webpack-5
arrowFunction: false,
},
devtoolModuleFilenameTemplate: "webpack:///[resource-path]",
},
resolve: {
alias: {
// https://preactjs.com/guide/v10/getting-started#aliasing-in-webpack
react: "preact/compat",
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
},
},
plugins: [
new MiniCssExtractPlugin({
filename: isProd ? "css/[name]-[contenthash:6].css" : "css/[name].css",
}),
new webpack.BannerPlugin({
banner: `/*! ${globalBannerText} */`,
raw: true,
}),
new LicensePlugin({
outputFilename: "third_party.txt",
perChunkOutput: false,
addBanner: true,
renderBanner: (filename) => `/*!
* ${globalBannerText}
* See here for third-party libraries: https://jarv.is/assets/${filename}
*/`,
additionalModules: [
{
name: "twemoji",
directory: path.resolve(__dirname, "node_modules/twemoji/"),
},
],
licenseFileOverrides: {
twemoji: "LICENSE-GRAPHICS", // we only use the emojis, not the bundled code
},
excludedPackageTest: (packageName) => packageName.startsWith("preact-"),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "assets/images/"),
to: "images/",
globOptions: {
dot: false,
},
},
{
from: path.resolve(__dirname, "node_modules/twemoji-emojis/vendor/svg/"),
to: "emoji/",
globOptions: {
dot: false,
},
},
],
}),
new webpack.EnvironmentPlugin({
// we need to dynamically inject the hcaptcha site key into the contact form
// fall back to the developer test key: https://docs.hcaptcha.com/#test-key-set-publisher-account
HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001",
}),
new AssetsManifestPlugin({
writeToDisk: true, // allow Hugo to access file in dev mode
output: path.resolve(__dirname, "data/manifest.json"),
publicPath: true,
integrity: true,
integrityHashes: ["sha384"],
customize: (entry) => {
// don't add thousands of unneeded twemoji graphics to the manifest
if (entry.key.startsWith("emoji/") || entry.key.endsWith(".map")) return false;
},
}),
new BundleAnalyzerPlugin({
analyzerMode: "disabled", // this is done in gulp via `yarn analyze-bundle` instead
generateStatsFile: true,
statsFilename: path.resolve(__dirname, "webpack_stats.json"),
}),
],
module: {
rules: [
{
test: /\.m?js$/,
enforce: "pre", // source-map-loader needs to come before everything else
use: ["source-map-loader"],
},
{
test: /\.m?js$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
{
test: /\.(sa|sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: true,
},
},
{
loader: "postcss-loader",
options: {
sourceMap: true,
postcssOptions: {
config: false,
plugins: [
autoprefixer(),
postcssSvgo({
encode: true,
}),
postcssFocus(),
],
},
},
},
{
loader: "sass-loader",
options: {
sourceMap: true,
},
},
],
},
{
test: /\.(png|jp(e*)g|svg|gif|ico)$/,
type: "asset/resource",
generator: {
filename: isProd ? "images/[name]-[contenthash:6][ext]" : "images/[name][ext]",
},
},
{
test: /\.(woff(2)?|ttf|otf|eot)$/,
type: "asset/resource",
generator: {
filename: isProd ? "fonts/[name]-[contenthash:6][ext]" : "fonts/[name][ext]",
},
},
],
},
performance: {
// only evaluate JS and CSS file sizes (ignore source maps, images, etc.)
assetFilter: (assetFilename) => /\.js$|\.css$/.test(assetFilename),
maxAssetSize: 990000, // ~99 KiB
},
optimization: {
sideEffects: true,
minimize: isProd,
minimizer: [
new TerserPlugin({
test: /\.js$/,
parallel: true,
terserOptions: {
sourceMap: true,
compress: {
drop_console: true,
passes: 3,
},
format: {
// cut all comments except for the banner declared above via LicensePlugin:
comments: (_astNode, comment) => comment.value.toLowerCase().includes("third-party libraries"),
ascii_only: true, // some symbols get disfigured otherwise
},
mangle: true,
},
extractComments: false,
}),
new CssMinimizerPlugin({
test: /\.css$/,
minify: CssMinimizerPlugin.cleanCssMinify,
minimizerOptions: {
compatibility: "*",
level: 2,
processImport: false,
sourceMap: true,
format: {
breaks: {
afterAtRule: true,
afterBlockBegins: true,
afterBlockEnds: true,
afterComment: true,
afterRuleEnds: true,
beforeBlockEnds: true,
},
spaces: {
beforeBlockBegins: true,
},
semicolonAfterLastProperty: true,
},
},
}),
],
},
devServer: {
static: {
directory: path.resolve(__dirname, "public"),
watch: true,
},
host: "0.0.0.0", // weird docker bind behavior
port: process.env.PORT || 1337,
compress: true,
liveReload: true,
setupExitSignals: false, // prevent dangling server when started via gulp
},
};
};