From a12139a82596451f3dbf385de59535c513115c1c Mon Sep 17 00:00:00 2001 From: Uni Date: Tue, 31 Mar 2026 21:53:56 +0200 Subject: [PATCH 1/5] 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 were bundled in the same chunk as content collection render() calls. --- packages/astro/src/assets/consts.ts | 4 ++ .../astro/src/assets/vite-plugin-assets.ts | 46 +++++++++++++- packages/astro/src/content/runtime.ts | 5 +- .../content-collection-picture-render.test.js | 57 ++++++++++++++++++ .../astro.config.mjs | 3 + .../package.json | 8 +++ .../src/assets/test-image.png | Bin 0 -> 70 bytes .../src/content.config.ts | 16 +++++ .../src/content/blog/post-1.md | 8 +++ .../src/pages/blog/[...slug].astro | 26 ++++++++ .../src/pages/index.astro | 12 ++++ pnpm-lock.yaml | 6 ++ 12 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 packages/astro/test/content-collection-picture-render.test.js create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/astro.config.mjs create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/package.json create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/assets/test-image.png create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/content.config.ts create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/content/blog/post-1.md create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/pages/blog/[...slug].astro create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/pages/index.astro diff --git a/packages/astro/src/assets/consts.ts b/packages/astro/src/assets/consts.ts index 67e99fbed0b5..bb5969034c57 100644 --- a/packages/astro/src/assets/consts.ts +++ b/packages/astro/src/assets/consts.ts @@ -1,6 +1,10 @@ export const VIRTUAL_MODULE_ID = 'astro:assets'; export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; export const VIRTUAL_SERVICE_ID = 'virtual:image-service'; +// Internal virtual module that exports only getImage (no component references). +// Used by the content runtime to avoid a TDZ when Picture/Image are in the same chunk. +export const VIRTUAL_GET_IMAGE_ID = 'virtual:astro-get-image'; +export const RESOLVED_VIRTUAL_GET_IMAGE_ID = '\0' + VIRTUAL_GET_IMAGE_ID; // Must keep the extension so we trigger the pipeline of CSS files export const VIRTUAL_IMAGE_STYLES_ID = 'virtual:astro:image-styles.css'; export const RESOLVED_VIRTUAL_IMAGE_STYLES_ID = '\0' + VIRTUAL_IMAGE_STYLES_ID; diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 2266e5467384..d9b88d9b29dc 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -17,9 +17,11 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; import { isAstroServerEnvironment } from '../environments.js'; import type { AstroSettings } from '../types/astro.js'; import { + RESOLVED_VIRTUAL_GET_IMAGE_ID, RESOLVED_VIRTUAL_IMAGE_STYLES_ID, RESOLVED_VIRTUAL_MODULE_ID, VALID_INPUT_FORMATS, + VIRTUAL_GET_IMAGE_ID, VIRTUAL_IMAGE_STYLES_ID, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID, @@ -140,7 +142,7 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl }, resolveId: { filter: { - id: new RegExp(`^(${VIRTUAL_SERVICE_ID}|${VIRTUAL_MODULE_ID})$`), + id: new RegExp(`^(${VIRTUAL_SERVICE_ID}|${VIRTUAL_MODULE_ID}|${VIRTUAL_GET_IMAGE_ID})$`), }, async handler(id) { if (id === VIRTUAL_SERVICE_ID) { @@ -152,13 +154,51 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl if (id === VIRTUAL_MODULE_ID) { return RESOLVED_VIRTUAL_MODULE_ID; } + if (id === VIRTUAL_GET_IMAGE_ID) { + return RESOLVED_VIRTUAL_GET_IMAGE_ID; + } }, }, load: { filter: { - id: new RegExp(`^(${RESOLVED_VIRTUAL_MODULE_ID})$`), + id: new RegExp(`^(${RESOLVED_VIRTUAL_MODULE_ID}|${RESOLVED_VIRTUAL_GET_IMAGE_ID})$`), }, - handler() { + handler(id) { + if (id === RESOLVED_VIRTUAL_GET_IMAGE_ID) { + // Lightweight module exporting only getImage + imageConfig. + // No component references (Image, Picture, Font) to avoid TDZ + // errors when the content runtime and component pages are + // bundled into the same prerender chunk (see #16036). + const isServerEnvironment = isAstroServerEnvironment(this.environment); + const getImageExport = isServerEnvironment + ? `import { getImage as getImageInternal } from "astro/assets"; + export const getImage = async (options) => await getImageInternal(options, imageConfig);` + : `import { AstroError, AstroErrorData } from "astro/errors"; + export const getImage = async () => { + throw new AstroError( + AstroErrorData.GetImageNotUsedOnServer.message, + AstroErrorData.GetImageNotUsedOnServer.hint, + ); + };`; + + const assetQueryParams = settings.adapter?.client?.assetQueryParams + ? `new URLSearchParams(${JSON.stringify( + Array.from(settings.adapter.client.assetQueryParams.entries()), + )})` + : 'undefined'; + + return { + code: ` + export const imageConfig = ${JSON.stringify(settings.config.image)}; + Object.defineProperty(imageConfig, 'assetQueryParams', { + value: ${assetQueryParams}, + enumerable: false, + configurable: true, + }); + ${getImageExport} + `, + }; + } const isServerEnvironment = isAstroServerEnvironment(this.environment); const getImageExport = isServerEnvironment ? `import { getImage as getImageInternal } from "astro/assets"; diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 71b49b6a816d..7136203ca095 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -454,8 +454,9 @@ async function updateImageReferencesInBody(html: string, fileName: string) { const imageObjects = new Map(); - // @ts-expect-error Virtual module resolved at runtime - const { getImage } = await import('astro:assets'); + // @ts-expect-error Internal virtual module that exports only getImage (no component + // references like Image/Picture) to avoid TDZ errors during prerender bundling (#16036). + const { getImage } = await import('virtual:astro-get-image'); // First load all the images. This is done outside of the replaceAll // function because getImage is async. diff --git a/packages/astro/test/content-collection-picture-render.test.js b/packages/astro/test/content-collection-picture-render.test.js new file mode 100644 index 000000000000..71ec5753de59 --- /dev/null +++ b/packages/astro/test/content-collection-picture-render.test.js @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +// Regression test for https://github.com/withastro/astro/issues/16036 +// Using the component on a prerendered page combined with render() +// on content collection entries caused a TDZ error during build: +// "ReferenceError: Cannot access '$$Picture' before initialization" +describe('Content collection with Picture component and render()', () => { + /** @type {import("./test-utils.js").Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ root: './fixtures/content-collection-picture-render/' }); + }); + + describe('Build', () => { + before(async () => { + await fixture.build(); + }); + + it('successfully builds pages using the Picture component', async () => { + const html = await fixture.readFile('/index.html'); + assert.ok(html, 'Expected index page to be generated'); + + const $ = cheerio.load(html); + const $picture = $('picture'); + assert.ok($picture.length, 'Expected element to be rendered'); + }); + + it('successfully builds content collection pages with render()', async () => { + const html = await fixture.readFile('/blog/post-1/index.html'); + assert.ok(html, 'Expected blog page to be generated'); + + const $ = cheerio.load(html); + assert.equal($('.title').text(), 'Post One'); + }); + + it('resolves cover image in content collection entry', async () => { + const html = await fixture.readFile('/blog/post-1/index.html'); + const $ = cheerio.load(html); + + const $img = $('.cover'); + assert.ok($img.attr('src'), 'Expected cover image to have a src'); + }); + + it('renders content body from content collection entry', async () => { + const html = await fixture.readFile('/blog/post-1/index.html'); + const $ = cheerio.load(html); + + const $content = $('.content'); + assert.ok($content.length, 'Expected content div to be present'); + assert.ok($content.text().includes('Hello world'), 'Expected rendered markdown content'); + }); + }); +}); diff --git a/packages/astro/test/fixtures/content-collection-picture-render/astro.config.mjs b/packages/astro/test/fixtures/content-collection-picture-render/astro.config.mjs new file mode 100644 index 000000000000..86dbfb924824 --- /dev/null +++ b/packages/astro/test/fixtures/content-collection-picture-render/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({}); diff --git a/packages/astro/test/fixtures/content-collection-picture-render/package.json b/packages/astro/test/fixtures/content-collection-picture-render/package.json new file mode 100644 index 000000000000..391b7cba3413 --- /dev/null +++ b/packages/astro/test/fixtures/content-collection-picture-render/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/content-collection-picture-render", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/content-collection-picture-render/src/assets/test-image.png b/packages/astro/test/fixtures/content-collection-picture-render/src/assets/test-image.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2de3749df299a6b84bf6ff1a0b393a1c1fd22b GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZYBTuKYyVd1A7xwz3mB) Q_dp2-Pgg&ebxsLQ0NDZ% + z.object({ + title: z.string(), + cover: image(), + }), +}); + +export const collections = { + blog, +}; diff --git a/packages/astro/test/fixtures/content-collection-picture-render/src/content/blog/post-1.md b/packages/astro/test/fixtures/content-collection-picture-render/src/content/blog/post-1.md new file mode 100644 index 000000000000..79a8b06e438b --- /dev/null +++ b/packages/astro/test/fixtures/content-collection-picture-render/src/content/blog/post-1.md @@ -0,0 +1,8 @@ +--- +title: Post One +cover: ../../assets/test-image.png +--- + +Hello world! Here is an image: + +![test image](../../assets/test-image.png) diff --git a/packages/astro/test/fixtures/content-collection-picture-render/src/pages/blog/[...slug].astro b/packages/astro/test/fixtures/content-collection-picture-render/src/pages/blog/[...slug].astro new file mode 100644 index 000000000000..3ace133f1141 --- /dev/null +++ b/packages/astro/test/fixtures/content-collection-picture-render/src/pages/blog/[...slug].astro @@ -0,0 +1,26 @@ +--- +import { getCollection, render } from 'astro:content'; + +export async function getStaticPaths() { + const posts = await getCollection('blog'); + return posts.map((post) => ({ + params: { slug: post.id }, + props: { post }, + })); +} + +const { post } = Astro.props; +const { Content } = await render(post); +--- + + + {post.data.title} + + +

