1

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.

1 Answer 1

0
  • 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.

Sign up to request clarification or add additional context in comments.

9 Comments

thank you for recommending a solution, I will give it a shot. I am also curious to learn why I am seeing the behaviour that I get. Any insights?
because neither Webpack nor the the html-webpack-plugin support these features that you need. Only the powerful HtmlBundlerPlugin works as expected, like Vite or Parcel. You can share a small repo on github with your assets and templates and I can help you to configure your Webpack.
It is surprising to hear that webpack and html-webpack-plugin don't support css files referenced in html templates for integration of those css files into dependency graph somewhere or copy them over like processed assets. I felt that I have my config file set up wrong somewhere. I will explore your plugin myself first before reaching out to you. thank you
your plugin works like a charm. One issue though, all build files are stored in the folder I specify and html file <link> point to that file with relative path using public path as prefix. is there a way that I can omit the leading folder from the link tags. I want all static files in 'static' folder inside dist lets say but the url in link tags should be '/filename' instead of '/static/filename' Is there a way to achieve this and still have css files insider 'dist/static/name.css'? and html files elsewhere inside dist say dis/html/main-outputhash.html' ?
1) output path for HTML file, see outputPath plugin option. OR define the path to html by entry-key: entry: { 'html/main': './src/views/main.html' }. The HTML file will be saved as dist/html/main.html
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.