mini-css-extract-plugin
mini-css-extract-plugin copied to clipboard
Support for Web Components, Shadow DOM, adoptedStyleSheets
Context
I want to generate a standalone component to be loaded in a shadow DOM.
It seems that this gets me most of what I need:
- own JS chunk
- standalone CSS chunk that includes all the CSS dependencies (sub-imports of other .css files)
import shadow from 'react-shadow';
/// partial, pseudo-code
const {Component} = await import('./Component');
return <shadow.div><Component/></shadow.div>
At this point, a Component.css
file is being generated and appended to document.head
.
**The problem is that this stylesheet should be inside shadow.div
Feature Proposal
I'm not sure what would be the best way to achieve this, but perhaps:
- add a magic comment to the
import()
to disable the automatic injection of the stylesheet e.g.import(/* webpackCss: no-inject */ './Component');
- return the URL as an additional property, so that it can be handled autonomously
e.g.
const {Component, __miniCssUrl} = await import('./Component');
Related links
- adoptedStyleSheets could be later used to inject the stylesheet in the shadow DOM: https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/adoptedStyleSheets
- you can see here how they currently load the CSS (i.e. as string in JS, mostly) https://github.com/Wildhoney/ReactShadow
- similar request in https://github.com/webpack-contrib/mini-css-extract-plugin/issues/110
- part of this request https://github.com/webpack-contrib/mini-css-extract-plugin/issues/204
Currently possible only with css-loader https://github.com/webpack-contrib/css-loader?tab=readme-ov-file#exporttype
The TL;DR of this issue is: can I specify anything other than document.head
as a dynamic chunk CSS injection point?
This is possible via style-loader
so perhaps it's possible to let style-loader
deal with dynamic chunks:
- https://webpack.js.org/loaders/style-loader/#function
Currently possible only with css-loader webpack-contrib/css-loader#exporttype
Indeed, the main challenge is that all the existing solutions seem to be around the way the CSS file is imported, but I need to act on the generated CSS chunk instead, as a whole, not on a specific real CSS file.
Indeed, the main challenge is that all the existing solutions seem to be around the way the CSS file is imported, but I need to act on the generated CSS chunk instead, as a whole, not on a specific real CSS file.
How do you imagine this if you have about 100 components?
It's not an issue with the number of components but rather 100 imports of one component, because each import would need to disable/alter the import()
magic comment.
In my project there would be a single import()
of the private component (seen above) and a wrapped component that returns a shadow DOM component with the stylesheet pre-loaded.
I'm not super convinced about the solution suggested above, I think that from webpack's viewpoint it's difficult to know in what context the code is being imported.
As a better solution, I think I'd have to move Component.tsx
to webpack's entry points and then load the stylesheet myself, however any sub-import()
calls would still load the stylesheet in the main document.
The easiest solution is to wrap the component in an iframe so that document.head
is always the right component, but iframes are hard to deal with.
@fregante Can you provide a small example how do you see it (using github repo), maybe I can provide solution right now, because specification clearly says what you should write https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets#examples,
What can I do here? Just allow to this plugin autogenerate:
// Create an empty "constructed" stylesheet
const sheet = new CSSStyleSheet();
// Apply a rule to the sheet
sheet.replaceSync("Your CSS code");
But you don't need this plugin, because css-loader already can do it, extra async import will decrease your perf...
Another solution - you can do it on own side using fetch(new URL("./file.css", import.meta.url))
and make CSSStyleSheet
by default, not sure we support new URL(...)
here right now, but it is not a big problem
The only way to do it in this scenario would be for import('./component.tsx')
to return something is not exported by component.tsx
, e.g.
// component.js
import './some-style.css'
import './some-other-style.css'
const {__stylesheet} = await import('./component.js')
const node = document.createElement("div");
const shadow = node.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [__stylesheet];
you can do it on own side using
fetch(new URL("./file.css", import.meta.url))
That's the issue, I don't have a file.css, the file to load is what import()
will generate after resolving all the dependencies of component.js
Best I can do is use import(/* webpackChunkName: "file" */ "./component.js")
and then expect file.css
to live in dist
as well. But the issue is that mini-css-extract-plugin
is still loading file.css
in document.head
You can disable runtime logic here using https://github.com/webpack-contrib/mini-css-extract-plugin?tab=readme-ov-file#runtime
Thank you, finally got around checking that out. Unfortunately that disables the injection for all generated bundles.
any sub-
import()
calls would still load the stylesheet in the main document
I don’t think there's really a comprehensive solution here that works with nested import()
statements, because two import("./more.js")
calls will only load the file and CSS once, even if they appear in two "contexts"
For truly isolated components, webpack needs to export a distinct configuration per component. This way I could set runtime: false
and deal with the loading.
In this scenario, I could also ask mini-css-extract-plugin
to accept a custom inject
function so that all of its injections will be in the right host (it would be a feature request)
It turns out, my first feature request was still the best way to achieve this:
add a magic comment to the
import()
to disable the automatic injection of the stylesheet e.g.import(/* webpackCss: no-inject */ './Component');
In the PR linked above you can see I'm manually detecting and disabling the injected stylesheet. It still won't supported nested import()
s, but it's something.
In my case I want to preserve the behavior so that a missed stylesheet can be detected and reported as an error, but if it might be useful to others.
Why do not use new URL("./file.css", import.meta.url)
, I can improve supporting it
What's "./file.css"? Where should I use that URL? How would it fix/avoid the issue?
As I understand it, you want to load CSS chunk and get CSSStyleSheet
, i.e:
// Pseudocode and yes, it is not statistical analizable
async function loadCssChunk(filename) {
const url = new URL(filename, import.neta.url);
const response = await fetch(url.toString());
const css = await response.text();
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
return sheet;
}
Shortly - import("./my-styles-for-component.css", { with { type: "css" } })
like we should in future due according spec
You don't want to use "css-loader" because it bloats chunks due to storing the CSS inside the JS file, right?
What CSS chunk?
If you're talking about the chunks generated by import()
, I'm already importing it via link
, it's a solved problem.
The problems here where a combination of:
- NOT injecting it into the main document by webpack
- injecting it into the shadow DOM by webpack
- knowing which stylesheets are created (webpackChunkName fixes this on one level, but not in nested dynamic imports)
- knowing which stylesheets are created
It turns out, this is still an issue. There are situations where the CSS bundle name is not what's suggested in webpackChunkName
: presumably this happens when part of the CSS is already injected as part of the main bundle, so webpack rightfully does not also add it to the deferred chunk.
Likely repro:
import "./file.css";
import(/* webpackChunkName: FOO */ "./file.js")
// file.js
import "./file.css";
import "./other.css";
console.log('file');
It will likely create:
- main.js: with the
import()
statement - main.css: with the contents of file.css
- FOO.js: with the console.log
- vendors-node_modules_primeicons_primeicons_css-node_modules_primereact_resources_primereact_m-b15087.css: with the contents of other.css
The long filename doesn't match the example but it's a real filename I'm seeing.
So I'm back to having to specify file.js
as one of the webpack entries as well, which safely generates file.css
.
The problem now is removing vendors-node_modules_primeicons_primeicons_css-node_modules_primereact_resources_primereact_m-b15087.css
from the document.
Reopening for this request specifically:
import(/* webpackCss: no-inject */ "./file.js");
Alternative solutions:
- don't extract CSS (leave it to css-loader)
- don't generate CSS file at all (drop CSS)
It feels like either one of these is already possible via some webpack config.
In my PR above I'm detecting and disabling any local stylesheets added while import()
is pending. This is quite verbose and might catch unrelated injections:
https://github.com/pixiebrix/pixiebrix-extension/blob/a6dc0fed1cf3a32d865e2177edd26a4f3dedbf14/src/components/IsolatedComponent.tsx#L35-L76
Reopening for this request specifically:
import(/* webpackCss: no-inject */ "./file.js");
I don't feel it is a right idea, what this import should return?
The import
should keep working as expected for the JS part; the magic comment would only prevent the extraction, generation and/or injection of any CSS encountered.
Depending on the specific verb chosen:
-
no-inject
: generatefile.css
, but don't inject it (this is either lost, or picked up by another plugin) -
no-extract
: ignore any CSS found, leave it to the next CSS plugin, if any; it's as ifmini-css-extract-plugin
wasn't installed at all -
discard
: drop any CSS module found, don't generatefile.css
at all
For me, the first choice is enough, while the last would be great
I don't really like this approach, since it will contradict the specification import
itself as, because - import
should return module, this approach only creates problems, which is why I want to find out what you ultimately want to get and where you are passing it to in order to understand how to solve it correctly without violating the specification.
Now we suppirt three things:
-
import * styles from "./style.css"
andimport(...)
return module itself (locals) -
new URL(..., import.meta.url)
returns URL to CSS files, so you can write a custom logic for inejction - Future (not implemented right now) -
import styles from "./style.css" with { type: "css" }
(or asset keyword, old spec) should generate CSS file, load them and createCSSStyleSheet
as I written above
These three things should solve any things.
Also we can setup the plugin only for CSS generation without runtime using runtime: false
, so CSS is like just assets.
For more flexibility we can implement import(/* webpackCss: no-inject */ "./file.js");
, but I want to make sure that this is really the last correct solution that we can implement, because it also, to some extent, looks more like a hack than a real solution
import
should return module
Where did I say to change that behavior? What I'm suggesting with no-inject
is exactly what the plugin already does with runtime: false
, except that it's applied to a specific import()
location rather than to all chunks.
Perhaps it needs to be import(/* mini-css-extract-plugin: no-runtime */ "./file.js");
to match the existing config name.
Got it, I will try to find a time on it, but I don't know when, anyway if you want to send a PR, feel free to do it