UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

236 lines (235 loc) • 7.13 kB
import { AstroError, AstroErrorData } from "../../../../core/errors/index.js"; import { isPromise } from "../../util.js"; import { chunkToByteArray, chunkToString, encoder } from "../common.js"; import { promiseWithResolvers } from "../util.js"; import { isHeadAndContent } from "./head-and-content.js"; import { isRenderTemplateResult } from "./render-template.js"; const DOCTYPE_EXP = /<!doctype html/i; async function renderToString(result, componentFactory, props, children, isPage = false, route) { const templateResult = await callComponentAsTemplateResultOrResponse( result, componentFactory, props, children, route ); if (templateResult instanceof Response) return templateResult; let str = ""; let renderedFirstPageChunk = false; if (isPage) { await bufferHeadContent(result); } const destination = { write(chunk) { if (isPage && !renderedFirstPageChunk) { renderedFirstPageChunk = true; if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) { const doctype = result.compressHTML ? "<!DOCTYPE html>" : "<!DOCTYPE html>\n"; str += doctype; } } if (chunk instanceof Response) return; str += chunkToString(result, chunk); } }; await templateResult.render(destination); return str; } async function renderToReadableStream(result, componentFactory, props, children, isPage = false, route) { const templateResult = await callComponentAsTemplateResultOrResponse( result, componentFactory, props, children, route ); if (templateResult instanceof Response) return templateResult; let renderedFirstPageChunk = false; if (isPage) { await bufferHeadContent(result); } return new ReadableStream({ start(controller) { const destination = { write(chunk) { if (isPage && !renderedFirstPageChunk) { renderedFirstPageChunk = true; if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) { const doctype = result.compressHTML ? "<!DOCTYPE html>" : "<!DOCTYPE html>\n"; controller.enqueue(encoder.encode(doctype)); } } if (chunk instanceof Response) { throw new AstroError({ ...AstroErrorData.ResponseSentError }); } const bytes = chunkToByteArray(result, chunk); controller.enqueue(bytes); } }; (async () => { try { await templateResult.render(destination); controller.close(); } catch (e) { if (AstroError.is(e) && !e.loc) { e.setLocation({ file: route?.component }); } setTimeout(() => controller.error(e), 0); } })(); }, cancel() { result.cancelled = true; } }); } async function callComponentAsTemplateResultOrResponse(result, componentFactory, props, children, route) { const factoryResult = await componentFactory(result, props, children); if (factoryResult instanceof Response) { return factoryResult; } else if (isHeadAndContent(factoryResult)) { if (!isRenderTemplateResult(factoryResult.content)) { throw new AstroError({ ...AstroErrorData.OnlyResponseCanBeReturned, message: AstroErrorData.OnlyResponseCanBeReturned.message( route?.route, typeof factoryResult ), location: { file: route?.component } }); } return factoryResult.content; } else if (!isRenderTemplateResult(factoryResult)) { throw new AstroError({ ...AstroErrorData.OnlyResponseCanBeReturned, message: AstroErrorData.OnlyResponseCanBeReturned.message(route?.route, typeof factoryResult), location: { file: route?.component } }); } return factoryResult; } async function bufferHeadContent(result) { const iterator = result._metadata.propagators.values(); while (true) { const { value, done } = iterator.next(); if (done) { break; } const returnValue = await value.init(result); if (isHeadAndContent(returnValue)) { result._metadata.extraHead.push(returnValue.head); } } } async function renderToAsyncIterable(result, componentFactory, props, children, isPage = false, route) { const templateResult = await callComponentAsTemplateResultOrResponse( result, componentFactory, props, children, route ); if (templateResult instanceof Response) return templateResult; let renderedFirstPageChunk = false; if (isPage) { await bufferHeadContent(result); } let error = null; let next = null; const buffer = []; let renderingComplete = false; const iterator = { async next() { if (result.cancelled) return { done: true, value: void 0 }; if (next !== null) { await next.promise; } else if (!renderingComplete && !buffer.length) { next = promiseWithResolvers(); await next.promise; } if (!renderingComplete) { next = promiseWithResolvers(); } if (error) { throw error; } let length = 0; for (let i = 0, len = buffer.length; i < len; i++) { length += buffer[i].length; } let mergedArray = new Uint8Array(length); let offset = 0; for (let i = 0, len = buffer.length; i < len; i++) { const item = buffer[i]; mergedArray.set(item, offset); offset += item.length; } buffer.length = 0; const returnValue = { // The iterator is done when rendering has finished // and there are no more chunks to return. done: length === 0 && renderingComplete, value: mergedArray }; return returnValue; }, async return() { result.cancelled = true; return { done: true, value: void 0 }; } }; const destination = { write(chunk) { if (isPage && !renderedFirstPageChunk) { renderedFirstPageChunk = true; if (!result.partial && !DOCTYPE_EXP.test(String(chunk))) { const doctype = result.compressHTML ? "<!DOCTYPE html>" : "<!DOCTYPE html>\n"; buffer.push(encoder.encode(doctype)); } } if (chunk instanceof Response) { throw new AstroError(AstroErrorData.ResponseSentError); } const bytes = chunkToByteArray(result, chunk); if (bytes.length > 0) { buffer.push(bytes); next?.resolve(); } else if (buffer.length > 0) { next?.resolve(); } } }; const renderResult = toPromise(() => templateResult.render(destination)); renderResult.catch((err) => { error = err; }).finally(() => { renderingComplete = true; next?.resolve(); }); return { [Symbol.asyncIterator]() { return iterator; } }; } function toPromise(fn) { try { const result = fn(); return isPromise(result) ? result : Promise.resolve(result); } catch (err) { return Promise.reject(err); } } export { renderToAsyncIterable, renderToReadableStream, renderToString };