Skip to content

Commit 7454854

Browse files
rururuxematipico
andauthored
fix(astro): Fix isHTMLString check failing in multi-realm environments (#16142)
* fix(container): don't escape slot HTML in renderToString during build * performance issue * oops * apply Erika's suggestion * use `isHTMLString` within `markHTMLString` * simplify test fixtures * format * remove the no longer used `Symbol.toStringTag` from `HTMLString` * rename to `htmlStringSymbol` * update changeset * Apply suggestion from @ematipico --------- Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
1 parent a7e7567 commit 7454854

9 files changed

Lines changed: 86 additions & 5 deletions

File tree

.changeset/jolly-ideas-sell.md

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 HTML content being incorrectly escaped as plain text when rendering a MDX component using the `AstroContainer` APIs.

packages/astro/src/runtime/server/escape.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ Object.defineProperty(HTMLBytes.prototype, Symbol.toStringTag, {
1414
},
1515
});
1616

17+
const htmlStringSymbol = Symbol.for('astro:html-string');
18+
1719
/**
1820
* A "blessed" extension of String that tells Astro that the string
1921
* has already been escaped. This helps prevent double-escaping of HTML.
2022
*/
2123
export class HTMLString extends String {
22-
get [Symbol.toStringTag]() {
23-
return 'HTMLString';
24-
}
24+
[htmlStringSymbol] = true;
2525
}
2626

2727
type BlessedType = string | HTMLBytes;
@@ -33,7 +33,7 @@ type BlessedType = string | HTMLBytes;
3333
*/
3434
export const markHTMLString = (value: any) => {
3535
// If value is already marked as an HTML string, there is nothing to do.
36-
if (value instanceof HTMLString) {
36+
if (isHTMLString(value)) {
3737
return value;
3838
}
3939
// Cast to `HTMLString` to mark the string as valid HTML. Any HTML escaping
@@ -48,7 +48,7 @@ export const markHTMLString = (value: any) => {
4848
};
4949

5050
export function isHTMLString(value: any): value is HTMLString {
51-
return value instanceof HTMLString;
51+
return !!value?.[htmlStringSymbol];
5252
}
5353

5454
function markHTMLBytes(bytes: Uint8Array) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import mdx from '@astrojs/mdx';
2+
import { defineConfig } from 'astro/config';
3+
4+
export default defineConfig({
5+
integrations: [mdx()],
6+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@test/mdx-astro-container-escape",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"@astrojs/mdx": "workspace:*",
7+
"astro": "workspace:*"
8+
},
9+
"scripts": {
10+
"dev": "astro dev"
11+
}
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="div"><slot /></div>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import { experimental_AstroContainer } from "astro/container";
3+
import { loadRenderers } from "astro:container";
4+
import { getContainerRenderer } from "@astrojs/mdx";
5+
import { Content } from '../posts/post.mdx'
6+
7+
const renderers = await loadRenderers([getContainerRenderer()]);
8+
const contentContainer = await experimental_AstroContainer.create({ renderers });
9+
const html = await contentContainer.renderToString(Content);
10+
---
11+
12+
<Fragment set:html={html} />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: Example
3+
---
4+
5+
import Div from '../components/Div.astro'
6+
7+
<Div>
8+
Hello, World!
9+
</Div>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 '../../../astro/test/test-utils.js';
5+
6+
describe('MDX Component & Astro Container escape issue', () => {
7+
let fixture;
8+
9+
before(async () => {
10+
fixture = await loadFixture({
11+
root: new URL('./fixtures/mdx-astro-container-escape/', import.meta.url),
12+
});
13+
});
14+
15+
describe('build', () => {
16+
before(async () => {
17+
await fixture.build();
18+
});
19+
20+
it('should render elements inside component without escaping', async () => {
21+
const html = await fixture.readFile('/index.html');
22+
const $ = cheerio.load(html);
23+
24+
assert.equal($('.div').text().includes('<p>'), false);
25+
});
26+
});
27+
});

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)