Skip to content

Commit 7eddf22

Browse files
Desel72Desel72
andauthored
fix(hmr): eagerly recompile on style-only change to prevent stale slots render (#15953)
Co-authored-by: Desel72 <unicorn@vmi3119072.contaboserver.net>
1 parent 5201ed4 commit 7eddf22

File tree

10 files changed

+157
-5
lines changed

10 files changed

+157
-5
lines changed

.changeset/fix-hmr-slots-render.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(hmr): eagerly recompile on style-only change to prevent stale slots render

packages/astro/src/vite-plugin-astro/hmr.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import type { HmrContext } from 'vite';
22
import type { Logger } from '../core/logger/core.js';
3+
import type { CompileAstroResult } from './compile.js';
34
import { parseAstroRequest } from './query.js';
45
import type { CompileMetadata } from './types.js';
56
import { frontmatterRE } from './utils.js';
67

78
interface HandleHotUpdateOptions {
89
logger: Logger;
10+
compile: (code: string, filename: string) => Promise<CompileAstroResult>;
911
astroFileToCompileMetadata: Map<string, CompileMetadata>;
1012
}
1113

1214
export async function handleHotUpdate(
1315
ctx: HmrContext,
14-
{ logger, astroFileToCompileMetadata }: HandleHotUpdateOptions,
16+
{ logger, compile, astroFileToCompileMetadata }: HandleHotUpdateOptions,
1517
) {
1618
// HANDLING 1: Invalidate compile metadata if CSS dependency updated
1719
//
@@ -35,9 +37,17 @@ export async function handleHotUpdate(
3537

3638
if (isStyleOnlyChanged(oldCode, newCode)) {
3739
logger.debug('watch', 'style-only change');
38-
// Invalidate its `astroFileToCompileMetadata` so that the next transform of Astro style virtual module
39-
// will re-generate it
40-
astroFileToCompileMetadata.delete(ctx.file);
40+
// Eagerly re-compile to update the metadata with the new CSS. This ensures
41+
// the compile metadata stays consistent so that subsequent SSR requests
42+
// (e.g. page refresh) can load the style virtual modules without needing
43+
// to re-compile from disk, avoiding potential stale state.
44+
try {
45+
await compile(newCode, ctx.file);
46+
} catch {
47+
// If re-compilation fails, fall back to deleting the metadata so the
48+
// load hook will re-compile lazily on the next request.
49+
astroFileToCompileMetadata.delete(ctx.file);
50+
}
4151
return ctx.modules.filter((mod) => {
4252
if (!mod.id) {
4353
return false;

packages/astro/src/vite-plugin-astro/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
310310
},
311311
},
312312
async handleHotUpdate(ctx) {
313-
return handleHotUpdate(ctx, { logger, astroFileToCompileMetadata });
313+
return handleHotUpdate(ctx, { logger, compile, astroFileToCompileMetadata });
314314
},
315315
},
316316
{
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { defineConfig } from 'astro/config';
2+
3+
export default defineConfig({});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@test/hmr-slots-render",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
type Props = {
3+
items: unknown[];
4+
};
5+
6+
const { items, ...attrs } = Astro.props;
7+
---
8+
9+
<ul {...attrs}>
10+
{
11+
items.map((item, index, list) => (
12+
<li set:html={Astro.slots.render("default", [item, index, list])} />
13+
))
14+
}
15+
</ul>
16+
17+
<style>
18+
ul {
19+
margin: 0;
20+
font-size: 0.5rem;
21+
}
22+
</style>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
const { name } = Astro.props;
3+
---
4+
<div class="item-wrapper">{name}</div>
5+
6+
<style>
7+
.item-wrapper {
8+
padding: 0.25rem;
9+
}
10+
</style>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
import Each from '../components/Each.astro';
3+
import Item from '../components/Item.astro';
4+
const items = ['one', 'two', 'three'];
5+
---
6+
<html>
7+
<head>
8+
<title>HMR Slots Render Test</title>
9+
</head>
10+
<body>
11+
<div id="result">
12+
<Each items={items}>
13+
{(item) => <Item name={item} />}
14+
</Each>
15+
</div>
16+
</body>
17+
</html>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import assert from 'node:assert/strict';
2+
import { after, before, describe, it } from 'node:test';
3+
import * as cheerio from 'cheerio';
4+
import { isWindows, loadFixture } from './test-utils.js';
5+
6+
describe('HMR: slots.render with callback args after style change', () => {
7+
/** @type {import('./test-utils').Fixture} */
8+
let fixture;
9+
/** @type {import('./test-utils').DevServer} */
10+
let devServer;
11+
12+
before(async () => {
13+
fixture = await loadFixture({ root: './fixtures/hmr-slots-render/' });
14+
devServer = await fixture.startDevServer();
15+
});
16+
17+
after(async () => {
18+
await devServer.stop();
19+
fixture.resetAllFiles();
20+
});
21+
22+
function verifyRendering($, label) {
23+
const items = $('#result .item-wrapper');
24+
assert.ok(
25+
items.length >= 3,
26+
`[${label}] Expected 3 item-wrappers, got ${items.length}. HTML:\n${$('#result').html()?.substring(0, 500)}`,
27+
);
28+
assert.equal($(items[0]).text(), 'one');
29+
assert.equal($(items[1]).text(), 'two');
30+
assert.equal($(items[2]).text(), 'three');
31+
32+
// Verify no escaped HTML source code visible (the bug symptom from #15925)
33+
const resultText = $('#result').text();
34+
assert.ok(
35+
!resultText.includes('data-astro-cid'),
36+
`[${label}] Found escaped data-astro-cid in output: ${resultText.substring(0, 300)}`,
37+
);
38+
}
39+
40+
it(
41+
'should render after style change in the slot-render component',
42+
{ skip: isWindows },
43+
async () => {
44+
// Initial fetch - verify correct rendering
45+
let res = await fixture.fetch('/');
46+
assert.equal(res.status, 200);
47+
verifyRendering(cheerio.load(await res.text()), 'initial');
48+
49+
// Style-only edit (triggers HMR style-only path)
50+
await fixture.editFile('/src/components/Each.astro', (c) =>
51+
c.replace('font-size: 0.5rem;', 'font-size: 1rem;'),
52+
);
53+
await new Promise((r) => setTimeout(r, 500));
54+
55+
// Page refresh after HMR - must still render correctly
56+
res = await fixture.fetch('/');
57+
assert.equal(res.status, 200);
58+
verifyRendering(cheerio.load(await res.text()), 'after style change');
59+
60+
// Second style edit + refresh
61+
await fixture.editFile('/src/components/Each.astro', (c) =>
62+
c.replace('font-size: 1rem;', 'font-size: 2rem;'),
63+
);
64+
await new Promise((r) => setTimeout(r, 500));
65+
66+
res = await fixture.fetch('/');
67+
assert.equal(res.status, 200);
68+
verifyRendering(cheerio.load(await res.text()), 'after 2nd style change');
69+
},
70+
);
71+
});

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)