├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── demo.txt ├── loaders ├── demo-loader.js └── pitch-loader.js ├── messages ├── en.json └── fi.json ├── package-lock.json ├── package.json ├── plugins ├── demo-plugin.js ├── test-entry.js └── test.js ├── run-loader.js ├── server.js ├── src ├── bootstrap.js ├── component.js ├── header.js ├── i18n.js ├── index.js ├── lazy.js ├── logo.png ├── main.css ├── mf.js ├── multi.js ├── shake.js ├── ssr.js └── worker.js ├── tailwind.config.js ├── tests ├── add.js ├── add.test.js └── index.js ├── webpack.config.js ├── webpack.i18n.js ├── webpack.mf.js ├── webpack.multi.js ├── webpack.parts.js └── webpack.ssr.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "modules": false 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% # Browser usage over 1% 2 | Last 2 versions # Or last two versions 3 | IE 8 # Or IE 8 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # General settings for whole project 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # Format specific overrides 13 | [*.js] 14 | indent_style = space 15 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | i18n-build/ 4 | static/ 5 | records.json 6 | stats.json 7 | 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Juho Vepsäläinen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-demo - Webpack demo for "SurviveJS - Webpack 5" 2 | 3 | See [SurviveJS - Webpack 5](http://survivejs.com/webpack/introduction/) to learn how/why this configuration works. 4 | 5 | ## License 6 | 7 | MIT. 8 | -------------------------------------------------------------------------------- /demo.txt: -------------------------------------------------------------------------------- 1 | {} 2 | 3 | -------------------------------------------------------------------------------- /loaders/demo-loader.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require("loader-utils"); 2 | 3 | module.exports = function (content) { 4 | const { name } = this.getOptions(); 5 | const url = loaderUtils.interpolateName(this, name, { content }); 6 | 7 | this.emitFile(url, content); 8 | 9 | const path = `__webpack_public_path__ + ${JSON.stringify(url)};`; 10 | 11 | return `export default ${path}`; 12 | }; 13 | -------------------------------------------------------------------------------- /loaders/pitch-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (input) { 2 | return input + this.getOptions().text; 3 | }; 4 | module.exports.pitch = function (remaining, preceding, input) { 5 | console.log(`Remaining: ${remaining}, preceding: ${preceding} 6 | Input: ${JSON.stringify(input, null, 2)} 7 | `); 8 | 9 | return "pitched"; 10 | }; 11 | -------------------------------------------------------------------------------- /messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "Hello world" 3 | } 4 | -------------------------------------------------------------------------------- /messages/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "Terve maailma" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-demo", 3 | "version": "3.0.8", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "deploy": "gh-pages -d dist", 8 | "build:i18n": "wp --config webpack.i18n.js", 9 | "build:ssr": "wp --config webpack.ssr.js", 10 | "build:multi": "wp --config webpack.multi.js", 11 | "build:stats": "wp --mode production --json > stats.json", 12 | "build:mf": "wp --config webpack.mf.js --mode production", 13 | "start:mf": "wp --config webpack.mf.js --mode development", 14 | "start": "wp --mode development", 15 | "build": "wp --mode production" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.17.8", 22 | "@babel/preset-env": "^7.16.11", 23 | "@babel/preset-react": "^7.16.7", 24 | "autoprefixer": "^10.4.4", 25 | "babel-loader": "^8.2.3", 26 | "browser-refresh": "^1.7.3", 27 | "css-loader": "^6.7.1", 28 | "css-minimizer-webpack-plugin": "^3.4.1", 29 | "express": "^4.17.3", 30 | "gh-pages": "^3.2.3", 31 | "git-revision-webpack-plugin": "^5.0.0", 32 | "glob": "^7.2.0", 33 | "loader-runner": "^4.2.0", 34 | "memfs": "^3.4.1", 35 | "mini-css-extract-plugin": "^2.6.0", 36 | "mini-html-webpack-plugin": "^3.1.3", 37 | "postcss-loader": "^6.2.1", 38 | "prettier": "^2.6.0", 39 | "purgecss-webpack-plugin": "^4.1.3", 40 | "tailwindcss": "^3.0.23", 41 | "terser-webpack-plugin": "^5.3.1", 42 | "webpack": "^5.70.0", 43 | "webpack-merge": "^5.8.0", 44 | "webpack-nano": "^1.1.1", 45 | "webpack-plugin-serve": "^1.6.0" 46 | }, 47 | "dependencies": { 48 | "react": "^17.0.2", 49 | "react-dom": "^17.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /plugins/demo-plugin.js: -------------------------------------------------------------------------------- 1 | const { sources, Compilation } = require("webpack"); 2 | 3 | module.exports = class DemoPlugin { 4 | constructor(options) { 5 | this.options = options; 6 | } 7 | apply(compiler) { 8 | const pluginName = "DemoPlugin"; 9 | const { name } = this.options; 10 | 11 | compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { 12 | compilation.hooks.processAssets.tap( 13 | { 14 | name: pluginName, 15 | // See lib/Compilation.js in webpack to understand different stages 16 | stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, 17 | }, 18 | () => compilation.emitAsset(name, new sources.RawSource("hello", true)) 19 | ); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/test-entry.js: -------------------------------------------------------------------------------- 1 | console.log("hello from entry"); -------------------------------------------------------------------------------- /plugins/test.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const { createFsFromVolume, Volume } = require("memfs"); 3 | 4 | // The compiler helper accepts filenames should be in the output 5 | // so it's possible to assert the output easily. 6 | function compile(config, filenames = []) { 7 | return new Promise((resolve, reject) => { 8 | const compiler = webpack(config); 9 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 10 | const memfs = compiler.outputFileSystem; 11 | 12 | compiler.run((err, stats) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | 17 | // Now only errors are captured from stats. 18 | // It's possible to capture more to assert. 19 | if (stats.hasErrors()) { 20 | return reject(stats.toString("errors-only")); 21 | } 22 | 23 | const ret = {}; 24 | filenames.forEach((filename) => { 25 | // The assumption is that webpack outputs behind ./dist. 26 | ret[filename] = memfs.readFileSync(`./dist/${filename}`, { 27 | encoding: "utf-8", 28 | }); 29 | }); 30 | return resolve(ret); 31 | }); 32 | }); 33 | } 34 | 35 | async function test() { 36 | console.log( 37 | await compile({ 38 | entry: "./test-entry.js", 39 | }), 40 | ["demo"] 41 | ); 42 | } 43 | 44 | test(); 45 | -------------------------------------------------------------------------------- /run-loader.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { runLoaders } = require("loader-runner"); 4 | 5 | runLoaders( 6 | { 7 | resource: "./demo.txt", 8 | loaders: [ 9 | { 10 | loader: path.resolve(__dirname, "./loaders/demo-loader"), 11 | options: { 12 | name: "demo.[ext]", 13 | }, 14 | }, 15 | path.resolve(__dirname, "./loaders/pitch-loader"), 16 | ], 17 | context: { 18 | emitFile: () => {}, 19 | }, 20 | readResource: fs.readFile.bind(fs), 21 | }, 22 | (err, result) => (err ? console.error(err) : console.log(result)) 23 | ); 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { renderToString } = require("react-dom/server"); 3 | 4 | const SSR = require("./static"); 5 | 6 | server(parseInt(process.env.PORT, 10) || 8080); 7 | 8 | function server(port) { 9 | const app = express(); 10 | 11 | app.use(express.static("static")); 12 | app.get("/", (req, res) => 13 | res.status(200).send(renderMarkup(renderToString(SSR))) 14 | ); 15 | 16 | app.listen(port, () => process.send && process.send("online")); 17 | } 18 | 19 | function renderMarkup(html) { 20 | return ` 21 | 22 | 23 | Webpack SSR Demo 24 | 25 | 26 | 27 |
${html}
28 | 29 | 30 | 31 | `; 32 | } 33 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import("./mf"); 2 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | import "!demo-loader?name=foo!./main.css"; 2 | 3 | export default (text = HELLO) => { 4 | const element = document.createElement("h1"); 5 | const worker = new Worker(new URL("./worker.js", import.meta.url)); 6 | const state = { text }; 7 | 8 | worker.addEventListener("message", ({ data: { text } }) => { 9 | state.text = text; 10 | element.innerHTML = text; 11 | }); 12 | 13 | element.innerHTML = state.text; 14 | element.onclick = () => worker.postMessage({ text: state.text }); 15 | 16 | return element; 17 | }; 18 | -------------------------------------------------------------------------------- /src/header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const Header = () =>

Demo

; 3 | export default Header; 4 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | import React, { useEffect, useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | 5 | const App = () => { 6 | const [language, setLanguage] = useState("en"); 7 | const [hello, setHello] = useState(""); 8 | const changeLanguage = () => setLanguage(language === "en" ? "fi" : "en"); 9 | 10 | useEffect(() => { 11 | translate(language, "hello").then(setHello).catch(console.error); 12 | }, [language]); 13 | 14 | return ( 15 |
16 | 17 |
{hello}
18 |
19 | ); 20 | }; 21 | 22 | function translate(locale, text) { 23 | return getLocaleData(locale).then((messages) => messages[text]); 24 | } 25 | 26 | async function getLocaleData(locale) { 27 | return import(`../messages/${locale}.json`); 28 | } 29 | 30 | const root = document.createElement("div"); 31 | root.setAttribute("id", "app"); 32 | document.body.appendChild(root); 33 | ReactDOM.render(, root); 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "react"; 2 | import "react-dom"; 3 | import "./main.css"; 4 | import component from "./component"; 5 | import { bake } from "./shake"; 6 | 7 | bake(); 8 | 9 | document.body.appendChild(component()); 10 | -------------------------------------------------------------------------------- /src/lazy.js: -------------------------------------------------------------------------------- 1 | export default "Hello from lazy"; 2 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/survivejs-demos/webpack-demo/0180bacbee06d96255f9e945136aadeada918290/src/logo.png -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | /* Write your utility classes here */ 5 | 6 | @tailwind utilities; 7 | 8 | body { 9 | background: cornsilk; 10 | background-image: url("./logo.png"); 11 | background-repeat: no-repeat; 12 | background-position: center; 13 | } 14 | -------------------------------------------------------------------------------- /src/mf.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import React from "react"; 3 | import "./main.css"; 4 | import Header from "mf/header"; 5 | 6 | function App() { 7 | const options = ["Hello world", "Hello fed", "Hello webpack"]; 8 | const [content, setContent] = React.useState("Changes on click."); 9 | 10 | return ( 11 |
12 |
13 | 27 |
{content}
28 |
29 | ); 30 | } 31 | 32 | const container = document.createElement("div"); 33 | document.body.appendChild(container); 34 | ReactDOM.render(, container); 35 | -------------------------------------------------------------------------------- /src/multi.js: -------------------------------------------------------------------------------- 1 | const element = document.createElement("div"); 2 | element.innerHTML = "hello multi"; 3 | document.body.appendChild(element); 4 | -------------------------------------------------------------------------------- /src/shake.js: -------------------------------------------------------------------------------- 1 | const shake = () => console.log("shake"); 2 | const bake = () => console.log("bake"); 3 | 4 | export { shake, bake }; -------------------------------------------------------------------------------- /src/ssr.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const ReactDOM = require("react-dom"); 3 | 4 | const SSR =
alert("hello")}>Hello world
; 5 | 6 | // Render only in the browser, export otherwise 7 | if (typeof document === "undefined") { 8 | module.exports = SSR; 9 | } else { 10 | ReactDOM.hydrate(SSR, document.getElementById("app")); 11 | } 12 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | self.onmessage = ({ data: { text } }) => { 2 | self.postMessage({ text: text + text }); 3 | }; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /tests/add.js: -------------------------------------------------------------------------------- 1 | module.exports = (a, b) => a + b; 2 | -------------------------------------------------------------------------------- /tests/add.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const add = require("./add"); 3 | 4 | describe("Demo", () => { 5 | it("should add correctly", () => { 6 | assert.equal(add(1, 1), 2); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | // Skip execution in Node 2 | if (module.hot) { 3 | const context = require.context( 4 | "mocha-loader!./", // Process through mocha-loader 5 | false, // Skip recursive processing 6 | /\.test.js$/ // Pick only files ending with .test.js 7 | ); 8 | 9 | // Execute each test suite 10 | context.keys().forEach(context); 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { mode } = require("webpack-nano/argv"); 2 | const { merge } = require("webpack-merge"); 3 | const path = require("path"); 4 | 5 | const parts = require("./webpack.parts"); 6 | 7 | const cssLoaders = [parts.autoprefix(), parts.tailwind()]; 8 | 9 | const commonConfig = merge([ 10 | { 11 | resolveLoader: { 12 | alias: { 13 | "demo-loader": path.resolve(__dirname, "loaders/demo-loader.js"), 14 | }, 15 | }, 16 | }, 17 | { 18 | output: { 19 | // Tweak this to match your GitHub project name 20 | publicPath: "/", 21 | }, 22 | }, 23 | { entry: ["./src"] }, 24 | parts.page({ title: "Demo" }), 25 | parts.clean(), 26 | parts.extractCSS({ loaders: cssLoaders }), 27 | parts.loadImages({ 28 | limit: 15000, 29 | }), 30 | parts.loadJavaScript(), 31 | parts.setFreeVariable("HELLO", "hello from config"), 32 | ]); 33 | 34 | const productionConfig = merge([ 35 | { 36 | output: { 37 | chunkFilename: "[name].[contenthash].js", 38 | filename: "[name].[contenthash].js", 39 | assetModuleFilename: "[name].[contenthash][ext][query]", 40 | }, 41 | recordsPath: path.join(__dirname, "records.json"), 42 | }, 43 | parts.minifyJavaScript(), 44 | parts.minifyCSS({ 45 | options: { 46 | preset: ["default"], 47 | }, 48 | }), 49 | parts.eliminateUnusedCSS(), 50 | parts.generateSourceMaps({ type: "source-map" }), 51 | { 52 | optimization: { 53 | splitChunks: { 54 | chunks: "all", 55 | }, 56 | runtimeChunk: { 57 | name: "runtime", 58 | }, 59 | }, 60 | }, 61 | parts.attachRevision(), 62 | ]); 63 | 64 | const developmentConfig = merge([parts.devServer()]); 65 | 66 | const getConfig = (mode) => { 67 | switch (mode) { 68 | case "production": 69 | return merge(commonConfig, productionConfig, { mode }); 70 | case "development": 71 | return merge(commonConfig, developmentConfig, { mode }); 72 | default: 73 | throw new Error(`Trying to use an unknown mode, ${mode}`); 74 | } 75 | }; 76 | 77 | module.exports = getConfig(mode); 78 | -------------------------------------------------------------------------------- /webpack.i18n.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { MiniHtmlWebpackPlugin } = require("mini-html-webpack-plugin"); 3 | const APP_SOURCE = path.join(__dirname, "src"); 4 | 5 | module.exports = { 6 | mode: "production", 7 | entry: { index: path.join(APP_SOURCE, "i18n.js") }, 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.js$/, 12 | include: APP_SOURCE, 13 | use: "babel-loader", 14 | }, 15 | ], 16 | }, 17 | plugins: [new MiniHtmlWebpackPlugin()], 18 | }; 19 | -------------------------------------------------------------------------------- /webpack.mf.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { component, mode } = require("webpack-nano/argv"); 3 | const { merge } = require("webpack-merge"); 4 | const parts = require("./webpack.parts"); 5 | 6 | const commonConfig = merge([ 7 | { 8 | output: { publicPath: "/" }, 9 | }, 10 | parts.loadJavaScript(), 11 | parts.loadImages(), 12 | parts.page(), 13 | parts.extractCSS({ loaders: [parts.tailwind()] }), 14 | ]); 15 | 16 | const configs = { 17 | development: merge( 18 | { entry: ["webpack-plugin-serve/client"] }, 19 | parts.devServer() 20 | ), 21 | production: {}, 22 | }; 23 | 24 | const shared = { 25 | react: { singleton: true }, 26 | "react-dom": { singleton: true }, 27 | }; 28 | const componentConfigs = { 29 | app: merge( 30 | { 31 | entry: [path.join(__dirname, "src", "bootstrap.js")], 32 | }, 33 | parts.page(), 34 | parts.federateModule({ 35 | name: "app", 36 | remotes: { mf: "mf@/mf.js" }, 37 | shared, 38 | }) 39 | ), 40 | header: merge( 41 | { 42 | entry: [path.join(__dirname, "src", "header.js")], 43 | }, 44 | parts.federateModule({ 45 | name: "mf", 46 | filename: "mf.js", 47 | exposes: { "./header": "./src/header" }, 48 | shared, 49 | }) 50 | ), 51 | }; 52 | 53 | if (!component) throw new Error("Missing component name"); 54 | 55 | module.exports = merge( 56 | commonConfig, 57 | configs[mode], 58 | { mode }, 59 | componentConfigs[component] 60 | ); 61 | -------------------------------------------------------------------------------- /webpack.multi.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const parts = require("./webpack.parts"); 3 | 4 | module.exports = merge( 5 | { mode: "production", entry: { app: "./src/multi.js" } }, 6 | parts.page({ title: "Demo" }), 7 | parts.page({ title: "Another", url: "another" }) 8 | ); 9 | -------------------------------------------------------------------------------- /webpack.parts.js: -------------------------------------------------------------------------------- 1 | const { WebpackPluginServe } = require("webpack-plugin-serve"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const path = require("path"); 4 | const glob = require("glob"); 5 | const PurgeCSSPlugin = require("purgecss-webpack-plugin"); 6 | const webpack = require("webpack"); 7 | const { GitRevisionPlugin } = require("git-revision-webpack-plugin"); 8 | const TerserPlugin = require("terser-webpack-plugin"); 9 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 10 | const { MiniHtmlWebpackPlugin } = require("mini-html-webpack-plugin"); 11 | const { ModuleFederationPlugin } = require("webpack").container; 12 | 13 | const ALL_FILES = glob.sync(path.join(__dirname, "src/*.js")); 14 | const APP_SOURCE = path.join(__dirname, "src"); 15 | 16 | exports.federateModule = ({ name, filename, exposes, remotes, shared }) => ({ 17 | plugins: [ 18 | new ModuleFederationPlugin({ name, filename, exposes, remotes, shared }), 19 | ], 20 | }); 21 | 22 | exports.page = ({ path = "", template, title, chunks } = {}) => ({ 23 | plugins: [ 24 | new MiniHtmlWebpackPlugin({ 25 | chunks, 26 | filename: `${path && path + "/"}index.html`, 27 | context: { title }, 28 | template, 29 | }), 30 | ], 31 | }); 32 | 33 | exports.setFreeVariable = (key, value) => { 34 | const env = {}; 35 | env[key] = JSON.stringify(value); 36 | 37 | return { 38 | plugins: [new webpack.DefinePlugin(env)], 39 | }; 40 | }; 41 | 42 | exports.minifyCSS = ({ options }) => ({ 43 | optimization: { 44 | minimizer: [new CssMinimizerPlugin({ minimizerOptions: options })], 45 | }, 46 | }); 47 | 48 | exports.minifyJavaScript = () => ({ 49 | optimization: { 50 | minimizer: [new TerserPlugin()], 51 | }, 52 | }); 53 | 54 | exports.attachRevision = () => ({ 55 | plugins: [ 56 | new webpack.BannerPlugin({ 57 | banner: new GitRevisionPlugin().version(), 58 | }), 59 | ], 60 | }); 61 | 62 | exports.clean = () => ({ 63 | output: { 64 | clean: true, 65 | }, 66 | }); 67 | 68 | exports.loadJavaScript = () => ({ 69 | module: { 70 | rules: [ 71 | { 72 | test: /\.js$/, 73 | include: APP_SOURCE, // Consider extracting as a parameter 74 | use: "babel-loader", 75 | }, 76 | ], 77 | }, 78 | }); 79 | 80 | exports.eliminateUnusedCSS = () => ({ 81 | plugins: [ 82 | new PurgeCSSPlugin({ 83 | paths: ALL_FILES, // Consider extracting as a parameter 84 | extractors: [ 85 | { 86 | extractor: (content) => 87 | content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [], 88 | extensions: ["html"], 89 | }, 90 | ], 91 | }), 92 | ], 93 | }); 94 | 95 | exports.extractCSS = ({ options = {}, loaders = [] } = {}) => { 96 | return { 97 | module: { 98 | rules: [ 99 | { 100 | test: /\.css$/, 101 | use: [ 102 | { loader: MiniCssExtractPlugin.loader, options }, 103 | "css-loader", 104 | ].concat(loaders), 105 | // If you distribute your code as a package and want to 106 | // use _Tree Shaking_, then you should mark CSS extraction 107 | // to emit side effects. For most use cases, you don't 108 | // have to worry about setting flag. 109 | sideEffects: true, 110 | }, 111 | ], 112 | }, 113 | plugins: [ 114 | new MiniCssExtractPlugin({ 115 | filename: "[name].[contenthash].css", 116 | }), 117 | ], 118 | }; 119 | }; 120 | 121 | exports.devServer = () => ({ 122 | watch: true, 123 | plugins: [ 124 | new WebpackPluginServe({ 125 | port: parseInt(process.env.PORT, 10) || 8080, 126 | static: "./dist", // Expose if output.path changes 127 | liveReload: true, 128 | waitForBuild: true, 129 | }), 130 | ], 131 | }); 132 | 133 | exports.tailwind = () => ({ 134 | loader: "postcss-loader", 135 | options: { 136 | postcssOptions: { 137 | plugins: [require("tailwindcss")()], 138 | }, 139 | }, 140 | }); 141 | 142 | exports.autoprefix = () => ({ 143 | loader: "postcss-loader", 144 | options: { 145 | postcssOptions: { 146 | plugins: [require("autoprefixer")()], 147 | }, 148 | }, 149 | }); 150 | 151 | exports.loadImages = ({ limit } = {}) => ({ 152 | module: { 153 | rules: [ 154 | { 155 | test: /\.(png|jpg)$/, 156 | type: "asset", 157 | parser: { 158 | dataUrlCondition: { 159 | maxSize: limit, 160 | }, 161 | }, 162 | }, 163 | ], 164 | }, 165 | }); 166 | 167 | exports.generateSourceMaps = ({ type }) => ({ 168 | devtool: type, 169 | }); 170 | -------------------------------------------------------------------------------- /webpack.ssr.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const APP_SOURCE = path.join(__dirname, "src"); 4 | 5 | module.exports = { 6 | mode: "production", 7 | entry: { index: path.join(APP_SOURCE, "ssr.js") }, 8 | output: { 9 | path: path.join(__dirname, "static"), 10 | filename: "[name].js", 11 | libraryTarget: "umd", 12 | globalObject: "this", 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | include: APP_SOURCE, 19 | use: "babel-loader", 20 | }, 21 | ], 22 | }, 23 | }; 24 | --------------------------------------------------------------------------------