├── 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 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------