diff options
-rw-r--r-- | .changeset/large-beds-cheer.md | 5 | ||||
-rw-r--r-- | packages/astro/src/core/render/dev/index.ts | 8 | ||||
-rw-r--r-- | packages/astro/src/runtime/client/hmr.ts | 47 | ||||
-rw-r--r-- | packages/astro/test/0-css.test.js | 10 | ||||
-rw-r--r-- | packages/astro/test/astro-markdown-css.test.js | 2 | ||||
-rw-r--r-- | packages/astro/test/astro-partial-html.test.js | 4 | ||||
-rw-r--r-- | packages/astro/test/component-library.test.js | 2 |
7 files changed, 49 insertions, 29 deletions
diff --git a/.changeset/large-beds-cheer.md b/.changeset/large-beds-cheer.md new file mode 100644 index 000000000..10a3aecff --- /dev/null +++ b/.changeset/large-beds-cheer.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix duplicated CSS when using HMR diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 7de9a979a..abce2c4e4 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -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, }); }); diff --git a/packages/astro/src/runtime/client/hmr.ts b/packages/astro/src/runtime/client/hmr.ts index ba17fc526..f3a3074f3 100644 --- a/packages/astro/src/runtime/client/hmr.ts +++ b/packages/astro/src/runtime/client/hmr.ts @@ -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'; +} diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js index 7d7000584..9f8d901ce 100644 --- a/packages/astro/test/0-css.test.js +++ b/packages/astro/test/0-css.test.js @@ -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{'); diff --git a/packages/astro/test/astro-markdown-css.test.js b/packages/astro/test/astro-markdown-css.test.js index d6f98e068..c6d8bc71a 100644 --- a/packages/astro/test/astro-markdown-css.test.js +++ b/packages/astro/test/astro-markdown-css.test.js @@ -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}`); }); }); diff --git a/packages/astro/test/astro-partial-html.test.js b/packages/astro/test/astro-partial-html.test.js index 83d357693..6073f1bd1 100644 --- a/packages/astro/test/astro-partial-html.test.js +++ b/packages/astro/test/astro-partial-html.test.js @@ -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;}/); }); diff --git a/packages/astro/test/component-library.test.js b/packages/astro/test/component-library.test.js index 9158c5b2e..bab16230b 100644 --- a/packages/astro/test/component-library.test.js +++ b/packages/astro/test/component-library.test.js @@ -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; } |