Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
|
141149e794 | ||
|
cebad76cad | ||
|
5f32bf9ef9 | ||
|
c1c7c3f31b |
31 changed files with 2158 additions and 1967 deletions
11
.changeset/tasty-hornets-return.md
Normal file
11
.changeset/tasty-hornets-return.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Moves head injection to happen during rendering
|
||||||
|
|
||||||
|
This change makes it so that head injection; to insert component stylesheets, hoisted scripts, for example, to happen during rendering than as a post-rendering step.
|
||||||
|
|
||||||
|
This is to enable streaming. This change will only be noticeable if you are rendering your `<head>` element inside of a framework component. If that is the case then the head items will be injected before the first non-head element in an Astro file instead.
|
||||||
|
|
||||||
|
In the future we may offer a `<Astro.Head>` component as a way to control where these scripts/styles are inserted.
|
|
@ -0,0 +1,4 @@
|
||||||
|
<html>
|
||||||
|
<head><title>Preact component</title></head>
|
||||||
|
<body><slot></slot></body>
|
||||||
|
</html>
|
|
@ -1,4 +1,5 @@
|
||||||
---
|
---
|
||||||
|
layout: ../components/Layout.astro
|
||||||
setup: |
|
setup: |
|
||||||
import Counter from '../components/Counter.jsx';
|
import Counter from '../components/Counter.jsx';
|
||||||
import PreactComponent from '../components/JSXComponent.jsx';
|
import PreactComponent from '../components/JSXComponent.jsx';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<html>
|
||||||
|
<head><title>React component</title></head>
|
||||||
|
<body><slot></slot></body>
|
||||||
|
</html>
|
|
@ -1,4 +1,5 @@
|
||||||
---
|
---
|
||||||
|
layout: ../components/Layout.astro
|
||||||
setup: |
|
setup: |
|
||||||
import Counter from '../components/Counter.jsx';
|
import Counter from '../components/Counter.jsx';
|
||||||
import ReactComponent from '../components/JSXComponent.jsx';
|
import ReactComponent from '../components/JSXComponent.jsx';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<html>
|
||||||
|
<head><title>Solid component</title></head>
|
||||||
|
<body><slot></slot></body>
|
||||||
|
</html>
|
|
@ -1,4 +1,5 @@
|
||||||
---
|
---
|
||||||
|
layout: ../components/Layout.astro
|
||||||
setup: |
|
setup: |
|
||||||
import Counter from '../components/Counter.jsx';
|
import Counter from '../components/Counter.jsx';
|
||||||
import SolidComponent from '../components/SolidComponent.jsx';
|
import SolidComponent from '../components/SolidComponent.jsx';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<html>
|
||||||
|
<head><title>Solid component</title></head>
|
||||||
|
<body><slot></slot></body>
|
||||||
|
</html>
|
|
@ -1,4 +1,5 @@
|
||||||
---
|
---
|
||||||
|
layout: ../components/Layout.astro
|
||||||
setup: |
|
setup: |
|
||||||
import Counter from '../components/Counter.svelte';
|
import Counter from '../components/Counter.svelte';
|
||||||
import SvelteComponent from '../components/SvelteComponent.svelte';
|
import SvelteComponent from '../components/SvelteComponent.svelte';
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
"test:e2e:match": "playwright test -g"
|
"test:e2e:match": "playwright test -g"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^0.16.1",
|
"@astrojs/compiler": "^0.17.0",
|
||||||
"@astrojs/language-server": "^0.13.4",
|
"@astrojs/language-server": "^0.13.4",
|
||||||
"@astrojs/markdown-remark": "^0.11.3",
|
"@astrojs/markdown-remark": "^0.11.3",
|
||||||
"@astrojs/prism": "0.4.1",
|
"@astrojs/prism": "0.4.1",
|
||||||
|
@ -105,7 +105,6 @@
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"html-entities": "^2.3.3",
|
"html-entities": "^2.3.3",
|
||||||
"html-escaper": "^3.0.3",
|
"html-escaper": "^3.0.3",
|
||||||
"htmlparser2": "^7.2.0",
|
|
||||||
"kleur": "^4.1.4",
|
"kleur": "^4.1.4",
|
||||||
"magic-string": "^0.25.9",
|
"magic-string": "^0.25.9",
|
||||||
"micromorph": "^0.1.2",
|
"micromorph": "^0.1.2",
|
||||||
|
|
|
@ -1004,7 +1004,6 @@ export interface SSRElement {
|
||||||
export interface SSRMetadata {
|
export interface SSRMetadata {
|
||||||
renderers: SSRLoadedRenderer[];
|
renderers: SSRLoadedRenderer[];
|
||||||
pathname: string;
|
pathname: string;
|
||||||
needsHydrationStyles: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSRResult {
|
export interface SSRResult {
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await render({
|
const response = await render({
|
||||||
links,
|
links,
|
||||||
logging: this.#logging,
|
logging: this.#logging,
|
||||||
markdown: manifest.markdown,
|
markdown: manifest.markdown,
|
||||||
|
@ -119,17 +119,7 @@ export class App {
|
||||||
request,
|
request,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.type === 'response') {
|
return response;
|
||||||
return result.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = result.html;
|
|
||||||
let init = result.response;
|
|
||||||
let headers = init.headers as Headers;
|
|
||||||
let bytes = this.#encoder.encode(html);
|
|
||||||
headers.set('Content-Type', 'text/html');
|
|
||||||
headers.set('Content-Length', bytes.byteLength.toString());
|
|
||||||
return new Response(bytes, init);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #callEndpoint(
|
async #callEndpoint(
|
||||||
|
|
|
@ -251,13 +251,14 @@ async function generatePath(
|
||||||
}
|
}
|
||||||
body = result.body;
|
body = result.body;
|
||||||
} else {
|
} else {
|
||||||
const result = await render(options);
|
const response = await render(options);
|
||||||
|
|
||||||
// If there's a redirect or something, just do nothing.
|
// If there's a redirect or something, just do nothing.
|
||||||
if (result.type !== 'html') {
|
if (response.status !== 200 || !response.body) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body = result.html;
|
|
||||||
|
body = await response.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type);
|
const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type);
|
||||||
|
|
|
@ -85,9 +85,7 @@ export interface RenderOptions {
|
||||||
|
|
||||||
export async function render(
|
export async function render(
|
||||||
opts: RenderOptions
|
opts: RenderOptions
|
||||||
): Promise<
|
): Promise<Response> {
|
||||||
{ type: 'html'; html: string; response: ResponseInit } | { type: 'response'; response: Response }
|
|
||||||
> {
|
|
||||||
const {
|
const {
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
|
@ -144,38 +142,11 @@ export async function render(
|
||||||
ssr,
|
ssr,
|
||||||
});
|
});
|
||||||
|
|
||||||
let page: Awaited<ReturnType<typeof renderPage>>;
|
|
||||||
if (!Component.isAstroComponentFactory) {
|
if (!Component.isAstroComponentFactory) {
|
||||||
const props: Record<string, any> = { ...(pageProps ?? {}), 'server:root': true };
|
const props: Record<string, any> = { ...(pageProps ?? {}), 'server:root': true };
|
||||||
const html = await renderComponent(result, Component.name, Component, props, null);
|
const html = await renderComponent(result, Component.name, Component, props, null);
|
||||||
page = {
|
return new Response(html.toString(), result.response);
|
||||||
type: 'html',
|
|
||||||
html: html.toString(),
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
page = await renderPage(result, Component, pageProps, null);
|
return await renderPage(result, Component, pageProps, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.type === 'response') {
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = page.html;
|
|
||||||
// handle final head injection if it hasn't happened already
|
|
||||||
if (html.indexOf('<!--astro:head:injected-->') == -1) {
|
|
||||||
html = (await renderHead(result)) + html;
|
|
||||||
}
|
|
||||||
// cleanup internal state flags
|
|
||||||
html = html.replace('<!--astro:head:injected-->', '');
|
|
||||||
|
|
||||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
|
||||||
if (!/<!doctype html/i.test(html)) {
|
|
||||||
html = '<!DOCTYPE html>\n' + html;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'html',
|
|
||||||
html,
|
|
||||||
response: result.response,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
import type * as vite from 'vite';
|
|
||||||
|
|
||||||
import htmlparser2 from 'htmlparser2';
|
|
||||||
|
|
||||||
/** Inject tags into HTML (note: for best performance, group as many tags as possible into as few calls as you can) */
|
|
||||||
export function injectTags(html: string, tags: vite.HtmlTagDescriptor[]): string {
|
|
||||||
let output = html;
|
|
||||||
if (!tags.length) return output;
|
|
||||||
|
|
||||||
const pos = { 'head-prepend': -1, head: -1, 'body-prepend': -1, body: -1 };
|
|
||||||
|
|
||||||
// parse html
|
|
||||||
const parser = new htmlparser2.Parser({
|
|
||||||
onopentag(tagname) {
|
|
||||||
if (tagname === 'head') pos['head-prepend'] = parser.endIndex + 1;
|
|
||||||
if (tagname === 'body') pos['body-prepend'] = parser.endIndex + 1;
|
|
||||||
},
|
|
||||||
onclosetag(tagname) {
|
|
||||||
if (tagname === 'head') pos['head'] = parser.startIndex;
|
|
||||||
if (tagname === 'body') pos['body'] = parser.startIndex;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
parser.write(html);
|
|
||||||
parser.end();
|
|
||||||
|
|
||||||
// inject
|
|
||||||
const lastToFirst = Object.entries(pos).sort((a, b) => b[1] - a[1]);
|
|
||||||
lastToFirst.forEach(([name, i]) => {
|
|
||||||
if (i === -1) {
|
|
||||||
// if page didn’t generate <head> or <body>, guess
|
|
||||||
if (name === 'head-prepend' || name === 'head') i = 0;
|
|
||||||
if (name === 'body-prepend' || name === 'body') i = html.length;
|
|
||||||
}
|
|
||||||
let selected = tags.filter(({ injectTo }) => {
|
|
||||||
if (name === 'head-prepend' && !injectTo) {
|
|
||||||
return true; // "head-prepend" is the default
|
|
||||||
} else {
|
|
||||||
return injectTo === name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!selected.length) return;
|
|
||||||
output = output.substring(0, i) + serializeTags(selected) + html.substring(i);
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resource = Record<string, string>;
|
|
||||||
|
|
||||||
/** Collect resources (scans final, rendered HTML so expressions have been applied) */
|
|
||||||
export function collectResources(html: string): Resource[] {
|
|
||||||
let resources: Resource[] = [];
|
|
||||||
const parser = new htmlparser2.Parser({
|
|
||||||
// <link> tags are self-closing, so only use onopentag (avoid onattribute or onclosetag)
|
|
||||||
onopentag(tagname, attrs) {
|
|
||||||
if (tagname === 'link') resources.push(attrs);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
parser.write(html);
|
|
||||||
parser.end();
|
|
||||||
return resources;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------
|
|
||||||
// Everything below © Vite. Rather than invent our own tag creating API, we borrow
|
|
||||||
// Vite’s `transformIndexHtml()` API for ease-of-use and consistency. But we need
|
|
||||||
// to borrow a few private methods in Vite to make that available here.
|
|
||||||
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts
|
|
||||||
//
|
|
||||||
// See LICENSE for more info.
|
|
||||||
// -------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const unaryTags = new Set(['link', 'meta', 'base']);
|
|
||||||
|
|
||||||
function serializeTag({ tag, attrs, children }: vite.HtmlTagDescriptor, indent = ''): string {
|
|
||||||
if (unaryTags.has(tag)) {
|
|
||||||
return `<${tag}${serializeAttrs(attrs)}>`;
|
|
||||||
} else {
|
|
||||||
return `<${tag}${serializeAttrs(attrs)}>${serializeTags(
|
|
||||||
children,
|
|
||||||
incrementIndent(indent)
|
|
||||||
)}</${tag}>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeTags(tags: vite.HtmlTagDescriptor['children'], indent = ''): string {
|
|
||||||
if (typeof tags === 'string') {
|
|
||||||
return tags;
|
|
||||||
} else if (tags && tags.length) {
|
|
||||||
return tags.map((tag) => `${indent}${serializeTag(tag, indent)}\n`).join('');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeAttrs(attrs: vite.HtmlTagDescriptor['attrs']): string {
|
|
||||||
let res = '';
|
|
||||||
for (const key in attrs) {
|
|
||||||
if (typeof attrs[key] === 'boolean') {
|
|
||||||
res += attrs[key] ? ` ${key}` : ``;
|
|
||||||
} else {
|
|
||||||
res += ` ${key}=${JSON.stringify(attrs[key])}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementIndent(indent = '') {
|
|
||||||
return `${indent}${indent[0] === '\t' ? '\t' : ' '}`;
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type { HtmlTagDescriptor, ViteDevServer } from 'vite';
|
import type { ViteDevServer } from 'vite';
|
||||||
import type {
|
import type {
|
||||||
AstroConfig,
|
AstroConfig,
|
||||||
AstroRenderer,
|
AstroRenderer,
|
||||||
|
@ -17,7 +17,6 @@ import { RouteCache } from '../route-cache.js';
|
||||||
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
|
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
|
||||||
import { collectMdMetadata } from '../util.js';
|
import { collectMdMetadata } from '../util.js';
|
||||||
import { getStylesForURL } from './css.js';
|
import { getStylesForURL } from './css.js';
|
||||||
import { injectTags } from './html.js';
|
|
||||||
import { resolveClientDevPath } from './resolve.js';
|
import { resolveClientDevPath } from './resolve.js';
|
||||||
|
|
||||||
export interface SSROptions {
|
export interface SSROptions {
|
||||||
|
@ -45,10 +44,6 @@ export interface SSROptions {
|
||||||
|
|
||||||
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
||||||
|
|
||||||
export type RenderResponse =
|
|
||||||
| { type: 'html'; html: string; response: ResponseInit }
|
|
||||||
| { type: 'response'; response: Response };
|
|
||||||
|
|
||||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||||
|
|
||||||
async function loadRenderer(
|
async function loadRenderer(
|
||||||
|
@ -99,7 +94,7 @@ export async function render(
|
||||||
renderers: SSRLoadedRenderer[],
|
renderers: SSRLoadedRenderer[],
|
||||||
mod: ComponentInstance,
|
mod: ComponentInstance,
|
||||||
ssrOpts: SSROptions
|
ssrOpts: SSROptions
|
||||||
): Promise<RenderResponse> {
|
): Promise<Response> {
|
||||||
const {
|
const {
|
||||||
astroConfig,
|
astroConfig,
|
||||||
filePath,
|
filePath,
|
||||||
|
@ -167,7 +162,7 @@ export async function render(
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = await coreRender({
|
let response = await coreRender({
|
||||||
links,
|
links,
|
||||||
styles,
|
styles,
|
||||||
logging,
|
logging,
|
||||||
|
@ -191,32 +186,13 @@ export async function render(
|
||||||
ssr: isBuildingToSSR(astroConfig),
|
ssr: isBuildingToSSR(astroConfig),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (route?.type === 'endpoint' || content.type === 'response') {
|
return response;
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inject tags
|
|
||||||
const tags: HtmlTagDescriptor[] = [];
|
|
||||||
|
|
||||||
// add injected tags
|
|
||||||
let html = injectTags(content.html, tags);
|
|
||||||
|
|
||||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
|
||||||
if (!/<!doctype html/i.test(html)) {
|
|
||||||
html = '<!DOCTYPE html>\n' + content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'html',
|
|
||||||
html,
|
|
||||||
response: content.response,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ssr(
|
export async function ssr(
|
||||||
preloadedComponent: ComponentPreload,
|
preloadedComponent: ComponentPreload,
|
||||||
ssrOpts: SSROptions
|
ssrOpts: SSROptions
|
||||||
): Promise<RenderResponse> {
|
): Promise<Response> {
|
||||||
const [renderers, mod] = preloadedComponent;
|
const [renderers, mod] = preloadedComponent;
|
||||||
return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors won’t get caught below
|
return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors won’t get caught below
|
||||||
}
|
}
|
||||||
|
|
|
@ -221,7 +221,6 @@ ${extra}`
|
||||||
},
|
},
|
||||||
resolve,
|
resolve,
|
||||||
_metadata: {
|
_metadata: {
|
||||||
needsHydrationStyles: false,
|
|
||||||
renderers,
|
renderers,
|
||||||
pathname,
|
pathname,
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
|
|
||||||
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
||||||
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
||||||
|
import { createResponse } from './response.js';
|
||||||
import {
|
import {
|
||||||
determineIfNeedsHydrationScript,
|
determineIfNeedsHydrationScript,
|
||||||
determinesIfNeedsDirectiveScript,
|
determinesIfNeedsDirectiveScript,
|
||||||
|
@ -40,19 +41,21 @@ const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|pre
|
||||||
// INVESTIGATE: Can we have more specific types both for the argument and output?
|
// INVESTIGATE: Can we have more specific types both for the argument and output?
|
||||||
// If these are intentional, add comments that these are intention and why.
|
// If these are intentional, add comments that these are intention and why.
|
||||||
// Or maybe type UserValue = any; ?
|
// Or maybe type UserValue = any; ?
|
||||||
async function _render(child: any): Promise<any> {
|
async function * _render(child: any): AsyncIterable<any> {
|
||||||
child = await child;
|
child = await child;
|
||||||
if (child instanceof HTMLString) {
|
if (child instanceof HTMLString) {
|
||||||
return child;
|
yield child;
|
||||||
} else if (Array.isArray(child)) {
|
} else if (Array.isArray(child)) {
|
||||||
return markHTMLString((await Promise.all(child.map((value) => _render(value)))).join(''));
|
for(const value of child) {
|
||||||
|
yield markHTMLString(await _render(value));
|
||||||
|
}
|
||||||
} else if (typeof child === 'function') {
|
} else if (typeof child === 'function') {
|
||||||
// Special: If a child is a function, call it automatically.
|
// Special: If a child is a function, call it automatically.
|
||||||
// This lets you do {() => ...} without the extra boilerplate
|
// This lets you do {() => ...} without the extra boilerplate
|
||||||
// of wrapping it in a function and calling it.
|
// of wrapping it in a function and calling it.
|
||||||
return _render(child());
|
yield * _render(child());
|
||||||
} else if (typeof child === 'string') {
|
} else if (typeof child === 'string') {
|
||||||
return markHTMLString(escapeHTML(child));
|
yield markHTMLString(escapeHTML(child));
|
||||||
} else if (!child && child !== 0) {
|
} else if (!child && child !== 0) {
|
||||||
// do nothing, safe to ignore falsey values.
|
// do nothing, safe to ignore falsey values.
|
||||||
}
|
}
|
||||||
|
@ -62,9 +65,11 @@ async function _render(child: any): Promise<any> {
|
||||||
child instanceof AstroComponent ||
|
child instanceof AstroComponent ||
|
||||||
Object.prototype.toString.call(child) === '[object AstroComponent]'
|
Object.prototype.toString.call(child) === '[object AstroComponent]'
|
||||||
) {
|
) {
|
||||||
return markHTMLString(await renderAstroComponent(child));
|
yield * renderAstroComponent(child);
|
||||||
|
} else if(typeof child === 'object' && Symbol.asyncIterator in child) {
|
||||||
|
yield * child;
|
||||||
} else {
|
} else {
|
||||||
return child;
|
yield child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +88,7 @@ export class AstroComponent {
|
||||||
return 'AstroComponent';
|
return 'AstroComponent';
|
||||||
}
|
}
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
async *[Symbol.asyncIterator]() {
|
||||||
const { htmlParts, expressions } = this;
|
const { htmlParts, expressions } = this;
|
||||||
|
|
||||||
for (let i = 0; i < htmlParts.length; i++) {
|
for (let i = 0; i < htmlParts.length; i++) {
|
||||||
|
@ -91,7 +96,7 @@ export class AstroComponent {
|
||||||
const expression = expressions[i];
|
const expression = expressions[i];
|
||||||
|
|
||||||
yield markHTMLString(html);
|
yield markHTMLString(html);
|
||||||
yield _render(expression);
|
yield * _render(expression);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,9 +125,14 @@ export function createComponent(cb: AstroComponentFactory) {
|
||||||
return cb;
|
return cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderSlot(_result: any, slotted: string, fallback?: any) {
|
export async function renderSlot(_result: any, slotted: string, fallback?: any): Promise<string> {
|
||||||
if (slotted) {
|
if (slotted) {
|
||||||
return await _render(slotted);
|
let iterator = _render(slotted);
|
||||||
|
let content = '';
|
||||||
|
for await(const chunk of iterator) {
|
||||||
|
content += chunk;
|
||||||
|
}
|
||||||
|
return markHTMLString(content);
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
@ -307,13 +317,17 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
// This is a custom element without a renderer. Because of that, render it
|
// This is a custom element without a renderer. Because of that, render it
|
||||||
// as a string and the user is responsible for adding a script tag for the component definition.
|
// as a string and the user is responsible for adding a script tag for the component definition.
|
||||||
if (!html && typeof Component === 'string') {
|
if (!html && typeof Component === 'string') {
|
||||||
html = await renderAstroComponent(
|
const iterable = renderAstroComponent(
|
||||||
await render`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
|
await render`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
|
||||||
(children == null || children == '') && voidElementNames.test(Component)
|
(children == null || children == '') && voidElementNames.test(Component)
|
||||||
? `/>`
|
? `/>`
|
||||||
: `>${children == null ? '' : children}</${Component}>`
|
: `>${children == null ? '' : children}</${Component}>`
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
|
html = '';
|
||||||
|
for await(const chunk of iterable) {
|
||||||
|
html += chunk;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hydration) {
|
if (!hydration) {
|
||||||
|
@ -334,7 +348,6 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
{ renderer: renderer!, result, astroId, props },
|
{ renderer: renderer!, result, astroId, props },
|
||||||
metadata as Required<AstroComponentMetadata>
|
metadata as Required<AstroComponentMetadata>
|
||||||
);
|
);
|
||||||
result._metadata.needsHydrationStyles = true;
|
|
||||||
|
|
||||||
// Render a template if no fragment is provided.
|
// Render a template if no fragment is provided.
|
||||||
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
|
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
|
||||||
|
@ -563,16 +576,6 @@ Update your code to remove this warning.`);
|
||||||
return handler.call(mod, proxy, request);
|
return handler.call(mod, proxy, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replaceHeadInjection(result: SSRResult, html: string): Promise<string> {
|
|
||||||
let template = html;
|
|
||||||
// <!--astro:head--> injected by compiler
|
|
||||||
// Must be handled at the end of the rendering process
|
|
||||||
if (template.indexOf('<!--astro:head-->') > -1) {
|
|
||||||
template = template.replace('<!--astro:head-->', await renderHead(result));
|
|
||||||
}
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calls a component and renders it into a string of HTML
|
// Calls a component and renders it into a string of HTML
|
||||||
export async function renderToString(
|
export async function renderToString(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
|
@ -586,8 +589,11 @@ export async function renderToString(
|
||||||
throw response;
|
throw response;
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = await renderAstroComponent(Component);
|
let html = '';
|
||||||
return template;
|
for await(const chunk of renderAstroComponent(Component)) {
|
||||||
|
html += chunk;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderPage(
|
export async function renderPage(
|
||||||
|
@ -595,29 +601,40 @@ export async function renderPage(
|
||||||
componentFactory: AstroComponentFactory,
|
componentFactory: AstroComponentFactory,
|
||||||
props: any,
|
props: any,
|
||||||
children: any
|
children: any
|
||||||
): Promise<{ type: 'html'; html: string } | { type: 'response'; response: Response }> {
|
): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const response = await componentFactory(result, props, children);
|
const factoryReturnValue = await componentFactory(result, props, children);
|
||||||
|
|
||||||
if (isAstroComponent(response)) {
|
if (isAstroComponent(factoryReturnValue)) {
|
||||||
let template = await renderAstroComponent(response);
|
let iterable = renderAstroComponent(factoryReturnValue);
|
||||||
const html = await replaceHeadInjection(result, template);
|
let stream = new ReadableStream({
|
||||||
return {
|
start(controller) {
|
||||||
type: 'html',
|
async function read() {
|
||||||
html,
|
let i = 0;
|
||||||
};
|
for await(const chunk of iterable) {
|
||||||
|
let html = chunk.toString();
|
||||||
|
if(i === 0) {
|
||||||
|
if (!/<!doctype html/i.test(html)) {
|
||||||
|
controller.enqueue('<!DOCTYPE html>\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.enqueue(html);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
read();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let init = result.response;
|
||||||
|
let response = createResponse(stream, init);
|
||||||
|
return response;
|
||||||
} else {
|
} else {
|
||||||
return {
|
return factoryReturnValue;
|
||||||
type: 'response',
|
|
||||||
response,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Response) {
|
if (err instanceof Response) {
|
||||||
return {
|
return err;
|
||||||
type: 'response',
|
|
||||||
response: err,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -633,47 +650,44 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
|
const alreadyHeadRenderedResults = new WeakSet<SSRResult>();
|
||||||
// styles and scripts into the head.
|
|
||||||
export async function renderHead(result: SSRResult): Promise<string> {
|
export async function renderHead(result: SSRResult): Promise<string> {
|
||||||
|
alreadyHeadRenderedResults.add(result);
|
||||||
const styles = Array.from(result.styles)
|
const styles = Array.from(result.styles)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((style) => renderElement('style', style));
|
.map((style) => renderElement('style', style));
|
||||||
let needsHydrationStyles = result._metadata.needsHydrationStyles;
|
|
||||||
const scripts = Array.from(result.scripts)
|
const scripts = Array.from(result.scripts)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((script, i) => {
|
.map((script, i) => {
|
||||||
if ('data-astro-component-hydration' in script.props) {
|
|
||||||
needsHydrationStyles = true;
|
|
||||||
}
|
|
||||||
return renderElement('script', script);
|
return renderElement('script', script);
|
||||||
});
|
});
|
||||||
if (needsHydrationStyles) {
|
|
||||||
styles.push(
|
|
||||||
renderElement('style', {
|
|
||||||
props: {},
|
|
||||||
children: 'astro-island, astro-fragment { display: contents; }',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const links = Array.from(result.links)
|
const links = Array.from(result.links)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((link) => renderElement('link', link, false));
|
.map((link) => renderElement('link', link, false));
|
||||||
return markHTMLString(
|
return markHTMLString(
|
||||||
links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->'
|
links.join('\n') + styles.join('\n') + scripts.join('\n')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
// This function is called by Astro components that do not contain a <head> component
|
||||||
let template = [];
|
// This accomodates the fact that using a <head> is optional in Astro, so this
|
||||||
|
// is called before a component's first non-head HTML element. If the head was
|
||||||
|
// already injected it is a noop.
|
||||||
|
export function maybeRenderHead(result: SSRResult): string | Promise<string> {
|
||||||
|
if(alreadyHeadRenderedResults.has(result)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return renderHead(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function * renderAstroComponent(component: InstanceType<typeof AstroComponent>): AsyncIterable<string> {
|
||||||
for await (const value of component) {
|
for await (const value of component) {
|
||||||
if (value || value === 0) {
|
if (value || value === 0) {
|
||||||
template.push(value);
|
for await(const chunk of _render(value)) {
|
||||||
|
yield markHTMLString(chunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return markHTMLString(await _render(template));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function componentIsHTMLElement(Component: unknown) {
|
function componentIsHTMLElement(Component: unknown) {
|
||||||
|
|
69
packages/astro/src/runtime/server/response.ts
Normal file
69
packages/astro/src/runtime/server/response.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
|
||||||
|
const isNodeJS = typeof process === 'object' && Object.prototype.toString.call(process);
|
||||||
|
|
||||||
|
let RuntimeResponse: typeof Response | undefined;
|
||||||
|
|
||||||
|
function createResponseClass() {
|
||||||
|
RuntimeResponse = class extends Response {
|
||||||
|
#isStream: boolean;
|
||||||
|
#body: any;
|
||||||
|
constructor(body?: BodyInit | null, init?: ResponseInit) {
|
||||||
|
let isStream = body instanceof ReadableStream;
|
||||||
|
super(isStream ? null : body, init);
|
||||||
|
this.#isStream = isStream;
|
||||||
|
this.#body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
get body() {
|
||||||
|
return this.#body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async text(): Promise<string> {
|
||||||
|
if(this.#isStream && isNodeJS) {
|
||||||
|
let body = this.#body as ReadableStream<string>;
|
||||||
|
let reader = body.getReader();
|
||||||
|
let text = '';
|
||||||
|
while(true) {
|
||||||
|
let r = await reader.read();
|
||||||
|
if(r.value) {
|
||||||
|
text += r.value.toString();
|
||||||
|
}
|
||||||
|
if(r.done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return super.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||||
|
if(this.#isStream && isNodeJS) {
|
||||||
|
let body = this.#body as ReadableStream<string>;
|
||||||
|
let reader = body.getReader();
|
||||||
|
let encoder = new TextEncoder();
|
||||||
|
let chunks: number[] = [];
|
||||||
|
while(true) {
|
||||||
|
let r = await reader.read();
|
||||||
|
if(r.value) {
|
||||||
|
chunks.push(...encoder.encode(r.value));
|
||||||
|
}
|
||||||
|
if(r.done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Uint8Array.from(chunks);
|
||||||
|
}
|
||||||
|
return super.arrayBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RuntimeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createResponse(body?: BodyInit | null, init?: ResponseInit) {
|
||||||
|
if(typeof RuntimeResponse === 'undefined') {
|
||||||
|
return new (createResponseClass())(body, init);
|
||||||
|
}
|
||||||
|
return new RuntimeResponse(body, init);
|
||||||
|
}
|
|
@ -59,7 +59,7 @@ export function getPrescripts(type: PrescriptType, directive: string): string {
|
||||||
// deps to be loaded immediately.
|
// deps to be loaded immediately.
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'both':
|
case 'both':
|
||||||
return `<script>${getDirectiveScriptText(directive) + islandScript}</script>`;
|
return `<style>astro-island,astro-fragment{display:contents}</style><script>${getDirectiveScriptText(directive) + islandScript}</script>`;
|
||||||
case 'directive':
|
case 'directive':
|
||||||
return `<script>${getDirectiveScriptText(directive)}</script>`;
|
return `<script>${getDirectiveScriptText(directive)}</script>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import { Readable } from 'stream';
|
|
||||||
import stripAnsi from 'strip-ansi';
|
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
import type { AstroConfig, ManifestData } from '../@types/astro';
|
import type { AstroConfig, ManifestData } from '../@types/astro';
|
||||||
|
import type { SSROptions } from '../core/render/dev/index';
|
||||||
|
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||||
import { fixViteErrorMessage } from '../core/errors.js';
|
import { fixViteErrorMessage } from '../core/errors.js';
|
||||||
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
||||||
import * as msg from '../core/messages.js';
|
import * as msg from '../core/messages.js';
|
||||||
import { appendForwardSlash } from '../core/path.js';
|
import { appendForwardSlash } from '../core/path.js';
|
||||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
|
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
|
||||||
import type { RenderResponse, SSROptions } from '../core/render/dev/index';
|
|
||||||
import { preload, ssr } from '../core/render/dev/index.js';
|
import { preload, ssr } from '../core/render/dev/index.js';
|
||||||
import { RouteCache } from '../core/render/route-cache.js';
|
import { RouteCache } from '../core/render/route-cache.js';
|
||||||
import { createRequest } from '../core/request.js';
|
import { createRequest } from '../core/request.js';
|
||||||
|
@ -75,7 +76,12 @@ async function writeWebResponse(res: http.ServerResponse, webResponse: Response)
|
||||||
|
|
||||||
res.writeHead(status, _headers);
|
res.writeHead(status, _headers);
|
||||||
if (body) {
|
if (body) {
|
||||||
if (body instanceof Readable) {
|
if(Symbol.for('astro.responseBody') in webResponse) {
|
||||||
|
let stream = (webResponse as any)[Symbol.for('astro.responseBody')];
|
||||||
|
for await(const chunk of stream) {
|
||||||
|
res.write(chunk.toString());
|
||||||
|
}
|
||||||
|
} else if (body instanceof Readable) {
|
||||||
body.pipe(res);
|
body.pipe(res);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,23 +99,10 @@ async function writeWebResponse(res: http.ServerResponse, webResponse: Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeSSRResult(
|
async function writeSSRResult(
|
||||||
result: RenderResponse,
|
webResponse: Response,
|
||||||
res: http.ServerResponse,
|
res: http.ServerResponse
|
||||||
statusCode: 200 | 404
|
|
||||||
) {
|
) {
|
||||||
if (result.type === 'response') {
|
return writeWebResponse(res, webResponse);
|
||||||
const { response } = result;
|
|
||||||
await writeWebResponse(res, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { html, response: init } = result;
|
|
||||||
const headers = init.headers as Headers;
|
|
||||||
|
|
||||||
headers.set('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
headers.set('Content-Length', Buffer.byteLength(html, 'utf-8').toString());
|
|
||||||
|
|
||||||
return writeWebResponse(res, new Response(html, init));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handle404Response(
|
async function handle404Response(
|
||||||
|
@ -296,7 +289,7 @@ async function handleRequest(
|
||||||
routeCache,
|
routeCache,
|
||||||
viteServer,
|
viteServer,
|
||||||
});
|
});
|
||||||
return await writeSSRResult(result, res, statusCode);
|
return await writeSSRResult(result, res);
|
||||||
} else {
|
} else {
|
||||||
return handle404Response(origin, config, req, res);
|
return handle404Response(origin, config, req, res);
|
||||||
}
|
}
|
||||||
|
@ -326,7 +319,7 @@ async function handleRequest(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await ssr(preloadedComponent, options);
|
const result = await ssr(preloadedComponent, options);
|
||||||
return await writeSSRResult(result, res, statusCode);
|
return await writeSSRResult(result, res);
|
||||||
}
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
const err = fixViteErrorMessage(createSafeError(_err), viteServer);
|
const err = fixViteErrorMessage(createSafeError(_err), viteServer);
|
||||||
|
|
|
@ -65,7 +65,7 @@ describe('CSS', function () {
|
||||||
|
|
||||||
it('Using hydrated components adds astro-island styles', async () => {
|
it('Using hydrated components adds astro-island styles', async () => {
|
||||||
const inline = $('style').html();
|
const inline = $('style').html();
|
||||||
expect(inline).to.include('display: contents');
|
expect(inline).to.include('display:contents');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('<style lang="sass">', async () => {
|
it('<style lang="sass">', async () => {
|
||||||
|
|
|
@ -40,4 +40,10 @@ describe('Partial HTML', async () => {
|
||||||
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
|
||||||
expect(allInjectedStyles).to.match(/h1{color:red;}/);
|
expect(allInjectedStyles).to.match(/h1{color:red;}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('pages with a head, injection happens inside', async () => {
|
||||||
|
const html = await fixture.fetch('/with-head').then((res) => res.text());
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('style')).to.have.lengthOf(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
9
packages/astro/test/fixtures/astro-partial-html/src/pages/with-head.astro
vendored
Normal file
9
packages/astro/test/fixtures/astro-partial-html/src/pages/with-head.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>testing</title>
|
||||||
|
<style>body { color: blue; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>testing</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
0
packages/astro/test/fixtures/streaming/astro.config.mjs
vendored
Normal file
0
packages/astro/test/fixtures/streaming/astro.config.mjs
vendored
Normal file
7
packages/astro/test/fixtures/streaming/src/components/Header.astro
vendored
Normal file
7
packages/astro/test/fixtures/streaming/src/components/Header.astro
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
import { wait } from '../wait';
|
||||||
|
await wait(10);
|
||||||
|
---
|
||||||
|
<header>
|
||||||
|
<h1>My Site</h1>
|
||||||
|
</header>
|
28
packages/astro/test/fixtures/streaming/src/pages/index.astro
vendored
Normal file
28
packages/astro/test/fixtures/streaming/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
import Header from '../components/Header.astro';
|
||||||
|
import { wait } from '../wait';
|
||||||
|
|
||||||
|
async function * list(callback: (num: number) => any) {
|
||||||
|
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
for(const num of nums) {
|
||||||
|
await wait(20);
|
||||||
|
yield callback(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head><title>Testing</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Title</h1>
|
||||||
|
<section>
|
||||||
|
<Header />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{list(num => (
|
||||||
|
<li>Number {num}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
packages/astro/test/fixtures/streaming/src/wait.ts
vendored
Normal file
4
packages/astro/test/fixtures/streaming/src/wait.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
export function wait(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
73
packages/astro/test/streaming.test.js
Normal file
73
packages/astro/test/streaming.test.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { isWindows, loadFixture } from './test-utils.js';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import testAdapter from './test-adapter.js';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
|
|
||||||
|
describe('Streaming', () => {
|
||||||
|
if (isWindows) return;
|
||||||
|
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/streaming/',
|
||||||
|
adapter: testAdapter(),
|
||||||
|
experimental: {
|
||||||
|
ssr: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Development', () => {
|
||||||
|
/** @type {import('./test-utils').DevServer} */
|
||||||
|
let devServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
devServer = await fixture.startDevServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Body is chunked', async () => {
|
||||||
|
let res = await fixture.fetch('/');
|
||||||
|
let chunks = [];
|
||||||
|
for await(const bytes of res.body) {
|
||||||
|
let chunk = bytes.toString('utf-8');
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
expect(chunks.length).to.be.greaterThan(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Production', () => {
|
||||||
|
before(async () => {
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can get the full html body', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
const html = await response.text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect($('header h1')).to.have.a.lengthOf(1);
|
||||||
|
expect($('ul li')).to.have.a.lengthOf(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Body is chunked', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
let chunks = [];
|
||||||
|
for await(const bytes of response.body) {
|
||||||
|
let chunk = bytes.toString('utf-8');
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
expect(chunks.length).to.be.greaterThan(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
2
packages/webapi/mod.d.ts
vendored
2
packages/webapi/mod.d.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
export { pathToPosix } from './lib/utils';
|
export { pathToPosix } from './lib/utils';
|
||||||
export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter } from './mod.js';
|
export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, } from './mod.js';
|
||||||
export declare const polyfill: {
|
export declare const polyfill: {
|
||||||
(target: any, options?: PolyfillOptions): any;
|
(target: any, options?: PolyfillOptions): any;
|
||||||
internals(target: any, name: string): any;
|
internals(target: any, name: string): any;
|
||||||
|
|
3513
pnpm-lock.yaml
generated
3513
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue