summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/large-beds-cheer.md5
-rw-r--r--packages/astro/src/core/render/dev/index.ts8
-rw-r--r--packages/astro/src/runtime/client/hmr.ts47
-rw-r--r--packages/astro/test/0-css.test.js10
-rw-r--r--packages/astro/test/astro-markdown-css.test.js2
-rw-r--r--packages/astro/test/astro-partial-html.test.js4
-rw-r--r--packages/astro/test/component-library.test.js2
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;
}