javascripthtmlwebpackwebpack-file-loaderwebpack-html-loader

Webpack 4 generates wrong src path for img tags in different levels of folder structure


I have a Webpack 4 project to make a multi language admin-dashboard with this folder and file structure:

admin-dashboard
|
|--build
|  |--assets
|  |  |--img
|  |  |--font
|  |--fa
|  |  |--index.html ----> rtl html output (gets wrong img src like "assets/img/img.png")
|  |--index.html    ----> ltr html output (gets right img src like "assets/img/img.png")
|  |--style.css
|  |--style-rtl.css
|  |--script.js
|  |--script-rtl.js
|
|--config   ---> containing my Webpack config files for production and development
|  |--webpack.dev.js
|  |--webpack.prod.js
|
|--node_modules
|--src
|  |--assets
|  |  |--img
|  |  |--font
|  |
|  |--i18n
|  |  |--fa
|  |  |  |--index.html ----> rtl html template ---> <img src="../../assets/img/img.png" />
|  |  |--index.html    ----> ltr html template ---> <img src="../assets/img/img.png" />
|  |
|  |--js
|  |--locales
|  |--scss
|  |--templates
|
|--.babelrc
|--package.json
|--postcss.config.js

The paths of images in i18n folder in my src folder are correct while developing the project but when I build the project it makes a build folder in root of project with its own assets folder. As you can see in file structure above, the ltr html file in build folder has the correct path to assets folder placed in build folder but the rtl version in fa folder refers to the same path which is wrong. These 2 files make based on 2 html files in src/i18n. Plus all links to favicon images in header of the both html files remain intact in built version of files which is another problem but other injected links done by Webpack html plugin all injected correctly with correct paths in both html files.

This is my webpack configuration for production mode: webpack.prod.js:

const webpack = require('webpack');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  entry: {
    main: './src/js/main.js',
    'main-rtl': './src/js/main-rtl.js',
  },
  output: {
    path: path.join(__dirname, '../build'),
    filename: '[name].[chunkhash:8].bundle.js',
    chunkFilename: '[name].[chunkhash:8].chunk.js',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader', 
        },
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader', 
          'postcss-loader', 
          'sass-loader',
        ],
      },
      {
        test: /\.(png|svg|jpe?g|gif)$/i,
        use: [
          {
            loader: 'file-loader', 
            options: {
              name: '[name].[ext]',
              outputPath: 'assets/img',
              esModule: false,
            },
          },
        ],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader', 
            options: {
              name: '[name].[ext]',
              outputPath: 'assets/font',
            },
          },
        ],
      },
      {
        test: /\.html$/i,
        use: {
          loader: 'html-loader',
          options: {
            attributes: {
              list: [
                {
                  tag: 'img',
                  attribute: 'src',
                  type: 'src',
                },
                {
                  tag: 'img',
                  attribute: 'srcset',
                  type: 'srcset',
                },
                {
                  tag: 'img',
                  attribute: 'data-src',
                  type: 'src',
                },
                {
                  tag: 'img',
                  attribute: 'data-srcset',
                  type: 'srcset',
                },
              ],

            },
          },
        },
      },
    ],
  },
  optimization: {
    minimizer: [new TerserJSPlugin(), new OptimizeCSSAssetsPlugin()],
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
      chunks: 'all',
    },
    runtimeChunk: {
      name: 'runtime',
    },
  },
  plugins: [
    // load jQuery
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),

    new CleanWebpackPlugin(),

    new MiniCssExtractPlugin({
      filename: '[name].[chunkhash:8].bundle.css',
      chunkFilename: '[name].[chunkhash:8].chunk.css',
    }),

    new HtmlWebpackPlugin({
      chunks: ['main'],
      template: 'src/i18n/index.html',
      filename: 'index.html',
    }),
    new HtmlWebpackPlugin({
      chunks: ['main-rtl'],
      template: 'src/i18n/fa/index.html',
      filename: 'fa/index.html',
    }),

  ],
};

So, the problem is when I build the project with my webpack.prod.js config, it gives me 2 html files in build folder for ltr and rtl directions which in rtl version index.html it generates wrong src path for img tags and in both files it doesn't change the paths of favicon links and the mentioned links remain the same as source html files but other links which are injected with Webpack html plugin are all correctly injected with correct paths in each html file.

I need 2 html files generate with correct img tag src paths based on any level which the file is in the folder structure.

any help will be so much appreciated.


