Remove duplicate CSS in dev (#5917)

* fix(#5817): remove duplicate CSS in dev

* chore: add changeset

Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
Nate Moore 2023-01-23 12:03:19 -06:00 committed by GitHub
parent 4987d6f44c
commit 7325df4121
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 107 additions and 8 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix duplicate CSS in dev mode when `vite.css.devSourcemap` is provided

View file

@ -0,0 +1,38 @@
import { expect } from '@playwright/test';
import { getColor, isWindows, testFactory } from './test-utils.js';
const test = testFactory({
root: './fixtures/css/',
});
let devServer;
test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer();
});
test.afterAll(async () => {
await devServer.stop();
});
test.describe('CSS Sourcemap HMR', () => {
test.skip(isWindows, 'TODO: fix css hmr in windows');
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
const html = await astro.fetch('/').then(res => res.text());
// style[data-astro-dev-id] should exist in initial SSR'd markup
expect(html).toMatch('data-astro-dev-id');
await page.goto(astro.resolveUrl('/'));
// Ensure JS has initialized
await page.waitForTimeout(500);
// style[data-astro-dev-id] should NOT exist once JS runs
expect(await page.locator('style[data-astro-dev-id]').count()).toEqual(0);
// style[data-vite-dev-id] should exist now
expect(await page.locator('style[data-vite-dev-id]').count()).toBeGreaterThan(0);
});
});

View file

@ -30,4 +30,22 @@ test.describe('CSS HMR', () => {
expect(await getColor(h)).toBe('rgb(0, 128, 0)');
});
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
const html = await astro.fetch('/').then(res => res.text());
// style[data-astro-dev-id] should exist in initial SSR'd markup
expect(html).toMatch('data-astro-dev-id');
await page.goto(astro.resolveUrl('/'));
// Ensure JS has initialized
await page.waitForTimeout(500);
// style[data-astro-dev-id] should NOT exist once JS runs
expect(await page.locator('style[data-astro-dev-id]').count()).toEqual(0);
// style[data-vite-dev-id] should exist now
expect(await page.locator('style[data-vite-dev-id]').count()).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,7 @@
export default {
vite: {
css: {
devSourcemap: true,
}
}
};

View file

@ -0,0 +1,8 @@
{
"name": "@e2e/css-sourcemaps",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1 @@
/// <reference types="astro/client" />

View file

@ -0,0 +1,9 @@
<h1>hello world</h1>
<style>
@import "../styles/main.css";
h1 {
color: var(--h1-color);
}
</style>

View file

@ -0,0 +1,3 @@
:root {
--h1-color: red;
}

View file

@ -10,7 +10,7 @@ import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import { enhanceViteSSRError } from '../../errors/dev/index.js';
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
import type { ModuleLoader } from '../../module-loader/index';
import { isPage, resolveIdToUrl } from '../../util.js';
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
import { filterFoundRenderers, loadRenderer } from '../renderer.js';
import { getStylesForURL } from './css.js';
@ -133,7 +133,11 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
});
// But we still want to inject the styles to avoid FOUC
styles.add({
props: {},
props: {
type: 'text/css',
// Track the ID so we can match it to Vite's injected style later
'data-astro-dev-id': viteID(new URL(`.${url}`, env.settings.config.root))
},
children: content,
});
});

View file

@ -1,15 +1,15 @@
/// <reference types="vite/client" />
if (import.meta.hot) {
// Vite injects `<style type="text/css">` for ESM imports of styles
// but Astro also SSRs with `<style>` blocks. This MutationObserver
// Vite injects `<style type="text/css" data-vite-dev-id>` for ESM imports of styles
// but Astro also SSRs with `<style type="text/css" data-astro-dev-id>` 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();
injectedStyles.get(node.getAttribute('data-vite-dev-id')!)?.remove();
}
}
}
@ -31,8 +31,8 @@ if (import.meta.hot) {
function getInjectedStyles() {
const injectedStyles = new Map<string, Element>();
document.querySelectorAll<HTMLStyleElement>('style').forEach((el) => {
injectedStyles.set(el.innerHTML.trim(), el);
document.querySelectorAll<HTMLStyleElement>('style[data-astro-dev-id]').forEach((el) => {
injectedStyles.set(el.getAttribute('data-astro-dev-id')!, el);
});
return injectedStyles;
}
@ -42,5 +42,5 @@ function isStyle(node: Node): node is HTMLStyleElement {
}
function isViteInjectedStyle(node: Node): node is HTMLStyleElement {
return isStyle(node) && node.getAttribute('type') === 'text/css';
return isStyle(node) && node.getAttribute('type') === 'text/css' && !!node.getAttribute('data-vite-dev-id');
}

View file

@ -657,6 +657,12 @@ importers:
dependencies:
astro: link:../../..
packages/astro/e2e/fixtures/css-sourcemaps:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/e2e/fixtures/error-cyclic:
specifiers:
'@astrojs/preact': workspace:*