webpack / webpack

A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.
https://webpack.js.org
MIT License
64.67k stars 8.8k forks source link

Webpack 5 "Production" mode breaks url resolution/resolve-url-loader for .scss files calling .scss files in another directory containing url(...) #13468

Closed pmurphy-cs closed 3 years ago

pmurphy-cs commented 3 years ago

Bug report

What is the current behavior?

Enabling production mode breaks url resolution/resolve-url-loader for scss files calling scss files in another directory containing url(...).

Here's an example of the directory structure/import order that's currently failing (dir/filenames changed to protect the innocent)

import order: fileThatWebpackThinksIsInRootFolder.scss -> file3.scss -> file2.scss -> fileWhereUrlIsActuallyBeingUsed.scss

This import chain works fine in development mode, and was working in production mode when we were on Webpack 4. After upgrading to WP5, it no longer works in production mode, and instead of looking for imgThatIsImported.png in src/css/base, the build is erroring out because it's expecting it in src/js/app/styles

Furthermore, if we follow the official mode documentation here, https://webpack.js.org/configuration/mode/#root, and manually enable all of the documented optimizations/plugins that production mode should enable, our build still passes when the mode flag is set to 'none' or 'development'. This makes it seem as if the documentation for what the mode settings generate for a config is currently out of date.

If the current behavior is a bug, please provide the steps to reproduce.

We have 3 webpack configs, common, dev, and prod

webpack.common

'use strict';

const { merge } = require('webpack-merge');
const path = require('path');
const webpack = require('webpack');

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const cultureMap = require('./src/js/common/localization/culture-map');
const supportedLocales = Object.values(cultureMap.dateCultures);
// uncomment the below import and the call to new BundleAnalyzerPlugin() below
// to get a bundle visualization when running build-dev
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const baseConfig = {
    entry: {
        /* we have ~30 entry points for different sub-applications */
    },
    externals: {
        jquery: 'jQuery',
        ga: 'ga' // Google Analytics
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.scss$/,
                use: [
                    process.env.NODE_ENV === 'development' ?  MiniCssExtractPlugin.loader : 'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            sourceMap: true,
                            url: false,
                            modules: {
                                localIdentName: '[name]-[local]-[hash:base64:5]'
                            },
                            importLoaders: 3
                        }
                    },
                    { loader: 'postcss-loader', options: { sourceMap: true }  },
                    { loader: 'resolve-url-loader', options: { sourceMap: true } },
                    { loader: 'sass-loader', options: { sourceMap: true } },
                ]
            },
            {
                test: /\.(ttf|eot|svg|woff(2)?)(\S+)?$/,
                use: 'file-loader?name=[name].[ext]'
            }
        ]
    },
    optimization: {
        runtimeChunk: {
            name: 'runtime'
        },
        splitChunks: {
            cacheGroups: {
                vendor: {
                    name: function (module) {
                        // We are splitting our vendor file into 2 to keep the size a bit more manageable
                        // This will also hopefully keep vendor-core a bit more stable since those libraries should not change often
                        return /[\\/]node_modules[\\/](connected-react-router|core-js|date-fns|history|ramda|react|react-dom|react-redux|react-router|react-router-dom|redux)[\\/]/.test(
                            module.context
                        )
                            ? 'vendor-core'
                            : 'vendor';
                    },
                    chunks: 'all',
                    minChunks: 2,
                    enforce: true,
                    test: /[\\/]node_modules[\\/]/,
                    priority: 20
                },
                common: {
                    name: 'common',
                    chunks: 'all',
                    minChunks: 3,
                    test: /[j|t]sx?$/,
                    reuseExistingChunk: true,
                    enforce: true,
                    priority: 10
                },
                styles: {
                    name: 'common',
                    enforce: true,
                    chunks: 'all',
                    test: /\.scss$/
                }
            }
        },
    },
    plugins: [
        new MiniCssExtractPlugin({
            // Options similar to the same options in webpackOptions.output
            // both options are optional
            filename: '../../css/components/[name].css',
            chunkFilename: '../../css/components/[id].css'
        }),
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            ga: 'ga'
        }),
        new webpack.NoEmitOnErrorsPlugin(),
        new webpack.optimize.ModuleConcatenationPlugin(),
        new webpack.ContextReplacementPlugin(
            /date\-fns[\/\\]/,
            new RegExp(`[/\\\\\](${supportedLocales.join('|')})[/\\\\\]`)
        ),
        // include the below during dev if troubleshooting bundle size
        // new BundleAnalyzerPlugin({ generateStatsFile: true })
    ],
    resolve: {
        alias: {
            common: path.resolve(__dirname, 'src/js/common'),
            css: path.resolve(__dirname, 'src/css'),
        },
        extensions: ['.json', '.js', '.jsx', '.ts', '.tsx']
    }
};

