webdiscus/html-bundler-webpack-plugin

Inlining CSS via ?inline query is broken when importing CSS/SCSS in JavaScript files

daltonboll opened this issue · 13 comments

Current behaviour

When I import a .css file or .scss file into a JavaScript file by using an import statement like import './styles.css?inline' or import './styles.css', I get an error when I build my project:

[INFO] ERROR in tutorial/js/tutorial.dc4ea4af8c0e4076240407f27b85c4bd.js
[INFO] tutorial/js/tutorial.dc4ea4af8c0e4076240407f27b85c4bd.js from Terser plugin
[INFO] Unexpected token: punc (.) [tutorial/js/tutorial.dc4ea4af8c0e4076240407f27b85c4bd.js:1,0]
[INFO]     at js_error (app/node_modules/terser/dist/bundle.min.js:536:11)
[INFO]     at croak (app/node_modules/terser/dist/bundle.min.js:1264:9)
[INFO]     at token_error (app/node_modules/terser/dist/bundle.min.js:1272:9)
[INFO]     at unexpected (app/node_modules/terser/dist/bundle.min.js:1278:9)
[INFO]     at statement (app/node_modules/terser/dist/bundle.min.js:1408:17)
[INFO]     at _embed_tokens_wrapper (app/node_modules/terser/dist/bundle.min.js:1329:26)
[INFO]     at parse_toplevel (app/node_modules/terser/dist/bundle.min.js:3607:23)
[INFO]     at parse (app/node_modules/terser/dist/bundle.min.js:3620:7)
[INFO]     at minify_sync_or_async (app/node_modules/terser/dist/bundle.min.js:31881:42)
[INFO]     at minify_sync_or_async.next (<anonymous>)
[INFO] 
[INFO]  HTML Bundler Plugin  ▶▶▶ (webpack 5.93.0) compiled with 1 error in 5608 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE

Expected behaviour

Per this documentation, I would expect that importing the CSS in a JS file with this syntax would automatically inject the CSS into the HTML.

Reproduction Example

I'm using webpack to serve an app with a Spring Boot backend and a React frontend. I'm using babel transpile JS/JSX. The relevant files I'm using are found below:

styles.css (or styles.scss -- I've tried both)

.heading {
  color: red;
}

tutorial.js

import "./styles.css?inline"; // <= If I comment this out, the application runs without error and the JS is executed on the page just fine (the CSS is just absent from the webpage)
// import "./styles.css"; <= this syntax also causes an error

console.log('hello world!');

tutorial.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
  <title>Title</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <p class="heading">Tutorial!</p>
  <script src="../js/tutorial.js" type="application/javascript"></script>
</body>

webpack.config.js

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

const FRONT_END_DIRECTORY = "src/main/frontend";
const BUILD_OUTPUT_DIRECTORY = "target/classes/static";

module.exports = (env, argv) => ({
  target: 'web',

  output: {
    path: path.resolve(__dirname, `./${BUILD_OUTPUT_DIRECTORY}`),
    clean: true,
    publicPath: '',
  },

  plugins: [
    new HtmlBundlerPlugin({
      entry: `${FRONT_END_DIRECTORY}/`,
      js: {
        filename: <redacted_function>,
        outputPath: '',
      },
      css: {
        filename: <redacted_function>,
        outputPath: '',
      },
    }),
  ],

  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: path.resolve(__dirname, `./${FRONT_END_DIRECTORY}`),
        use: ['babel-loader']
      },
      {
        test: /\.(s?css)$/,
        include: path.resolve(__dirname, `./${FRONT_END_DIRECTORY}`),
        oneOf: [
          {
            resourceQuery: /inline/, // <= matches e.g., styles.scss?inline. I've also tried just not doing this query at all and always using 'css-loader' and 'sass-loader' with the default configurations.
            // TODO: is there some other configuration that I have to do in order to get the imports to work?
            use: [
              {
                loader: 'css-loader',
                options: {
                  // exportType: 'css-style-sheet', <= doesn't work, though it does allow me to import a CSSStyleSheet object and inject the CSS into the HTML using adoptedStyleSheets
                  // exportType: 'string', <= doesn't work
                  exportType: 'array', // <= doesn't work
                },
              },
              {
                loader: 'sass-loader',
              },
            ],
          },
          {
            use: [
              'css-loader',
              'sass-loader',
            ],
          }
        ],

      },
      {
        test: /\.(jpe?g|png|gif|svg|webp|ico)$/i,
        include: path.resolve(__dirname, `./${FRONT_END_DIRECTORY}`),
        type: 'asset',
        generator: {
          filename: <redacted_function>,
        },
        parser: {
          dataUrlCondition: {
            maxSize: 2 * 1024
          }
        },
      },
    ]
  },

  resolve: {
    modules: [
      path.resolve(__dirname, `./${FRONT_END_DIRECTORY}`),
      'node_modules'
    ],

    extensions: ['.js', '.jsx', '...'],

    alias: {
      '@bootstrap': path.join(__dirname, 'src/main/frontend/external/node_modules/bootstrap/scss'),
      '@bootstrap-base$': path.join(__dirname, 'src/main/frontend/external/bootstrap-base.scss'),
      Bootstrap$: path.resolve(__dirname, 'src/main/frontend/external/Bootstrap.js'),
      Popper$: path.resolve(__dirname, 'src/main/frontend/external/Popper.js'),
      React$: path.resolve(__dirname, 'src/main/frontend/external/React.js'),
      ReactDOM$: path.resolve(__dirname, 'src/main/frontend/external/ReactDOM.js'),
      ReactPopper$: path.resolve(__dirname, 'src/main/frontend/external/ReactPopper.js'),
    },
  },

  devServer: {
    port: 8081,
    compress: true,
    watchFiles: [
      `${FRONT_END_DIRECTORY}/**/*.html`,
      `${FRONT_END_DIRECTORY}/**/*.js`,
      `${FRONT_END_DIRECTORY}/**/*.jsx`,
      `${FRONT_END_DIRECTORY}/**/*.scss`,
      `${FRONT_END_DIRECTORY}/**/*.css`,
    ],
    proxy: [
      {
        context: '**',
        target: 'http://localhost:8080',
        secure: false,
        prependPath: false,
        headers: {
          'X-Devserver': '1',
        }
      }
    ]
  }
});

