From 6c50ff1b841738976d98cd5afa051f052efab2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaan=20Barmore-Gen=C3=A7?= Date: Sat, 18 May 2024 23:51:44 -0500 Subject: [PATCH] Add blog post about extensions and shadow dom --- .vscode/tasks.json | 28 ++++ package.json | 3 +- scripts/new-post.js | 23 +++ ...24.05.12.svelte-customizable-components.md | 2 +- ...solate-injected-browser-extension-compo.md | 140 ++++++++++++++++++ 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 scripts/new-post.js create mode 100644 src/routes/posts/2024.05.18.using-shadow-dom-to-isolate-injected-browser-extension-compo.md diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..180206a --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + ] +} diff --git a/package.json b/package.json index 0e1b03c..86d3515 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/new-post.js b/scripts/new-post.js new file mode 100644 index 0000000..4b10bc5 --- /dev/null +++ b/scripts/new-post.js @@ -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); diff --git a/src/routes/posts/2024.05.12.svelte-customizable-components.md b/src/routes/posts/2024.05.12.svelte-customizable-components.md index 20583ca..d0d7503 100644 --- a/src/routes/posts/2024.05.12.svelte-customizable-components.md +++ b/src/routes/posts/2024.05.12.svelte-customizable-components.md @@ -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 diff --git a/src/routes/posts/2024.05.18.using-shadow-dom-to-isolate-injected-browser-extension-compo.md b/src/routes/posts/2024.05.18.using-shadow-dom-to-isolate-injected-browser-extension-compo.md new file mode 100644 index 0000000..98f91f6 --- /dev/null +++ b/src/routes/posts/2024.05.18.using-shadow-dom-to-isolate-injected-browser-extension-compo.md @@ -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( + "#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(); +``` + +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( + "#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(); +``` + +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://*/*", ""], + "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.