diff --git a/.changeset/rotten-cups-happen.md b/.changeset/rotten-cups-happen.md new file mode 100644 index 000000000000..e484c098f790 --- /dev/null +++ b/.changeset/rotten-cups-happen.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix duplicate CSS in dev mode when `vite.css.devSourcemap` is provided diff --git a/packages/astro/e2e/css-sourcemaps.test.js b/packages/astro/e2e/css-sourcemaps.test.js new file mode 100644 index 000000000000..07cea4cb016d --- /dev/null +++ b/packages/astro/e2e/css-sourcemaps.test.js @@ -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); + }); +}); diff --git a/packages/astro/e2e/css.test.js b/packages/astro/e2e/css.test.js index b817c419ade6..745a540eeb1b 100644 --- a/packages/astro/e2e/css.test.js +++ b/packages/astro/e2e/css.test.js @@ -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); + }); }); diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/astro.config.mjs b/packages/astro/e2e/fixtures/css-sourcemaps/astro.config.mjs new file mode 100644 index 000000000000..7e8fac1e7f7c --- /dev/null +++ b/packages/astro/e2e/fixtures/css-sourcemaps/astro.config.mjs @@ -0,0 +1,7 @@ +export default { + vite: { + css: { + devSourcemap: true, + } + } +}; diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/package.json b/packages/astro/e2e/fixtures/css-sourcemaps/package.json new file mode 100644 index 000000000000..1fa4c2c79fac --- /dev/null +++ b/packages/astro/e2e/fixtures/css-sourcemaps/package.json @@ -0,0 +1,8 @@ +{ + "name": "@e2e/css-sourcemaps", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/src/env.d.ts b/packages/astro/e2e/fixtures/css-sourcemaps/src/env.d.ts new file mode 100644 index 000000000000..8c34fb45e7cf --- /dev/null +++ b/packages/astro/e2e/fixtures/css-sourcemaps/src/env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/src/pages/index.astro b/packages/astro/e2e/fixtures/css-sourcemaps/src/pages/index.astro new file mode 100644 index 000000000000..7275177f961d --- /dev/null +++ b/packages/astro/e2e/fixtures/css-sourcemaps/src/pages/index.astro @@ -0,0 +1,9 @@ +

hello world

+ + diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/src/styles/main.css b/packages/astro/e2e/fixtures/css-sourcemaps/src/styles/main.css new file mode 100644 index 000000000000..c80a6cde1b69 --- /dev/null +++ b/packages/astro/e2e/fixtures/css-sourcemaps/src/styles/main.css @@ -0,0 +1,3 @@ +:root { + --h1-color: red; +} diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 7b5df9482be8..7ea008d69670 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -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, }); }); diff --git a/packages/astro/src/runtime/client/hmr.ts b/packages/astro/src/runtime/client/hmr.ts index f3a3074f3a46..b71c39990f24 100644 --- a/packages/astro/src/runtime/client/hmr.ts +++ b/packages/astro/src/runtime/client/hmr.ts @@ -1,15 +1,15 @@ /// if (import.meta.hot) { - // Vite injects `