{post.data.title}

+ cover +
+ +
+ + diff --git a/packages/astro/test/fixtures/content-collection-picture-render/src/pages/index.astro b/packages/astro/test/fixtures/content-collection-picture-render/src/pages/index.astro new file mode 100644 index 000000000000..facd1fd4cb55 --- /dev/null +++ b/packages/astro/test/fixtures/content-collection-picture-render/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import { Picture } from 'astro:assets'; +import testImage from '../assets/test-image.png'; +--- + + + Picture Page + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 127f98557456..332cb21ed512 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2696,6 +2696,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/content-collection-picture-render: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/content-collection-references: dependencies: astro: From fe77105dc26db770c265ef527491f96889cac09f Mon Sep 17 00:00:00 2001 From: Uni Date: Wed, 1 Apr 2026 05:23:13 +0200 Subject: [PATCH 2/5] chore: add changeset for Picture TDZ fix --- .changeset/fix-picture-tdz-content-render.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-picture-tdz-content-render.md diff --git a/.changeset/fix-picture-tdz-content-render.md b/.changeset/fix-picture-tdz-content-render.md new file mode 100644 index 000000000000..bb809102d1db --- /dev/null +++ b/.changeset/fix-picture-tdz-content-render.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a build error (`Cannot access '$$Picture' before initialization`) that occurred when a prerendered page used the `` component and another page called `render()` on content collection entries From 940b31e1f4346d07a86392df42cda8dce26516a3 Mon Sep 17 00:00:00 2001 From: Uni Date: Wed, 1 Apr 2026 06:23:28 +0200 Subject: [PATCH 3/5] 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. --- packages/astro/src/assets/consts.ts | 2 +- packages/astro/src/content/runtime.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/assets/consts.ts b/packages/astro/src/assets/consts.ts index bb5969034c57..6255be0d38e4 100644 --- a/packages/astro/src/assets/consts.ts +++ b/packages/astro/src/assets/consts.ts @@ -3,7 +3,7 @@ export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; export const VIRTUAL_SERVICE_ID = 'virtual:image-service'; // Internal virtual module that exports only getImage (no component references). // Used by the content runtime to avoid a TDZ when Picture/Image are in the same chunk. -export const VIRTUAL_GET_IMAGE_ID = 'virtual:astro-get-image'; +export const VIRTUAL_GET_IMAGE_ID = 'virtual:astro:get-image'; export const RESOLVED_VIRTUAL_GET_IMAGE_ID = '\0' + VIRTUAL_GET_IMAGE_ID; // Must keep the extension so we trigger the pipeline of CSS files export const VIRTUAL_IMAGE_STYLES_ID = 'virtual:astro:image-styles.css'; diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 7136203ca095..a189962b28c5 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -456,7 +456,7 @@ async function updateImageReferencesInBody(html: string, fileName: string) { // @ts-expect-error Internal virtual module that exports only getImage (no component // references like Image/Picture) to avoid TDZ errors during prerender bundling (#16036). - const { getImage } = await import('virtual:astro-get-image'); + const { getImage } = await import('virtual:astro:get-image'); // First load all the images. This is done outside of the replaceAll // function because getImage is async. From 8f2d9e5ca4d0d11a236d0aef8bf9fc70ed3ad434 Mon Sep 17 00:00:00 2001 From: uni Date: Thu, 2 Apr 2026 16:19:40 +0200 Subject: [PATCH 4/5] 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. --- packages/astro/dev-only.d.ts | 6 ++++++ packages/astro/src/content/runtime.ts | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index a4c1e7ea9e71..bb45e0c1aefe 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -86,3 +86,9 @@ declare module 'virtual:astro:component-metadata' { declare module 'virtual:astro:app' { export const createApp: import('./src/core/app/types.js').CreateApp; } + +declare module 'virtual:astro:get-image' { + export const getImage: ( + options: import('./src/types/public/index.js').UnresolvedImageTransform, + ) => Promise; +} diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index a189962b28c5..c24c84541e98 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -454,8 +454,6 @@ async function updateImageReferencesInBody(html: string, fileName: string) { const imageObjects = new Map(); - // @ts-expect-error Internal virtual module that exports only getImage (no component - // references like Image/Picture) to avoid TDZ errors during prerender bundling (#16036). const { getImage } = await import('virtual:astro:get-image'); // First load all the images. This is done outside of the replaceAll From 38866ae63e7b44270d3cfbb8e141e9928edac912 Mon Sep 17 00:00:00 2001 From: Alexander Niebuhr <45965090+alexanderniebuhr@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:25:12 +0200 Subject: [PATCH 5/5] Apply suggestion from @alexanderniebuhr --- .changeset/fix-picture-tdz-content-render.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-picture-tdz-content-render.md b/.changeset/fix-picture-tdz-content-render.md index bb809102d1db..66882038697f 100644 --- a/.changeset/fix-picture-tdz-content-render.md +++ b/.changeset/fix-picture-tdz-content-render.md @@ -2,4 +2,4 @@ 'astro': patch --- -Fixes a build error (`Cannot access '$$Picture' before initialization`) that occurred when a prerendered page used the `` component and another page called `render()` on content collection entries +Fixes a build error that occurred when a pre-rendered page used the `` component and another page called `render()` on content collection entries.