module.exports = {
    modern: merge(baseConfig, {
        output: {
            path: path.join(__dirname, '..', 'static', 'js', 'modern'),
            publicPath: '/public/static/js/modern/',
            pathinfo: false
        },
        module: {
            rules: [
                {
                    test: /\.[t|j]sx?$/,
                    loader: 'babel-loader',
                    include: path.join(__dirname, 'src', 'js'),
                    options: {
                        cacheDirectory: true,
                        presets: [
                            [
                                '@babel/preset-env',
                                {
                                    modules: false,
                                    corejs: 3,
                                    useBuiltIns: 'usage',
                                    targets: {
                                        esmodules: true
                                    }
                                }
                            ]
                        ]
                    }
                }
            ]
        }
    }),
    legacy: merge(baseConfig, {
        output: {
            path: path.join(__dirname, '..', 'static', 'js', 'legacy'),
            publicPath: '/public/static/js/legacy/'
        },
        module: {
            rules: [
                {
                    test: /\.[t|j]sx?$/,
                    loader: 'babel-loader',
                    include: path.join(__dirname, 'src', 'js')
                }
            ]
        }
    })
};
webpack.dev

'use strict';

const { merge } = require('webpack-merge');
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const common = require('./webpack.common.js');

const devOutput = {
    mode: 'development',
    devtool: 'eval-cheap-module-source-map',
    output: {
        chunkFilename: '[id].js',
        filename: '[name].js'
    }
};

module.exports = [
    merge(common.legacy, devOutput),
    merge(common.modern, devOutput)
];
webpack.prod

'use strict';

const { merge } = require('webpack-merge');
const webpack = require('webpack');
const path = require('path');
const SaveAssetsJson = require('assets-webpack-plugin');

const common = require('./webpack.common.js');

const prodOutput = {
    mode: 'none',
    devtool: 'nosources-source-map',
    output: {
        filename: '[name].[contenthash].js'
    }
};

module.exports = [
    merge(common.legacy, prodOutput, {
        plugins: [
            new SaveAssetsJson({
                filename: 'webpack-assets-legacy.json',
                path: path.resolve('../static/'),
                includeAllFileTypes: false
            }),
        ]
    }),
    merge(common.modern, prodOutput, {
        plugins: [
            new SaveAssetsJson({
                filename: 'webpack-assets-modern.json',
                path: path.resolve('../static/'),
                includeAllFileTypes: false
            }),
        ]
    })
];

What is the expected behavior?

urls in .scss files that are called by other .scss files that don't share the same parent directory should still resolve properly in production mode in webpack 5

At the very least, mode documentation should be updated to reflect the exact config changes that each mode makes in webpack 5 so that devs can at least troubleshoot optimizations/plugins in an informed way.

Other relevant information: webpack version: 5.35.1 Node.js version: 14.17.0 Operating System: Windows 10 Additional tools:

webpack-bot commented 3 years ago

For maintainers only:

alexander-akait commented 3 years ago

Not related to webpack itself, please open an issue in resolve-url-loader or please use discussion and provide example of repo and I will look and say where problem. Problems with url in sass/scss described here https://github.com/webpack-contrib/sass-loader#problems-with-url and resolve-url-loader have a lot of limitation, I strongly recommend do not use resolve-url-loader.

You can put link on source code here or provide example of the problem. But I am closing because webpack don't touch your CSS files.

alexander-akait commented 3 years ago

Also please remove webpack.optimize.ModuleConcatenationPlugin from configuration, two plugins will break your JS, it is enabled by default in production mode

alexander-akait commented 3 years ago

Nothing from optimizations/plugins do not change your url in CSS

pmurphy-cs commented 3 years ago

Hey @alexander-akait , thanks for the quick reply.

By any chance do you have a list of the config changes that the mode flag will make for "production" | "development" | "none" in webpack 5? It appears the current documentation no longer accurately reflects what will be enabled/disabled

alexander-akait commented 3 years ago

@pmurphy-cs just more plugins for JS optimization, but it is 99.99% is not a problem for you here, none of these plugins touch url in CSS

Sets process.env.NODE_ENV on DefinePlugin to value production. Enables deterministic mangled names for modules and chunks, FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin and TerserPlugin.

It is right