├── templates └── .gitkeep ├── src ├── templates ├── css │ ├── vendor.pcss │ ├── pages │ │ └── homepage.pcss │ ├── components │ │ ├── typography.pcss │ │ ├── webfonts.pcss │ │ └── global.pcss │ └── app.pcss ├── img │ └── favicon-src.png ├── vue │ └── Confetti.vue └── js │ ├── app.js │ └── workbox-catch-handler.js ├── .gitignore ├── example.env ├── tailwind.config.js ├── .stylelintrc.json ├── README.md ├── postcss.config.js ├── LICENSE.md ├── webpack.dev.js ├── CHANGELOG.md ├── webpack.settings.js ├── package.json ├── webpack.common.js └── webpack.prod.js /templates/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/templates: -------------------------------------------------------------------------------- 1 | ../templates/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /src/css/vendor.pcss: -------------------------------------------------------------------------------- 1 | /** 2 | * vendor.css 3 | * 4 | * All vendor CSS is imported here. 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /src/css/pages/homepage.pcss: -------------------------------------------------------------------------------- 1 | /** 2 | * pages/homepage.pcss 3 | * 4 | * Styles for the Home page. 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /src/css/components/typography.pcss: -------------------------------------------------------------------------------- 1 | /** 2 | * components/typography.css 3 | * 4 | * Typography rules. 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /src/css/components/webfonts.pcss: -------------------------------------------------------------------------------- 1 | /** 2 | * components/webfonts.css 3 | * 4 | * Project webfonts. 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /src/img/favicon-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/annotated-webpack-config/webpack-4/src/img/favicon-src.png -------------------------------------------------------------------------------- /src/css/components/global.pcss: -------------------------------------------------------------------------------- 1 | /** 2 | * components/global.css 3 | * 4 | * Project-wide styles 5 | * 6 | */ 7 | 8 | body { 9 | background-color: yellow; 10 | } 11 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # webpack example settings for Homestead/Vagrant 2 | PUBLIC_PATH="/dist/" 3 | DEVSERVER_PUBLIC="http://192.168.10.10:8080" 4 | DEVSERVER_HOST="0.0.0.0" 5 | DEVSERVER_POLL=1 6 | DEVSERVER_PORT=8080 7 | DEVSERVER_HTTPS=0 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | // Extend the default Tailwind config here 4 | extend: { 5 | }, 6 | // Replace the default Tailwind config here 7 | }, 8 | corePlugins: {}, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-recommended", 3 | "rules": { 4 | "at-rule-no-unknown": [ true, { 5 | "ignoreAtRules": [ 6 | "screen", 7 | "extends", 8 | "responsive", 9 | "tailwind" 10 | ] 11 | }], 12 | "block-no-empty": null 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Annotated webpack 4 Config 2 | 3 | This is the companion github repo for the [An Annotated webpack 4 Config for Frontend Web Development](https://nystudio107.com/blog/an-annotated-webpack-4-config-for-frontend-web-development) article. 4 | 5 | It contains the full webpack config and ancillary files discussed in the article. 6 | 7 | Please see the article for details. 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import')({ 4 | plugins: [ 5 | require('stylelint') 6 | ] 7 | }), 8 | require('tailwindcss')('./tailwind.config.js'), 9 | require('postcss-preset-env')({ 10 | autoprefixer: { grid: true }, 11 | features: { 12 | 'nesting-rules': true 13 | } 14 | }) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/vue/Confetti.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 25 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | // Import our CSS 2 | import styles from '../css/app.pcss'; 3 | 4 | // App main 5 | const main = async () => { 6 | // Async load the vue module 7 | const { default: Vue } = await import(/* webpackChunkName: "vue" */ 'vue'); 8 | // Create our vue instance 9 | const vm = new Vue({ 10 | el: "#app", 11 | components: { 12 | 'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'), 13 | }, 14 | data: { 15 | }, 16 | methods: { 17 | }, 18 | mounted() { 19 | }, 20 | }); 21 | 22 | return vm; 23 | }; 24 | 25 | // Execute async function 26 | main().then( (vm) => { 27 | }); 28 | 29 | // Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept 30 | if (module.hot) { 31 | module.hot.accept(); 32 | } 33 | -------------------------------------------------------------------------------- /src/css/app.pcss: -------------------------------------------------------------------------------- 1 | /** 2 | * app.css 3 | * 4 | * The entry point for the css. 5 | * 6 | */ 7 | 8 | /** 9 | * This injects Tailwind's base styles, which is a combination of 10 | * Normalize.css and some additional base styles. 11 | */ 12 | @import "tailwindcss/base"; 13 | 14 | /** 15 | * This injects any component classes registered by plugins. 16 | * 17 | */ 18 | @import 'tailwindcss/components'; 19 | 20 | /** 21 | * Here we add custom component classes; stuff we want loaded 22 | * *before* the utilities so that the utilities can still 23 | * override them. 24 | * 25 | */ 26 | @import './components/global.pcss'; 27 | @import './components/typography.pcss'; 28 | @import './components/webfonts.pcss'; 29 | 30 | /** 31 | * This injects all of Tailwind's utility classes, generated based on your 32 | * config file. 33 | * 34 | */ 35 | @import 'tailwindcss/utilities'; 36 | 37 | /** 38 | * Include styles for individual pages 39 | * 40 | */ 41 | @import './pages/homepage.pcss'; 42 | 43 | /** 44 | * Include vendor css. 45 | * 46 | */ 47 | @import 'vendor.pcss'; 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 nystudio107 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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 NONINFRINGEMENT. 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. 10 | -------------------------------------------------------------------------------- /src/js/workbox-catch-handler.js: -------------------------------------------------------------------------------- 1 | // fallback URLs 2 | const FALLBACK_HTML_URL = '/offline.html'; 3 | const FALLBACK_IMAGE_URL = '/offline.svg'; 4 | 5 | // This "catch" handler is triggered when any of the other routes fail to 6 | // generate a response. 7 | // https://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_route 8 | workbox.routing.setCatchHandler(({event, request, url}) => { 9 | // Use event, request, and url to figure out how to respond. 10 | // One approach would be to use request.destination, see 11 | // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c 12 | switch (request.destination) { 13 | case 'document': 14 | return caches.match(FALLBACK_HTML_URL); 15 | break; 16 | 17 | case 'image': 18 | return caches.match(FALLBACK_IMAGE_URL); 19 | break; 20 | 21 | default: 22 | // If we don't have a fallback, just return an error response. 23 | return Response.error(); 24 | } 25 | }); 26 | 27 | // Use a stale-while-revalidate strategy for all other requests. 28 | workbox.routing.setDefaultHandler( 29 | workbox.strategies.staleWhileRevalidate() 30 | ); 31 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | // webpack.dev.js - developmental builds 2 | 3 | // node modules 4 | const merge = require('webpack-merge'); 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | // webpack plugins 9 | const DashboardPlugin = require('webpack-dashboard/plugin'); 10 | 11 | // config files 12 | const common = require('./webpack.common.js'); 13 | const pkg = require('./package.json'); 14 | const settings = require('./webpack.settings.js'); 15 | 16 | // Configure the webpack-dev-server 17 | const configureDevServer = () => { 18 | return { 19 | public: settings.devServerConfig.public(), 20 | contentBase: path.resolve(__dirname, settings.paths.templates), 21 | host: settings.devServerConfig.host(), 22 | port: settings.devServerConfig.port(), 23 | https: !!parseInt(settings.devServerConfig.https()), 24 | disableHostCheck: true, 25 | hot: true, 26 | overlay: true, 27 | watchContentBase: true, 28 | watchOptions: { 29 | poll: !!parseInt(settings.devServerConfig.poll()), 30 | ignored: /node_modules/, 31 | }, 32 | headers: { 33 | 'Access-Control-Allow-Origin': '*' 34 | }, 35 | }; 36 | }; 37 | 38 | // Configure Image loader 39 | const configureImageLoader = () => { 40 | return { 41 | test: /\.(png|jpe?g|gif|svg|webp)$/i, 42 | use: [ 43 | { 44 | loader: 'file-loader', 45 | options: { 46 | name: 'img/[name].[ext]' 47 | } 48 | } 49 | ] 50 | }; 51 | }; 52 | 53 | // Configure the Postcss loader 54 | const configurePostcssLoader = () => { 55 | return { 56 | test: /\.(pcss|css)$/, 57 | use: [ 58 | { 59 | loader: 'style-loader', 60 | }, 61 | { 62 | loader: 'vue-style-loader', 63 | }, 64 | { 65 | loader: 'css-loader', 66 | options: { 67 | importLoaders: 2, 68 | sourceMap: true 69 | } 70 | }, 71 | { 72 | loader: 'resolve-url-loader' 73 | }, 74 | { 75 | loader: 'postcss-loader', 76 | options: { 77 | sourceMap: true, 78 | config: { 79 | path: path.resolve(__dirname), 80 | } 81 | } 82 | } 83 | ] 84 | }; 85 | }; 86 | 87 | // Development module exports 88 | module.exports = merge( 89 | common.modernConfig, 90 | { 91 | output: { 92 | filename: path.join('./js', '[name].js'), 93 | publicPath: settings.devServerConfig.public() + '/', 94 | }, 95 | mode: 'development', 96 | devtool: 'inline-source-map', 97 | devServer: configureDevServer(), 98 | module: { 99 | rules: [ 100 | configurePostcssLoader(), 101 | configureImageLoader(), 102 | ], 103 | }, 104 | plugins: [ 105 | new webpack.HotModuleReplacementPlugin(), 106 | new DashboardPlugin(), 107 | ], 108 | } 109 | ); 110 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Annotated webpack 4 config changelog 2 | 3 | ## 1.1.7 - 2020.08.13 4 | ### Fixed 5 | * Modern config only for local dev, [fixing multi-compiler issues](https://github.com/webpack/webpack-dev-server/issues/2355) with HRM 6 | 7 | ## 1.1.6 - 2020.08.12 8 | ### Changed 9 | * Remove `[hash]` from dev config to eliminate potential [memory errors](https://github.com/webpack/webpack-dev-server/issues/438) 10 | * Use `[contenthash]` in production instead of [hash or chunkhash](https://github.com/webpack/webpack.js.org/issues/2096) 11 | * Replaced moment.js with vanilla JavaScript 12 | 13 | ## 1.1.5 - 2020-02-05 14 | ### Changed 15 | * Removed entirely the concept of a "modern" and "legacy" build from the `webpack.dev.js`; we don't need legacy builds with `webpack-dev-server` 16 | 17 | ### Fixed 18 | * Changed deprecated use of `cacheFirst` to `CacheFirst` in the Workbox config 19 | 20 | ## 1.1.4 - 2019-11-08 21 | ### Changed 22 | * Added `settings.babelLoaderConfig.include` 23 | 24 | ## 1.1.3 - 2019-07-03 25 | ### Changed 26 | * Updated to use `core-js` version `^3.0.0` 27 | * Removed explicit `new webpack.optimize.ModuleConcatenationPlugin()` 28 | * Moved the `CleanWebpackPlugin` to the modern build, which fixes it wiping out the modern build 29 | 30 | ## 1.1.2 - 2019-06-14 31 | ### Changed 32 | * Updated to use the latest `clean-webpack-plugin` 33 | 34 | ## 1.1.1 - 2019-06-05 35 | ### Changed 36 | * Use destructuring for ESM module imports 37 | * Added `module.hot.accept()` to the example entry point `app.js` 38 | 39 | ## 1.1.0 - 2019-05-31 40 | ### Changed 41 | * Switched over to using `webpack-dashboard` `^3.0.0` 42 | * Added `debug` to `scripts` to bypass `webpack-dashboard` as needed 43 | * Removed `sane` since `webpack-dev-server ^3.3.0` fixes the sub-directory [watchContentBase issue](https://github.com/webpack/webpack-dev-server/issues/1694) 44 | * Update to [TailWind CSS](https://tailwindcss.com/) `^1.0.0` 45 | 46 | ## 1.0.6 - 2019-05-18 47 | ### Changed 48 | * Removed the now deprecated `@babel/polyfill` since we're using `core-js` directly 49 | 50 | ## 1.0.5 - 2019-05-14 51 | ### Changed 52 | * Use `@babel/preset-env` with `usage` polyfills as per the article [Working with Babel 7 and Webpack](https://www.thebasement.be/working-with-babel-7-and-webpack/#a-cleaner-approach) 53 | * By default, exclude `/(node_modules|bower_components)/` in `webpack.settings.js` 54 | * Added `core-js@2` and `regenerator-runtime` to the `package.json` dependencies 55 | 56 | ## 1.0.4 - 2019-05-13 57 | ### Changed 58 | * Fixed an issue where the `cacheDirectory` was specified in the wrong place, resulting in obscure build errors 59 | * Removed `pcss` from the whitelist config, because it can't handle PostCSS 60 | 61 | ## 1.0.3 - 2019-05-13 62 | ### Changed 63 | * Fixed an error where the default `excludes` should be an empty array `[]` instead of an empty string 64 | * Added `corejs` specification in the `babel-loader` options 65 | 66 | ## 1.0.2 - 2019-05-02 67 | ### Changed 68 | * Moved the `excludes` babel-loader config to `webpack.settings.js` 69 | * Changed the default babel-loader `excludes` config to nothing (was `/node_modules/`) to by default transpile everything 70 | * Set `cacheDirectory` babel-loader config to `true` 71 | 72 | ## 1.0.1 - 2019-03-10 73 | ### Changed 74 | * Added support for [gzip'd static resources](https://medium.com/@selvaganesh93/how-to-serve-webpack-gzipped-file-in-production-using-nginx-692eadbb9f1c) 75 | * Updated bundle dependencies to most recent versions 76 | 77 | ## 1.0.0 - 2018-10.23 78 | ### Added 79 | - Initial release 80 | -------------------------------------------------------------------------------- /webpack.settings.js: -------------------------------------------------------------------------------- 1 | // webpack.settings.js - webpack settings config 2 | 3 | // node modules 4 | require('dotenv').config(); 5 | 6 | // Webpack settings exports 7 | // noinspection WebpackConfigHighlighting 8 | module.exports = { 9 | name: "Example Project", 10 | copyright: "Example Company, Inc.", 11 | paths: { 12 | src: { 13 | base: "./src/", 14 | css: "./src/css/", 15 | js: "./src/js/" 16 | }, 17 | dist: { 18 | base: "./web/dist/", 19 | clean: [ 20 | '**/*', 21 | ] 22 | }, 23 | templates: "./templates/" 24 | }, 25 | urls: { 26 | live: "https://example.com/", 27 | local: "http://example.test/", 28 | critical: "http://example.test/", 29 | publicPath: () => process.env.PUBLIC_PATH || "/dist/", 30 | }, 31 | vars: { 32 | cssName: "styles" 33 | }, 34 | entries: { 35 | "app": "app.js" 36 | }, 37 | babelLoaderConfig: { 38 | exclude: [ 39 | /(node_modules|bower_components)/ 40 | ], 41 | }, 42 | copyWebpackConfig: [ 43 | { 44 | from: "./src/js/workbox-catch-handler.js", 45 | to: "js/[name].[ext]" 46 | } 47 | ], 48 | criticalCssConfig: { 49 | base: "./web/dist/criticalcss/", 50 | suffix: "_critical.min.css", 51 | criticalHeight: 1200, 52 | criticalWidth: 1200, 53 | ampPrefix: "amp_", 54 | ampCriticalHeight: 19200, 55 | ampCriticalWidth: 600, 56 | pages: [ 57 | { 58 | url: "", 59 | template: "index" 60 | } 61 | ] 62 | }, 63 | devServerConfig: { 64 | public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080", 65 | host: () => process.env.DEVSERVER_HOST || "localhost", 66 | poll: () => process.env.DEVSERVER_POLL || false, 67 | port: () => process.env.DEVSERVER_PORT || 8080, 68 | https: () => process.env.DEVSERVER_HTTPS || false, 69 | }, 70 | manifestConfig: { 71 | basePath: "" 72 | }, 73 | purgeCssConfig: { 74 | paths: [ 75 | "./templates/**/*.{twig,html}", 76 | "./src/vue/**/*.{vue,html}" 77 | ], 78 | whitelist: [ 79 | "./src/css/components/**/*.{css}" 80 | ], 81 | whitelistPatterns: [], 82 | extensions: [ 83 | "html", 84 | "js", 85 | "twig", 86 | "vue" 87 | ] 88 | }, 89 | saveRemoteFileConfig: [ 90 | { 91 | url: "https://www.google-analytics.com/analytics.js", 92 | filepath: "js/analytics.js" 93 | } 94 | ], 95 | createSymlinkConfig: [ 96 | { 97 | origin: "img/favicons/favicon.ico", 98 | symlink: "../favicon.ico" 99 | } 100 | ], 101 | webappConfig: { 102 | logo: "./src/img/favicon-src.png", 103 | prefix: "img/favicons/" 104 | }, 105 | workboxConfig: { 106 | swDest: "../sw.js", 107 | precacheManifestFilename: "js/precache-manifest.[manifestHash].js", 108 | importScripts: [ 109 | "/dist/js/workbox-catch-handler.js" 110 | ], 111 | exclude: [ 112 | /\.(png|jpe?g|gif|svg|webp)$/i, 113 | /\.map$/, 114 | /^manifest.*\\.js(?:on)?$/, 115 | ], 116 | globDirectory: "./web/", 117 | globPatterns: [ 118 | "offline.html", 119 | "offline.svg" 120 | ], 121 | offlineGoogleAnalytics: true, 122 | runtimeCaching: [ 123 | { 124 | urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/, 125 | handler: "CacheFirst", 126 | options: { 127 | cacheName: "images", 128 | expiration: { 129 | maxEntries: 20 130 | } 131 | } 132 | } 133 | ] 134 | } 135 | }; 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-project", 3 | "version": "1.1.7", 4 | "description": "Example Project brand website", 5 | "keywords": [ 6 | "Example", 7 | "Keywords" 8 | ], 9 | "homepage": "https://github.com/example-developer/example-project", 10 | "bugs": { 11 | "email": "someone@example-developer.com", 12 | "url": "https://github.com/example-developer/example-project/issues" 13 | }, 14 | "license": "SEE LICENSE IN LICENSE.md", 15 | "author": { 16 | "name": "Example Developer", 17 | "email": "someone@example-developer.com", 18 | "url": "https://example-developer.com" 19 | }, 20 | "browser": "/web/index.php", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/example-developer/example-project.git" 24 | }, 25 | "private": true, 26 | "scripts": { 27 | "debug": "webpack-dev-server --config webpack.dev.js", 28 | "dev": "webpack-dashboard -- webpack-dev-server --config webpack.dev.js", 29 | "build": "webpack --config webpack.prod.js --progress --hide-modules" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | "> 1%", 34 | "last 2 versions", 35 | "Firefox ESR" 36 | ], 37 | "legacyBrowsers": [ 38 | "> 1%", 39 | "last 2 versions", 40 | "Firefox ESR" 41 | ], 42 | "modernBrowsers": [ 43 | "last 2 Chrome versions", 44 | "not Chrome < 60", 45 | "last 2 Safari versions", 46 | "not Safari < 10.1", 47 | "last 2 iOS versions", 48 | "not iOS < 10.3", 49 | "last 2 Firefox versions", 50 | "not Firefox < 54", 51 | "last 2 Edge versions", 52 | "not Edge < 15" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.1.0", 57 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 58 | "@babel/plugin-transform-runtime": "^7.1.0", 59 | "@babel/preset-env": "^7.1.0", 60 | "@babel/register": "^7.0.0", 61 | "@babel/runtime": "^7.0.0", 62 | "@gfx/zopfli": "^1.0.11", 63 | "babel-loader": "^8.0.2", 64 | "clean-webpack-plugin": "^3.0.0", 65 | "compression-webpack-plugin": "^2.0.0", 66 | "copy-webpack-plugin": "^4.5.2", 67 | "create-symlink-webpack-plugin": "^1.0.0", 68 | "critical": "^1.3.4", 69 | "critical-css-webpack-plugin": "^0.2.0", 70 | "css-loader": "^2.1.0", 71 | "cssnano": "^4.1.0", 72 | "dotenv": "^6.1.0", 73 | "file-loader": "^2.0.0", 74 | "git-rev-sync": "^1.12.0", 75 | "glob-all": "^3.1.0", 76 | "html-webpack-plugin": "^3.2.0", 77 | "ignore-loader": "^0.1.2", 78 | "imagemin": "^6.0.0", 79 | "imagemin-gifsicle": "^6.0.0", 80 | "imagemin-mozjpeg": "^8.0.0", 81 | "imagemin-optipng": "^6.0.0", 82 | "imagemin-svgo": "^7.0.0", 83 | "imagemin-webp": "^5.0.0", 84 | "imagemin-webp-webpack-plugin": "^3.1.0", 85 | "img-loader": "^3.0.1", 86 | "mini-css-extract-plugin": "^0.4.3", 87 | "optimize-css-assets-webpack-plugin": "^5.0.1", 88 | "postcss": "^7.0.2", 89 | "postcss-import": "^12.0.0", 90 | "postcss-loader": "^3.0.0", 91 | "postcss-preset-env": "^6.4.0", 92 | "purgecss-webpack-plugin": "^1.3.0", 93 | "purgecss-whitelister": "^2.2.0", 94 | "resolve-url-loader": "^3.0.0", 95 | "save-remote-file-webpack-plugin": "^1.0.0", 96 | "stylelint": "^9.9.0", 97 | "stylelint-config-recommended": "^2.1.0", 98 | "style-loader": "^0.23.0", 99 | "symlink-webpack-plugin": "^0.0.4", 100 | "terser-webpack-plugin": "^1.1.0", 101 | "vue-loader": "^15.4.2", 102 | "vue-style-loader": "^4.1.2", 103 | "vue-template-compiler": "^2.5.17", 104 | "webapp-webpack-plugin": "https://github.com/brunocodutra/webapp-webpack-plugin.git", 105 | "webpack": "^4.19.1", 106 | "webpack-bundle-analyzer": "^3.0.2", 107 | "webpack-cli": "^3.1.1", 108 | "webpack-dashboard": "^3.0.0", 109 | "webpack-dev-server": "^3.3.0", 110 | "webpack-manifest-plugin": "^2.0.4", 111 | "webpack-merge": "^4.1.4", 112 | "webpack-notifier": "^1.6.0", 113 | "workbox-webpack-plugin": "^3.6.2" 114 | }, 115 | "dependencies": { 116 | "axios": "^0.18.0", 117 | "core-js": "^3.0.0", 118 | "regenerator-runtime": "^0.13.2", 119 | "tailwindcss": "^1.0.0", 120 | "vue": "^2.5.17", 121 | "vue-confetti": "^0.4.2" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | // webpack.common.js - common webpack config 2 | const LEGACY_CONFIG = 'legacy'; 3 | const MODERN_CONFIG = 'modern'; 4 | 5 | // node modules 6 | const path = require('path'); 7 | const merge = require('webpack-merge'); 8 | 9 | // webpack plugins 10 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 11 | const ManifestPlugin = require('webpack-manifest-plugin'); 12 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 13 | const WebpackNotifierPlugin = require('webpack-notifier'); 14 | 15 | // config files 16 | const pkg = require('./package.json'); 17 | const settings = require('./webpack.settings.js'); 18 | 19 | // Configure Babel loader 20 | const configureBabelLoader = (browserList) => { 21 | return { 22 | test: /\.js$/, 23 | exclude: settings.babelLoaderConfig.exclude, 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | cacheDirectory: true, 28 | sourceType: 'unambiguous', 29 | presets: [ 30 | [ 31 | '@babel/preset-env', { 32 | modules: false, 33 | corejs: { 34 | version: 3, 35 | proposals: true 36 | }, 37 | useBuiltIns: 'usage', 38 | targets: { 39 | browsers: browserList, 40 | }, 41 | } 42 | ], 43 | ], 44 | plugins: [ 45 | '@babel/plugin-syntax-dynamic-import', 46 | '@babel/plugin-transform-runtime', 47 | ], 48 | }, 49 | }, 50 | }; 51 | }; 52 | 53 | // Configure Entries 54 | const configureEntries = () => { 55 | let entries = {}; 56 | for (const [key, value] of Object.entries(settings.entries)) { 57 | entries[key] = path.resolve(__dirname, settings.paths.src.js + value); 58 | } 59 | 60 | return entries; 61 | }; 62 | 63 | // Configure Font loader 64 | const configureFontLoader = () => { 65 | return { 66 | test: /\.(ttf|eot|woff2?)$/i, 67 | use: [ 68 | { 69 | loader: 'file-loader', 70 | options: { 71 | name: 'fonts/[name].[ext]' 72 | } 73 | } 74 | ] 75 | }; 76 | }; 77 | 78 | // Configure Manifest 79 | const configureManifest = (fileName) => { 80 | return { 81 | fileName: fileName, 82 | basePath: settings.manifestConfig.basePath, 83 | map: (file) => { 84 | file.name = file.name.replace(/(\.[a-f0-9]{32})(\..*)$/, '$2'); 85 | return file; 86 | }, 87 | }; 88 | }; 89 | 90 | // Configure Vue loader 91 | const configureVueLoader = () => { 92 | return { 93 | test: /\.vue$/, 94 | loader: 'vue-loader' 95 | }; 96 | }; 97 | 98 | // The base webpack config 99 | const baseConfig = { 100 | name: pkg.name, 101 | entry: configureEntries(), 102 | output: { 103 | path: path.resolve(__dirname, settings.paths.dist.base), 104 | publicPath: settings.urls.publicPath() 105 | }, 106 | resolve: { 107 | alias: { 108 | 'vue$': 'vue/dist/vue.esm.js' 109 | } 110 | }, 111 | module: { 112 | rules: [ 113 | configureFontLoader(), 114 | configureVueLoader(), 115 | ], 116 | }, 117 | plugins: [ 118 | new WebpackNotifierPlugin({title: 'Webpack', excludeWarnings: true, alwaysNotify: true}), 119 | new VueLoaderPlugin(), 120 | ] 121 | }; 122 | 123 | // Legacy webpack config 124 | const legacyConfig = { 125 | module: { 126 | rules: [ 127 | configureBabelLoader(Object.values(pkg.browserslist.legacyBrowsers)), 128 | ], 129 | }, 130 | plugins: [ 131 | new CopyWebpackPlugin( 132 | settings.copyWebpackConfig 133 | ), 134 | new ManifestPlugin( 135 | configureManifest('manifest-legacy.json') 136 | ), 137 | ] 138 | }; 139 | 140 | // Modern webpack config 141 | const modernConfig = { 142 | module: { 143 | rules: [ 144 | configureBabelLoader(Object.values(pkg.browserslist.modernBrowsers)), 145 | ], 146 | }, 147 | plugins: [ 148 | new ManifestPlugin( 149 | configureManifest('manifest.json') 150 | ), 151 | ] 152 | }; 153 | 154 | // Common module exports 155 | // noinspection WebpackConfigHighlighting 156 | module.exports = { 157 | 'legacyConfig': merge.strategy({ 158 | module: 'prepend', 159 | plugins: 'prepend', 160 | })( 161 | baseConfig, 162 | legacyConfig, 163 | ), 164 | 'modernConfig': merge.strategy({ 165 | module: 'prepend', 166 | plugins: 'prepend', 167 | })( 168 | baseConfig, 169 | modernConfig, 170 | ), 171 | }; 172 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | // webpack.prod.js - production builds 2 | const LEGACY_CONFIG = 'legacy'; 3 | const MODERN_CONFIG = 'modern'; 4 | 5 | // node modules 6 | const git = require('git-rev-sync'); 7 | const glob = require('glob-all'); 8 | const merge = require('webpack-merge'); 9 | const path = require('path'); 10 | const webpack = require('webpack'); 11 | 12 | // webpack plugins 13 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 14 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 15 | const CompressionPlugin = require('compression-webpack-plugin'); 16 | const CreateSymlinkPlugin = require('create-symlink-webpack-plugin'); 17 | const CriticalCssPlugin = require('critical-css-webpack-plugin'); 18 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 19 | const ImageminWebpWebpackPlugin = require('imagemin-webp-webpack-plugin'); 20 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 21 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 22 | const PurgecssPlugin = require('purgecss-webpack-plugin'); 23 | const SaveRemoteFilePlugin = require('save-remote-file-webpack-plugin'); 24 | const TerserPlugin = require('terser-webpack-plugin'); 25 | const WebappWebpackPlugin = require('webapp-webpack-plugin'); 26 | const WhitelisterPlugin = require('purgecss-whitelister'); 27 | const WorkboxPlugin = require('workbox-webpack-plugin'); 28 | const zopfli = require('@gfx/zopfli'); 29 | 30 | // config files 31 | const common = require('./webpack.common.js'); 32 | const pkg = require('./package.json'); 33 | const settings = require('./webpack.settings.js'); 34 | 35 | // Custom PurgeCSS extractor for Tailwind that allows special characters in 36 | // class names. 37 | // 38 | // https://github.com/FullHuman/purgecss#extractor 39 | class TailwindExtractor { 40 | static extract(content) { 41 | return content.match(/[A-Za-z0-9-_:\/]+/g) || []; 42 | } 43 | } 44 | 45 | // Configure file banner 46 | const configureBanner = () => { 47 | const timestamp = new Date(); 48 | try { 49 | return { 50 | banner: [ 51 | '/*!', 52 | ' * @project ' + settings.name, 53 | ' * @name ' + '[filebase]', 54 | ' * @author ' + pkg.author.name, 55 | ' * @build ' + timestamp.toString(), 56 | ' * @release ' + git.long() + ' [' + git.branch() + ']', 57 | ' * @copyright Copyright (c) ' + timestamp.getFullYear() + ' ' + settings.copyright, 58 | ' *', 59 | ' */', 60 | '' 61 | ].join('\n'), 62 | raw: true 63 | }; 64 | } catch { 65 | return { 66 | banner: [ 67 | '/*!', 68 | ' * @project ' + settings.name, 69 | ' * @name ' + '[filebase]', 70 | ' * @author ' + pkg.author.name, 71 | ' * @build ' + timestamp.toString(), 72 | ' * @copyright Copyright (c) ' + timestamp.getFullYear() + ' ' + settings.copyright, 73 | ' *', 74 | ' */', 75 | '' 76 | ].join('\n'), 77 | raw: true 78 | }; 79 | } 80 | }; 81 | 82 | // Configure Bundle Analyzer 83 | const configureBundleAnalyzer = (buildType) => { 84 | if (buildType === LEGACY_CONFIG) { 85 | return { 86 | analyzerMode: 'static', 87 | reportFilename: 'report-legacy.html', 88 | }; 89 | } 90 | if (buildType === MODERN_CONFIG) { 91 | return { 92 | analyzerMode: 'static', 93 | reportFilename: 'report-modern.html', 94 | }; 95 | } 96 | }; 97 | 98 | // Configure Compression webpack plugin 99 | const configureCompression = () => { 100 | return { 101 | filename: '[path].gz[query]', 102 | test: /\.(js|css|html|svg)$/, 103 | threshold: 10240, 104 | minRatio: 0.8, 105 | deleteOriginalAssets: false, 106 | compressionOptions: { 107 | numiterations: 15, 108 | level: 9 109 | }, 110 | algorithm(input, compressionOptions, callback) { 111 | return zopfli.gzip(input, compressionOptions, callback); 112 | } 113 | }; 114 | }; 115 | 116 | // Configure Critical CSS 117 | const configureCriticalCss = () => { 118 | return (settings.criticalCssConfig.pages.map((row) => { 119 | const criticalSrc = settings.urls.critical + row.url; 120 | const criticalDest = settings.criticalCssConfig.base + row.template + settings.criticalCssConfig.suffix; 121 | let criticalWidth = settings.criticalCssConfig.criticalWidth; 122 | let criticalHeight = settings.criticalCssConfig.criticalHeight; 123 | // Handle Google AMP templates 124 | if (row.template.indexOf(settings.criticalCssConfig.ampPrefix) !== -1) { 125 | criticalWidth = settings.criticalCssConfig.ampCriticalWidth; 126 | criticalHeight = settings.criticalCssConfig.ampCriticalHeight; 127 | } 128 | console.log("source: " + criticalSrc + " dest: " + criticalDest); 129 | return new CriticalCssPlugin({ 130 | base: './', 131 | src: criticalSrc, 132 | dest: criticalDest, 133 | extract: false, 134 | inline: false, 135 | minify: true, 136 | width: criticalWidth, 137 | height: criticalHeight, 138 | }) 139 | }) 140 | ); 141 | }; 142 | 143 | // Configure Clean webpack 144 | const configureCleanWebpack = () => { 145 | return { 146 | cleanOnceBeforeBuildPatterns: settings.paths.dist.clean, 147 | verbose: true, 148 | dry: false 149 | }; 150 | }; 151 | 152 | // Configure Html webpack 153 | const configureHtml = () => { 154 | return { 155 | templateContent: '', 156 | filename: 'webapp.html', 157 | inject: false, 158 | }; 159 | }; 160 | 161 | // Configure Image loader 162 | const configureImageLoader = (buildType) => { 163 | if (buildType === LEGACY_CONFIG) { 164 | return { 165 | test: /\.(png|jpe?g|gif|svg|webp)$/i, 166 | use: [ 167 | { 168 | loader: 'file-loader', 169 | options: { 170 | name: 'img/[name].[contenthash].[ext]' 171 | } 172 | } 173 | ] 174 | }; 175 | } 176 | if (buildType === MODERN_CONFIG) { 177 | return { 178 | test: /\.(png|jpe?g|gif|svg|webp)$/i, 179 | use: [ 180 | { 181 | loader: 'file-loader', 182 | options: { 183 | name: 'img/[name].[contenthash].[ext]' 184 | } 185 | }, 186 | { 187 | loader: 'img-loader', 188 | options: { 189 | plugins: [ 190 | require('imagemin-gifsicle')({ 191 | interlaced: true, 192 | }), 193 | require('imagemin-mozjpeg')({ 194 | progressive: true, 195 | arithmetic: false, 196 | }), 197 | require('imagemin-optipng')({ 198 | optimizationLevel: 5, 199 | }), 200 | require('imagemin-svgo')({ 201 | plugins: [ 202 | {convertPathData: false}, 203 | ] 204 | }), 205 | ] 206 | } 207 | } 208 | ] 209 | }; 210 | } 211 | }; 212 | 213 | // Configure optimization 214 | const configureOptimization = (buildType) => { 215 | if (buildType === LEGACY_CONFIG) { 216 | return { 217 | splitChunks: { 218 | cacheGroups: { 219 | default: false, 220 | common: false, 221 | styles: { 222 | name: settings.vars.cssName, 223 | test: /\.(pcss|css|vue)$/, 224 | chunks: 'all', 225 | enforce: true 226 | } 227 | } 228 | }, 229 | minimizer: [ 230 | new TerserPlugin( 231 | configureTerser() 232 | ), 233 | new OptimizeCSSAssetsPlugin({ 234 | cssProcessorOptions: { 235 | map: { 236 | inline: false, 237 | annotation: true, 238 | }, 239 | safe: true, 240 | discardComments: true 241 | }, 242 | }) 243 | ] 244 | }; 245 | } 246 | if (buildType === MODERN_CONFIG) { 247 | return { 248 | minimizer: [ 249 | new TerserPlugin( 250 | configureTerser() 251 | ), 252 | ] 253 | }; 254 | } 255 | }; 256 | 257 | // Configure Postcss loader 258 | const configurePostcssLoader = (buildType) => { 259 | if (buildType === LEGACY_CONFIG) { 260 | return { 261 | test: /\.(pcss|css)$/, 262 | use: [ 263 | MiniCssExtractPlugin.loader, 264 | { 265 | loader: 'css-loader', 266 | options: { 267 | importLoaders: 2, 268 | sourceMap: true 269 | } 270 | }, 271 | { 272 | loader: 'resolve-url-loader' 273 | }, 274 | { 275 | loader: 'postcss-loader', 276 | options: { 277 | sourceMap: true 278 | } 279 | } 280 | ] 281 | }; 282 | } 283 | // Don't generate CSS for the modern config in production 284 | if (buildType === MODERN_CONFIG) { 285 | return { 286 | test: /\.(pcss|css)$/, 287 | loader: 'ignore-loader' 288 | }; 289 | } 290 | }; 291 | 292 | // Configure PurgeCSS 293 | const configurePurgeCss = () => { 294 | let paths = []; 295 | // Configure whitelist paths 296 | for (const [key, value] of Object.entries(settings.purgeCssConfig.paths)) { 297 | paths.push(path.join(__dirname, value)); 298 | } 299 | 300 | return { 301 | paths: glob.sync(paths), 302 | whitelist: WhitelisterPlugin(settings.purgeCssConfig.whitelist), 303 | whitelistPatterns: settings.purgeCssConfig.whitelistPatterns, 304 | extractors: [ 305 | { 306 | extractor: TailwindExtractor, 307 | extensions: settings.purgeCssConfig.extensions 308 | } 309 | ] 310 | }; 311 | }; 312 | 313 | // Configure terser 314 | const configureTerser = () => { 315 | return { 316 | cache: true, 317 | parallel: true, 318 | sourceMap: true 319 | }; 320 | }; 321 | 322 | // Configure Webapp webpack 323 | const configureWebapp = () => { 324 | return { 325 | logo: settings.webappConfig.logo, 326 | prefix: settings.webappConfig.prefix, 327 | cache: false, 328 | inject: 'force', 329 | favicons: { 330 | appName: pkg.name, 331 | appDescription: pkg.description, 332 | developerName: pkg.author.name, 333 | developerURL: pkg.author.url, 334 | path: settings.paths.dist.base, 335 | } 336 | }; 337 | }; 338 | 339 | // Configure Workbox service worker 340 | const configureWorkbox = () => { 341 | let config = settings.workboxConfig; 342 | 343 | return config; 344 | }; 345 | 346 | // Production module exports 347 | module.exports = [ 348 | merge( 349 | common.legacyConfig, 350 | { 351 | output: { 352 | filename: path.join('./js', '[name]-legacy.[contenthash].js'), 353 | }, 354 | mode: 'production', 355 | devtool: 'source-map', 356 | optimization: configureOptimization(LEGACY_CONFIG), 357 | module: { 358 | rules: [ 359 | configurePostcssLoader(LEGACY_CONFIG), 360 | configureImageLoader(LEGACY_CONFIG), 361 | ], 362 | }, 363 | plugins: [ 364 | new MiniCssExtractPlugin({ 365 | path: path.resolve(__dirname, settings.paths.dist.base), 366 | filename: path.join('./css', '[name].[contenthash].css'), 367 | }), 368 | new PurgecssPlugin( 369 | configurePurgeCss() 370 | ), 371 | new webpack.BannerPlugin( 372 | configureBanner() 373 | ), 374 | new HtmlWebpackPlugin( 375 | configureHtml() 376 | ), 377 | new WebappWebpackPlugin( 378 | configureWebapp() 379 | ), 380 | new CreateSymlinkPlugin( 381 | settings.createSymlinkConfig, 382 | true 383 | ), 384 | new SaveRemoteFilePlugin( 385 | settings.saveRemoteFileConfig 386 | ), 387 | new CompressionPlugin( 388 | configureCompression() 389 | ), 390 | new BundleAnalyzerPlugin( 391 | configureBundleAnalyzer(LEGACY_CONFIG), 392 | ), 393 | ].concat( 394 | configureCriticalCss() 395 | ) 396 | } 397 | ), 398 | merge( 399 | common.modernConfig, 400 | { 401 | output: { 402 | filename: path.join('./js', '[name].[contenthash].js'), 403 | }, 404 | mode: 'production', 405 | devtool: 'source-map', 406 | optimization: configureOptimization(MODERN_CONFIG), 407 | module: { 408 | rules: [ 409 | configurePostcssLoader(MODERN_CONFIG), 410 | configureImageLoader(MODERN_CONFIG), 411 | ], 412 | }, 413 | plugins: [ 414 | new CleanWebpackPlugin( 415 | configureCleanWebpack() 416 | ), 417 | new webpack.BannerPlugin( 418 | configureBanner() 419 | ), 420 | new ImageminWebpWebpackPlugin(), 421 | new WorkboxPlugin.GenerateSW( 422 | configureWorkbox() 423 | ), 424 | new CompressionPlugin( 425 | configureCompression() 426 | ), 427 | new BundleAnalyzerPlugin( 428 | configureBundleAnalyzer(MODERN_CONFIG), 429 | ), 430 | ] 431 | } 432 | ), 433 | ]; 434 | --------------------------------------------------------------------------------