/* * Copyright (c) 2016-2020 Martin Donath * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ import CopyPlugin from "copy-webpack-plugin" import EventHooksPlugin from "event-hooks-webpack-plugin" import * as fs from "fs" import { minify as minhtml } from "html-minifier" import IgnoreEmitPlugin from "ignore-emit-webpack-plugin" import ImageminPlugin from "imagemin-webpack-plugin" import MiniCssExtractPlugin = require("mini-css-extract-plugin") import * as path from "path" import { toPairs } from "ramda" import glob = require("tiny-glob") import { minify as minjs } from "terser" import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin" import { Configuration } from "webpack" import AssetsManifestPlugin from "webpack-assets-manifest" /* ---------------------------------------------------------------------------- * Helper functions * ------------------------------------------------------------------------- */ /** * Webpack base configuration * * @param args - Command-line arguments * * @returns Webpack configuration */ function config(args: Configuration): Configuration { const assets = {} return { mode: args.mode, /* Loaders */ module: { rules: [ /* TypeScript */ { test: /\.tsx?$/, use: [ { loader: "ts-loader", options: { experimentalWatchApi: true, transpileOnly: true, compilerOptions: { importHelpers: true, module: "esnext", target: "es2015" } } } ], exclude: /\/node_modules\// }, /* SASS stylesheets */ { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { url: false, sourceMap: true } }, { loader: "postcss-loader", options: { postcssOptions: { plugins: [ ["autoprefixer"], ["postcss-inline-svg", { paths: [ path.resolve(__dirname, "node_modules") ], encode: false }], ["postcss-svgo", { plugins: [ { removeDimensions: true }, { removeViewBox: false } ], encode: false }] ] }, sourceMap: true } }, { loader: "sass-loader", options: { implementation: require("sass"), sassOptions: { includePaths: [ "src/assets/stylesheets", "node_modules/modularscale-sass/stylesheets", "node_modules/material-design-color", "node_modules/material-shadows" ] }, sourceMap: true } } ] }, /* Search */ { test: require.resolve("lunr"), loader: "expose-loader", options: { exposes: ["lunr"] } } ] }, /* Module resolver */ resolve: { mainFields: ["es2015", "module", "main"], modules: [ __dirname, path.resolve(__dirname, "node_modules") ], extensions: [".scss", ".ts", ".tsx", ".js", ".json"], plugins: [ new TsconfigPathsPlugin() ] }, /* Plugins */ plugins: [ new IgnoreEmitPlugin(/\/stylesheets\/.*?\.js/), new AssetsManifestPlugin({ output: "assets/manifest.json", assets }) ], /* Source maps */ devtool: args.mode === "production" ? "source-map" : "eval", /* Filter false positives and omit verbosity */ stats: { entrypoints: false, excludeAssets: [ /\.(icons)/, /\/(images|lunr)\//, /\.(html|py|yml)$/ ], warningsFilter: [ /export '.[^']+' was not found in/ ] } } } /* ---------------------------------------------------------------------------- * Configuration * ------------------------------------------------------------------------- */ /** * Webpack configuration * * @param env - Webpack environment arguments * @param args - Command-line arguments * * @returns Webpack configurations */ export default (_env: never, args: Configuration): Configuration[] => { const hash = args.mode === "production" ? ".[chunkhash].min" : "" const base = config(args) return [ /* Application */ { ...base, entry: { "assets/javascripts/bundle": "src/assets/javascripts", "assets/stylesheets/main": "src/assets/stylesheets/main.scss", "assets/stylesheets/palette": "src/assets/stylesheets/palette.scss" }, output: { path: path.resolve(__dirname, "material"), filename: `[name]${hash}.js`, hashDigestLength: 8, libraryTarget: "window" }, /* Plugins */ plugins: [ ...base.plugins || [], /* Stylesheets */ new MiniCssExtractPlugin({ filename: `[name]${hash}.css` }), /* Improve performance by skipping dependencies in watch mode */ ...args.watch ? [] : [ /* FontAwesome icons */ new CopyPlugin({ patterns: [ { to: ".icons/fontawesome", from: "**/*.svg" }, { to: ".icons/fontawesome", from: "../LICENSE.txt" } ].map(pattern => ({ context: "node_modules/@fortawesome/fontawesome-free/svgs", ...pattern })) }), /* Material Design icons */ new CopyPlugin({ patterns: [ { to: ".icons/material", from: "*.svg" }, { to: ".icons/material", from: "../LICENSE" } ].map(pattern => ({ context: "node_modules/@mdi/svg/svg", ...pattern })) }), /* GitHub octicons */ new CopyPlugin({ patterns: [ { to: ".icons/octicons", from: "*.svg" }, { to: ".icons/octicons", from: "../../LICENSE" } ].map(pattern => ({ context: "node_modules/@primer/octicons/build/svg", ...pattern })) }), /* Search stemmers and segmenters */ new CopyPlugin({ patterns: [ { to: "assets/javascripts/lunr", from: "min/*.js" }, { to: "assets/javascripts/lunr/tinyseg.min.js", from: "tinyseg.js", transform: (content: Buffer) => minjs(`${content}`).code! } ].map(pattern => ({ context: "node_modules/lunr-languages", ...pattern })) }), /* Assets and configuration */ new CopyPlugin({ patterns: [ { from: ".icons/*.svg" }, { from: "assets/images/*" }, { from: "**/*.{py,yml}" } ].map(pattern => ({ context: "src", ...pattern })) }) ], /* Template files */ new CopyPlugin({ patterns: [ { from: "**/*.html", transform: (content: Buffer) => { const metadata = require("./package.json") const banner = "{#-\n" + " This file was automatically generated - do not edit\n" + "-#}\n" /* Normalize line feeds and minify HTML */ const html = content.toString().replace(/\r\n/gm, "\n") return banner + minhtml(html, { collapseBooleanAttributes: true, includeAutoGeneratedTags: false, minifyCSS: true, minifyJS: true, removeComments: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true }) /* Remove empty lines without collapsing everything */ .replace(/^\s*[\r\n]/gm, "") /* Write theme version into template */ .replace("$md-name$", metadata.name) .replace("$md-version$", metadata.version) } } ].map(pattern => ({ context: "src", ...pattern })) }), /* Hooks */ new EventHooksPlugin({ afterEmit: async () => { /* Replace asset URLs in templates */ if (args.mode === "production") { const manifest = require("./material/assets/manifest.json") for (const file of [ "material/base.html", "material/overrides/main.html" ]) { const template = toPairs(manifest) .reduce((content, [from, to]) => ( content.replace(new RegExp( `('|")${from}\\1`, "g"), `$1${to}$1` ) ), fs.readFileSync(file, "utf8")) /* Save template with replaced assets */ fs.writeFileSync(file, template, "utf8") } } /* Build search index for bundled icons */ const index = await glob("**/*.svg", { cwd: "material/.icons" }) fs.writeFileSync( "material/overrides/assets/javascripts/icons.json", JSON.stringify(index) ) } }), /* Minify SVGs */ new ImageminPlugin({ svgo: { plugins: [ { removeDimensions: true }, { removeViewBox: false } ] } }) ], /* Optimizations */ optimization: { splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: "assets/javascripts/vendor", chunks: "all" } } } } }, /* Search worker */ { ...base, entry: { "assets/javascripts/worker/search": "src/assets/javascripts/integrations/search/worker/main" }, output: { path: path.resolve(__dirname, "material"), filename: `[name]${hash}.js`, hashDigestLength: 8, libraryTarget: "var" } }, /* Overrides */ { ...base, entry: { "overrides/assets/javascripts/bundle": "src/overrides/assets/javascripts", "overrides/assets/stylesheets/main": "src/overrides/assets/stylesheets/main.scss" }, output: { path: path.resolve(__dirname, "material"), filename: `[name]${hash}.js`, hashDigestLength: 8, libraryTarget: "window" }, /* Plugins */ plugins: [ ...base.plugins || [], /* Stylesheets */ new MiniCssExtractPlugin({ filename: `[name]${hash}.css` }), ], /* Optimizations */ optimization: { splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: "overrides/assets/javascripts/vendor", chunks: "all" } } } } } ] }