πŸ‘‹ Ahoj πŸ‡¨πŸ‡Ώ

Webpack logo
ReactiveConf 2019

Secret Tips to Improve Your Webpack Config.

Photo by Ryan Lum on Unsplash

#1
You don't need a webpack config.

                            
                            const {resolve} = require("path");

                            module.exports = {
                                entry: resolve(process.cwd(), "src", "index.js"),
                                output: {
                                    path: resolve(process.cwd(), "dist"),
                                    filename: "main.js",
                                },
                            };
                            
                        

Possible modes are:

  • development
  • production
  • none
                            
                            webpack --mode development
                            
                        
                            
                            // webpack.config.js
                            module.exports = {
                                mode: "development",
                            };
                            
                        

#2
TypeScript support.

How to use TypeScript in your webpack config:

  1. Rename your webpack.config.js to webpack.config.ts.
  2. Install @types/webpack and ts-node.
  3. Create a variable using the webpack.Configuration type.
                            
                            import webpack from "webpack";

                            const config: webpack.Configuration = {
                                // ...
                            };

                            export default config;
                            
                        
                            
                            {
                                "compilerOptions": {
                                    "esModuleInterop": true
                                }
                            }
                            
                        

How does it work?

  • This is a webpack-cli feature.
  • webpack-cli uses interpret which maintains a dictionary of file extensions and associated module loaders.
  • ts-node needs to be available in your node_modules.

This means that you can also use Babel to precompile your webpack.config.js.

Rename it to webpack.config.babel.js and make sure that there is also a Babel config in your project directory.

#3
Function as config.

                            
                                module.exports = () => {
                                    return {
                                        mode: "development",
                                    };
                                };
                            
                        
Where is that useful? πŸ€”
                            
                                webpack --env.mode=production --env.debug
                            
                        
module.exports = (env) => {










};
module.exports = (env = {}) => {










};
module.exports = (env = {}) => {
    const {
        mode,
        debug,
    } = env;






};
module.exports = (env = {}) => {
    const {
        mode = "development",
        debug = false,
    } = env;






};
module.exports = (env = {}) => {
    const {
        mode = "development",
        debug = false,
    } = env;


    return {
        mode,
        output: { pathinfo: debug === true },
    };
};

This feature is called Environment Options.

#4
Multi-compiler mode.


                            module.exports = [
                                {
                                    name: "web",
                                    target: "web",
                                    output: { path: resolve(__dirname, "dist", "web") },
                                },
                                {
                                    name: "node",
                                    target: "node",
                                    output: { path: resolve(__dirname, "dist", "node") },
                                },
                            ];
                        
What exactly is this multi-compiler mode? πŸ€”

In multi-compiler mode webpack builds each config concurrently in the same process while re-using the file system cache.

Multi-compiler mode does not execute each build in parallel.


parallel-webpack

#5
Async webpack configs.


                            module.exports = async () => {
                                // load some config values from database
                                return {
                                    // ...
                                };
                            };
                        
Time for a little quiz!

Question: Is the following config valid?


                            module.exports = {
                                entry: async () => loadEntriesFromDb(),
                            };
                        

Answer: Yes, it's valid! πŸŽ‰πŸŽ‰πŸŽ‰


                            module.exports = {
                                entry: async () => loadEntriesFromDb(),
                            };
                        

Question: Is the following config valid?


                            module.exports = {
                                output: {
                                    path: fs.createWriteStream("/some/path"),
                                },
                            };
                        
Answer: No πŸ˜›
                            
                            webpack --watch
                            
                        

nodemon

                            
                                nodemon --exec "webpack --watch"
                            
                        

nodemon

                            
                                nodemon --watch webpack.config.js --exec "webpack --watch"
                            
                        

Only watch config files!

#6
Use nodemon.

#7
Use require.resolve()


                            module.exports = {
                                resolve: {
                                    alias: {
                                        "/does/not/exist.js": "./src/replaced.js",
                                    },
                                },
                            };
                        

                            module.exports = {
                                resolve: {
                                    alias: {
                                        [require.resolve("./src/index.js")]:
                                            require.resolve("./src/replaced.js"),
                                    },
                                },
                            };
                        

#8
Use .filter(Boolean)

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                loader: "style-loader",
            },
        ],
    },
};
module.exports = {
    module: {
        rules: [
            isDev && {
                test: /\.css$/,
                loader: "style-loader",
            },
        ].filter(Boolean),
    },
};

[ ... ].filter(Boolean) removes all falsy values from an array.


                            module.exports = {
                                plugins: [
                                    analyze && new BundleAnalyzerPlugin(),
                                ].filter(Boolean),
                            };
                        

#9
Avoid config logic.


                            const rules = [];
                            const plugins = [];

                            if (isDev) {
                                rules.unshift({
                                    test: /\.css$/,
                                    loader: "style-loader",
                                });
                            }
                            if (analyze) {
                                plugins.push(new BundleAnalyzerPlugin());
                            }

                            module.exports = {
                                module: {
                                    rules,
                                },
                                plugins
                            };
                        

                            module.exports = {
                                module: {
                                    rules: [
                                        isDev && {
                                            test: /\.css$/,
                                            loader: "style-loader",
                                        },
                                    ].filter(Boolean),
                                },
                                plugins: [
                                    analyze && new BundleAnalyzerPlugin(),
                                ].filter(Boolean),
                            };
                        

                            const webpackMerge = require("webpack-merge");
                            const devConfig = {
                                module: {
                                    rules: [{
                                        test: /\.css$/,
                                        loader: "style-loader",
                                    }]
                                }
                            };
                            module.exports = webpackMerge(
                                { /* base config */ },
                                mode === "development" ? devConfig : prodConfig
                            );
                        

