From 95d6a093d2f2241928a9f631c3f8c7319726ea6a Mon Sep 17 00:00:00 2001 From: Adam Chalemian Date: Fri, 18 Sep 2026 11:24:05 -0400 Subject: [PATCH] fix(incremental): resolve asset placeholders in modules or compiled CSS (#18058) --- .changeset/calm-caches-rest.md | 5 +++ .../core/build/plugins/plugin-incremental.ts | 3 +- .../units/build/plugin-incremental.test.ts | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-caches-rest.md diff --git a/.changeset/calm-caches-rest.md b/.changeset/calm-caches-rest.md new file mode 100644 index 000000000000..cae5a97f3097 --- /dev/null +++ b/.changeset/calm-caches-rest.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes incremental builds repeatedly rendering unchanged pages when modules or compiled CSS reference bundled assets. diff --git a/packages/astro/src/core/build/plugins/plugin-incremental.ts b/packages/astro/src/core/build/plugins/plugin-incremental.ts index 5943920c7b56..66ed5757deed 100644 --- a/packages/astro/src/core/build/plugins/plugin-incremental.ts +++ b/packages/astro/src/core/build/plugins/plugin-incremental.ts @@ -38,6 +38,7 @@ interface TransitiveGraphCache { const ASSET_PLACEHOLDERS = [ { token: '__ASTRO_ASSET_IMAGE__', pattern: /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g }, { token: '__VITE_ASSET__', pattern: /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g }, + { token: 'import.meta.ROLLDOWN_FILE_URL_', pattern: /import\.meta\.ROLLDOWN_FILE_URL_([\w$]+)/g }, ]; /** @@ -112,7 +113,7 @@ function hashModules( // did not see (e.g. virtual CSS modules). const compiled = compiledCss.get(id); if (compiled != null) { - hasher.update(compiled); + hasher.update(resolveAssetPlaceholders(graph, compiled)); } else { try { hasher.update(nodeFs.readFileSync(removeQueryString(id), 'utf-8')); diff --git a/packages/astro/test/units/build/plugin-incremental.test.ts b/packages/astro/test/units/build/plugin-incremental.test.ts index 0385aebe18e2..aaadf60a541c 100644 --- a/packages/astro/test/units/build/plugin-incremental.test.ts +++ b/packages/astro/test/units/build/plugin-incremental.test.ts @@ -39,6 +39,10 @@ function assetCode(handle: string) { return `export default "__VITE_ASSET__${handle}__"`; } +function rolldownAssetCode(handle: string) { + return `export default import.meta.ROLLDOWN_FILE_URL_${handle}`; +} + function pluginContext( codeByModule: Record, fileNames: Record, @@ -110,6 +114,18 @@ describe('pluginIncremental', () => { assert.equal(first, second); }); + it('is stable when Rolldown emits the same assets with different handles', () => { + const first = dependencyHash( + { [RED]: rolldownAssetCode(HANDLE_ONE), [VIDEO]: rolldownAssetCode(HANDLE_TWO) }, + { [HANDLE_ONE]: '_astro/red.aaaa.png', [HANDLE_TWO]: '_astro/clip.cccc.mp4' }, + ); + const second = dependencyHash( + { [RED]: rolldownAssetCode(HANDLE_TWO), [VIDEO]: rolldownAssetCode(HANDLE_ONE) }, + { [HANDLE_TWO]: '_astro/red.aaaa.png', [HANDLE_ONE]: '_astro/clip.cccc.mp4' }, + ); + assert.equal(first, second); + }); + it('changes when an imported image resolves to a different file name', () => { const code = { [RED]: imageCode(HANDLE_ONE), [BLUE]: imageCode(HANDLE_TWO) }; const first = dependencyHash(code, { @@ -158,6 +174,27 @@ describe('pluginIncremental', () => { }); describe('CSS preprocessor partials (#17974)', () => { + it('is stable when compiled CSS assets are emitted with different handles', () => { + const cssId = '/project/src/styles/global.css'; + const first = dependencyHash( + { [cssId]: '' }, + { [HANDLE_ONE]: '_astro/red.aaaa.png' }, + [cssId], + { + transforms: { [cssId]: `body { background: url(__VITE_ASSET__${HANDLE_ONE}__); }` }, + }, + ); + const second = dependencyHash( + { [cssId]: '' }, + { [HANDLE_TWO]: '_astro/red.aaaa.png' }, + [cssId], + { + transforms: { [cssId]: `body { background: url(__VITE_ASSET__${HANDLE_TWO}__); }` }, + }, + ); + assert.equal(first, second); + }); + it('changes when compiled CSS output changes even if entry file is unchanged', () => { const scssId = '/project/src/styles/global.scss'; // The entry file stays the same between builds; only the