webdiscus/html-bundler-webpack-plugin

Inlining imported css doesn't work in watch mode if a leaf component content is changed

sahilmob opened this issue · 13 comments

Current behaviour

I have and index.html that imports index.jsx which has a Layout Component, and the Layout Component imports a stylesheet from a package in node_modules, and the Layout component renders a child component, when I run webpack --watch --progress --mode development for the first time I get all the js and css injected in the html however, if I change the child component file and save, the generated html won't have the inline css, furthermore, if I go to the Layout component and save it to trigger rebuild, I get the css in the generated html.

Expected behaviour

The css should be included in the generated html in watch mode every time

Reproduction Example

./index.html

<div id="root"></div>
<script src="./index.js"></script>

./index.js

import React from "react";
import * as ReactDOM from "react-dom/client";
import Layout from "./Layout";
Import SomeComponent from "./SomeComponent";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(<Layout><SomeComponent /></Layout>);

./Layout.jsx

import "some-package/dist/index.css";
import React from "react";

export default function Layout({children}){
  reutrn <div>{children}</div>
}

./SomeComponent.jsx

import React from "react";

export default function SomeComponent(){
  reutrn <div>Some content</div>
}

webpack.config.js

const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const HtmlBundlerPlugin = require("html-bundler-webpack-plugin");