#10
Use module.rules[].oneOf.

What does the module.rules[].oneOf option do? πŸ€”

Three important things to remember about loaders:

  • The order of rules is important
  • Loaders are executed from bottom to top and from right to left
  • Every rule that matches gets applied
Time for a little quiz!

Given this rules config, what loaders will be executed on a .js module and in which order?


                            rules: [{
                                use: ["a-loader", "b-loader"],
                            }, {
                                use: ["c-loader"],
                            }],
                        
  1. b a
  2. a b c
  3. c b a
  4. none

Answer: 3.
Remember: From bottom to top and from right to left.


                            rules: [{
                                use: ["a-loader", "b-loader"],
                            }, {
                                use: ["c-loader"],
                            }],
                        

c-loader β†’  b-loader β†’  a-loader


                            rules: [{
                                oneOf: [{
                                    use: ["a-loader", "b-loader"],
                                }, {
                                    use: ["c-loader"],
                                }],
                            }],
                        

Now the answer is:
b-loader β†’  a-loader

#11
test for file extensions, include for absolute paths.

rules: [{
    include: [path.resolve(__dirname, "src")],



    use: ["ts-loader"],
}],
                        
rules: [{
    include: [path.resolve(__dirname, "src")],
    exclude: [/node_modules/],


    use: ["ts-loader"],
}],
                        
rules: [{
    include: [path.resolve(__dirname, "src")],
    exclude: [/node_modules/],
    test: [/\.ts$/, /\.tsx$/],

    use: ["ts-loader"],
}],
                        
rules: [{
    include: [path.resolve(__dirname, "src")],
    exclude: [/node_modules/],
    test: [/\.ts$/, /\.tsx$/],
    resource: [path.resolve(__dirname, "src")],
    use: ["ts-loader"],
}],
                        
rules: [{

    test: [/\.ts$/, /\.tsx$/],
    use: ["ts-loader"],
}],
                        
  • Use test for file extensions
  • Use include for directories and absolute paths
rules: [{
    include: [path.resolve(__dirname, "src")],
    test: [/\.ts$/, /\.tsx$/],
    use: ["ts-loader"],
}],
                        
  • Use test for file extensions
  • Use include for directories and absolute paths

Pro tip 😎: always use include

rules: [{
    include: [
        path.resolve(__dirname, "src"),

    ],
    test: /\.js$/,
    use: ["babel-loader"],
}],
                        
rules: [{
    include: [
        path.resolve(__dirname, "src"),
        path.resolve(__dirname, "node_modules", "modern-module"),
    ],
    test: /\.js$/,
    use: ["babel-loader"],
}],
                        

#12
"How to apply different loaders on the same module" Pt 1.

react-svg-loader


                                <svg height="100" width="100">
                                    <circle cx="50" cy="50" r="40" fill="red" />
                                </svg>
                            

                                export default () => (
                                    <svg height="100" width="100">
                                        <circle cx="50" cy="50" r="40" fill="red" />
                                    </svg>
                                );
                            

                            import CircleComponent from "./circle.svg";

                            const component = <CircleComponent />;
                        

                            body {
                                background: url("./circle.svg");
                            }
                        

Should webpack transform the module with the react-svg-loader or with the file-loader πŸ€”?

Solution #1


                            import CircleComponent from "./circle.svg?component";

                            const component = <CircleComponent />;
                        

                                rules: [{
                                    oneOf: [{
                                        test: /\.svg$/,
                                        resourceQuery: "?component",
                                        use: ["react-svg-loader"],
                                    }, {
                                        test: /\.svg$/,
                                        use: ["file-loader"],
                                    }]
                                }],
                            

#12
"How to apply different loaders on the same module" Pt 2.

Solution #2


                                rules: [{
                                    test: /\.svg$/,
                                    issuer: /\.jsx$/,
                                    use: ["react-svg-loader"],
                                },
                                {
                                    test: /\.svg$/,
                                    issuer: /\.css$/,
                                    use: ["file-loader"],
                                }],
                            

                            import CircleComponent from "./circle.svg";

                            const component = <CircleComponent />;
                        

                            body {
                                background: url("./circle.svg");
                            }
                        

Recap.

  • #1: You don't need a webpack config
  • #2: TypeScript support
  • #3: Function as config
  • #4: Multi-compiler mode
  • #5: Async webpack configs
  • #6: Use nodemon
  • #7: Use require.resolve()
  • #8: Use .filter(Boolean)
  • #9: Avoid config logic
  • #10: Use module.rules[].oneOf
  • #11: test for file extensions, include for absolute paths
  • #12: How to apply different loaders on the same module
    • Use the resourceQuery condition
    • Use the issuer condition

#13
Secret bonus.

All of these secrets are published on webpack.js.org.

github.com/jhnns/webpack-config-tips
πŸ™‡
@jhnnns @TheLarkInn