Add blog post about extensions and shadow dom
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Kaan Barmore-Genç 2024-05-18 23:51:44 -05:00
parent 7d93c9d88b
commit 6c50ff1b84
Signed by: kaan
GPG key ID: B2E280771CD62FCF
5 changed files with 194 additions and 2 deletions

28
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,28 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": [],
"label": "npm: dev",
"detail": "vite dev"
},
{
"type": "shell",
"command": "npm",
"args": ["run", "new-post", "${input:post-name}"],
"problemMatcher": [],
"label": "npm: new post",
"detail": "new post"
}
],
"inputs": [
{
"type": "promptString",
"id": "post-name",
"description": "Post name",
"default": "Incididunt aliqua adipisicing ipsum"
}
]
}

View file

@ -12,7 +12,8 @@
"format": "prettier --write .",
"pagefind": "pagefind",
"preview:pagefind": "pagefind --site .svelte-kit/output/prerendered/pages/",
"build:pagefind": "pagefind --site build/"
"build:pagefind": "pagefind --site build/",
"new-post": "node scripts/new-post.js"
},
"devDependencies": {
"@sveltejs/kit": "^1.27.6",

23
scripts/new-post.js Normal file
View file

@ -0,0 +1,23 @@
import { format, formatISO } from 'date-fns';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const title = process.argv[2] ?? 'New Blog Post';
const slug = title.toLowerCase().replaceAll(' ', '-').slice(0, 60);
const filename = `${format(new Date(), 'yyyy.MM.dd')}.${slug}.md`;
const contents = `---
title: '${title}'
date: ${formatISO(new Date())}
description: 'Blog post description here'
---
`;
console.log('dirname', __dirname);
console.log('filename', filename);
fs.writeFileSync(path.join(__dirname, '..', 'src', 'routes', 'posts', filename), contents);

View file

@ -1,5 +1,5 @@
---
title: Making Customizable Components in Svelte
title: 'Making Customizable Components in Svelte'
date: 2024-05-12T17:50:52+0000
tags:
- dev

View file

@ -0,0 +1,140 @@
---
title: 'Using Shadow DOM to isolate injected browser extension components'
date: 2024-05-18T23:08:16-05:00
description: 'I spent a weekend building a browser extension with React and TailwindCSS, but injected components can be a lot of trouble. This post explores how Shadow DOM saves the day (and your CSS sanity).'
---
I recently started experimenting with building a browser extension. I'm using
React and TailwindCSS --bundling it all with Vite-- creating some UI elements like buttons and injecting them
into certain web pages. Setting this up was super easy, I used [a repository template](https://github.com/JohnBra/vite-web-extension)
to get a PoC as quickly as possible, and a few minutes later I had something working!
One issue with injecting components into web pages though is that your
components are injected into the regular page DOM. This means that your
components can be affected by scripts or CSS on the page, and conversely your
CSS can affect existing elements on the page should anything collide.
This is a bit of a problem when using TailwindCSS. On one hand, utility classes
have the same meaning across websites. For example, `w-full` is always going to
be `width: 100%;`, because what else would it be? On the other hand, websites
_are_ always able to change what CSS these utility classes map to. `rounded`
might mean different amounts of rounding on different websites, and
`bg-color-primary` is going to map to different colors based on the color
palette of the website. And nobody can stop you if you want to change `w-full`
to set the width to 10 pixels.
Avoiding these collisions in the past would have involved prefixing all your IDs
and classes so that they are distinct from any ones you might encounter on the
page. Even then, Javascript code of the original page can traverse the
components you inject on the page and inspect or modify them! Nowadays though,
Shadow DOM allows us to easily isolate components on the page. Using Shadow DOM,
the CSS of the injected components is isolated from the rest of the page. That
isolation works in both directions, the CSS from the original page won't affect
the elements in the Shadow DOM, and the CSS injected into the Shadow DOM won't
affect the elements outside of it. Plus, Shadow DOM has an option to isolate the
injected components from Javascript of the original page so they can't traverse
into the Shadow DOM (the original page can still delete or move the root of the
Shadow DOM, but can't look inside of it).
And the code for this is very simple! Shadow DOM might sound a bit scary, but
it's actually very easy. Let's start with some basic code to inject a React
component into a page:
```ts
import styles from "./style.css";
import { createRoot } from "react-dom/client";
const injectionTarget = document.querySelector<HTMLDivElement>(
"#place-to-inject-my-component",
);
if (!injectionTarget) {
throw new Error("Can't find injection target");
}
const rootContainer = document.createElement("div");
injectionTarget.appendChild(rootContainer);
const root = createRoot(rootContainer);
root.render(<YourElement />);
```
To use Shadow DOM, we need to create the Shadow DOM and attach it to the page,
then inject our React root inside of the Shadow DOM. This is as easy as calling
`attachShadow`! Let's modify the example to use the Shadow DOM:
```ts
import styles from "./style.css";
import { createRoot } from "react-dom/client";
const injectionTarget = document.querySelector<HTMLDivElement>(
"#place-to-inject-my-component",
);
if (!injectionTarget) {
throw new Error("Can't find injection target");
}
const shadowDomRoot = document.createElement("div");
injectionTarget.appendChild(shadowDomRoot);
const shadow = shadowDomRoot.attachShadow({
// closed means the contents of the shadow can't be accessed from the page
mode: "closed"
});
const rootContainer = document.createElement("div");
shadow.appendChild(rootContainer);
const root = createRoot(rootContainer);
root.render(<YourElement />);
```
Nice! We just had to add about 3 lines of code, and now our components lives
inside it's own bubble.
Now, if you are following along with this example and running the code, you
might have noticed that your injected component suddenly has no styles applied
to it now. Well, that's the CSS isolation the Shadow DOM gives you. How do we
get around this? That's not too much work either, we just need to inject the CSS
into the page too.
First, you can use the manifest file to inject CSS into the page declaratively.
This is in fact what the repository template I linked above does. But that's not
what we want here, because we don't want the CSS to be just injected into the
page. We want it injected specifically into the Shadow DOM we just created. So,
we'll have to disable this declarative CSS injection.
```json
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"js": ["src/pages/content/index.tsx"],
// Make sure this is empty
"css": []
}
],
```
Now that we got rid of that, we'll want to import the CSS into our script and
inject that along too. The trick here is adding `?inline` to the end of the CSS
import so that rather than injecting the CSS into the page, Vite understands that
we want the contents of the CSS.
```ts
// Note the `?inline`
import styles from './style.css?inline';
import { createRoot } from 'react-dom/client';
// ... same as example above
// Create the style element, put the CSS into it,
// and inject it into the Shadow DOM.
const style = document.createElement('style');
style.innerHTML = styles;
shadow.appendChild(style);
// ... same as example above
```
Tada! Your component should now light up with all the fancy CSS --or utility
classes-- you added to it. While the Shadow DOM example is slightly more complex
than the most basic option I demonstrated first, the advantages you get in CSS
and JS isolation are worth it considering it only takes a few lines of text.