Environment

  • OS: macOS 14.2.1
  • version of Node.js: v20.15.1
  • version of Webpack: ^5.93.0
  • version of the Plugin: ^3.15.1
  • Babel versions:
    • @babel/core: ^7.24.9
    • @babel/preset-env: 7.24.8
    • @babel/preset-react: ^7.24.7
    • babel-loader: ^9.1.3

Additional context

Note that I realize that I could just have all CSS files be inline injected into the HTML via the global setting described here. However, I want the majority of my CSS files to remain separate. The reason why I'd like to selectively inline CSS files via JS imports is because I want each React component to have its own corresponding CSS/SCSS file.

I could utilize exportType = css-style-sheet and document.adoptedStyleSheets in each React component to manually inject the CSS into the html. However, it would definitely be nice to have this done automatically, because that extra step leaves room for errors and is just another thing to remember.

The documentation indicates that the behavior I desire should be available, so I'm assuming that this is a bug. Or maybe I have just misconfigured something. 😅

This is an absolutely amazing plugin by the way. It's so powerful, and this CSS import behavior is the last bit of configuration I need in order to have things working just the way I'd like them to. The documentation is also top-notch. Thank you so much for all of your incredible work on this!

Anyway, I would greatly appreciate any help you can provide. Thanks in advance! 😊

Hello @daltonboll,

thank you for the issue report. I try to reproduce issue and find a solution.

P.S. Would be nice if you could create a small repo with reproducible issue.

@daltonboll

Note that the ?inline query is the reserver for the bundler plugin and can't be used in the resourceQuery: /inline/ Webpack option.

If you want to import a CSSStyleSheet object using a query, then use other query, e.g. as in the doc - resourceQuery: /sheet/. Then the css-loader option exportType must be css-style-sheet.

@daltonboll

I can reproduce the bug.
Using ?ìnline query for single CSS file in JS doesn't work:
import './style-d.css?inline'; <= will not be injected into HTML, the CSS is exported into separate file.

I will fix it.

Hi @webdiscus - thanks so much for your helpful reply and for getting back to me so quickly. I started putting together a small test repository for you to take a look at. In doing so, I simplified my code a bit and then was able to get rid of the error that I included in my original post. That error was my fault; the function I was providing to filename for JS and CSS files was generating a filename with some missing syntax. Once I fixed it, I no longer got an error when using the import <filename>.css?inline and import <filename>.css statements in JS files.

However, as you mentioned in your reply, including the ?inline query still does not inject the CSS into the HTML. It does put it into a separate file though, which is actually totally fine for my use case. It would be nice if you could fix it at some point, but there's absolutely no rush, as I'm satisfied with importing the CSS without the ?inline query.

Thanks again! :)

@daltonboll

the v3.17.0 is released. Now you can use the ?inline query for single style files imported in JavaScript:

import './style-a.scss?inline'; // the extracted CSS will be injected into HTML
import './style-b.scss'; // the extracted CSS will be saved into separate output file
import './style-c.scss?inline'; // the extracted CSS will be injected into HTML
import './style-d.scss'; // the extracted CSS will be saved into separate output file

So the CSS from many files with ?inline query will be squashed and injected into HTML:

style-a.scss + style-c.scss | => inject CSS into HTML 

Other files will be squashed and saved into separate file:

style-b.scss + style-d.scss | => save CSS into file

For example, see please the test case. In the expected directory is the generated result.

This is awesome! I really appreciate your help with all of this. I will surely be recommending this plugin to everyone. :)

Thank you so much, and have a wonderful rest of your week!

-Dalton

@daltonboll Thank you for the donation!

@webdiscus I apologize for bothering you again about this, but I noticed that the CSS imports are still a bit buggy.

Specifically, when you use non-inline CSS imports and run webpack in development mode (webpack --mode development). By non-inline CSS imports, I mean imports in JS files of the form import "path/to/styles.scss"; These types of imports are working perfectly in production mode. However, in development mode, the styles are not getting applied correctly to the corresponding entry's html page. It appears as though they are not being included in the resulting bundled CSS file for the JS file that imported it.

The inline CSS imports (e.g., import "path/to/styles.scss?inline";) are working perfectly in both development and production mode though!

Could you please take another look at this when you get a chance? Thanks in advance! 🙂

@daltonboll can you please create small repo with reproducible issue

@webdiscus sure thing! Here's a small repo I created: https://github.com/daltonboll/debugging-html-bundler-webpack-plugin/tree/master

Info about how to reproduce is in the README.md. Thanks for your help! :)

@daltonboll thank you for the repo. I can reproduce the issue in dev mode und will fix it.

@daltonboll
the issue is fixed in the v3.17.3

Thank you!

@webdiscus Awesome! Glad to hear the repo helped. Thank you for fixing this. Have a wonderful weekend! 😊