Fix duplicate CSS in dev
mode (#4157)
* fix(hmr): remove SSR styles once HMR styles are injected * refactor: remove data-astro-injected tag * chore: add changeset Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
29da99c4b3
commit
025743849d
7 changed files with 49 additions and 29 deletions
5
.changeset/large-beds-cheer.md
Normal file
5
.changeset/large-beds-cheer.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fix duplicated CSS when using HMR
|
|
@ -148,8 +148,7 @@ export async function render(
|
|||
links.add({
|
||||
props: {
|
||||
rel: 'stylesheet',
|
||||
href,
|
||||
'data-astro-injected': true,
|
||||
href
|
||||
},
|
||||
children: '',
|
||||
});
|
||||
|
@ -162,15 +161,12 @@ export async function render(
|
|||
props: {
|
||||
type: 'module',
|
||||
src: url,
|
||||
'data-astro-injected': true,
|
||||
},
|
||||
children: '',
|
||||
});
|
||||
// But we still want to inject the styles to avoid FOUC
|
||||
styles.add({
|
||||
props: {
|
||||
'data-astro-injected': url,
|
||||
},
|
||||
props: {},
|
||||
children: content,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', async (payload) => {
|
||||
for (const file of payload.updates) {
|
||||
if (
|
||||
file.acceptedPath.includes('svelte&type=style') ||
|
||||
file.acceptedPath.includes('astro&type=style')
|
||||
) {
|
||||
// This will only be called after the svelte component has hydrated in the browser.
|
||||
// At this point Vite is tracking component style updates, we need to remove
|
||||
// styles injected by Astro for the component in favor of Vite's internal HMR.
|
||||
const injectedStyle = document.querySelector(
|
||||
`style[data-astro-injected="${file.acceptedPath}"]`
|
||||
);
|
||||
if (injectedStyle) {
|
||||
injectedStyle.parentElement?.removeChild(injectedStyle);
|
||||
// Vite injects `<style type="text/css">` for ESM imports of styles
|
||||
// but Astro also SSRs with `<style>` blocks. This MutationObserver
|
||||
// removes any duplicates as soon as they are hydrated client-side.
|
||||
const injectedStyles = getInjectedStyles();
|
||||
const mo = new MutationObserver((records) => {
|
||||
for (const record of records) {
|
||||
for (const node of record.addedNodes) {
|
||||
if (isViteInjectedStyle(node)) {
|
||||
injectedStyles.get(node.innerHTML.trim())?.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
mo.observe(document.documentElement, { subtree: true, childList: true });
|
||||
|
||||
// Vue `link` styles need to be manually refreshed in Firefox
|
||||
import.meta.hot.on('vite:beforeUpdate', async (payload) => {
|
||||
for (const file of payload.updates) {
|
||||
if (file.acceptedPath.includes('vue&type=style')) {
|
||||
const link = document.querySelector(`link[href="${file.acceptedPath}"]`);
|
||||
if (link) {
|
||||
|
@ -25,3 +28,19 @@ if (import.meta.hot) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getInjectedStyles() {
|
||||
const injectedStyles = new Map<string, Element>();
|
||||
document.querySelectorAll<HTMLStyleElement>('style').forEach((el) => {
|
||||
injectedStyles.set(el.innerHTML.trim(), el);
|
||||
});
|
||||
return injectedStyles;
|
||||
}
|
||||
|
||||
function isStyle(node: Node): node is HTMLStyleElement {
|
||||
return node.nodeType === node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === 'style';
|
||||
}
|
||||
|
||||
function isViteInjectedStyle(node: Node): node is HTMLStyleElement {
|
||||
return isStyle(node) && node.getAttribute('type') === 'text/css';
|
||||
}
|
||||
|
|
|
@ -298,7 +298,7 @@ describe('CSS', function () {
|
|||
});
|
||||
|
||||
it('resolves ESM style imports', async () => {
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||
const allInjectedStyles = $('style').text().replace(/\s*/g, '');
|
||||
|
||||
expect(allInjectedStyles, 'styles/imported-url.css').to.contain('.imported{');
|
||||
expect(allInjectedStyles, 'styles/imported-url.sass').to.contain('.imported-sass{');
|
||||
|
@ -306,7 +306,7 @@ describe('CSS', function () {
|
|||
});
|
||||
|
||||
it('resolves Astro styles', async () => {
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text();
|
||||
const allInjectedStyles = $('style').text();
|
||||
|
||||
expect(allInjectedStyles).to.contain('.linked-css:where(.astro-');
|
||||
expect(allInjectedStyles).to.contain('.linked-sass:where(.astro-');
|
||||
|
@ -325,7 +325,7 @@ describe('CSS', function () {
|
|||
expect((await fixture.fetch(href)).status, style).to.equal(200);
|
||||
}
|
||||
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||
const allInjectedStyles = $('style').text().replace(/\s*/g, '');
|
||||
|
||||
expect(allInjectedStyles).to.contain('.react-title{');
|
||||
expect(allInjectedStyles).to.contain('.react-sass-title{');
|
||||
|
@ -333,7 +333,7 @@ describe('CSS', function () {
|
|||
});
|
||||
|
||||
it('resolves CSS from Svelte', async () => {
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text();
|
||||
const allInjectedStyles = $('style').text();
|
||||
|
||||
expect(allInjectedStyles).to.contain('.svelte-css');
|
||||
expect(allInjectedStyles).to.contain('.svelte-sass');
|
||||
|
@ -347,7 +347,7 @@ describe('CSS', function () {
|
|||
expect((await fixture.fetch(href)).status, style).to.equal(200);
|
||||
}
|
||||
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||
const allInjectedStyles = $('style').text().replace(/\s*/g, '');
|
||||
|
||||
expect(allInjectedStyles).to.contain('.vue-css{');
|
||||
expect(allInjectedStyles).to.contain('.vue-sass{');
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('Imported markdown CSS', function () {
|
|||
expect(importedAstroComponent?.name).to.equal('h2');
|
||||
const cssClass = $(importedAstroComponent).attr('class')?.split(/\s+/)?.[0];
|
||||
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||
const allInjectedStyles = $('style').text().replace(/\s*/g, '');
|
||||
expect(allInjectedStyles).to.include(`h2:where(.${cssClass}){color:#00f}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('Partial HTML', async () => {
|
|||
expect(html).to.match(/^<!DOCTYPE html/);
|
||||
|
||||
// test 2: correct CSS present
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text();
|
||||
const allInjectedStyles = $('style').text();
|
||||
expect(allInjectedStyles).to.match(/\:where\(\.astro-[^{]+{color:red}/);
|
||||
});
|
||||
|
||||
|
@ -37,7 +37,7 @@ describe('Partial HTML', async () => {
|
|||
expect(html).to.match(/^<!DOCTYPE html/);
|
||||
|
||||
// test 2: link tag present
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||
const allInjectedStyles = $('style').text().replace(/\s*/g, '');
|
||||
expect(allInjectedStyles).to.match(/h1{color:red;}/);
|
||||
});
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ describe('Component Libraries', () => {
|
|||
const $ = cheerioLoad(html);
|
||||
|
||||
// Most styles are inlined in a <style> block in the dev server
|
||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||
const allInjectedStyles = $('style').text().replace(/\s*/g, '');
|
||||
if (expected.test(allInjectedStyles)) {
|
||||
return true;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue