Skip to content

Commit 7325df4

Browse files
authored
Remove duplicate CSS in dev (#5917)
* fix(#5817): remove duplicate CSS in dev * chore: add changeset Co-authored-by: Nate Moore <nate@astro.build>
1 parent 4987d6f commit 7325df4

File tree

11 files changed

+107
-8
lines changed

11 files changed

+107
-8
lines changed

.changeset/rotten-cups-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fix duplicate CSS in dev mode when `vite.css.devSourcemap` is provided
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect } from '@playwright/test';
2+
import { getColor, isWindows, testFactory } from './test-utils.js';
3+
4+
const test = testFactory({
5+
root: './fixtures/css/',
6+
});
7+
8+
let devServer;
9+
10+
test.beforeAll(async ({ astro }) => {
11+
devServer = await astro.startDevServer();
12+
});
13+
14+
test.afterAll(async () => {
15+
await devServer.stop();
16+
});
17+
18+
test.describe('CSS Sourcemap HMR', () => {
19+
test.skip(isWindows, 'TODO: fix css hmr in windows');
20+
21+
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
22+
const html = await astro.fetch('/').then(res => res.text());
23+
24+
// style[data-astro-dev-id] should exist in initial SSR'd markup
25+
expect(html).toMatch('data-astro-dev-id');
26+
27+
await page.goto(astro.resolveUrl('/'));
28+
29+
// Ensure JS has initialized
30+
await page.waitForTimeout(500);
31+
32+
// style[data-astro-dev-id] should NOT exist once JS runs
33+
expect(await page.locator('style[data-astro-dev-id]').count()).toEqual(0);
34+
35+
// style[data-vite-dev-id] should exist now
36+
expect(await page.locator('style[data-vite-dev-id]').count()).toBeGreaterThan(0);
37+
});
38+
});

packages/astro/e2e/css.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,22 @@ test.describe('CSS HMR', () => {
3030

3131
expect(await getColor(h)).toBe('rgb(0, 128, 0)');
3232
});
33+
34+
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
35+
const html = await astro.fetch('/').then(res => res.text());
36+
37+
// style[data-astro-dev-id] should exist in initial SSR'd markup
38+
expect(html).toMatch('data-astro-dev-id');
39+
40+
await page.goto(astro.resolveUrl('/'));
41+
42+
// Ensure JS has initialized
43+
await page.waitForTimeout(500);
44+
45+
// style[data-astro-dev-id] should NOT exist once JS runs
46+
expect(await page.locator('style[data-astro-dev-id]').count()).toEqual(0);
47+
48+
// style[data-vite-dev-id] should exist now
49+
expect(await page.locator('style[data-vite-dev-id]').count()).toBeGreaterThan(0);
50+
});
3351
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
vite: {
3+
css: {
4+
devSourcemap: true,
5+
}
6+
}
7+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@e2e/css-sourcemaps",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="astro/client" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<h1>hello world</h1>
2+
3+
<style>
4+
@import "../styles/main.css";
5+
6+
h1 {
7+
color: var(--h1-color);
8+
}
9+
</style>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:root {
2+
--h1-color: red;
3+
}

packages/astro/src/core/render/dev/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
1010
import { enhanceViteSSRError } from '../../errors/dev/index.js';
1111
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
1212
import type { ModuleLoader } from '../../module-loader/index';
13-
import { isPage, resolveIdToUrl } from '../../util.js';
13+
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
1414
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
1515
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
1616
import { getStylesForURL } from './css.js';
@@ -133,7 +133,11 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
133133
});
134134
// But we still want to inject the styles to avoid FOUC
135135
styles.add({
136-
props: {},
136+
props: {
137+
type: 'text/css',
138+
// Track the ID so we can match it to Vite's injected style later
139+
'data-astro-dev-id': viteID(new URL(`.${url}`, env.settings.config.root))
140+
},
137141
children: content,
138142
});
139143
});

packages/astro/src/runtime/client/hmr.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/// <reference types="vite/client" />
22

33
if (import.meta.hot) {
4-
// Vite injects `<style type="text/css">` for ESM imports of styles
5-
// but Astro also SSRs with `<style>` blocks. This MutationObserver
4+
// Vite injects `<style type="text/css" data-vite-dev-id>` for ESM imports of styles
5+
// but Astro also SSRs with `<style type="text/css" data-astro-dev-id>` blocks. This MutationObserver
66
// removes any duplicates as soon as they are hydrated client-side.
77
const injectedStyles = getInjectedStyles();
88
const mo = new MutationObserver((records) => {
99
for (const record of records) {
1010
for (const node of record.addedNodes) {
1111
if (isViteInjectedStyle(node)) {
12-
injectedStyles.get(node.innerHTML.trim())?.remove();
12+
injectedStyles.get(node.getAttribute('data-vite-dev-id')!)?.remove();
1313
}
1414
}
1515
}
@@ -31,8 +31,8 @@ if (import.meta.hot) {
3131

3232
function getInjectedStyles() {
3333
const injectedStyles = new Map<string, Element>();
34-
document.querySelectorAll<HTMLStyleElement>('style').forEach((el) => {
35-
injectedStyles.set(el.innerHTML.trim(), el);
34+
document.querySelectorAll<HTMLStyleElement>('style[data-astro-dev-id]').forEach((el) => {
35+
injectedStyles.set(el.getAttribute('data-astro-dev-id')!, el);
3636
});
3737
return injectedStyles;
3838
}
@@ -42,5 +42,5 @@ function isStyle(node: Node): node is HTMLStyleElement {
4242
}
4343

4444
function isViteInjectedStyle(node: Node): node is HTMLStyleElement {
45-
return isStyle(node) && node.getAttribute('type') === 'text/css';
45+
return isStyle(node) && node.getAttribute('type') === 'text/css' && !!node.getAttribute('data-vite-dev-id');
4646
}

0 commit comments

Comments
 (0)