module.exports = (env) => {
  return {
    resolve: {
      extensions: [".tsx", ".jsx", ".ts", ".js"],
    },
    plugins: [
      new CopyPlugin({
        patterns: [
          {
            from: "./",
            to: "resources",
            filter: (file) => !file.endsWith(".jsx") && !file.endsWith(".html"),
          },
        ],
      }),
      new HtmlBundlerPlugin({
        filename: "[name].ftl",
        entry: "./",
        postprocess: (content) => {
          return content.concat("<head></head>");
        },
        js: {
          inline: true,
        },
        css: {
          inline: true,
        },
      }),
      {
        apply(compiler) {
          const pluginName = "inline-template-plugin";

          compiler.hooks.compilation.tap(pluginName, (compilation) => {
            const hooks = HtmlBundlerPlugin.getHooks(compilation);

            hooks.beforeEmit.tap(pluginName, (content) => {
              return (
                '<#import "template.ftl" as layout>' +
                "<@layout.registrationLayout displayInfo=social.displayInfo; section>" +
                content +
                "</@layout.registrationLayout>"
              );
            });
          });
        },
      },
    ],
    module: {
      rules: [
        {
          test: /.(js|jsx|ts|tsx)$/,
          use: {
            loader: "babel-loader",
            options: {
              plugins: [["remove-comments"]],
              presets: [["@babel/preset-env"], "@babel/preset-react"],
            },
          },
        },
        {
          test: /.(js|jsx|ts|tsx)$/,
          include: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
        {
          test: /.(ts|tsx)$/,
          use: "ts-loader",
          exclude: /node_modules/,
        },
        {
          test: /\.(css|sass|scss)$/,
          use: ["css-loader", "sass-loader"],
        },
        {
          test: /\.(ico|png|jp?g|webp|svg)$/,
          type: "asset/resource",
          generator: {
            outputPath: () => {
              return "resources";
            },
            filename: ({ filename }) => {
              const base = path.basename(filename);
              return "/img/" + base;
            },
          },
        },
        {
          test: /[\\/]fonts|node_modules[\\/].+(woff(2)?|ttf|otf|eot)$/i,
          type: "asset/resource",

          generator: {
            outputPath: () => {
              return "resources";
            },
            filename: ({ filename }) => {
              const base = path.basename(filename);
              return "/fonts/" + base;
            },
          },
        },
      ],
    },
    output: {
      clean: true,
      publicPath: "public",
    },
    watchOptions: {
      ignored: ["node_modules", "dist"],
    },
    cache: {
      type: "memory",
      cacheUnaffected: false,
    },
    devtool: false,
  };
};

Environment

  • OS: Windows
  • version of Node.js: 21.2.0
  • version of Webpack: 5.90.1
  • version of the Plugin: 3.4.12

Additional context

I noticed that the beforeEmit hook isn't being called after saving SomeComponent while its being called for Layout component

Hallo @sahilmob,

Thanks for the issue report.
I'll try to fix it over the weekend.

@sahilmob

I cannot reproduce the issue.
I have created the manual test watch-imported-css-inline with nested components. After change any file all imported CSS will be inlined into HTML.

  1. start development: npm start, open in your browser the url : http://localhost:8080
  2. change src/home.html => ok
  3. change src/style.css => ok
  4. change src/main.js => ok
  5. change src/component-a/style.css => ok
  6. change src/component-a/index.js => ok
  7. change src/component-b/style.css => ok
  8. change src/component-b/index.js => ok

Please:

  • create a repo based on the watch-imported-css-inline example, without heavy React
  • describe the specific steps of what you are doing: what file (e.g. src/path/to/style.css) are you changing, after which occurs issue

@sahilmob
Is the issue reproducible if you don't use your custom "inline-template-plugin"?

@webdiscus
Yes its is! what is interesting though is that style-loader seems to be the root cause of the problem, I noticed that style-loader doesn't work very well with this plugin.

Initially my workaround was to create a local .css file and @import "~some-package/dist/index.css"; inside it, and then import the local css file into Layout.tsx, but when I went back to reproduce this issue without my custom plugin (and import some-package/dist/index.css in Layout.tsx component), I noticed that I cannot compile the app with the following error

ERROR in [entry] [initial] Spread syntax requires ...iterable[Symbol.iterator] to be a function

I then disabled style-loader and enabled my custom plugin, it worked fine!

Please note that I've added style-loader very recently, after reporting this bug, and after doing the aforementioned workaround so that's why it was compiling successfully.

Why you use the bundler plugin with style-loader?
The bundler plugin is designed to replace the style-loader and is absolutely incompatible for together work.
The bundler plugin can extract CSS and inline into HTML.
The style-loader do the same. Only one different: style-loader can HMR without site reloading after changes, the bundler plugin requires the site reloading.

I don't understand what doing your inline-template-plugin. It's look like a wrapper over generated HTML with a templating things: <#import "template.ftl" as layout>.... Why you don't write this wrapper directly in HTML template file?

Yes you are right l, I use style loader for HMR.

Also you are right about the custom plugin. I convert the html into Freemarket template (ftl)and the reason why I don't write it inside the html is that I want to inline js and css, and the ftl syntax breaks html parsing.

@sahilmob

  1. please create a small repo with reproducible issue
  2. describe exactly and very clear the steps to reproduce the problem.

WARNING

Without your repo, I can't help you, sorry.

Sure. Thanks

here is the test case for .ftl template.

You can use you .ftl template as an entry. Just disable the preprocessor: false option.

Your source tempalte:

<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=true displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
  <#if section = "styles">
    <link rel="stylesheet" href="./scss/styles.scss" />
  </#if>
  <#if section = "scripts">
    <script typo="module" src="./js/main.js"></script>
  </#if>
  <img src="./images/picture.png" />
</@layout.registrationLayout>

the HtmlBundlerPlugin config:

new HtmlBundlerPlugin({
      filename: '[name].ftl', // <=  output filename of template
      test: /\.ftl$/, // <= add it to detect *.ftl files
      entry: {
         index: 'src/index.ftl',
      },
      // OR entry: 'src/',
      js: {
        inline: true,
      },
      css: {
        inline: true,
      },
      preprocessor: false, // <= disable it for processing *.ftl template w/o compilation
    }),

The generated output template file:

<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=true displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
  <#if section = "styles">
    <style>...inlined CSS...</style>
  </#if>
  <#if section = "scripts">
    <script>... inlined JS code ...</script>
  </#if>
  <img src="img/picture.7b396424.png" />
</@layout.registrationLayout>

So you don't need additional inline-template-plugin.

@sahilmob

for info: I'm working on the HMR supporting for styles. So, after changes in a SCSS/CSS file, the generated CSS will be updated in the browser without reloading, similar it works in style-loader. This it takes a lot of time, because it is very very complex. But this works already for styles imported in JS very well. Now I work on HMR supporting for styles defined directly in HTML.

P.S. what is with your test repo for using .ftl templates? Is it yet actual?

Thanks for your efforts. My repos is private unfortunately.

Thanks for your efforts. My repos is private unfortunately.

you can create a public demo repo with fake data to reproduce your issue.
Without the reproducible issue I can't help you, you should understand it ;-)

@sahilmob is the issue still actual?