Skip to content

Commit

Permalink
Support streaming inside of slots (#6775)
Browse files Browse the repository at this point in the history
* Rename renderSlot to renderSlotToString for internal sync usage

* Support streaming inside of slots

* Fix lame lint warning

* Update compiler to fix test

* Up the wait

* Use compiler 1.3.1

* It should be exactly 3
  • Loading branch information
matthewp committed Apr 7, 2023
1 parent f211245 commit fa84f1a
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-bugs-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Support streaming inside of slots
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
renderHTMLElement,
renderPage,
renderScriptElement,
renderSlotToString,
renderSlot,
renderStyleElement,
renderTemplate as render,
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/runtime/server/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']]);
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/runtime/server/render/dom.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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}>`
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/runtime/server/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
44 changes: 26 additions & 18 deletions packages/astro/src/runtime/server/render/slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 = [];
Expand Down
22 changes: 9 additions & 13 deletions packages/astro/test/astro-slots.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,28 @@ 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);

expect($('#a')).to.have.lengthOf(1);
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);

expect($('#a')).to.have.lengthOf(0);
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);

Expand All @@ -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);

Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<section>
<slot />
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
import { wait } from '../wait';
const { ms } = Astro.props;
await wait(ms);
---
<slot></slot>
24 changes: 24 additions & 0 deletions packages/astro/test/fixtures/streaming/src/pages/slot.astro
Original file line number Diff line number Diff line change
@@ -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>
15 changes: 13 additions & 2 deletions packages/astro/test/streaming.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/',
Expand All @@ -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', () => {
Expand All @@ -60,7 +72,6 @@ describe('Streaming', () => {
const request = new Request('http:https://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);
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

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

0 comments on commit fa84f1a

Please sign in to comment.