url-loader / file-loader breaking relative paths in css output using webpack

12,983

Solution 1

SOLVED

Solved thanks to this post on github: https://github.com/webpack-contrib/mini-css-extract-plugin/issues/44#issuecomment-379059788.

Simply add the publicPath option to the MiniCssExtractPlugin like so:

...
{
    test: /\.scss$/,
    use: [
        {
            loader: MiniCssExtractPlugin.loader,
            options: {
                publicPath: '../../' // path to director where assets folder is located
            }
        },
        {
            loader: 'css-loader',
            options: {
                sourceMap: devMode,
                importLoaders: 2
            }
        },
        {
            loader: 'sass-loader',
            options: {
                sourceMap: devMode
            }
        }
    ]
},
...

To use the style-loader in development mode instead of MiniCssExtractPlugin like in my original webpack.config.js you will have to add the option to conditionally, because style-loader doesn't accept a publicPath option. I did so at the bottom of webpack.config.js like so:

if (!devMode) {
    module.exports.module.rules[0].use[0].options = { publicPath: '../../' };
}

Then make sure the first object in the rules array is for scss. Kind of a messy way to add this conditionally but it will do for now.

Solution 2

Spent ages on this! publicPath setting below was missing!

 output: {
     publicPath: '/'
 },

Solution 3

Try adding the publicPath option

 {
    loader: 'url-loader',
    options: {
        limit: 8192,
        name: "[name].[ext]"
        publicPath: '/assets/img/   //<-- assuming assets is in web root
    }
 }

And change style.scss to

body {
    background: url('background.jpg');
}

Solution 4

Here is my solution:

const devMode = process.env.NODE_ENV !== 'production';

...rules: [
        {
            test: /\.scss$/,
            use: [
                devMode ? 'style-loader' :
                {
                loader: MiniCssExtractPlugin.loader,
                    options: {
                        publicPath: '../',
                    }
                },
                {
                    // translates CSS into CommonJS
                    loader: 'css-loader',
                    options: {
                        sourceMap: true,
                    },
                },
                {
                    // Autoprefixer usw.
                    loader: 'postcss-loader',
                    options: {
                        ident: 'postcss',
                    },
                },
                {
                    // compiles Sass to CSS, using Node Sass by default
                    loader: 'sass-loader',
                    options: {
                        sourceMap: true,
                    },
                }
            ],
        },
    ]
Share:
12,983
Jordan Walker
Author by

Jordan Walker

Updated on June 08, 2022

Comments

  • Jordan Walker
    Jordan Walker almost 2 years

    I am using webpack with some plugins and loaders to take my src/ folder and build a dist/ folder. url-loader (which falls back to file-loader when images are larger than a specific limit) is outputting images it finds in my html and scss files to the correct directory as expected. However, it changes the relative paths in those files and in doing so outputs a css file with an incorrect path.

    File structure:

    src/
        index.html
        assets/
            js/
                index.js
            scss/
                style.scss
            img/
                pic.jpg
                background.jpg
    
    dist/
        index.html
        assets/
            js/
                index.js
            css/
                style.css
            img/
                pic.jpg
                background.jpg
    

    As you can see my dist/ folder mirrors my src/ folder except that scss is compiled to css.

    src/index.js imports index.html and style.scss so that those files can be parsed by webpack and any images in them can be handled by url-loader:

    index.js

    import '../../index.html';
    import '../scss/style.scss';
    

    style.scss sets a background image on the body using a relative path:

    style.scss

    body {
        background: url('../img/background.jpg');
    }
    

    index.html just displays an image, also using a relative path:

    index.html

    <img src="./assets/img/pic.jpg" alt="pic">
    

    I use HtmlWebpackPlugin to copy across my html files, since it allows me to specify which chunks to automatically include as script tags. For the css, I either inject it into the html files with style-loader in development, or extract it into its own file in production with MiniCssExtractPlugin.

    However, when webpack parses index.html and style.scss, the relative paths are replaced with 'assets/img/pic.jpg' and 'assets/img/backgorund.jpg' respectively. This doesn't break the path in index.html since it happens to be effectively the same relative path, but it is clearly a problem for style.css. How would I stop url-loader from changing the relative paths, or just generate the correct ones? Also note that when the css is injected into the html with style-loader in development, the path works since it is then relative to the html file. Ideally webpack should be able to generate the correct relative path depending on whether I extract the css in production or inject it in development.

    I've tried using resolve-url-loader, specifying publicPath and outputPath, and of course searching for answers online but have had no luck.

    webpack.config.js

    const path = require('path');
    const webpack = require('webpack');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    
    const devMode = process.env.NODE_ENV !== 'production';
    
    module.exports = {
        mode: devMode ? 'development' : 'production',
        entry: {
            index: './src/assets/js/index.js',
        },
        output: {
            filename: 'assets/js/[name].js',
            path: path.resolve(__dirname, 'dist')
        },
        devServer: {
            contentBase: path.join(__dirname, 'src'),
            watchContentBase: true,
            hot: devMode,
        },
        devtool: devMode ? 'source-map' : '(none)',
        plugins: [
            new CleanWebpackPlugin(['dist']),
            new HtmlWebpackPlugin({
                filename: 'index.html',
                template: 'src/index.html',
            }),
            new MiniCssExtractPlugin({
                filename: 'assets/css/style.css',
            })
        ],
        module: {
            rules: [
                {
                    test: /\.html$/,
                    use: [
                        {
                            loader: 'html-loader'
                        }
                    ]
                },
                {
                    test: /\.(jp(e?)g|png|svg|gif)$/,
                    use: [
                        {
                            loader: 'url-loader',
                            options: {
                                limit: 8192,
                                name: 'assets/img/[name].[ext]'
                            }
                        }
                    ]
                },
                {
                    test: /\.scss$/,
                    use: [
                        {
                            loader: devMode ? 'style-loader' : MiniCssExtractPlugin.loader
                        },
                        {
                            loader: 'css-loader',
                            options: {
                                sourceMap: devMode,
                                importLoaders: 2
                            }
                        },
                        {
                            loader: 'sass-loader',
                            options: {
                                sourceMap: devMode
                            }
                        }
                    ]
                }
            ]
        }
    };
    
    if (devMode) {
        module.exports.plugins.push(new webpack.HotModuleReplacementPlugin());
    }
    
  • Jordan Walker
    Jordan Walker over 5 years
    Then webpack fails to find the image from the style.scss file since it looks for it in the same folder. (Module not found: Error: Can't resolve './background.jpg' in '/Users/jlwalker/demo/src/assets/scss'). Also it makes the paths in the dist/ output files absolute paths, whereas I would ideally prefer to use relative paths.
  • varoons
    varoons over 5 years
    What was the issue with resolve-url-loader. Did you add it above sass-loader?
  • Jordan Walker
    Jordan Walker over 5 years
    As far as I'm aware resolve-url-loader allows you to use paths in your sass partials that are relative to those partials, instead of being relative to the sass file which imports those partials. I was having a different issue. But I've managed to get it working now, thanks to this comment. Will update my post soon. Thanks for looking into it for me anyway!
  • Joel Davey
    Joel Davey almost 4 years
    Works for me with file-loader