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/server-island-slot-scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a case where `<script>` tags from components passed as slots to server islands were not included in the response
17 changes: 15 additions & 2 deletions packages/astro/src/runtime/server/render/server-islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { renderChild } from './any.js';
import { createThinHead, type ThinHead } from './astro/head-and-content.js';
import type { RenderDestination } from './common.js';
import { createRenderInstruction } from './instruction.js';
import { type ComponentSlots, renderSlotToString } from './slot.js';
import { type ComponentSlots, type SlotString, renderSlotToString } from './slot.js';

const internalProps = new Set([
'server:component-path',
Expand Down Expand Up @@ -158,7 +158,20 @@ export class ServerIslandComponent {
for (const name in this.slots) {
if (name !== 'fallback') {
const content = await renderSlotToString(this.result, this.slots[name]);
renderedSlots[name] = content.toString();
let slotHtml = content.toString();
// Append script instructions so that components passed as slots
// to server:defer components retain their scripts in the island response.
// renderSlotToString returns a SlotString (typed as string) that carries
// render instructions stripped from the HTML content.
const slotContent = content as unknown as SlotString;
if (Array.isArray(slotContent.instructions)) {
for (const instruction of slotContent.instructions) {
if (instruction.type === 'script') {
slotHtml += instruction.content;
}
}
}
renderedSlots[name] = slotHtml;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
---
<div class="counter">
<span data-count>0</span>
<button data-increment>Increment</button>
</div>

<script>
document.querySelectorAll("[data-increment]").forEach((btn) => {
btn.addEventListener("click", () => {
const countEl = btn.previousElementSibling;
if (countEl) {
countEl.textContent = String(Number(countEl.textContent) + 1);
}
});
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---
<div id="wrapper">
<slot name="content" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import Wrapper from '../components/Wrapper.astro';
import ScriptedCounter from '../components/ScriptedCounter.astro';
---
<html>
<head>
<title>Slot with script</title>
</head>
<body>
<Wrapper server:defer>
<ScriptedCounter slot="content" />
<div slot="fallback">Loading...</div>
</Wrapper>
</body>
</html>
14 changes: 14 additions & 0 deletions packages/astro/test/server-islands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ describe('Server islands', () => {
assert.equal(fetchMatch.length, 2, 'should include props in the query string');
assert.equal(fetchMatch[1], '', 'should not include encrypted empty props');
});

it('includes script from slotted component in island response', async () => {
const res = await fixture.fetch('/slot-with-script');
assert.equal(res.status, 200);
const html = await res.text();
// Extract the island fetch URL from the page
const urlMatch = html.match(/fetch\('(\/_server-islands\/Wrapper\?[^']+)'/);
assert.ok(urlMatch, 'should have a server island fetch URL');
const islandRes = await fixture.fetch(urlMatch[1]);
assert.equal(islandRes.status, 200);
const islandHtml = await islandRes.text();
assert.ok(islandHtml.includes('<script'), 'island response should include the script tag from the slotted component');
assert.ok(islandHtml.includes('data-increment'), 'island response should include the slotted component HTML');
});
});

describe('prod', () => {
Expand Down