Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(css): add support for injecting css into custom elements #12206

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/config/shared-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,26 @@ export default defineConfig({
})
```

## css.inject

- **Type:** `string | ((node: Element) => void)`

A (stringified) function with the signature `(node: Element) => void` that is used to inject CSS style tags when importing CSS in JS.

If passed a function it will be stringified and can therefore not rely on any variables on the outer scopes.

Note this does not affect `<link >` tags added to `index.html`.

```js
export default defineConfig({
css: {
inject: (node) => {
document.body.querySelector('custom-element').shadowRoot.appendChild(node)
},
},
})
```

## css.devSourcemap

- **Experimental**
Expand Down
15 changes: 12 additions & 3 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,12 @@ const sheetsMap = new Map<string, HTMLStyleElement>()
// because after build it will be a single css file
let lastInsertedStyle: HTMLStyleElement | undefined

export function updateStyle(id: string, content: string): void {
export function updateStyle(
id: string,
content: string,
inject?: (style: HTMLStyleElement) => void,
): void {
console.log('updateStyle', id, content)
let style = sheetsMap.get(id)
if (!style) {
style = document.createElement('style')
Expand All @@ -346,7 +351,11 @@ export function updateStyle(id: string, content: string): void {
style.textContent = content

if (!lastInsertedStyle) {
document.head.appendChild(style)
if (inject) {
inject(style)
} else {
document.head.appendChild(style)
}

// reset lastInsertedStyle after async
// because dynamically imported css will be splitted into a different file
Expand All @@ -366,7 +375,7 @@ export function updateStyle(id: string, content: string): void {
export function removeStyle(id: string): void {
const style = sheetsMap.get(id)
if (style) {
document.head.removeChild(style)
style.remove()
sheetsMap.delete(id)
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export interface CSSOptions {
* @experimental
*/
devSourcemap?: boolean
/**
* Stringified function with the signature `(node: Element) => void`
* that is used to inject stylesheets.
*
* By default styles are appended to the document.head.
*/
inject?: string | ((node: Element) => void)
}

export interface CSSModulesOptions {
Expand Down Expand Up @@ -401,7 +408,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)}`,
`const __vite__id = ${JSON.stringify(id)}`,
`const __vite__css = ${JSON.stringify(cssContent)}`,
`__vite__updateStyle(__vite__id, __vite__css)`,
`const __vite__inject_css = ${config.css?.inject}`,
`__vite__updateStyle(__vite__id, __vite__css, __vite__inject_css)`,
// css modules exports change on edit so it can't self accept
`${
modulesCode ||
Expand Down
24 changes: 24 additions & 0 deletions playground/css-inject/__tests__/css-inject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from 'vitest'
import { editFile, getColor, page, untilUpdated } from '~utils'

test('css inject', async () => {
const linkedOutside = await page.$('.linked.outside')
const linkedInside = await page.$('.linked.inside')
const importedOutside = await page.$('.imported.outside')
const importedInside = await page.$('.imported.inside')

expect(await getColor(linkedOutside)).toBe('red')
expect(await getColor(linkedInside)).toBe('black')
expect(await getColor(importedOutside)).toBe('black')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the tests are failing on this line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They fail for production mode which is not in our scope. One would need to find another way for production mode support. As we worked around this issue I'll close this PR. Anyone is welcome to re-use my work to continue on this.

expect(await getColor(importedInside)).toBe('red')

editFile('linked.css', (code) => code.replace('color: red', 'color: blue'))

await untilUpdated(() => getColor(linkedOutside), 'blue')
expect(await getColor(linkedInside)).toBe('black')

editFile('imported.css', (code) => code.replace('color: red', 'color: blue'))

await untilUpdated(() => getColor(importedInside), 'blue')
expect(await getColor(importedOutside)).toBe('black')
})
3 changes: 3 additions & 0 deletions playground/css-inject/imported.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.imported {
color: red;
}
37 changes: 37 additions & 0 deletions playground/css-inject/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script></script>
<link rel="stylesheet" href="./linked.css" />

<div class="wrapper">
<h1>CSS Inject</h1>

<p class="linked outside">&lt;linked&gt;</p>

<p class="imported outside">&lt;imported&gt;</p>

<custom-element></custom-element>
</div>

<script type="module">
customElements.define(
'custom-element',
class CustomElement extends HTMLElement {
constructor() {
super()
console.log('ctor')
this.attachShadow({ mode: 'open' })
}

connectedCallback() {
console.log('connected')

this.shadowRoot.innerHTML = `
<p class="linked inside">&lt;linked&gt;</p>

<p class="imported inside">&lt;imported&gt;</p>
`

import('./imported.css')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this feature to "alternate" the import globally. Also it does not seem to be flexible when you have multiple shadow roots.

Wondering why you want to do it this implicit way, instead of appending the CSS manually with ?inline

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?inline can't be applied (as far as I'm aware) to styles within Vue SFCs. Which is my use case.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antfu does ?inline works for css imported by 3rd party lib?

}
},
)
</script>
3 changes: 3 additions & 0 deletions playground/css-inject/linked.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.linked {
color: red;
}
11 changes: 11 additions & 0 deletions playground/css-inject/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@vitejs/test-css-sourcemap",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
"preview": "vite preview"
}
}
19 changes: 19 additions & 0 deletions playground/css-inject/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @type {import('vite').UserConfig}
*/
module.exports = {
resolve: {
alias: {
'@': __dirname,
},
},
css: {
devSourcemap: true,
inject: (node) => {
document.body.querySelector('custom-element').shadowRoot.appendChild(node)
},
},
build: {
sourcemap: true,
},
}