Solution

  • I did a lot of research, find nothing helpful for the problem. So, I came up with this idea of having two separate production webpack config files for my ltr and rtl outputs of my project. Therefore, I modified my webpack.prod.js file like this:

    const webpack = require('webpack');
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const TerserJSPlugin = require('terser-webpack-plugin');
    const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    
    module.exports = {
      entry: {
        main: './src/js/main.js' // ---> the only entry we have is main
      },
      output: {
        path: path.join(__dirname, '../build'), 
        filename: '[name].[chunkhash:8].bundle.js',
        chunkFilename: '[name].[chunkhash:8].chunk.js',
      },
      mode: 'production',
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader', 
            },
          },
          {
            test: /\.(sa|sc|c)ss$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader', 
              'postcss-loader', 
              'sass-loader',
            ],
          },
          {
            test: /\.(png|svg|jpe?g|gif)$/i,
            use: [
              {
                loader: 'file-loader', 
                options: {
                  name: '[name].[ext]',
                  outputPath: 'assets/img',
                  esModule: false,
                },
              },
            ],
          },
          {
            test: /\.(woff|woff2|eot|ttf|otf)$/,
            use: [
              {
                loader: 'file-loader', 
                options: {
                  name: '[name].[ext]',
                  outputPath: 'assets/font',
                },
              },
            ],
          },
          {
            test: /\.html$/i,
            use: {
              loader: 'html-loader',
              options: {
                attributes: {
                  list: [
                    {
                      tag: 'img',
                      attribute: 'src',
                      type: 'src',
                    },
                    {
                      tag: 'img',
                      attribute: 'srcset',
                      type: 'srcset',
                    },
                    {
                      tag: 'img',
                      attribute: 'data-src',
                      type: 'src',
                    },
                    {
                      tag: 'img',
                      attribute: 'data-srcset',
                      type: 'srcset',
                    },
                  ],
    
                },
              },
            },
          },
        ],
      },
      optimization: {
        minimizer: [new TerserJSPlugin(), new OptimizeCSSAssetsPlugin()],
        splitChunks: {
          cacheGroups: {
            commons: {
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors',
              chunks: 'all',
            },
          },
          chunks: 'all',
        },
        runtimeChunk: {
          name: 'runtime',
        },
      },
      plugins: [
        // load jQuery
        new webpack.ProvidePlugin({
          $: 'jquery',
          jQuery: 'jquery',
        }),
    
        new CleanWebpackPlugin(),
    
        new MiniCssExtractPlugin({
          filename: '[name].[chunkhash:8].bundle.css',
          chunkFilename: '[name].[chunkhash:8].chunk.css',
        }),
    
        new HtmlWebpackPlugin({
          chunks: ['main'],
          template: 'src/i18n/index.html',
          filename: 'index.html',
        }),
        // new HtmlWebpackPlugin({
          // chunks: ['main-rtl'],
          // template: 'src/i18n/fa/index.html',    ----> this part removed too
          // filename: 'fa/index.html',
        // }),
    
      ],
    };
    

    Then I made another webpack config file named webpack.prod.rtl.js for rtl output of my project and changed the outputPath of my project file-loader and also I modified filename in my HtmlWebpackPlugin as you can see below:

    const webpack = require('webpack');
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const TerserJSPlugin = require('terser-webpack-plugin');
    const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    
    module.exports = {
      entry: {
        'main-rtl': './src/js/main-rtl.js', // main-rtl instead of main
      },
      output: {
        path: path.join(__dirname, '../build/fa'),
        filename: '[name].[chunkhash:8].bundle.js',
        chunkFilename: '[name].[chunkhash:8].chunk.js',
      },
      mode: 'production',
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader', 
            },
          },
          {
            test: /\.(sa|sc|c)ss$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader', 
              'postcss-loader', 
              'sass-loader',
            ],
          },
          {
            test: /\.(png|svg|jpe?g|gif)$/i,
            use: [
              {
                loader: 'file-loader', 
                options: {
                  name: '[name].[ext]',
                  outputPath: '../assets/img',
                  esModule: false,
                },
              },
            ],
          },
          {
            test: /\.(woff|woff2|eot|ttf|otf)$/,
            use: [
              {
                loader: 'file-loader', 
                options: {
                  name: '[name].[ext]',
                  outputPath: '../assets/font',
                },
              },
            ],
          },
          {
            test: /\.html$/i,
            use: {
              loader: 'html-loader',
              options: {
                attributes: {
                  list: [
                    {
                      tag: 'img',
                      attribute: 'src',
                      type: 'src',
                    },
                    {
                      tag: 'img',
                      attribute: 'srcset',
                      type: 'srcset',
                    },
                    {
                      tag: 'img',
                      attribute: 'data-src',
                      type: 'src',
                    },
                    {
                      tag: 'img',
                      attribute: 'data-srcset',
                      type: 'srcset',
                    },
                  ],
    
                },
              },
            },
          },
        ],
      },
      optimization: {
        minimizer: [new TerserJSPlugin(), new OptimizeCSSAssetsPlugin()],
        splitChunks: {
          cacheGroups: {
            commons: {
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors',
              chunks: 'all',
            },
          },
          chunks: 'all',
        },
        runtimeChunk: {
          name: 'runtime',
        },
      },
      plugins: [
        // load jQuery
        new webpack.ProvidePlugin({
          $: 'jquery',
          jQuery: 'jquery',
        }),
    
        new CleanWebpackPlugin(),
    
        new MiniCssExtractPlugin({
          filename: '[name].[chunkhash:8].bundle.css',
          chunkFilename: '[name].[chunkhash:8].chunk.css',
        }),
    
        new HtmlWebpackPlugin({
          chunks: ['main-rtl'],
          template: 'src/i18n/fa/index.html',
          filename: 'index.html',  // ---> changed from fa/index.html to index.html
        }),
    
      ],
    };
    

    Plus, I have a npm package installed called npm-run-all to run some npm cli commands in parallel. So, when I want ltr and rtl versions of my project at the same time I can build them both through these commands in script section of my package.json file:

    "scripts": {
        // other scripts,
        "build-ltr": "webpack --config=config/webpack.prod.js",
        "build-rtl": "webpack --config=config/webpack.prod.rtl.js",
        "build": "npm-run-all --parallel build-ltr build-rtl"
      },
    

    For example, after running npm run build it gives me both ltr and rtl versions of my project in previous folder structure that I mentioned in the question but with correct image src paths.

    So, I decided to share the solution. Definitely there are other solutions out there but this is something I came up with to solve my problem for now.

    Hopefully this will be helpful for others.