Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-picture-tdz-content-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a build error that occurred when a pre-rendered page used the `<Picture>` component and another page called `render()` on content collection entries.
6 changes: 6 additions & 0 deletions packages/astro/dev-only.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('./src/types/public/index.js').GetImageResult>;
}
4 changes: 4 additions & 0 deletions packages/astro/src/assets/consts.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
46 changes: 43 additions & 3 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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";
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,7 @@ async function updateImageReferencesInBody(html: string, fileName: string) {

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

// @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.
Expand Down
57 changes: 57 additions & 0 deletions packages/astro/test/content-collection-picture-render.test.js
Original file line number Diff line number Diff line change
@@ -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 <Picture> 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 <picture> 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');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from 'astro/config';

export default defineConfig({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/content-collection-picture-render",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Post One
cover: ../../assets/test-image.png
---

Hello world! Here is an image:

![test image](../../assets/test-image.png)
Original file line number Diff line number Diff line change
@@ -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);
---
<html>
<head>
<title>{post.data.title}</title>
</head>
<body>
<h1 class="title">{post.data.title}</h1>
<img class="cover" src={post.data.cover.src} width={post.data.cover.width} height={post.data.cover.height} alt="cover" />
<div class="content">
<Content />
</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import { Picture } from 'astro:assets';
import testImage from '../assets/test-image.png';
---
<html>
<head>
<title>Picture Page</title>
</head>
<body>
<Picture class="hero" src={testImage} alt="Test image" formats={['webp']} />
</body>
</html>
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading