diff --git a/.changeset/fix-picture-tdz-content-render.md b/.changeset/fix-picture-tdz-content-render.md new file mode 100644 index 000000000000..66882038697f --- /dev/null +++ b/.changeset/fix-picture-tdz-content-render.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a build error that occurred when a pre-rendered page used the `` component and another page called `render()` on content collection entries. 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/assets/consts.ts b/packages/astro/src/assets/consts.ts index 67e99fbed0b5..6255be0d38e4 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..c24c84541e98 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -454,8 +454,7 @@ 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'); + 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 000000000000..0f2de3749df2 Binary files /dev/null and b/packages/astro/test/fixtures/content-collection-picture-render/src/assets/test-image.png differ diff --git a/packages/astro/test/fixtures/content-collection-picture-render/src/content.config.ts b/packages/astro/test/fixtures/content-collection-picture-render/src/content.config.ts new file mode 100644 index 000000000000..8be8cdaa9485 --- /dev/null +++ b/packages/astro/test/fixtures/content-collection-picture-render/src/content.config.ts @@ -0,0 +1,16 @@ +import { defineCollection } from 'astro:content'; +import { z } from 'astro/zod'; +import { glob } from 'astro/loaders'; + +const blog = defineCollection({ + loader: glob({ pattern: '**/*.md', base: './src/content/blog' }), + schema: ({ image }) => + 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 2e641f4f2096..52bf0caae127 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2711,6 +2711,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: