Skip to content

Commit 5bcd03c

Browse files
Desel72uni
andauthored
fix(assets): resolve Picture TDZ error when combined with content render() (#16171)
* fix(assets): resolve Picture TDZ error when combined with content render (#16036) Introduce an internal virtual module (virtual:astro-get-image) that exports only getImage and imageConfig without any Astro component references. The content runtime now imports from this narrower module instead of astro:assets, breaking the circular initialization dependency that caused a TDZ ReferenceError when prerendered pages using <Picture> were bundled in the same chunk as content collection render() calls. * chore: add changeset for Picture TDZ fix * fix: rename virtual module to virtual:astro:get-image Use colon separator (virtual:astro:*) instead of hyphen so the module matches existing optimizeDeps.exclude patterns in both Astro core and the Cloudflare adapter. The hyphenated name was not excluded from Vite's dependency optimizer, causing esbuild to fail resolving it. * fix: add virtual:astro:get-image to dev-only.d.ts and remove ts-expect-error Add type declaration for the virtual:astro:get-image module so TypeScript recognizes the import without needing a @ts-expect-error directive. * Apply suggestion from @alexanderniebuhr --------- Co-authored-by: uni <uni@vmi3197945.contaboserver.net>
1 parent 39a4c43 commit 5bcd03c

14 files changed

Lines changed: 195 additions & 5 deletions

File tree

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+
Fixes a build error that occurred when a pre-rendered page used the `<Picture>` component and another page called `render()` on content collection entries.

packages/astro/dev-only.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,9 @@ declare module 'virtual:astro:component-metadata' {
8686
declare module 'virtual:astro:app' {
8787
export const createApp: import('./src/core/app/types.js').CreateApp;
8888
}
89+
90+
declare module 'virtual:astro:get-image' {
91+
export const getImage: (
92+
options: import('./src/types/public/index.js').UnresolvedImageTransform,
93+
) => Promise<import('./src/types/public/index.js').GetImageResult>;
94+
}

packages/astro/src/assets/consts.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export const VIRTUAL_MODULE_ID = 'astro:assets';
22
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
33
export const VIRTUAL_SERVICE_ID = 'virtual:image-service';
4+
// Internal virtual module that exports only getImage (no component references).
5+
// Used by the content runtime to avoid a TDZ when Picture/Image are in the same chunk.
6+
export const VIRTUAL_GET_IMAGE_ID = 'virtual:astro:get-image';
7+
export const RESOLVED_VIRTUAL_GET_IMAGE_ID = '\0' + VIRTUAL_GET_IMAGE_ID;
48
// Must keep the extension so we trigger the pipeline of CSS files
59
export const VIRTUAL_IMAGE_STYLES_ID = 'virtual:astro:image-styles.css';
610
export const RESOLVED_VIRTUAL_IMAGE_STYLES_ID = '\0' + VIRTUAL_IMAGE_STYLES_ID;

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js';
1717
import { isAstroServerEnvironment } from '../environments.js';
1818
import type { AstroSettings } from '../types/astro.js';
1919
import {
20+
RESOLVED_VIRTUAL_GET_IMAGE_ID,
2021
RESOLVED_VIRTUAL_IMAGE_STYLES_ID,
2122
RESOLVED_VIRTUAL_MODULE_ID,
2223
VALID_INPUT_FORMATS,
24+
VIRTUAL_GET_IMAGE_ID,
2325
VIRTUAL_IMAGE_STYLES_ID,
2426
VIRTUAL_MODULE_ID,
2527
VIRTUAL_SERVICE_ID,
@@ -140,7 +142,7 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
140142
},
141143
resolveId: {
142144
filter: {
143-
id: new RegExp(`^(${VIRTUAL_SERVICE_ID}|${VIRTUAL_MODULE_ID})$`),
145+
id: new RegExp(`^(${VIRTUAL_SERVICE_ID}|${VIRTUAL_MODULE_ID}|${VIRTUAL_GET_IMAGE_ID})$`),
144146
},
145147
async handler(id) {
146148
if (id === VIRTUAL_SERVICE_ID) {
@@ -152,13 +154,51 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
152154
if (id === VIRTUAL_MODULE_ID) {
153155
return RESOLVED_VIRTUAL_MODULE_ID;
154156
}
157+
if (id === VIRTUAL_GET_IMAGE_ID) {
158+
return RESOLVED_VIRTUAL_GET_IMAGE_ID;
159+
}
155160
},
156161
},
157162
load: {
158163
filter: {
159-
id: new RegExp(`^(${RESOLVED_VIRTUAL_MODULE_ID})$`),
164+
id: new RegExp(`^(${RESOLVED_VIRTUAL_MODULE_ID}|${RESOLVED_VIRTUAL_GET_IMAGE_ID})$`),
160165
},
161-
handler() {
166+
handler(id) {
167+
if (id === RESOLVED_VIRTUAL_GET_IMAGE_ID) {
168+
// Lightweight module exporting only getImage + imageConfig.
169+
// No component references (Image, Picture, Font) to avoid TDZ
170+
// errors when the content runtime and component pages are
171+
// bundled into the same prerender chunk (see #16036).
172+
const isServerEnvironment = isAstroServerEnvironment(this.environment);
173+
const getImageExport = isServerEnvironment
174+
? `import { getImage as getImageInternal } from "astro/assets";
175+
export const getImage = async (options) => await getImageInternal(options, imageConfig);`
176+
: `import { AstroError, AstroErrorData } from "astro/errors";
177+
export const getImage = async () => {
178+
throw new AstroError(
179+
AstroErrorData.GetImageNotUsedOnServer.message,
180+
AstroErrorData.GetImageNotUsedOnServer.hint,
181+
);
182+
};`;
183+
184+
const assetQueryParams = settings.adapter?.client?.assetQueryParams
185+
? `new URLSearchParams(${JSON.stringify(
186+
Array.from(settings.adapter.client.assetQueryParams.entries()),
187+
)})`
188+
: 'undefined';
189+
190+
return {
191+
code: `
192+
export const imageConfig = ${JSON.stringify(settings.config.image)};
193+
Object.defineProperty(imageConfig, 'assetQueryParams', {
194+
value: ${assetQueryParams},
195+
enumerable: false,
196+
configurable: true,
197+
});
198+
${getImageExport}
199+
`,
200+
};
201+
}
162202
const isServerEnvironment = isAstroServerEnvironment(this.environment);
163203
const getImageExport = isServerEnvironment
164204
? `import { getImage as getImageInternal } from "astro/assets";

packages/astro/src/content/runtime.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,7 @@ async function updateImageReferencesInBody(html: string, fileName: string) {
454454

455455
const imageObjects = new Map<string, GetImageResult>();
456456

457-
// @ts-expect-error Virtual module resolved at runtime
458-
const { getImage } = await import('astro:assets');
457+
const { getImage } = await import('virtual:astro:get-image');
459458

460459
// First load all the images. This is done outside of the replaceAll
461460
// function because getImage is async.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import assert from 'node:assert/strict';
2+
import { before, describe, it } from 'node:test';
3+
import * as cheerio from 'cheerio';
4+
import { loadFixture } from './test-utils.js';
5+
6+
// Regression test for https://github.com/withastro/astro/issues/16036
7+
// Using the <Picture> component on a prerendered page combined with render()
8+
// on content collection entries caused a TDZ error during build:
9+
// "ReferenceError: Cannot access '$$Picture' before initialization"
10+
describe('Content collection with Picture component and render()', () => {
11+
/** @type {import("./test-utils.js").Fixture} */
12+
let fixture;
13+
14+
before(async () => {
15+
fixture = await loadFixture({ root: './fixtures/content-collection-picture-render/' });
16+
});
17+
18+
describe('Build', () => {
19+
before(async () => {
20+
await fixture.build();
21+
});
22+
23+
it('successfully builds pages using the Picture component', async () => {
24+
const html = await fixture.readFile('/index.html');
25+
assert.ok(html, 'Expected index page to be generated');
26+
27+
const $ = cheerio.load(html);
28+
const $picture = $('picture');
29+
assert.ok($picture.length, 'Expected <picture> element to be rendered');
30+
});
31+
32+
it('successfully builds content collection pages with render()', async () => {
33+
const html = await fixture.readFile('/blog/post-1/index.html');
34+
assert.ok(html, 'Expected blog page to be generated');
35+
36+
const $ = cheerio.load(html);
37+
assert.equal($('.title').text(), 'Post One');
38+
});
39+
40+
it('resolves cover image in content collection entry', async () => {
41+
const html = await fixture.readFile('/blog/post-1/index.html');
42+
const $ = cheerio.load(html);
43+
44+
const $img = $('.cover');
45+
assert.ok($img.attr('src'), 'Expected cover image to have a src');
46+
});
47+
48+
it('renders content body from content collection entry', async () => {
49+
const html = await fixture.readFile('/blog/post-1/index.html');
50+
const $ = cheerio.load(html);
51+
52+
const $content = $('.content');
53+
assert.ok($content.length, 'Expected content div to be present');
54+
assert.ok($content.text().includes('Hello world'), 'Expected rendered markdown content');
55+
});
56+
});
57+
});
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/content-collection-picture-render",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
70 Bytes
Loading
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineCollection } from 'astro:content';
2+
import { z } from 'astro/zod';
3+
import { glob } from 'astro/loaders';
4+
5+
const blog = defineCollection({
6+
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
7+
schema: ({ image }) =>
8+
z.object({
9+
title: z.string(),
10+
cover: image(),
11+
}),
12+
});
13+
14+
export const collections = {
15+
blog,
16+
};

0 commit comments

Comments
 (0)