Skip to content

Commit 3d525ef

Browse files
authored
Prevent removal of nested slots within islands (#7093)
* Prevent removal of nested slots within islands * Fix build errors
1 parent e9fc2c2 commit 3d525ef

File tree

24 files changed

+288
-26
lines changed

24 files changed

+288
-26
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@astrojs/preact': minor
3+
'@astrojs/svelte': minor
4+
'@astrojs/react': minor
5+
'@astrojs/solid-js': minor
6+
'@astrojs/vue': minor
7+
'astro': minor
8+
---
9+
10+
Prevent removal of nested slots within islands
11+
12+
This change introduces a new flag that renderers can add called `supportsAstroStaticSlot`. What this does is let Astro know that the render is sending `<astro-static-slot>` as placeholder values for static (non-hydrated) slots which Astro will then remove.
13+
14+
This change is completely backwards compatible, but fixes bugs caused by combining ssr-only and client-side framework components like so:
15+
16+
```astro
17+
<Component>
18+
<div>
19+
<Component client:load>
20+
<span>Nested</span>
21+
</Component>
22+
</div>
23+
</Component>
24+
```

packages/astro/src/@types/astro.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export interface AstroComponentMetadata {
9494
hydrateArgs?: any;
9595
componentUrl?: string;
9696
componentExport?: { value: string; namespace?: boolean };
97+
astroStaticSlot: true;
9798
}
9899

99100
/** The flags supported by the Astro CLI */
@@ -1718,6 +1719,7 @@ export interface SSRLoadedRenderer extends AstroRenderer {
17181719
html: string;
17191720
attrs?: Record<string, string>;
17201721
}>;
1722+
supportsAstroStaticSlot?: boolean;
17211723
};
17221724
}
17231725

packages/astro/src/runtime/server/render/component.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ function isHTMLComponent(Component: unknown) {
5454
return Component && typeof Component === 'object' && (Component as any)['astro:html'];
5555
}
5656

