webdiscus / html-bundler-webpack-plugin

Renders Eta, EJS, Handlebars, Nunjucks, Pug, Twig templates "out of the box". Uses HTML template as entry point. Resolves source files of scripts, styles, images in HTML. Supports for Vue.
ISC License
119 stars 12 forks source link

Not All Expected CSS Files Were Generated #68

Closed DylanVause closed 5 months ago

DylanVause commented 5 months ago

Current behaviour

I build a React component Foo with Foo.css for styles. I use Foo in Page A and Page B, which are two near-identical HTML pages powered by React. Oddly, Foo is only styled in either Page A or Page B, but not both. Note that the same issue apples to component Bar.

Foo.tsx:

import { ReactElement } from "react";
import "./Foo.css";

export default function Foo(p: { children?: string | ReactElement | ReactElement[] }) {
    return (<div className="foo">
        {p.children}
    </div>
    );
}

Foo.css:

.foo {
    color: rgb(255, 158, 30);
    background-color: white;
    border: 1px solid rgb(255, 158, 30);
    border-radius: 5px;
}

pageA.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Page A</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="pageA.tsx"></script>
    </body>
</html>

pageB.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Page B</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="pageB.tsx"></script>
    </body>
</html>

pageA.tsx (pageB.tsx is similar):

import { createRoot } from 'react-dom/client';
import Foo from '../components/Bar';
import Bar from '../components/Foo'; 

const root = createRoot(document.getElementById('app')!);

root.render(
    <div style={{display: 'flex', flexDirection: 'column'}}>
        <p>Page A</p>
        <a href='/pages/pageB.html'>Visit Page B</a>
        <b>Styled components:</b>
        <Foo>Foo</Foo>
        <Bar>Bar</Bar>
    </div>
);

For some reason, in the output directory, only one of pageA.{contenthash}.css or pageB.{contenthash}.css is generated, but not both. Expected behaviour is that both would be generated.

The generated HTML files look like this: pageA.html (generated):

<!DOCTYPE html>
<html>
    <head>
        <title>Page A</title>
    <!-- 
        Where is <link href="../css/pageA.{contenthash}.css" rel="stylesheet">? 
    -->
    </head>
    <body>
        <div id="app"></div>
        <script src="../js/pageA.f6d77860.js"></script>
    </body>
</html>

pageB.html (generated):

<!DOCTYPE html>
<html>
    <head>
        <title>Page B</title>
    <link href="../css/pageB.e7abdc32.css" rel="stylesheet">
</head>
    <body>
        <div id="app"></div>
        <script src="../js/pageB.ef1af23d.js"></script>
    </body>
</html>

Expected behaviour

Foo should be styled in both Page A and Page B. Both pageA.{contenthash}.css andpageB.{contenthash}.css should be generated and included in pageA.html and pageB.html.

Reproduction Example

I have a mini reproduction example here: https://github.com/DylanVause/webpack-multiple-css-imports-bug

Environment

Additional context

My Webpack config is in the reproduction repository, but I will include it here as well for convenience:

const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');

module.exports = [
    {
        name: 'client',
        mode: 'development',
        output: {
            path: path.resolve(__dirname, 'client-dist'),
            clean: true
        },
        resolve: {
            alias: {
                client: path.resolve(__dirname, 'client')
            },
            extensions: ['.js', '.ts', '.tsx']
        },
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    include: path.resolve(__dirname, 'client'),
                    exclude: /node_modules/,
                    use: [
                        {
                            loader: 'ts-loader',
                            options: {
                                transpileOnly: true,
                            },
                        },
                    ],
                },
                {
                    test: /\.css$/,
                    use: 'css-loader',
                    include: path.resolve(__dirname, 'client'),
                    exclude: /node_modules/,
                },
                {
                    test: /\.(ico|png|jp?g|svg)/,
                    type: 'asset/resource',
                    include: path.resolve(__dirname, 'client'),
                    exclude: /node_modules/,
                    generator: {
                        filename: 'img/[name].[hash:8][ext]'
                    }
                },
            ],
        },
        plugins: [
            new HtmlBundlerPlugin({
                entry: 'client/',
                js: {
                    filename: 'js/[name].[contenthash:8].js'
                },
                css: {
                    filename: 'css/[name].[contenthash:8].css'
                },
            })
        ],
        optimization: {
            usedExports: true,
        }
    },
];

Thank you for investigating this issue. I apologize in advance if this 'bug' is an error on my part.

webdiscus commented 5 months ago

Hello @DylanVause

thanks for the issue report. I will research this problem later this week.

webdiscus commented 5 months ago
import '@mui/material/Stack'; // <= disable this line or
import './index.css'; // <= disable this line to build successfully
// ^^^ If both these lines are enabled it will fail to build, regardless of the content of index.css

This bug has been already fixed. In your repo is missing the index.css. Just add the pages/index.css file and the build will not fail.

I can reproduce the issue from the subj.

DylanVause commented 5 months ago

Sorry, I forgot to commit to the repo. That is old code, the issue still exists. I have committed the new code.

DylanVause commented 5 months ago

I cloned the plugin repo to see if I can debug it. Removing line 1004 of /src/Plugin/AssetCompiler.js seems to fix the issue, but I'm not sure what the line is for. Are there issues with removing this line?

if (!Collection.importStyleRootIssuers.has(issuer)) continue; // I removed this line
webdiscus commented 5 months ago

It is very angry bug/feature in Webpack:

If the same c.css file was imported in many js files: a.js and b.js, then Webpack processes the CSS module only for 1st a.js, others parents will be ignored, then we lost the relation for other parents: a.js -> c.css (ok) but b.js -> c.css (lost).

This is a biggest problem in Webpack: it has no cache/data where is saved all parents (incoming connections) of a dependency. Therefore we need walk through each JS file to search in dependencies of the parent, that is very dirty but the single working solution :-/

The original mini-css-extract-plugin plugin use a different way to find imported CSS, but it is much more complex and not faster.

@DylanVause Thank you for the fix in one line! All tests passed. The fix is in the version 3.4.7. The side-effect of the fix: increases build time for cases when many js files do not import css.

DylanVause commented 5 months ago

Thank you for the update. I can confirm 3.4.7 works on my end.