extension.js
extension.js copied to clipboard
Shadow DOM styles support
Hi there and thanks for the hard work!
I'm trying to build an extension that places elements into a page via content scripts (one of them most common usecases I would say). Now the only way to do this reliably really is through the Shadow DOM, to make sure generic page styles don't bleed through (my favourite example is unironically example.com that defines styles on div).
Unfortunately that means that styles need to be injected into the shadow DOM as well and that is currently abstracted away in https://github.com/extension-js/extension.js/tree/main/programs/develop/webpack/plugin-css without possibilities to intervene from userland, where they are injected into the body by the style loader.
What is your recommended approach regarding non-tailwind styles in a shadow DOM with extension.js?
Ultimately it would need to be a bundled file (in-memory while webpack serve and a flat file after building) that can be injected at will, right?
import css from "../bundled.css?inline";
// create variable to attach the tailwind stylesheet
let style = document.createElement('style');
// attach the stylesheet as text
style.textContent = css;
// apply the style
shadowWrapper.appendChild(style);
hey @tom2strobl, I've been thinking about this for a while.
so far, Extension.js inline the styles during dev (via style-loader) and output the files on production (via mini-css-extract-plugin). to make shado-dom content_scripts to work internally, I need to flag CSS imports from content_scripts to output the files instead of inlining them. I also need to consider a way to flag such CSS to the compiler. I'm thinking something about a "use shadow-dom" string at the top of the file which would tell the compiler to output styles instead of inlining them.
thoughts on this?
Firstly, thanks for caring and thinking about how to approach it!
Secondly – I started a private local fork to be able to override more stuff in the long run and holy cow you invested a shitload of time in the tooling around extension & browser handling. Thank you so much for the hard work!
To get back on topic: I think it's pretty tricky to solve with a nice DX on your end for all cases and I'm not entirely sure if you want to cover them all. It's probably better to offer the possibility to override (well, "webpack-merge") webpacks configs and optionally offer some recipes for users to implement stuff themselves (as you kinda do with the gazillion examples you already provide).
In my case I'm not directly loading a content_script per se, but instead do an executeScript call on action as this makes sure regular browsing is not slowed down at all by the extension as nothing gets injected. And only if the user decides to use the extension (alas click the extension icon in the toolbar) any code will be executed.
This means that the style injection step (in my case) needs to happen at an arbitrary point in time when the "content"-script is executed.
Afaik you already build to dist in dev for hmr stuff and clean dist in lifecycle events, so I think I'll just try to enable useMiniCssExtractPlugin in dev aswell for the style loaders and play around with injecting (and deduping) the css at runtime. I'll report back how it went!
so basically (also in development) doing new MiniCssExtractPlugin({ runtime: false }) and useMiniCssExtractPlugin: true emits the content.css perfectly actually.
in my executeScript()'ed content.ts I can now simply do:
[...]
// my shadow root
const shadowRoot = appContainer.attachShadow({ mode: 'open' })
// check if already injected, and do so if not already
const cssFiles = Array.from(document.querySelectorAll('link[data-myspecialdenominator]'))
if (cssFiles.length === 0) {
const linkElement = document.createElement('link')
linkElement.rel = 'stylesheet'
linkElement.href = chrome.runtime.getURL('scripts/content.css')
linkElement.dataset.myspecialdenominator = ''
shadowRoot.appendChild(linkElement)
}
[...]
which nicely injects the style into the shadow root. Now onto trying to figure out HMR 🔨
So I just saw that you updated the docs and I read up about extension.config.js which is exactly everything I was hoping for in terms of ability to override stuff (eg. another thing is I'm importing from a different package in the same monorepo where these sources need to be transpiled as well and your plugin-js-frameworks/index.ts has a fixed include on the project path, so that wouldn't work; a simple config.module.rules.push() of a new ts rule for the other packages in the monorepo works perfectly and cleanly without any forking necessary 🤜🏻🤛🏻).
Regarding the shadow-dom – after some experimentation I realized that HMR with the previously mentioned setup is more painful than the hottest curry at that thai place around the corner, so I opted for a different approach:
Since 1) I use react anyway, 2) I do like the DX of css-in-js solutions like styled-components and 3) any JS HMRs very well with extension-js, I opted for https://github.com/Wildhoney/ReactShadow because it handles the generation of a StyleSheetManager per shadowRoot automatically, completely bypassing the need of webpack css handling and manual injection. Since it's just JS at the end, it HMRs really nicely out of the box.
hey @tom2strobl thanks for the kind words and for taking time to dig into this! really appreaciated.
didn't know about ReactShadow, thanks for sharing.
if you think there is something I can do to help you/other developers looking for content_scripts with shadow-dom in the future, let me know!
@tom2strobl just updated [email protected] and all content_script templates are built with shadow-dom.
solid ✨