Skip to content

Commit

Permalink
Yield out potentional slot instructions when rendering dynamic tags (#…
Browse files Browse the repository at this point in the history
…4981)

* Yield out potentional slot instructions when rendering dynamic tags

* Adding a changeset

* yield instead of return

* Handle the fact that renderComponent returns an iterable

* Only yield out html once
  • Loading branch information
matthewp committed Oct 5, 2022
1 parent 8f9791d commit 1f890b3
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-doors-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Ensure dynamic tags have their slot instructions yielded
3 changes: 2 additions & 1 deletion packages/astro/src/runtime/server/jsx.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-console */
import type { ComponentIterable } from './render/component';
import { SSRResult } from '../../@types/astro.js';
import { AstroJSX, isVNode } from '../../jsx-runtime/index.js';
import {
Expand Down Expand Up @@ -129,7 +130,7 @@ Did you forget to import the component or is it possible there is a typo?`);
}
await Promise.all(slotPromises);

let output: string | AsyncIterable<string | HTMLBytes | RenderInstruction>;
let output: ComponentIterable;
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
output = await renderComponent(
result,
Expand Down
18 changes: 13 additions & 5 deletions packages/astro/src/runtime/server/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function guessRenderers(componentUrl?: string): string[] {
}

type ComponentType = 'fragment' | 'html' | 'astro-factory' | 'unknown';
export type ComponentIterable = AsyncIterable<string | HTMLBytes | RenderInstruction>;

function getComponentType(Component: unknown): ComponentType {
if (Component === Fragment) {
Expand All @@ -54,7 +55,7 @@ export async function renderComponent(
Component: unknown,
_props: Record<string | number, any>,
slots: any = {}
): Promise<string | AsyncIterable<string | HTMLBytes | RenderInstruction>> {
): Promise<ComponentIterable> {
Component = await Component;

switch (getComponentType(Component)) {
Expand Down Expand Up @@ -279,10 +280,17 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
}

if (!hydration) {
if (isPage || renderer?.name === 'astro:jsx') {
return html;
}
return markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
return (async function *() {
if (slotInstructions) {
yield* slotInstructions;
}

if (isPage || renderer?.name === 'astro:jsx') {
yield html;
} else {
yield markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
}
})();
}

// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
Expand Down
58 changes: 32 additions & 26 deletions packages/astro/src/runtime/server/render/page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SSRResult } from '../../../@types/astro';
import type { AstroComponentFactory } from './index';
import type { ComponentIterable } from './component';

import { isHTMLString } from '../escape.js';
import { createResponse } from '../response.js';
Expand All @@ -19,6 +20,29 @@ function nonAstroPageNeedsHeadInjection(pageComponent: NonAstroPageComponent): b
return needsHeadRenderingSymbol in pageComponent && !!pageComponent[needsHeadRenderingSymbol];
}

async function iterableToHTMLBytes(
result: SSRResult,
iterable: ComponentIterable,
onDocTypeInjection?: (parts: HTMLParts) => Promise<void>
): Promise<Uint8Array> {
const parts = new HTMLParts();
let i = 0;
for await (const chunk of iterable) {
if (isHTMLString(chunk)) {
if (i === 0) {
if (!/<!doctype html/i.test(String(chunk))) {
parts.append('<!DOCTYPE html>\n', result);
if(onDocTypeInjection) {
await onDocTypeInjection(parts);
}
}
}
}
parts.append(chunk, result);
}
return parts.toArrayBuffer();
}

export async function renderPage(
result: SSRResult,
componentFactory: AstroComponentFactory | NonAstroPageComponent,
Expand All @@ -35,21 +59,16 @@ export async function renderPage(
pageProps,
null
);
let html = output.toString();
if (!/<!doctype html/i.test(html)) {
let rest = html;
html = `<!DOCTYPE html>`;
// This symbol currently exists for md components, but is something that could
// be added for any page-level component that's not an Astro component.
// to signal that head rendering is needed.

// Accumulate the HTML string and append the head if necessary.
const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
if (nonAstroPageNeedsHeadInjection(componentFactory)) {
for await (let chunk of maybeRenderHead(result)) {
html += chunk;
parts.append(chunk, result);
}
}
html += rest;
}
const bytes = encoder.encode(html);
});

return new Response(bytes, {
headers: new Headers([
['Content-Type', 'text/html; charset=utf-8'],
Expand Down Expand Up @@ -80,7 +99,7 @@ export async function renderPage(
}
}

let bytes = chunkToByteArray(result, chunk);
const bytes = chunkToByteArray(result, chunk);
controller.enqueue(bytes);
i++;
}
Expand All @@ -93,20 +112,7 @@ export async function renderPage(
},
});
} else {
let parts = new HTMLParts();
let i = 0;
for await (const chunk of iterable) {
if (isHTMLString(chunk)) {
if (i === 0) {
if (!/<!doctype html/i.test(String(chunk))) {
parts.append('<!DOCTYPE html>\n', result);
}
}
}
parts.append(chunk, result);
i++;
}
body = parts.toArrayBuffer();
body = await iterableToHTMLBytes(result, iterable);
headers.set('Content-Length', body.byteLength.toString());
}

Expand Down
19 changes: 19 additions & 0 deletions packages/astro/test/astro-slot-with-client.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';

describe('Slots with client: directives', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({ root: './fixtures/astro-slot-with-client/' });
await fixture.build();
});

it('Tags of dynamic tags works', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('script')).to.have.a.lengthOf(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';

export default defineConfig({
integrations: [
preact()
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/astro-slot-with-client",
"dependencies": {
"astro": "workspace:*",
"@astrojs/preact": "workspace:*",
"preact": "^10.11.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div id="default">
<slot />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export default function(props) {
return (
<div class="thing">
{ props.c }
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
import Slotted from '../components/Slotted.astro';
import Thing from '../components/Thing.jsx';
const Tag = 'section';
---

<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Slotted>
<Tag>
<span>More</span>
<Thing client:load>
<div slot="c">
inner content
</div>
</Thing>
</Tag>
</Slotted>
</body>
</html>
10 changes: 10 additions & 0 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 1f890b3

Please sign in to comment.