Support streaming inside of slots (#6775)
* 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
This commit is contained in:
parent
f2112452ad
commit
fa84f1a7d2
14 changed files with 103 additions and 49 deletions
5
.changeset/old-bugs-watch.md
Normal file
5
.changeset/old-bugs-watch.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Support streaming inside of slots
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -17,6 +17,7 @@ export {
|
|||
renderHTMLElement,
|
||||
renderPage,
|
||||
renderScriptElement,
|
||||
renderSlotToString,
|
||||
renderSlot,
|
||||
renderStyleElement,
|
||||
renderTemplate as render,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}>`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
): AsyncGenerator<any, void, undefined> {
|
||||
if (slotted) {
|
||||
let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted);
|
||||
yield * iterator;
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
yield * renderSlot(result, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderSlotToString(
|
||||
result: SSRResult,
|
||||
slotted: ComponentSlotValue | RenderTemplateResult,
|
||||
fallback?: ComponentSlotValue | RenderTemplateResult
|
||||
): Promise<string> {
|
||||
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;
|
||||
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 markHTMLString(new SlotString(content, instructions));
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
return renderSlot(result, fallback);
|
||||
}
|
||||
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 = [];
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
3
packages/astro/test/fixtures/streaming/src/components/BareComponent.astro
vendored
Normal file
3
packages/astro/test/fixtures/streaming/src/components/BareComponent.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<section>
|
||||
<slot />
|
||||
</section>
|
6
packages/astro/test/fixtures/streaming/src/components/Wait.astro
vendored
Normal file
6
packages/astro/test/fixtures/streaming/src/components/Wait.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
import { wait } from '../wait';
|
||||
const { ms } = Astro.props;
|
||||
await wait(ms);
|
||||
---
|
||||
<slot></slot>
|
24
packages/astro/test/fixtures/streaming/src/pages/slot.astro
vendored
Normal file
24
packages/astro/test/fixtures/streaming/src/pages/slot.astro
vendored
Normal file
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue