cssoptimizationwebpackbundling-and-minificationcss-loader

Webpack CSS Output Is Always Minified


For my specific project, I need to control the minification of my CSS and only minify certain files. I am very close to a working solution using OptimizeCSSAssetsPlugin where I use the assetNameRegExp option to choose the CSS files I want to minify.

I have spent a while now trying to figure out why all my other CSS files are still being minified. It turns out that sass-loader will always minify your CSS when in production mode whether you want it to or not.

Here is my full webpack.config.js.

const CopyWebpackPlugin = require('copy-webpack-plugin');
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
const FractalWebpackPlugin = require('fractal-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const path = require('path');
const PrettierPlugin = require('prettier-webpack-plugin');
const StyleLintPlugin = require('stylelint-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = (env, argv) => {
    /**
     * Determine if is production mode from the command executed
     */
    const isProduction = argv.mode === 'production';

    /**
     * Common paths
     */
    const paths = {
        src: 'src',
        dev: 'public',
        prod: 'public'
    };

    /**
     * Generate the settings for webpack depending on if it is
     * development or production mode.
     */
    const settings = {
        mode: isProduction ? 'production' : 'development',
        outputDir: isProduction ? paths.prod : paths.dev,
        fractal: {
            mode: isProduction ? 'build' : 'server',
            sync: isProduction ? false : true
        }
    };

    return {
        // Mode is set by --mode property in command
        mode: settings.mode,

        /**
         * 3 entries:
         *      designSystem: This is Design System UI specific CSS
         *      website: This is website & component specific CSS
         *      app: This is the website & component specific JS
         */
        entry: {
            /**
             * Main website and Design System CSS files
             */
            designSystem: path.resolve(__dirname, `./${paths.src}/theme/scss/theme.scss`),
            website: path.resolve(__dirname, `./${paths.src}/scss/styles.scss`),

            /**
             * Specific enteries for all comonents to generate a CSS file specific to that component
             */
            headings: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/02-headings/headings.scss`),
            paragraphs: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/03-paragraphs/paragraphs.scss`),
            inlineElements: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/04-inline-elements/inline-elements.scss`),
            ordered: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/05-lists/ordered/ordered.scss`),
            unordered: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/05-lists/unordered/unordered.scss`),
            images: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/06-images/images.scss`),
            spacers: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/01-layout/02-spacers/spacers.scss`),
            primaryButton: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/02-buttons/primary-button/primary-button.scss`),
            secondaryButton: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/02-buttons/secondary-button/secondary-button.scss`),
            tertiaryButton: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/02-buttons/tertiary-button/tertiary-button.scss`),
            checkboxes: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/checkboxes/checkboxes.scss`),
            inputs: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/inputs/inputs.scss`),
            labels: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/labels/labels.scss`),
            radios: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/radios/radios.scss`),
            selects: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/selects/selects.scss`),
            textareas: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/textareas/textareas.scss`),
            footer: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/04-footer/footer.scss`),
            navigation: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/05-navigation/navigation.scss`),
            informationPanel: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/06-information-panel/information-panel.scss`),
            informationPill: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/07-information-pill/information-pill.scss`),
            modal: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/08-modal/modal.scss`),

            /**
             * Main website and Design System JS files
             */
            app: [
                'regenerator-runtime/runtime',
                'core-js/modules/es6.array.from',
                'core-js/modules/es6.array.for-each',
                'core-js/modules/es6.object.assign',
                'core-js/modules/es6.promise',
                path.resolve(__dirname, `./${paths.src}/js/app.js`)
            ]
        },

        /**
         * JS output goes into the scripts folder and depending on mode will
         * either go into the public or the dist folder with it's chunks
         */
        output: {
            path: path.resolve(__dirname, `./${settings.outputDir}`),
            filename: 'scripts/[name].js',
            chunkFilename: 'scripts/[name].chunk.js'
        },

        module: {
            rules: [
                {
                    parser: {
                        amd: false
                    }
                },
                {
                    /**
                     * Load JS files with Babel Loader and set to transpile code to work
                     * in IE10 and above.
                     */
                    test: /\.(js)$/,
                    exclude: /node_modules/,
                    use: [
                        {
                            loader: 'babel-loader',
                            options: {
                                configFile: './babel.config.js',
                                presets: [
                                    [
                                        '@babel/preset-env',
                                        {
                                            useBuiltIns: 'entry',
                                            corejs: '^3.1.4',
                                            targets: {
                                                browsers: ['defaults, ie >= 10']
                                            }
                                        }
                                    ]
                                ]
                            }
                        },
                        {
                            loader: 'eslint-loader',
                            options: {
                                configFile: '.eslintrc.json'
                            }
                        }
                    ]
                },
                {
                    /**
                     * Load SASS files with 2 loaders
                     *      PostCSS: This converts the SCSS to CSS, adds in polyfills for flexbox,
                     *               auto prefixes and adds in normalise CSS.
                     *      SASS Loader: This generates source maps for CSS.
                     */
                    test: /\.(scss|sass)$/,
                    use: [
                        {
                            loader: MiniCssExtractPlugin.loader
                        },
                        {
                            loader: 'css-loader',
                            options: {
                                sourceMap: true
                            }
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                plugins: () => [
                                    require('postcss-flexbugs-fixes'),
                                    require('postcss-preset-env')({
                                        autoprefixer: {
                                            flexbox: 'no-2009'
                                        },
                                        stage: 3
                                    }),
                                    require('autoprefixer')()
                                ],
                                sourceMap: true,
                                minimize: false
                            }
                        },
                        {
                            loader: 'sass-loader',
                            options: {
                                sourceMap: true,
                                minimize: false,
                                outputStyle: 'uncompressed'
                            }
                        }
                    ]
                },
                {
                    /**
                     * This looks for all images and uses the File Loader to move them to
                     * the output directory. It excludes the fonts directory so there is no
                     * duplication of SVG files
                     */
                    test: /\.(png|jpg|jpeg|gif|svg)$/,
                    exclude: /fonts/,
                    use: [
                        {
                            loader: 'file-loader',
                            options: {
                                name: '[folder]/[name].[ext]',
                                outputPath: '/images'
                            }
                        }
                    ]
                },
                {
                    /**
                     * This looks for all font files and uses the File Loader to
                     * move hem to the output directory. It excludes the images directory
                     * so there is no duplication of SVG files
                     */
                    test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                    exclude: /images/,
                    use: [
                        {
                            loader: 'file-loader',
                            options: {
                                name: '[folder]/[name].[ext]',
                                outputPath: '/fonts'
                            }
                        }
                    ]
                }
            ]
        },

        plugins: [
            /**
             * This prevents webpack from generating a JS file for SCSS entries
             */
            new FixStyleOnlyEntriesPlugin(),
            /**
             * Runs SASS linting
             */
            new StyleLintPlugin({
                configFile: '.stylelintrc.json',
                context: 'src',
                files: '**/*.scss',
                failOnError: false,
                quiet: false,
                emitErrors: true
            }),
            /**
             * This outputs SCSS entires into CSS files and thier chunks
             */
            new MiniCssExtractPlugin({
                filename: 'style/[name].css',
                chunkFilename: 'style/[name].chunk.css'
            }),
            /**
             * Runs Fractal in either server mode for dev and build mode for
             * production.
             */
            new FractalWebpackPlugin({
                mode: settings.fractal.mode,
                sync: settings.fractal.sync
            }),
            /**
             * Copies images over to the output directory
             */
            new CopyWebpackPlugin([
                {
                    from: path.resolve(__dirname, `./${paths.src}/images`),
                    to: 'images'
                }
            ]),
            // new PrettierPlugin()
        ],

        /**
         * This only runs when in production mode and will minify JS and CSS
         */
        optimization: {
            minimize: true,
            minimizer: [
                new OptimizeCSSAssetsPlugin({
                    assetNameRegExp: /style\/(website|designSystem).css/,
                    cssProcessor: require('cssnano')
                }),
                new TerserPlugin({
                    include: /\/js/,
                    exclude: /\/scss/
                })
            ]
        },

        /**
         * Generates source maps
         */
        devtool: 'source-maps'
    };
};

Solution

  • I finally figured out my issue and wanted to post the answer so if anyone else in the future comes across this issue they can solve it.

    Despite what my question says, to start with I actually didn't know which loader was causing the issue, after doing some research I initially thought css-loader was the culprit. I did some digging into the code and found that there is no minification in css-loader. The next loader to look into was sass-loader, after lots of research I eventually figured out sass-loader was doing the minification. Looking into the sass-loader docs I didn't seem to find any information about minification or how to stop it. After lots of googling, I eventually found the very poorly documented option outputStyle .

    From what I can find outputStyle takes 3 options:

    outputStyle: 'compressed'
    outputStyle: 'uncompressed'
    outputStyle: 'expanded'
    

    This was my magic option, while sass-loader seems to take no notice of minimize: false in webpack.config.js it will listen to the outputStyle option. This will turn off the minification for all CSS files. This then allows OptimizeCSSAssetsPlugin to come into play and minify the files you need.

    Here is the new sass-loader code:

    {
        loader: 'sass-loader',
        options: {
            sourceMap: true,
            minimize: false,
            outputStyle: 'expanded'
        }
    }
    

    Here is the full webpack.config.js

    const CopyWebpackPlugin = require('copy-webpack-plugin');
    const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
    const FractalWebpackPlugin = require('fractal-webpack-plugin');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    const path = require('path');
    const PrettierPlugin = require('prettier-webpack-plugin');
    const StyleLintPlugin = require('stylelint-webpack-plugin');
    const TerserPlugin = require('terser-webpack-plugin');
    
    module.exports = (env, argv) => {
        /**
         * Determine if it is production mode from the command executed
         */
        const isProduction = argv.mode === 'production';
    
        /**
         * Common paths
         */
        const paths = {
            src: 'src',
            dev: 'public',
            prod: 'public'
        };
    
        /**
         * Generate the settings for webpack depending on if it is
         * development or production mode.
         */
        const settings = {
            mode: isProduction ? 'production' : 'development',
            outputDir: isProduction ? paths.prod : paths.dev,
            fractal: {
                mode: isProduction ? 'build' : 'server',
                sync: isProduction ? false : true
            }
        };
    
        return {
            // Mode is set by --mode property in command
            mode: settings.mode,
    
            /**
             * 3 entries:
             *      designSystem: This is Design System UI specific CSS
             *      website: This is website & component specific CSS
             *      app: This is the website & component specific JS
             */
            entry: {
                /**
                 * Main website and Design System CSS files
                 */
                designSystem: path.resolve(__dirname, `./${paths.src}/theme/scss/theme.scss`),
                website: path.resolve(__dirname, `./${paths.src}/scss/styles.scss`),
    
                /**
                 * Specific enteries for all comonents to generate a CSS file specific to that component
                 */
                headings: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/02-headings/headings.scss`),
                paragraphs: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/03-paragraphs/paragraphs.scss`),
                inlineElements: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/04-inline-elements/inline-elements.scss`),
                ordered: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/05-lists/ordered/ordered.scss`),
                unordered: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/03-typography/05-lists/unordered/unordered.scss`),
                images: path.resolve(__dirname, `./${paths.src}/patterns/01-branding/06-images/images.scss`),
                spacers: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/01-layout/02-spacers/spacers.scss`),
                primaryButton: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/02-buttons/primary-button/primary-button.scss`),
                secondaryButton: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/02-buttons/secondary-button/secondary-button.scss`),
                tertiaryButton: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/02-buttons/tertiary-button/tertiary-button.scss`),
                checkboxes: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/checkboxes/checkboxes.scss`),
                inputs: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/inputs/inputs.scss`),
                labels: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/labels/labels.scss`),
                radios: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/radios/radios.scss`),
                selects: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/selects/selects.scss`),
                textareas: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/03-form-elements/textareas/textareas.scss`),
                footer: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/04-footer/footer.scss`),
                navigation: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/05-navigation/navigation.scss`),
                informationPanel: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/06-information-panel/information-panel.scss`),
                informationPill: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/07-information-pill/information-pill.scss`),
                modal: path.resolve(__dirname, `./${paths.src}/patterns/02-ui-components/08-modal/modal.scss`),
    
                /**
                 * Main website and Design System JS files
                 */
                app: [
                    'regenerator-runtime/runtime',
                    'core-js/modules/es6.array.from',
                    'core-js/modules/es6.array.for-each',
                    'core-js/modules/es6.object.assign',
                    'core-js/modules/es6.promise',
                    path.resolve(__dirname, `./${paths.src}/js/app.js`)
                ]
            },
    
            /**
             * JS output goes into the scripts folder and depending on mode will
             * either go into the public or the dist folder with it's chunks
             */
            output: {
                path: path.resolve(__dirname, `./${settings.outputDir}`),
                filename: 'scripts/[name].js',
                chunkFilename: 'scripts/[name].chunk.js'
            },
    
            module: {
                rules: [
                    {
                        parser: {
                            amd: false
                        }
                    },
                    {
                        /**
                         * Load JS files with Babel Loader and set to transpile code to work
                         * in IE10 and above.
                         */
                        test: /\.(js)$/,
                        exclude: /node_modules/,
                        use: [
                            {
                                loader: 'babel-loader',
                                options: {
                                    configFile: './babel.config.js',
                                    presets: [
                                        [
                                            '@babel/preset-env',
                                            {
                                                useBuiltIns: 'entry',
                                                corejs: '^3.1.4',
                                                targets: {
                                                    browsers: ['defaults, ie >= 10']
                                                }
                                            }
                                        ]
                                    ]
                                }
                            },
                            {
                                loader: 'eslint-loader',
                                options: {
                                    configFile: '.eslintrc.json'
                                }
                            }
                        ]
                    },
                    {
                        /**
                         * Load SASS files with 2 loaders
                         *      PostCSS: This converts the SCSS to CSS, adds in polyfills for flexbox,
                         *               auto prefixes and adds in normalise CSS.
                         *      SASS Loader: This generates source maps for CSS.
                         */
                        test: /\.(scss|sass)$/,
                        use: [
                            {
                                loader: MiniCssExtractPlugin.loader
                            },
                            {
                                loader: 'css-loader',
                                options: {
                                    sourceMap: true
                                }
                            },
                            {
                                loader: 'postcss-loader',
                                options: {
                                    plugins: () => [
                                        require('postcss-flexbugs-fixes'),
                                        require('postcss-preset-env')({
                                            autoprefixer: {
                                                flexbox: 'no-2009'
                                            },
                                            stage: 3
                                        }),
                                        require('autoprefixer')()
                                    ],
                                    sourceMap: true,
                                    minimize: false
                                }
                            },
                            {
                                loader: 'sass-loader',
                                options: {
                                    sourceMap: true,
                                    minimize: false,
                                    outputStyle: 'expanded'
                                }
                            }
                        ]
                    },
                    {
                        /**
                         * This looks for all images and uses the File Loader to move them to
                         * the output directory. It excludes the fonts directory so there is no
                         * duplication of SVG files
                         */
                        test: /\.(png|jpg|jpeg|gif|svg)$/,
                        exclude: /fonts/,
                        use: [
                            {
                                loader: 'file-loader',
                                options: {
                                    name: '[folder]/[name].[ext]',
                                    outputPath: '/images'
                                }
                            }
                        ]
                    },
                    {
                        /**
                         * This looks for all font files and uses the File Loader to
                         * move hem to the output directory. It excludes the images directory
                         * so there is no duplication of SVG files
                         */
                        test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
                        exclude: /images/,
                        use: [
                            {
                                loader: 'file-loader',
                                options: {
                                    name: '[folder]/[name].[ext]',
                                    outputPath: '/fonts'
                                }
                            }
                        ]
                    }
                ]
            },
    
            plugins: [
                /**
                 * This prevents webpack from generating a JS file for SCSS entries
                 */
                new FixStyleOnlyEntriesPlugin(),
                /**
                 * Runs SASS linting
                 */
                new StyleLintPlugin({
                    configFile: '.stylelintrc.json',
                    context: 'src',
                    files: '**/*.scss',
                    failOnError: false,
                    quiet: false,
                    emitErrors: true
                }),
                /**
                 * This outputs SCSS entires into CSS files and thier chunks
                 */
                new MiniCssExtractPlugin({
                    filename: 'style/[name].css',
                    chunkFilename: 'style/[name].chunk.css'
                }),
                /**
                 * Runs Fractal in either server mode for dev and build mode for
                 * production.
                 */
                new FractalWebpackPlugin({
                    mode: settings.fractal.mode,
                    sync: settings.fractal.sync
                }),
                /**
                 * Copies images over to the output directory
                 */
                new CopyWebpackPlugin([
                    {
                        from: path.resolve(__dirname, `./${paths.src}/images`),
                        to: 'images'
                    }
                ]),
                // new PrettierPlugin()
            ],
    
            /**
             * This only runs when in production mode and will minify JS and CSS
             */
            optimization: {
                minimize: true,
                minimizer: [
                    new OptimizeCSSAssetsPlugin({
                        assetNameRegExp: /style\/(website|designSystem).css/,
                        cssProcessor: require('cssnano')
                    }),
                    new TerserPlugin({
                        include: /\/js/,
                        exclude: /\/scss/
                    })
                ]
            },
    
            /**
             * Generates source maps
             */
            devtool: 'source-maps'
        };
    };