summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/old-bugs-watch.md5
-rw-r--r--packages/astro/package.json2
-rw-r--r--packages/astro/src/core/render/result.ts6
-rw-r--r--packages/astro/src/runtime/server/index.ts1
-rw-r--r--packages/astro/src/runtime/server/render/component.ts6
-rw-r--r--packages/astro/src/runtime/server/render/dom.ts4
-rw-r--r--packages/astro/src/runtime/server/render/index.ts2
-rw-r--r--packages/astro/src/runtime/server/render/slot.ts44
-rw-r--r--packages/astro/test/astro-slots.test.js22
-rw-r--r--packages/astro/test/fixtures/streaming/src/components/BareComponent.astro3
-rw-r--r--packages/astro/test/fixtures/streaming/src/components/Wait.astro6
-rw-r--r--packages/astro/test/fixtures/streaming/src/pages/slot.astro24
-rw-r--r--packages/astro/test/streaming.test.js15
-rw-r--r--pnpm-lock.yaml8
14 files changed, 101 insertions, 47 deletions
diff --git a/.changeset/old-bugs-watch.md b/.changeset/old-bugs-watch.md
new file mode 100644
index 000000000..f0265a242
--- /dev/null
+++ b/.changeset/old-bugs-watch.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Support streaming inside of slots
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 81d5ca0a0..3b57f3b40 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -106,7 +106,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
- "@astrojs/compiler": "^1.3.0",
+ "@astrojs/compiler": "^1.3.1",
"@astrojs/language-server": "^0.28.3",
"@astrojs/markdown-remark": "^2.1.3",
"@astrojs/telemetry": "^2.1.0",
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index fe88c8b6b..93f3fbaad 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -9,7 +9,7 @@ import type {
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
-import { renderSlot, stringifyChunk, type ComponentSlots } from '../../runtime/server/index.js';
+import { renderSlotToString, stringifyChunk, type ComponentSlots } from '../../runtime/server/index.js';
import { renderJSX } from '../../runtime/server/jsx.js';
import { AstroCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
@@ -105,7 +105,7 @@ class Slots {
const expression = getFunctionExpression(component);
if (expression) {
const slot = () => expression(...args);
- return await renderSlot(result, slot).then((res) => (res != null ? String(res) : res));
+ return await renderSlotToString(result, slot).then((res) => (res != null ? String(res) : res));
}
// JSX
if (typeof component === 'function') {
@@ -115,7 +115,7 @@ class Slots {
}
}
- const content = await renderSlot(result, this.#slots[name]);
+ const content = await renderSlotToString(result, this.#slots[name]);
const outHTML = stringifyChunk(result, content);
return outHTML;
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index ebc234ac2..6f575676b 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -17,6 +17,7 @@ export {
renderHTMLElement,
renderPage,
renderScriptElement,
+ renderSlotToString,
renderSlot,
renderStyleElement,
renderTemplate as render,
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index 66b0a1d76..4d2b441e1 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -17,7 +17,7 @@ import {
} from './astro/index.js';
import { Fragment, Renderer, stringifyChunk } from './common.js';
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
-import { renderSlot, renderSlots, type ComponentSlots } from './slot.js';
+import { renderSlotToString, renderSlots, type ComponentSlots } from './slot.js';
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
const rendererAliases = new Map([['solid', 'solid-js']]);
@@ -207,7 +207,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
}
} else {
if (metadata.hydrate === 'only') {
- html = await renderSlot(result, slots?.fallback);
+ html = await renderSlotToString(result, slots?.fallback);
} else {
({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call(
{ result },
@@ -332,7 +332,7 @@ function sanitizeElementName(tag: string) {
}
async function renderFragmentComponent(result: SSRResult, slots: ComponentSlots = {}) {
- const children = await renderSlot(result, slots?.default);
+ const children = await renderSlotToString(result, slots?.default);
if (children == null) {
return children;
}
diff --git a/packages/astro/src/runtime/server/render/dom.ts b/packages/astro/src/runtime/server/render/dom.ts
index 352b60436..803f29995 100644
--- a/packages/astro/src/runtime/server/render/dom.ts
+++ b/packages/astro/src/runtime/server/render/dom.ts
@@ -1,7 +1,7 @@
import type { SSRResult } from '../../../@types/astro';
import { markHTMLString } from '../escape.js';
-import { renderSlot } from './slot.js';
+import { renderSlotToString } from './slot.js';
import { toAttributeString } from './util.js';
export function componentIsHTMLElement(Component: unknown) {
@@ -23,7 +23,7 @@ export async function renderHTMLElement(
}
return markHTMLString(
- `<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`
+ `<${name}${attrHTML}>${await renderSlotToString(result, slots?.default)}</${name}>`
);
}
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index 33e3dfe8f..30f85b3ac 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -10,7 +10,7 @@ export { renderComponent, renderComponentToIterable } from './component.js';
export { renderHTMLElement } from './dom.js';
export { maybeRenderHead, renderHead } from './head.js';
export { renderPage } from './page.js';
-export { renderSlot, type ComponentSlots } from './slot.js';
+export { renderSlotToString, renderSlot, type ComponentSlots } from './slot.js';
export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
export type { RenderInstruction } from './types';
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts
index cd6bc064b..b2ee52d1f 100644
--- a/packages/astro/src/runtime/server/render/slot.ts
+++ b/packages/astro/src/runtime/server/render/slot.ts
@@ -25,32 +25,40 @@ export function isSlotString(str: string): str is any {
return !!(str as any)[slotString];
}
-export async function renderSlot(
+export async function * renderSlot(
result: SSRResult,
slotted: ComponentSlotValue | RenderTemplateResult,
fallback?: ComponentSlotValue | RenderTemplateResult
-): Promise<string> {
+): AsyncGenerator<any, void, undefined> {
if (slotted) {
let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted);
- let content = '';
- let instructions: null | RenderInstruction[] = null;
- for await (const chunk of iterator) {
- if (typeof (chunk as any).type === 'string') {
- if (instructions === null) {
- instructions = [];
- }
- instructions.push(chunk);
- } else {
- content += chunk;
- }
- }
- return markHTMLString(new SlotString(content, instructions));
+ yield * iterator;
}
if (fallback) {
- return renderSlot(result, fallback);
+ yield * renderSlot(result, fallback);
+ }
+}
+
+export async function renderSlotToString(
+ result: SSRResult,
+ slotted: ComponentSlotValue | RenderTemplateResult,
+ fallback?: ComponentSlotValue | RenderTemplateResult
+): Promise<string> {
+ let content = '';
+ let instructions: null | RenderInstruction[] = null;
+ let iterator = renderSlot(result, slotted, fallback);
+ for await (const chunk of iterator) {
+ if (typeof (chunk as any).type === 'string') {
+ if (instructions === null) {
+ instructions = [];
+ }
+ instructions.push(chunk);
+ } else {
+ content += chunk;
+ }
}
- return '';
+ return markHTMLString(new SlotString(content, instructions));
}
interface RenderSlotsResult {
@@ -67,7 +75,7 @@ export async function renderSlots(
if (slots) {
await Promise.all(
Object.entries(slots).map(([key, value]) =>
- renderSlot(result, value).then((output: any) => {
+ renderSlotToString(result, value).then((output: any) => {
if (output.instructions) {
if (slotInstructions === null) {
slotInstructions = [];
diff --git a/packages/astro/test/astro-slots.test.js b/packages/astro/test/astro-slots.test.js
index 3c0b2be49..db0d8d338 100644
--- a/packages/astro/test/astro-slots.test.js
+++ b/packages/astro/test/astro-slots.test.js
@@ -75,9 +75,8 @@ describe('Slots', () => {
expect($('#default').children('astro-component')).to.have.lengthOf(1);
});
- it('Slots API work on Components', async () => {
- // IDs will exist whether the slots are filled or not
- {
+ describe('Slots API work on Components', () => {
+ it('IDs will exist whether the slots are filled or not', async () => {
const html = await fixture.readFile('/slottedapi-default/index.html');
const $ = cheerio.load(html);
@@ -85,10 +84,9 @@ describe('Slots', () => {
expect($('#b')).to.have.lengthOf(1);
expect($('#c')).to.have.lengthOf(1);
expect($('#default')).to.have.lengthOf(1);
- }
+ });
- // IDs will not exist because the slots are not filled
- {
+ it('IDs will not exist because the slots are not filled', async () => {
const html = await fixture.readFile('/slottedapi-empty/index.html');
const $ = cheerio.load(html);
@@ -96,10 +94,9 @@ describe('Slots', () => {
expect($('#b')).to.have.lengthOf(0);
expect($('#c')).to.have.lengthOf(0);
expect($('#default')).to.have.lengthOf(0);
- }
+ });
- // IDs will exist because the slots are filled
- {
+ it('IDs will exist because the slots are filled', async () => {
const html = await fixture.readFile('/slottedapi-filled/index.html');
const $ = cheerio.load(html);
@@ -108,10 +105,9 @@ describe('Slots', () => {
expect($('#c')).to.have.lengthOf(1);
expect($('#default')).to.have.lengthOf(0); // the default slot is not filled
- }
+ });
- // Default ID will exist because the default slot is filled
- {
+ it('Default ID will exist because the default slot is filled', async () => {
const html = await fixture.readFile('/slottedapi-default-filled/index.html');
const $ = cheerio.load(html);
@@ -120,7 +116,7 @@ describe('Slots', () => {
expect($('#c')).to.have.lengthOf(0);
expect($('#default')).to.have.lengthOf(1); // the default slot is filled
- }
+ });
});
it('Slots.render() API', async () => {
diff --git a/packages/astro/test/fixtures/streaming/src/components/BareComponent.astro b/packages/astro/test/fixtures/streaming/src/components/BareComponent.astro
new file mode 100644
index 000000000..fb20c0a2b
--- /dev/null
+++ b/packages/astro/test/fixtures/streaming/src/components/BareComponent.astro
@@ -0,0 +1,3 @@
+<section>
+ <slot />
+</section>
diff --git a/packages/astro/test/fixtures/streaming/src/components/Wait.astro b/packages/astro/test/fixtures/streaming/src/components/Wait.astro
new file mode 100644
index 000000000..5a2e956be
--- /dev/null
+++ b/packages/astro/test/fixtures/streaming/src/components/Wait.astro
@@ -0,0 +1,6 @@
+---
+import { wait } from '../wait';
+const { ms } = Astro.props;
+await wait(ms);
+---
+<slot></slot>
diff --git a/packages/astro/test/fixtures/streaming/src/pages/slot.astro b/packages/astro/test/fixtures/streaming/src/pages/slot.astro
new file mode 100644
index 000000000..ea918cc6f
--- /dev/null
+++ b/packages/astro/test/fixtures/streaming/src/pages/slot.astro
@@ -0,0 +1,24 @@
+---
+import BareComponent from '../components/BareComponent.astro';
+import Wait from '../components/Wait.astro';
+---
+
+<html>
+ <head>
+ <title>Testing</title>
+ </head>
+ <body>
+ <h1>Testing</h1>
+ <BareComponent>
+ <h1>Section title</h1>
+ <Wait ms={50}>
+ <p>Section content</p>
+ </Wait>
+ <h2>Next section</h2>
+ <Wait ms={50}>
+ <p>Section content</p>
+ </Wait>
+ <p>Paragraph 3</p>
+ </BareComponent>
+ </body>
+</html>
diff --git a/packages/astro/test/streaming.test.js b/packages/astro/test/streaming.test.js
index 47dedac22..c7b835de1 100644
--- a/packages/astro/test/streaming.test.js
+++ b/packages/astro/test/streaming.test.js
@@ -9,6 +9,8 @@ describe('Streaming', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
+ let decoder = new TextDecoder();
+
before(async () => {
fixture = await loadFixture({
root: './fixtures/streaming/',
@@ -33,11 +35,21 @@ describe('Streaming', () => {
let res = await fixture.fetch('/');
let chunks = [];
for await (const bytes of streamAsyncIterator(res.body)) {
- let chunk = bytes.toString('utf-8');
+ let chunk = decoder.decode(bytes);
chunks.push(chunk);
}
expect(chunks.length).to.be.greaterThan(1);
});
+
+ it('Body of slots is chunked', async () => {
+ let res = await fixture.fetch('/slot');
+ let chunks = [];
+ for await (const bytes of streamAsyncIterator(res.body)) {
+ let chunk = decoder.decode(bytes);
+ chunks.push(chunk);
+ }
+ expect(chunks.length).to.equal(3);
+ });
});
describe('Production', () => {
@@ -60,7 +72,6 @@ describe('Streaming', () => {
const request = new Request('http://example.com/');
const response = await app.render(request);
let chunks = [];
- let decoder = new TextDecoder();
for await (const bytes of streamAsyncIterator(response.body)) {
let chunk = decoder.decode(bytes);
chunks.push(chunk);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 74c7a775a..cfc4d3a50 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -426,7 +426,7 @@ importers:
packages/astro:
specifiers:
- '@astrojs/compiler': ^1.3.0
+ '@astrojs/compiler': ^1.3.1
'@astrojs/language-server': ^0.28.3
'@astrojs/markdown-remark': ^2.1.3
'@astrojs/telemetry': ^2.1.0
@@ -519,7 +519,7 @@ importers:
yargs-parser: ^21.0.1
zod: ^3.17.3
dependencies:
- '@astrojs/compiler': 1.3.0
+ '@astrojs/compiler': 1.3.1
'@astrojs/language-server': 0.28.3
'@astrojs/markdown-remark': link:../markdown/remark
'@astrojs/telemetry': link:../telemetry
@@ -4271,8 +4271,8 @@ packages:
/@astrojs/compiler/0.31.4:
resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==}
- /@astrojs/compiler/1.3.0:
- resolution: {integrity: sha512-VxSj3gh/UTB/27rkRCT7SvyGjWtuxUO7Jf7QqDduch7j/gr/uA5P/Q5I/4zIIrZjy2yQAKyKLoox2QI2mM/BSA==}
+ /@astrojs/compiler/1.3.1:
+ resolution: {integrity: sha512-xV/3r+Hrfpr4ECfJjRjeaMkJvU73KiOADowHjhkqidfNPVAWPzbqw1KePXuMK1TjzMvoAVE7E163oqfH3lDwSw==}
dev: false
/@astrojs/language-server/0.28.3: