webdiscus / html-bundler-webpack-plugin

Alternative to html-webpack-plugin ✅ Renders Eta, EJS, Handlebars, Nunjucks, Pug, Twig templates "out of the box" ✅ Resolves source files of scripts, styles, images in HTML ✅ Uses a template as entry point
ISC License
147 stars 15 forks source link

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

Closed sahilmob closed 4 months ago

sahilmob commented 9 months ago

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

Additional context

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

webdiscus commented 9 months ago

Hallo @sahilmob,

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

webdiscus commented 9 months ago

@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:

webdiscus commented 9 months ago

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

sahilmob commented 9 months ago

@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.

webdiscus commented 9 months ago

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?

sahilmob commented 9 months ago

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.

webdiscus commented 9 months ago

@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.

sahilmob commented 9 months ago

Sure. Thanks

webdiscus commented 9 months ago

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.

webdiscus commented 6 months ago

@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?

sahilmob commented 6 months ago

Thanks for your efforts. My repos is private unfortunately.

webdiscus commented 6 months ago

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 ;-)

webdiscus commented 4 months ago

@sahilmob is the issue still actual?