webpackwebpack-5html-webpack-pluginmini-css-extract-pluginwebpack-html-loader

Processing .css files included inside link tags in HtmlWebpackPlugin template


Brief Description: I am running into unexpected outputs/issues when working with .css files referenced inside <link> tag in the .html template provided to HtmlWebpackPlugin along with using MiniCssExtractPlugin and some loaders.

Directory structure:

| package.json
| webpack.prod.js
| src
  | css
    | main.css
    | another.css
    | ...
  | images
    | image1.png
      ...
  | scripts
    | script1.js
    | script2.js
  | templates
    | file1.html
    | file2.html
      ...
| ...

File contents for directory structure shown above: script1.js contains reference to script2.js but does not have any reference to css files. .css files are only referenced using <link> tags in .html files contained in ./src/templates folder.

webpack.prod.js contents:

const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

const htmlTemplates = fs.readdirSync(path.resolve(__dirname, 'src/templates')).filter(file => /\.html$/i.test(file));

module.exports = {
    mode: "production",
    entry: {
        index: "./src/scripts/script1.js",
    },
    output: {
        filename: 'static/[name]-[contenthash].bundle.js',
        path: path.resolve(__dirname, 'build'),
        publicPath: '/',
        assetModuleFilename: "static/[name]-markedByAssetModuleFilename-[contenthash].bundle[ext]",
        clean: true,
    },
    optimization: {
        minimizer: [
            new CssMinimizerPlugin(),
            new TerserPlugin({
                extractComments: false, //omit the creation of a separate LICENSE file
                terserOptions: {
                    format: {
                      comments: false, //remove the comments from the bundled output
                    },
                }            
            }),
        ],
    },
    plugins: [
        ...htmlTemplates.map((file) => {
            return new HtmlWebpackPlugin({
                template: `./src/templates/${file}`,
                filename: `htmlFiles/${file.replace(/\.html$/, '.bundle.html')}`,
                inject: 'head',
                scriptLoading: 'module',
                minify: {
                    collapseWhitespace: true,
                    removeComments: true,
                    removeRedundantAttributes: true,
                    removeScriptTypeAttributes: true,
                    removeStyleLinkTypeAttributes: true,
                    useShortDoctype: true,
                    minifyJS: true,
                    minifyCSS: true
                    }
            });    
        }),
        new MiniCssExtractPlugin({
            filename: 'static/[name]-[contenthash].bundle.css',
        }),
    ],
    module: {
        rules: [
            {
                test: /\.js$/i,
                exclude: [/node_modules/],
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                    },
                },
            },
            {
                test: /\.html$/i,
                use: ["html-loader"]
            },
            {
                test: /\.(png|jpe?g|gif|svg)$/i,
                type: "asset/resource",
            },
            {
                test: /\.css$/i,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ],
            }
        ],
    },
};

package.json

  "devDependencies": {
    "@babel/preset-env": "^7.23.6",
    "babel-loader": "^9.1.3",
    "css-loader": "^6.8.1",
    "css-minimizer-webpack-plugin": "^5.0.1",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.6.0",
    "mini-css-extract-plugin": "^2.7.6",
    "terser-webpack-plugin": "^5.3.10",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
    ...
    ...
  }

Output that I expected:

When I run webpack --config webpack.prod.js I should get ./build/htmlFiles folder containing all the "processed" html files. These html files should have script tag for bundled script1.js and should have link tags pointing to css files that were processed. Processed css files should be named according to filename specified for MiniCssExtractPlugin and not per output.assetModuleFilename. The css files should not be empty.

Output that I get:

The .html files inside ./build/htmlFiles folder have link tags and script tag pointing to css files and script that is output in ./build folder. So far so good.

I discover that inside ./build/static folder the .css files are all named according to name specified under ouput.assetModuleFilename. All .css files are also empty and look like this:

// extracted by mini-css-extract-plugin
export {};

I figured out that if I modify module.rules to have the include property such as shown below and pointing to a file that does not exist then all the .css files inside ./build/static have the css contents inside but no longer say // extracted by mini-css-extract-plugin Although, they are still named according to output.assetModuleFilename. If include does point to actual file that exists then that file inside ./build/static will not have css content and will be empty. Other processed .css files inside .build/static will have content then. I don't understand what is going on here.

{
  test: /\.css$/i,
  use: [
    MiniCssExtractPlugin.loader,    //2. Extract css into files
    'css-loader'                    //1. Turns css into commonjs
  ],
  include: path.resolve(__dirname, './src/css/nonExistentFile.css'),
}

How do I get the output that I expect? I want to use all css inside my html templates using <link> tags pointing to processed .css files inside build directory. I don't want to have css import statements inside shared script1.js file. Each .html template file points to its own .css file using <link> tags.

I have tried reading documentation for last 2 days but cant figure out webpack just yet. This is my first time using webpack.


Solution

    • I want to use all css inside my html templates using link tags
    • I don't want to have css import statements inside shared script1.js file

    you can use the html-bundler-webpack-plugin.

    This plugin allow to use JS and CSS/SCSS source files directly in HTML tags.

    All source file paths in your template will be resolved and auto-replaced with correct URLs in the bundled output. The resolved assets will be processed via Webpack plugins/loaders and placed into the output directory.

    For example, there is HTML template included references to SCSS, JS and image:

    <html>
      <head>
        <!-- relative path to SCSS source file -->
        <link href="./styles.scss" rel="stylesheet" />
        <!-- relative path to JS source file -->
        <script src="./main.js" defer="defer"></script>
      </head>
      <body>
        <h1>Hello World!</h1>
        <!-- relative path to image source file -->
        <img src="./picture.png" />
      </body>
    </html>
    

    The generated HTML contains the output filenames:

    <html>
      <head>
        <link href="css/styles.05e4dd86.css" rel="stylesheet" />
        <script src="js/main.f4b855d8.js" defer="defer"></script>
      </head>
      <body>
        <h1>Hello World!</h1>
        <img src="img/picture.58b43bd8.png" />
      </body>
    </html>
    

    Simple Webpack config:

    const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
    
    module.exports = {
      plugins: [
        new HtmlBundlerPlugin({
          // define a relative or absolute path to entry templates
          entry: 'src/views/',
          // OR define many templates manually
          entry: {
            index: 'src/views/home.html', // => dist/index.html
            'news/sport': 'src/views/news/sport/index.html', // => dist/news/sport.html
          },
          js: {
            // output filename of compiled JavaScript, used if `inline` option is false (defaults)
            filename: 'js/[name].[contenthash:8].js',
            //inline: true, // inlines JS into HTML
          },
          css: {
            // output filename of extracted CSS, used if `inline` option is false (defaults)
            filename: 'css/[name].[contenthash:8].css',
            //inline: true, // inlines CSS into HTML
          },
        }),
      ],
      // loaders for styles, images, etc.
      module: {
        rules: [
          {
            test: /\.(css|sass|scss)$/,
            use: ['css-loader', 'sass-loader'],
          },
          {
            test: /\.(ico|png|jp?g|webp|svg)$/,
            type: 'asset/resource',
          },
        ],
      },
    };
    

    Note

    Using the HTML bundler plugin, the HTML template is the entry point, not JS. You don't need anymore define SCSS and JS files in Webpack entry.

    The HTML bundler plugin replaces the functionality of the html-webpack-plugin and mini-css-extract-plugin.