57+
const ASTRO_SLOT_EXP = /\<\/?astro-slot\b[^>]*>/g;
58+
const ASTRO_STATIC_SLOT_EXP = /\<\/?astro-static-slot\b[^>]*>/g;
59+
function removeStaticAstroSlot(html: string, supportsAstroStaticSlot: boolean) {
60+
const exp = supportsAstroStaticSlot ? ASTRO_STATIC_SLOT_EXP : ASTRO_SLOT_EXP;
61+
return html.replace(exp, '');
62+
}
63+
5764
async function renderFrameworkComponent(
5865
result: SSRResult,
5966
displayName: string,
@@ -68,7 +75,10 @@ async function renderFrameworkComponent(
6875
}
6976

7077
const { renderers, clientDirectives } = result._metadata;
71-
const metadata: AstroComponentMetadata = { displayName };
78+
const metadata: AstroComponentMetadata = {
79+
astroStaticSlot: true,
80+
displayName
81+
};
7282

7383
const { hydration, isPage, props } = extractDirectives(_props, clientDirectives);
7484
let html = '';
@@ -263,7 +273,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
263273
if (isPage || renderer?.name === 'astro:jsx') {
264274
yield html;
265275
} else if (html && html.length > 0) {
266-
yield markHTMLString(html.replace(/\<\/?astro-slot\b[^>]*>/g, ''));
276+
yield markHTMLString(removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false));
267277
} else {
268278
yield '';
269279
}
@@ -288,7 +298,11 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
288298
if (html) {
289299
if (Object.keys(children).length > 0) {
290300
for (const key of Object.keys(children)) {
291-
if (!html.includes(key === 'default' ? `<astro-slot>` : `<astro-slot name="${key}">`)) {
301+
let tagName = renderer?.ssr?.supportsAstroStaticSlot ?
302+
!!metadata.hydrate ? 'astro-slot' : 'astro-static-slot'
303+
: 'astro-slot';
304+
let expectedHTML = key === 'default' ? `<${tagName}>` : `<${tagName} name="${key}">`;
305+
if (!html.includes(expectedHTML)) {
292306
unrenderedSlots.push(key);
293307
}
294308
}

packages/astro/src/vite-plugin-jsx/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export default function jsx({ settings, logging }: AstroPluginJSXOptions): Plugi
202202
Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment.
203203
Add ${colors.cyan(
204204
IMPORT_STATEMENTS[defaultRendererName] || `import '${defaultRendererName}';`
205-
)} or ${colors.cyan(`/* jsxImportSource: ${defaultRendererName} */`)} to this file.
205+
)} or ${colors.cyan(`/** @jsxImportSource: ${defaultRendererName} */`)} to this file.
206206
`
207207
);
208208
return null;

packages/astro/test/astro-slots-nested.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as cheerio from 'cheerio';
33
import { loadFixture } from './test-utils.js';
44

55
describe('Nested Slots', () => {
6+
/** @type {import('./test-utils').Fixture} */
67
let fixture;
78

89
before(async () => {
@@ -23,4 +24,38 @@ describe('Nested Slots', () => {
2324
const $ = cheerio.load(html);
2425
expect($('script')).to.have.a.lengthOf(1, 'script rendered');
2526
});
27+
28+
describe('Client components nested inside server-only framework components', () => {
29+
/** @type {cheerio.CheerioAPI} */
30+
let $;
31+
before(async () => {
32+
const html = await fixture.readFile('/server-component-nested/index.html');
33+
$ = cheerio.load(html);
34+
});
35+
36+
it('react', () => {
37+
expect($('#react astro-slot')).to.have.a.lengthOf(1);
38+
expect($('#react astro-static-slot')).to.have.a.lengthOf(0);
39+
});
40+
41+
it('vue', () => {
42+
expect($('#vue astro-slot')).to.have.a.lengthOf(1);
43+
expect($('#vue astro-static-slot')).to.have.a.lengthOf(0);
44+
});
45+
46+
it('preact', () => {
47+
expect($('#preact astro-slot')).to.have.a.lengthOf(1);
48+
expect($('#preact astro-static-slot')).to.have.a.lengthOf(0);
49+
});
50+
51+
it('solid', () => {
52+
expect($('#solid astro-slot')).to.have.a.lengthOf(1);
53+
expect($('#solid astro-static-slot')).to.have.a.lengthOf(0);
54+
});
55+
56+
it('svelte', () => {
57+
expect($('#svelte astro-slot')).to.have.a.lengthOf(1);
58+
expect($('#svelte astro-static-slot')).to.have.a.lengthOf(0);
59+
});
60+
});
2661
});
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { defineConfig } from 'astro/config';
22
import react from '@astrojs/react';
3+
import preact from '@astrojs/preact';
4+
import solid from '@astrojs/solid-js';
5+
import svelte from '@astrojs/svelte';
6+
import vue from '@astrojs/vue';
37

48
export default defineConfig({
5-
integrations: [react()]
9+
integrations: [
10+
react(),
11+
preact(),
12+
solid(),
13+
svelte(),
14+
vue()
15+
]
616
});

packages/astro/test/fixtures/astro-slots-nested/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
"version": "0.0.0",
44
"private": true,
55
"dependencies": {
6+
"@astrojs/preact": "workspace:*",
67
"@astrojs/react": "workspace:*",
8+
"@astrojs/vue": "workspace:*",
9+
"@astrojs/solid-js": "workspace:*",
10+
"@astrojs/svelte": "workspace:*",
711
"astro": "workspace:*",
812
"react": "^18.2.0",
9-
"react-dom": "^18.2.0"
13+
"react-dom": "^18.2.0",
14+
"solid-js": "^1.7.4",
15+
"svelte": "^3.58.0",
16+
"vue": "^3.2.47",
17+
"preact": "^10.13.2"
1018
}
1119
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React from 'react';
2+
13
export default function Inner() {
24
return <span>Inner</span>;
35
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React, { Fragment } from 'react';
2+
3+
export default function PassesChildren({ children }) {
4+
return <Fragment>{ children }</Fragment>;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { h, Fragment } from 'preact';
2+
3+
export default function PassesChildren({ children }) {
4+
return <Fragment>{ children }</Fragment>;
5+
}

0 commit comments

Comments
 (0)