summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/shy-dogs-return.md5
-rw-r--r--packages/astro/src/runtime/server/astro-global.ts54
-rw-r--r--packages/astro/src/runtime/server/endpoint.ts74
-rw-r--r--packages/astro/src/runtime/server/hydration.ts10
-rw-r--r--packages/astro/src/runtime/server/index.ts976
-rw-r--r--packages/astro/src/runtime/server/render/any.ts51
-rw-r--r--packages/astro/src/runtime/server/render/astro.ts124
-rw-r--r--packages/astro/src/runtime/server/render/common.ts43
-rw-r--r--packages/astro/src/runtime/server/render/component.ts350
-rw-r--r--packages/astro/src/runtime/server/render/dom.ts42
-rw-r--r--packages/astro/src/runtime/server/render/head.ts43
-rw-r--r--packages/astro/src/runtime/server/render/index.ts17
-rw-r--r--packages/astro/src/runtime/server/render/page.ts99
-rw-r--r--packages/astro/src/runtime/server/render/types.ts8
-rw-r--r--packages/astro/src/runtime/server/render/util.ts128
15 files changed, 1071 insertions, 953 deletions
diff --git a/.changeset/shy-dogs-return.md b/.changeset/shy-dogs-return.md
new file mode 100644
index 000000000..3d22d8275
--- /dev/null
+++ b/.changeset/shy-dogs-return.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Adds warning in dev when using client: directive on Astro component
diff --git a/packages/astro/src/runtime/server/astro-global.ts b/packages/astro/src/runtime/server/astro-global.ts
new file mode 100644
index 000000000..5ffca377a
--- /dev/null
+++ b/packages/astro/src/runtime/server/astro-global.ts
@@ -0,0 +1,54 @@
+import type { AstroGlobalPartial } from '../../@types/astro';
+
+// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
+const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
+
+/** Create the Astro.fetchContent() runtime function. */
+function createDeprecatedFetchContentFn() {
+ return () => {
+ throw new Error('Deprecated: Astro.fetchContent() has been replaced with Astro.glob().');
+ };
+}
+
+/** Create the Astro.glob() runtime function. */
+function createAstroGlobFn() {
+ const globHandler = (importMetaGlobResult: Record<string, any>, globValue: () => any) => {
+ let allEntries = [...Object.values(importMetaGlobResult)];
+ if (allEntries.length === 0) {
+ throw new Error(`Astro.glob(${JSON.stringify(globValue())}) - no matches found.`);
+ }
+ // Map over the `import()` promises, calling to load them.
+ return Promise.all(allEntries.map((fn) => fn()));
+ };
+ // Cast the return type because the argument that the user sees (string) is different from the argument
+ // that the runtime sees post-compiler (Record<string, Module>).
+ return globHandler as unknown as AstroGlobalPartial['glob'];
+}
+
+// This is used to create the top-level Astro global; the one that you can use
+// Inside of getStaticPaths.
+export function createAstro(
+ filePathname: string,
+ _site: string | undefined,
+ projectRootStr: string
+): AstroGlobalPartial {
+ const site = _site ? new URL(_site) : undefined;
+ const referenceURL = new URL(filePathname, `http://localhost`);
+ const projectRoot = new URL(projectRootStr);
+ return {
+ site,
+ generator: `Astro v${ASTRO_VERSION}`,
+ fetchContent: createDeprecatedFetchContentFn(),
+ glob: createAstroGlobFn(),
+ // INVESTIGATE is there a use-case for multi args?
+ resolve(...segments: string[]) {
+ let resolved = segments.reduce((u, segment) => new URL(segment, u), referenceURL).pathname;
+ // When inside of project root, remove the leading path so you are
+ // left with only `/src/images/tower.png`
+ if (resolved.startsWith(projectRoot.pathname)) {
+ resolved = '/' + resolved.slice(projectRoot.pathname.length);
+ }
+ return resolved;
+ },
+ };
+}
diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts
new file mode 100644
index 000000000..95bea8b64
--- /dev/null
+++ b/packages/astro/src/runtime/server/endpoint.ts
@@ -0,0 +1,74 @@
+
+import type {
+ APIContext,
+ EndpointHandler,
+ Params
+} from '../../@types/astro';
+
+function getHandlerFromModule(mod: EndpointHandler, method: string) {
+ // If there was an exact match on `method`, return that function.
+ if (mod[method]) {
+ return mod[method];
+ }
+ // Handle `del` instead of `delete`, since `delete` is a reserved word in JS.
+ if (method === 'delete' && mod['del']) {
+ return mod['del'];
+ }
+ // If a single `all` handler was used, return that function.
+ if (mod['all']) {
+ return mod['all'];
+ }
+ // Otherwise, no handler found.
+ return undefined;
+}
+
+/** Renders an endpoint request to completion, returning the body. */
+export async function renderEndpoint(mod: EndpointHandler, request: Request, params: Params) {
+ const chosenMethod = request.method?.toLowerCase();
+ const handler = getHandlerFromModule(mod, chosenMethod);
+ if (!handler || typeof handler !== 'function') {
+ throw new Error(
+ `Endpoint handler not found! Expected an exported function for "${chosenMethod}"`
+ );
+ }
+
+ if (handler.length > 1) {
+ // eslint-disable-next-line no-console
+ console.warn(`
+API routes with 2 arguments have been deprecated. Instead they take a single argument in the form of:
+
+export function get({ params, request }) {
+ //...
+}
+
+Update your code to remove this warning.`);
+ }
+
+ const context = {
+ request,
+ params,
+ };
+
+ const proxy = new Proxy(context, {
+ get(target, prop) {
+ if (prop in target) {
+ return Reflect.get(target, prop);
+ } else if (prop in params) {
+ // eslint-disable-next-line no-console
+ console.warn(`
+API routes no longer pass params as the first argument. Instead an object containing a params property is provided in the form of:
+
+export function get({ params }) {
+ // ...
+}
+
+Update your code to remove this warning.`);
+ return Reflect.get(params, prop);
+ } else {
+ return undefined;
+ }
+ },
+ }) as APIContext & Params;
+
+ return handler.call(mod, proxy, request);
+}
diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts
index e14924dfe..c4cfc6ec6 100644
--- a/packages/astro/src/runtime/server/hydration.ts
+++ b/packages/astro/src/runtime/server/hydration.ts
@@ -8,7 +8,9 @@ import { escapeHTML } from './escape.js';
import { serializeProps } from './serialize.js';
import { serializeListValue } from './util.js';
-const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only'];
+const HydrationDirectivesRaw = ['load', 'idle', 'media', 'visible', 'only'];
+const HydrationDirectives = new Set(HydrationDirectivesRaw);
+export const HydrationDirectiveProps = new Set(HydrationDirectivesRaw.map(n => `client:${n}`));
export interface HydrationMetadata {
directive: string;
@@ -68,11 +70,9 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
extracted.hydration.value = value;
// throw an error if an invalid hydration directive was provided
- if (HydrationDirectives.indexOf(extracted.hydration.directive) < 0) {
+ if (!HydrationDirectives.has(extracted.hydration.directive)) {
throw new Error(
- `Error: invalid hydration directive "${key}". Supported hydration methods: ${HydrationDirectives.map(
- (d) => `"client:${d}"`
- ).join(', ')}`
+ `Error: invalid hydration directive "${key}". Supported hydration methods: ${Array.from(HydrationDirectiveProps).join(', ')}`
);
}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index df3de955a..c60aaf59b 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -1,27 +1,3 @@
-import type {
- APIContext,
- AstroComponentMetadata,
- AstroGlobalPartial,
- EndpointHandler,
- Params,
- SSRElement,
- SSRLoadedRenderer,
- SSRResult,
-} from '../../@types/astro';
-
-import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
-import { extractDirectives, generateHydrateScript, HydrationMetadata } from './hydration.js';
-import { createResponse } from './response.js';
-import {
- determineIfNeedsHydrationScript,
- determinesIfNeedsDirectiveScript,
- getPrescripts,
- PrescriptType,
-} from './scripts.js';
-import { serializeProps } from './serialize.js';
-import { shorthash } from './shorthash.js';
-import { serializeListValue } from './util.js';
-
export {
escapeHTML,
HTMLString,
@@ -30,99 +6,36 @@ export {
} from './escape.js';
export type { Metadata } from './metadata';
export { createMetadata } from './metadata.js';
+export type { AstroComponentFactory, RenderInstruction } from './render/index.js';
+import type { AstroComponentFactory } from './render/index.js';
-export const voidElementNames =
- /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
-const htmlBooleanAttributes =
- /^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i;
-const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
-// Note: SVG is case-sensitive!
-const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
-// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
-const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
-
-// INVESTIGATE:
-// 2. Less anys when possible and make it well known when they are needed.
-
-// Used to render slots and expressions
-// 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.
-// Or maybe type UserValue = any; ?
-async function* _render(child: any): AsyncIterable<any> {
- child = await child;
- if (child instanceof HTMLString) {
- yield child;
- } else if (Array.isArray(child)) {
- for (const value of child) {
- yield markHTMLString(await _render(value));
- }
- } else if (typeof child === 'function') {
- // Special: If a child is a function, call it automatically.
- // This lets you do {() => ...} without the extra boilerplate
- // of wrapping it in a function and calling it.
- yield* _render(child());
- } else if (typeof child === 'string') {
- yield markHTMLString(escapeHTML(child));
- } else if (!child && child !== 0) {
- // do nothing, safe to ignore falsey values.
- }
- // Add a comment explaining why each of these are needed.
- // Maybe create clearly named function for what this is doing.
- else if (
- child instanceof AstroComponent ||
- Object.prototype.toString.call(child) === '[object AstroComponent]'
- ) {
- yield* renderAstroComponent(child);
- } else if (typeof child === 'object' && Symbol.asyncIterator in child) {
- yield* child;
- } else {
- yield child;
- }
-}
-
-// The return value when rendering a component.
-// This is the result of calling render(), should this be named to RenderResult or...?
-export class AstroComponent {
- private htmlParts: TemplateStringsArray;
- private expressions: any[];
-
- constructor(htmlParts: TemplateStringsArray, expressions: any[]) {
- this.htmlParts = htmlParts;
- this.expressions = expressions;
- }
-
- get [Symbol.toStringTag]() {
- return 'AstroComponent';
- }
-
- async *[Symbol.asyncIterator]() {
- const { htmlParts, expressions } = this;
-
- for (let i = 0; i < htmlParts.length; i++) {
- const html = htmlParts[i];
- const expression = expressions[i];
+import { Renderer } from './render/index.js';
+import { markHTMLString } from './escape.js';
- yield markHTMLString(html);
- yield* _render(expression);
- }
- }
-}
-
-function isAstroComponent(obj: any): obj is AstroComponent {
- return (
- typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]'
- );
-}
-
-export async function render(htmlParts: TemplateStringsArray, ...expressions: any[]) {
- return new AstroComponent(htmlParts, expressions);
-}
+export { createAstro } from './astro-global.js';
+export {
+ addAttribute,
+ voidElementNames,
+ defineScriptVars,
+ maybeRenderHead,
+ renderAstroComponent,
+ renderComponent,
+ renderHead,
+ renderHTMLElement,
+ renderPage,
+ renderSlot,
+ renderTemplate,
+ renderTemplate as render,
+ renderToString,
+ stringifyChunk,
+ Fragment,
+ Renderer as Renderer
+} from './render/index.js';
+export { renderEndpoint } from './endpoint.js';
+
+import { addAttribute } from './render/index.js';
-// The callback passed to to $$createComponent
-export interface AstroComponentFactory {
- (result: any, props: any, slots: any): ReturnType<typeof render> | Response;
- isAstroComponentFactory?: boolean;
-}
+export const ClientOnlyPlaceholder = 'astro-client-only';
// Used in creating the component. aka the main export.
export function createComponent(cb: AstroComponentFactory) {
@@ -132,22 +45,6 @@ export function createComponent(cb: AstroComponentFactory) {
return cb;
}
-export async function renderSlot(result: any, slotted: string, fallback?: any): Promise<string> {
- if (slotted) {
- let iterator = _render(slotted);
- let content = '';
- for await (const chunk of iterator) {
- if ((chunk as any).type === 'directive') {
- content += stringifyChunk(result, chunk);
- } else {
- content += chunk;
- }
- }
- return markHTMLString(content);
- }
- return fallback;
-}
-
export function mergeSlots(...slotted: unknown[]) {
const slots: Record<string, () => any> = {};
for (const slot of slotted) {
@@ -161,34 +58,6 @@ export function mergeSlots(...slotted: unknown[]) {
return slots;
}
-export const Fragment = Symbol.for('astro:fragment');
-export const Renderer = Symbol.for('astro:renderer');
-export const ClientOnlyPlaceholder = 'astro-client-only';
-
-function guessRenderers(componentUrl?: string): string[] {
- const extname = componentUrl?.split('.').pop();
- switch (extname) {
- case 'svelte':
- return ['@astrojs/svelte'];
- case 'vue':
- return ['@astrojs/vue'];
- case 'jsx':
- case 'tsx':
- return ['@astrojs/react', '@astrojs/preact'];
- default:
- return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue', '@astrojs/svelte'];
- }
-}
-
-function formatList(values: string[]): string {
- if (values.length === 1) {
- return values[0];
- }
- return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
-}
-
-const rendererAliases = new Map([['solid', 'solid-js']]);
-
/** @internal Assosciate JSX components with a specific renderer (see /src/vite-plugin-jsx/tag.ts) */
export function __astro_tag_component__(Component: unknown, rendererName: string) {
if (!Component) return;
@@ -200,428 +69,10 @@ export function __astro_tag_component__(Component: unknown, rendererName: string
});
}
-export async function renderComponent(
- result: SSRResult,
- displayName: string,
- Component: unknown,
- _props: Record<string | number, any>,
- slots: any = {}
-): Promise<string | AsyncIterable<string | RenderInstruction>> {
- Component = await Component;
- if (Component === Fragment) {
- const children = await renderSlot(result, slots?.default);
- if (children == null) {
- return children;
- }
- return markHTMLString(children);
- }
-
- if (Component && typeof Component === 'object' && (Component as any)['astro:html']) {
- const children: Record<string, string> = {};
- if (slots) {
- await Promise.all(
- Object.entries(slots).map(([key, value]) =>
- renderSlot(result, value as string).then((output) => {
- children[key] = output;
- })
- )
- );
- }
- const html = (Component as any).render({ slots: children });
- return markHTMLString(html);
- }
-
- if (Component && (Component as any).isAstroComponentFactory) {
- async function* renderAstroComponentInline(): AsyncGenerator<
- string | RenderInstruction,
- void,
- undefined
- > {
- let iterable = await renderToIterable(result, Component as any, _props, slots);
- yield* iterable;
- }
-
- return renderAstroComponentInline();
- }
-
- if (!Component && !_props['client:only']) {
- throw new Error(
- `Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
- );
- }
-
- const { renderers } = result._metadata;
- const metadata: AstroComponentMetadata = { displayName };
-
- const { hydration, isPage, props } = extractDirectives(_props);
- let html = '';
-
- if (hydration) {
- metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
- metadata.hydrateArgs = hydration.value;
- metadata.componentExport = hydration.componentExport;
- metadata.componentUrl = hydration.componentUrl;
- }
- const probableRendererNames = guessRenderers(metadata.componentUrl);
-
- if (
- Array.isArray(renderers) &&
- renderers.length === 0 &&
- typeof Component !== 'string' &&
- !componentIsHTMLElement(Component)
- ) {
- const message = `Unable to render ${metadata.displayName}!
-
-There are no \`integrations\` set in your \`astro.config.mjs\` file.
-Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`;
- throw new Error(message);
- }
-
- const children: Record<string, string> = {};
- if (slots) {
- await Promise.all(
- Object.entries(slots).map(([key, value]) =>
- renderSlot(result, value as string).then((output) => {
- children[key] = output;
- })
- )
- );
- }
-
- // Call the renderers `check` hook to see if any claim this component.
- let renderer: SSRLoadedRenderer | undefined;
- if (metadata.hydrate !== 'only') {
- // If this component ran through `__astro_tag_component__`, we already know
- // which renderer to match to and can skip the usual `check` calls.
- // This will help us throw most relevant error message for modules with runtime errors
- if (Component && (Component as any)[Renderer]) {
- const rendererName = (Component as any)[Renderer];
- renderer = renderers.find(({ name }) => name === rendererName);
- }
-
- if (!renderer) {
- let error;
- for (const r of renderers) {
- try {
- if (await r.ssr.check.call({ result }, Component, props, children)) {
- renderer = r;
- break;
- }
- } catch (e) {
- error ??= e;
- }
- }
-
- // If no renderer is found and there is an error, throw that error because
- // it is likely a problem with the component code.
- if (!renderer && error) {
- throw error;
- }
- }
-
- if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
- const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
-
- return output;
- }
- } else {
- // Attempt: use explicitly passed renderer name
- if (metadata.hydrateArgs) {
- const passedName = metadata.hydrateArgs;
- const rendererName = rendererAliases.has(passedName)
- ? rendererAliases.get(passedName)
- : passedName;
- renderer = renderers.find(
- ({ name }) => name === `@astrojs/${rendererName}` || name === rendererName
- );
- }
- // Attempt: user only has a single renderer, default to that
- if (!renderer && renderers.length === 1) {
- renderer = renderers[0];
- }
- // Attempt: can we guess the renderer from the export extension?
- if (!renderer) {
- const extname = metadata.componentUrl?.split('.').pop();
- renderer = renderers.filter(
- ({ name }) => name === `@astrojs/${extname}` || name === extname
- )[0];
- }
- }
-
- // If no one claimed the renderer
- if (!renderer) {
- if (metadata.hydrate === 'only') {
- // TODO: improve error message
- throw new Error(`Unable to render ${metadata.displayName}!
-
-Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.
-Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames
- .map((r) => r.replace('@astrojs/', ''))
- .join('|')}" />
-`);
- } else if (typeof Component !== 'string') {
- const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name));
- const plural = renderers.length > 1;
- if (matchingRenderers.length === 0) {
- throw new Error(`Unable to render ${metadata.displayName}!
-
-There ${plural ? 'are' : 'is'} ${renderers.length} renderer${
- plural ? 's' : ''
- } configured in your \`astro.config.mjs\` file,
-but ${plural ? 'none were' : 'it was not'} able to server-side render ${metadata.displayName}.
-
-Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`);
- } else if (matchingRenderers.length === 1) {
- // We already know that renderer.ssr.check() has failed
- // but this will throw a much more descriptive error!
- renderer = matchingRenderers[0];
- ({ html } = await renderer.ssr.renderToStaticMarkup.call(
- { result },
- Component,
- props,
- children,
- metadata
- ));
- } else {
- throw new Error(`Unable to render ${metadata.displayName}!
-
-This component likely uses ${formatList(probableRendererNames)},
-but Astro encountered an error during server-side rendering.
-
-Please ensure that ${metadata.displayName}:
-1. Does not unconditionally access browser-specific globals like \`window\` or \`document\`.
- If this is unavoidable, use the \`client:only\` hydration directive.
-2. Does not conditionally return \`null\` or \`undefined\` when rendered on the server.
-
-If you're still stuck, please open an issue on GitHub or join us at https://astro.build/chat.`);
- }
- }
- } else {
- if (metadata.hydrate === 'only') {
- html = await renderSlot(result, slots?.fallback);
- } else {
- ({ html } = await renderer.ssr.renderToStaticMarkup.call(
- { result },
- Component,
- props,
- children,
- metadata
- ));
- }
- }
-
- // HACK! The lit renderer doesn't include a clientEntrypoint for custom elements, allow it
- // to render here until we find a better way to recognize when a client entrypoint isn't required.
- if (
- renderer &&
- !renderer.clientEntrypoint &&
- renderer.name !== '@astrojs/lit' &&
- metadata.hydrate
- ) {
- throw new Error(
- `${metadata.displayName} component has a \`client:${metadata.hydrate}\` directive, but no client entrypoint was provided by ${renderer.name}!`
- );
- }
-
- // 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.
- if (!html && typeof Component === 'string') {
- const childSlots = Object.values(children).join('');
- const iterable = renderAstroComponent(
- await render`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
- childSlots === '' && voidElementNames.test(Component)
- ? `/>`
- : `>${childSlots}</${Component}>`
- )}`
- );
- html = '';
- for await (const chunk of iterable) {
- html += chunk;
- }
- }
-
- if (!hydration) {
- if (isPage || renderer?.name === 'astro:jsx') {
- return html;
- }
- return markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
- }
-
- // Include componentExport name, componentUrl, and props in hash to dedupe identical islands
- const astroId = shorthash(
- `<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serializeProps(
- props
- )}`
- );
-
- const island = await generateHydrateScript(
- { renderer: renderer!, result, astroId, props },
- metadata as Required<AstroComponentMetadata>
- );
-
- // Render template if not all astro fragments are provided.
- let unrenderedSlots: string[] = [];
- if (html) {
- if (Object.keys(children).length > 0) {
- for (const key of Object.keys(children)) {
- if (!html.includes(key === 'default' ? `<astro-slot>` : `<astro-slot name="${key}">`)) {
- unrenderedSlots.push(key);
- }
- }
- }
- } else {
- unrenderedSlots = Object.keys(children);
- }
- const template =
- unrenderedSlots.length > 0
- ? unrenderedSlots
- .map(
- (key) =>
- `<template data-astro-template${key !== 'default' ? `="${key}"` : ''}>${
- children[key]
- }</template>`
- )
- .join('')
- : '';
-
- island.children = `${html ?? ''}${template}`;
-
- if (island.children) {
- island.props['await-children'] = '';
- }
-
- async function* renderAll() {
- yield { type: 'directive', hydration, result };
- yield markHTMLString(renderElement('astro-island', island, false));
- }
-
- return renderAll();
-}
-
-/** Create the Astro.fetchContent() runtime function. */
-function createDeprecatedFetchContentFn() {
- return () => {
- throw new Error('Deprecated: Astro.fetchContent() has been replaced with Astro.glob().');
- };
-}
-
-/** Create the Astro.glob() runtime function. */
-function createAstroGlobFn() {
- const globHandler = (importMetaGlobResult: Record<string, any>, globValue: () => any) => {
- let allEntries = [...Object.values(importMetaGlobResult)];
- if (allEntries.length === 0) {
- throw new Error(`Astro.glob(${JSON.stringify(globValue())}) - no matches found.`);
- }
- // Map over the `import()` promises, calling to load them.
- return Promise.all(allEntries.map((fn) => fn()));
- };
- // Cast the return type because the argument that the user sees (string) is different from the argument
- // that the runtime sees post-compiler (Record<string, Module>).
- return globHandler as unknown as AstroGlobalPartial['glob'];
-}
-
-// This is used to create the top-level Astro global; the one that you can use
-// Inside of getStaticPaths.
-export function createAstro(
- filePathname: string,
- _site: string | undefined,
- projectRootStr: string
-): AstroGlobalPartial {
- const site = _site ? new URL(_site) : undefined;
- const referenceURL = new URL(filePathname, `http://localhost`);
- const projectRoot = new URL(projectRootStr);
- return {
- site,
- generator: `Astro v${ASTRO_VERSION}`,
- fetchContent: createDeprecatedFetchContentFn(),
- glob: createAstroGlobFn(),
- // INVESTIGATE is there a use-case for multi args?
- resolve(...segments: string[]) {
- let resolved = segments.reduce((u, segment) => new URL(segment, u), referenceURL).pathname;
- // When inside of project root, remove the leading path so you are
- // left with only `/src/images/tower.png`
- if (resolved.startsWith(projectRoot.pathname)) {
- resolved = '/' + resolved.slice(projectRoot.pathname.length);
- }
- return resolved;
- },
- };
-}
-
-const toAttributeString = (value: any, shouldEscape = true) =>
- shouldEscape ? String(value).replace(/&/g, '&#38;').replace(/"/g, '&#34;') : value;
-
-const kebab = (k: string) =>
- k.toLowerCase() === k ? k : k.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
-const toStyleString = (obj: Record<string, any>) =>
- Object.entries(obj)
- .map(([k, v]) => `${kebab(k)}:${v}`)
- .join(';');
-
-const STATIC_DIRECTIVES = new Set(['set:html', 'set:text']);
-
-// A helper used to turn expressions into attribute key/value
-export function addAttribute(value: any, key: string, shouldEscape = true) {
- if (value == null) {
- return '';
- }
-
- if (value === false) {
- if (htmlEnumAttributes.test(key) || svgEnumAttributes.test(key)) {
- return markHTMLString(` ${key}="false"`);
- }
- return '';
- }
-
- // compiler directives cannot be applied dynamically, log a warning and ignore.
- if (STATIC_DIRECTIVES.has(key)) {
- // eslint-disable-next-line no-console
- console.warn(`[astro] The "${key}" directive cannot be applied dynamically at runtime. It will not be rendered as an attribute.
-
-Make sure to use the static attribute syntax (\`${key}={value}\`) instead of the dynamic spread syntax (\`{...{ "${key}": value }}\`).`);
- return '';
- }
-
- // support "class" from an expression passed into an element (#782)
- if (key === 'class:list') {
- const listValue = toAttributeString(serializeListValue(value));
- if (listValue === '') {
- return '';
- }
- return markHTMLString(` ${key.slice(0, -5)}="${listValue}"`);
- }
-
- // support object styles for better JSX compat
- if (key === 'style' && !(value instanceof HTMLString) && typeof value === 'object') {
- return markHTMLString(` ${key}="${toStyleString(value)}"`);
- }
-
- // support `className` for better JSX compat
- if (key === 'className') {
- return markHTMLString(` class="${toAttributeString(value, shouldEscape)}"`);
- }
-
- // Boolean values only need the key
- if (value === true && (key.startsWith('data-') || htmlBooleanAttributes.test(key))) {
- return markHTMLString(` ${key}`);
- } else {
- return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`);
- }
-}
-
-// Adds support for `<Component {...value} />
-function internalSpreadAttributes(values: Record<any, any>, shouldEscape = true) {
- let output = '';
- for (const [key, value] of Object.entries(values)) {
- output += addAttribute(value, key, shouldEscape);
- }
- return markHTMLString(output);
-}
-
// Adds support for `<Component {...value} />
export function spreadAttributes(
values: Record<any, any>,
- name?: string,
+ _name?: string,
{ class: scopedClassName }: { class?: string } = {}
) {
let output = '';
@@ -654,374 +105,3 @@ export function defineStyleVars(defs: Record<any, any> | Record<any, any>[]) {
}
return markHTMLString(output);
}
-
-// converts (most) arbitrary strings to valid JS identifiers
-const toIdent = (k: string) =>
- k.trim().replace(/(?:(?<!^)\b\w|\s+|[^\w]+)/g, (match, index) => {
- if (/[^\w]|\s/.test(match)) return '';
- return index === 0 ? match : match.toUpperCase();
- });
-
-// Adds variables to an inline script.
-export function defineScriptVars(vars: Record<any, any>) {
- let output = '';
- for (const [key, value] of Object.entries(vars)) {
- output += `let ${toIdent(key)} = ${JSON.stringify(value)};\n`;
- }
- return markHTMLString(output);
-}
-
-function getHandlerFromModule(mod: EndpointHandler, method: string) {
- // If there was an exact match on `method`, return that function.
- if (mod[method]) {
- return mod[method];
- }
- // Handle `del` instead of `delete`, since `delete` is a reserved word in JS.
- if (method === 'delete' && mod['del']) {
- return mod['del'];
- }
- // If a single `all` handler was used, return that function.
- if (mod['all']) {
- return mod['all'];
- }
- // Otherwise, no handler found.
- return undefined;
-}
-
-/** Renders an endpoint request to completion, returning the body. */
-export async function renderEndpoint(mod: EndpointHandler, request: Request, params: Params) {
- const chosenMethod = request.method?.toLowerCase();
- const handler = getHandlerFromModule(mod, chosenMethod);
- if (!handler || typeof handler !== 'function') {
- throw new Error(
- `Endpoint handler not found! Expected an exported function for "${chosenMethod}"`
- );
- }
-
- if (handler.length > 1) {
- // eslint-disable-next-line no-console
- console.warn(`
-API routes with 2 arguments have been deprecated. Instead they take a single argument in the form of:
-
-export function get({ params, request }) {
- //...
-}
-
-Update your code to remove this warning.`);
- }
-
- const context = {
- request,
- params,
- };
-
- const proxy = new Proxy(context, {
- get(target, prop) {
- if (prop in target) {
- return Reflect.get(target, prop);
- } else if (prop in params) {
- // eslint-disable-next-line no-console
- console.warn(`
-API routes no longer pass params as the first argument. Instead an object containing a params property is provided in the form of:
-
-export function get({ params }) {
- // ...
-}
-
-Update your code to remove this warning.`);
- return Reflect.get(params, prop);
- } else {
- return undefined;
- }
- },
- }) as APIContext & Params;
-
- return handler.call(mod, proxy, request);
-}
-
-// Calls a component and renders it into a string of HTML
-export async function renderToString(
- result: SSRResult,
- componentFactory: AstroComponentFactory,
- props: any,
- children: any
-): Promise<string> {
- const Component = await componentFactory(result, props, children);
-
- if (!isAstroComponent(Component)) {
- const response: Response = Component;
- throw response;
- }
-
- let html = '';
- for await (const chunk of renderAstroComponent(Component)) {
- html += stringifyChunk(result, chunk);
- }
- return html;
-}
-
-export async function renderToIterable(
- result: SSRResult,
- componentFactory: AstroComponentFactory,
- props: any,
- children: any
-): Promise<AsyncIterable<string | RenderInstruction>> {
- const Component = await componentFactory(result, props, children);
-
- if (!isAstroComponent(Component)) {
- // eslint-disable-next-line no-console
- console.warn(
- `Returning a Response is only supported inside of page components. Consider refactoring this logic into something like a function that can be used in the page.`
- );
- const response: Response = Component;
- throw response;
- }
-
- return renderAstroComponent(Component);
-}
-
-const encoder = new TextEncoder();
-
-// Rendering produces either marked strings of HTML or instructions for hydration.
-// These directive instructions bubble all the way up to renderPage so that we
-// can ensure they are added only once, and as soon as possible.
-export function stringifyChunk(result: SSRResult, chunk: string | RenderInstruction) {
- switch ((chunk as any).type) {
- case 'directive': {
- const { hydration } = chunk as RenderInstruction;
- let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
- let needsDirectiveScript =
- hydration && determinesIfNeedsDirectiveScript(result, hydration.directive);
-
- let prescriptType: PrescriptType = needsHydrationScript
- ? 'both'
- : needsDirectiveScript
- ? 'directive'
- : null;
- if (prescriptType) {
- let prescripts = getPrescripts(prescriptType, hydration.directive);
- return markHTMLString(prescripts);
- } else {
- return '';
- }
- }
- default: {
- return chunk.toString();
- }
- }
-}
-
-export async function renderPage(
- result: SSRResult,
- componentFactory: AstroComponentFactory,
- props: any,
- children: any,
- streaming: boolean
-): Promise<Response> {
- if (!componentFactory.isAstroComponentFactory) {
- const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
- const output = await renderComponent(
- result,
- componentFactory.name,
- componentFactory,
- pageProps,
- null
- );
- let html = output.toString();
- if (!/<!doctype html/i.test(html)) {
- let rest = html;
- html = `<!DOCTYPE html>`;
- for await (let chunk of maybeRenderHead(result)) {
- html += chunk;
- }
- html += rest;
- }
- return new Response(html, {
- headers: new Headers([
- ['Content-Type', 'text/html; charset=utf-8'],
- ['Content-Length', Buffer.byteLength(html, 'utf-8').toString()],
- ]),
- });
- }
- const factoryReturnValue = await componentFactory(result, props, children);
-
- if (isAstroComponent(factoryReturnValue)) {
- let iterable = renderAstroComponent(factoryReturnValue);
- let init = result.response;
- let headers = new Headers(init.headers);
- let body: BodyInit;
-
- if (streaming) {
- body = new ReadableStream({
- start(controller) {
- async function read() {
- let i = 0;
- try {
- for await (const chunk of iterable) {
- let html = stringifyChunk(result, chunk);
-
- if (i === 0) {
- if (!/<!doctype html/i.test(html)) {
- controller.enqueue(encoder.encode('<!DOCTYPE html>\n'));
- }
- }
- controller.enqueue(encoder.encode(html));
- i++;
- }
- controller.close();
- } catch (e) {
- controller.error(e);
- }
- }
- read();
- },
- });
- } else {
- body = '';
- let i = 0;
- for await (const chunk of iterable) {
- let html = stringifyChunk(result, chunk);
- if (i === 0) {
- if (!/<!doctype html/i.test(html)) {
- body += '<!DOCTYPE html>\n';
- }
- }
- body += html;
- i++;
- }
- const bytes = encoder.encode(body);
- headers.set('Content-Length', bytes.byteLength.toString());
- }
-
- let response = createResponse(body, { ...init, headers });
- return response;
- } else {
- return factoryReturnValue;
- }
-}
-
-// Filter out duplicate elements in our set
-const uniqueElements = (item: any, index: number, all: any[]) => {
- const props = JSON.stringify(item.props);
- const children = item.children;
- return (
- index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children)
- );
-};
-
-const alreadyHeadRenderedResults = new WeakSet<SSRResult>();
-export function renderHead(result: SSRResult): Promise<string> {
- alreadyHeadRenderedResults.add(result);
- const styles = Array.from(result.styles)
- .filter(uniqueElements)
- .map((style) => renderElement('style', style));
- // Clear result.styles so that any new styles added will be inlined.
- result.styles.clear();
- const scripts = Array.from(result.scripts)
- .filter(uniqueElements)
- .map((script, i) => {
- return renderElement('script', script, false);
- });
- const links = Array.from(result.links)
- .filter(uniqueElements)
- .map((link) => renderElement('link', link, false));
- return markHTMLString(links.join('\n') + styles.join('\n') + scripts.join('\n'));
-}
-
-// This function is called by Astro components that do not contain a <head> component
-// 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 async function* maybeRenderHead(result: SSRResult): AsyncIterable<string> {
- if (alreadyHeadRenderedResults.has(result)) {
- return;
- }
- yield renderHead(result);
-}
-
-export interface RenderInstruction {
- type: 'directive';
- result: SSRResult;
- hydration: HydrationMetadata;
-}
-
-export async function* renderAstroComponent(
- component: InstanceType<typeof AstroComponent>
-): AsyncIterable<string | RenderInstruction> {
- for await (const value of component) {
- if (value || value === 0) {
- for await (const chunk of _render(value)) {
- switch (chunk.type) {
- case 'directive': {
- yield chunk;
- break;
- }
- default: {
- yield markHTMLString(chunk);
- break;
- }
- }
- }
- }
- }
-}
-
-function componentIsHTMLElement(Component: unknown) {
- return typeof HTMLElement !== 'undefined' && HTMLElement.isPrototypeOf(Component as object);
-}
-
-export async function renderHTMLElement(
- result: SSRResult,
- constructor: typeof HTMLElement,
- props: any,
- slots: any
-) {
- const name = getHTMLElementName(constructor);
-
- let attrHTML = '';
-
- for (const attr in props) {
- attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
- }
-
- return markHTMLString(
- `<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`
- );
-}
-
-function getHTMLElementName(constructor: typeof HTMLElement) {
- const definedName = (
- customElements as CustomElementRegistry & { getName(_constructor: typeof HTMLElement): string }
- ).getName(constructor);
- if (definedName) return definedName;
-
- const assignedName = constructor.name
- .replace(/^HTML|Element$/g, '')
- .replace(/[A-Z]/g, '-$&')
- .toLowerCase()
- .replace(/^-/, 'html-');
- return assignedName;
-}
-
-function renderElement(
- name: string,
- { props: _props, children = '' }: SSRElement,
- shouldEscape = true
-) {
- // Do not print `hoist`, `lang`, `is:global`
- const { lang: _, 'data-astro-id': astroId, 'define:vars': defineVars, ...props } = _props;
- if (defineVars) {
- if (name === 'style') {
- delete props['is:global'];
- delete props['is:scoped'];
- }
- if (name === 'script') {
- delete props.hoist;
- children = defineScriptVars(defineVars) + '\n' + children;
- }
- }
- if ((children == null || children == '') && voidElementNames.test(name)) {
- return `<${name}${internalSpreadAttributes(props, shouldEscape)} />`;
- }
- return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
-}
diff --git a/packages/astro/src/runtime/server/render/any.ts b/packages/astro/src/runtime/server/render/any.ts
new file mode 100644
index 000000000..2f4987708
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/any.ts
@@ -0,0 +1,51 @@
+import { AstroComponent, renderAstroComponent } from './astro.js';
+import { markHTMLString, HTMLString, escapeHTML } from '../escape.js';
+import { stringifyChunk } from './common.js';
+
+export async function* renderChild(child: any): AsyncIterable<any> {
+ child = await child;
+ if (child instanceof HTMLString) {
+ yield child;
+ } else if (Array.isArray(child)) {
+ for (const value of child) {
+ yield markHTMLString(await renderChild(value));
+ }
+ } else if (typeof child === 'function') {
+ // Special: If a child is a function, call it automatically.
+ // This lets you do {() => ...} without the extra boilerplate
+ // of wrapping it in a function and calling it.
+ yield* renderChild(child());
+ } else if (typeof child === 'string') {
+ yield markHTMLString(escapeHTML(child));
+ } else if (!child && child !== 0) {
+ // do nothing, safe to ignore falsey values.
+ }
+ // Add a comment explaining why each of these are needed.
+ // Maybe create clearly named function for what this is doing.
+ else if (
+ child instanceof AstroComponent ||
+ Object.prototype.toString.call(child) === '[object AstroComponent]'
+ ) {
+ yield* renderAstroComponent(child);
+ } else if (typeof child === 'object' && Symbol.asyncIterator in child) {
+ yield* child;
+ } else {
+ yield child;
+ }
+}
+
+export async function renderSlot(result: any, slotted: string, fallback?: any): Promise<string> {
+ if (slotted) {
+ let iterator = renderChild(slotted);
+ let content = '';
+ for await (const chunk of iterator) {
+ if ((chunk as any).type === 'directive') {
+ content += stringifyChunk(result, chunk);
+ } else {
+ content += chunk;
+ }
+ }
+ return markHTMLString(content);
+ }
+ return fallback;
+}
diff --git a/packages/astro/src/runtime/server/render/astro.ts b/packages/astro/src/runtime/server/render/astro.ts
new file mode 100644
index 000000000..c9e9ac91f
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/astro.ts
@@ -0,0 +1,124 @@
+import type { SSRResult } from '../../../@types/astro';
+import type { RenderInstruction } from './types';
+import type { AstroComponentFactory } from './index';
+
+import { HydrationDirectiveProps } from '../hydration.js';
+import { stringifyChunk } from './common.js';
+import { markHTMLString } from '../escape.js';
+import { renderChild } from './any.js';
+
+// In dev mode, check props and make sure they are valid for an Astro component
+function validateComponentProps(props: any, displayName: string) {
+ if(import.meta.env?.DEV && props != null) {
+ for(const prop of Object.keys(props)) {
+ if(HydrationDirectiveProps.has(prop)) {
+ // eslint-disable-next-line
+ console.warn(`You are attempting to render <${displayName} ${prop} />, but ${displayName} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.`);
+ }
+ }
+ }
+}
+
+// The return value when rendering a component.
+// This is the result of calling render(), should this be named to RenderResult or...?
+export class AstroComponent {
+ private htmlParts: TemplateStringsArray;
+ private expressions: any[];
+
+ constructor(htmlParts: TemplateStringsArray, expressions: any[]) {
+ this.htmlParts = htmlParts;
+ this.expressions = expressions;
+ }
+
+ get [Symbol.toStringTag]() {
+ return 'AstroComponent';
+ }
+
+ async *[Symbol.asyncIterator]() {
+ const { htmlParts, expressions } = this;
+
+ for (let i = 0; i < htmlParts.length; i++) {
+ const html = htmlParts[i];
+ const expression = expressions[i];
+
+ yield markHTMLString(html);
+ yield* renderChild(expression);
+ }
+ }
+}
+
+// Determines if a component is an .astro component
+export function isAstroComponent(obj: any): obj is AstroComponent {
+ return (
+ typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]'
+ );
+}
+
+export async function* renderAstroComponent(
+ component: InstanceType<typeof AstroComponent>
+): AsyncIterable<string | RenderInstruction> {
+ for await (const value of component) {
+ if (value || value === 0) {
+ for await (const chunk of renderChild(value)) {
+ switch (chunk.type) {
+ case 'directive': {
+ yield chunk;
+ break;
+ }
+ default: {
+ yield markHTMLString(chunk);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+// Calls a component and renders it into a string of HTML
+export async function renderToString(
+ result: SSRResult,
+ componentFactory: AstroComponentFactory,
+ props: any,
+ children: any
+): Promise<string> {
+ const Component = await componentFactory(result, props, children);
+
+ if (!isAstroComponent(Component)) {
+ const response: Response = Component;
+ throw response;
+ }
+
+ let html = '';
+ for await (const chunk of renderAstroComponent(Component)) {
+ html += stringifyChunk(result, chunk);
+ }
+ return html;
+}
+
+export async function renderToIterable(
+ result: SSRResult,
+ componentFactory: AstroComponentFactory,
+ displayName: string,
+ props: any,
+ children: any
+): Promise<AsyncIterable<string | RenderInstruction>> {
+ validateComponentProps(props, displayName);
+ const Component = await componentFactory(result, props, children);
+
+ if (!isAstroComponent(Component)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Returning a Response is only supported inside of page components. Consider refactoring this logic into something like a function that can be used in the page.`
+ );
+
+ const response = Component;
+ throw response;
+ }
+
+ return renderAstroComponent(Component);
+}
+
+export async function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]) {
+ return new AstroComponent(htmlParts, expressions);
+}
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
new file mode 100644
index 000000000..cebbf5966
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -0,0 +1,43 @@
+import type { SSRResult } from '../../../@types/astro';
+import type { RenderInstruction } from './types.js';
+
+import { markHTMLString } from '../escape.js';
+import {
+ determineIfNeedsHydrationScript,
+ determinesIfNeedsDirectiveScript,
+ getPrescripts,
+PrescriptType,
+} from '../scripts.js';
+
+export const Fragment = Symbol.for('astro:fragment');
+export const Renderer = Symbol.for('astro:renderer');
+
+
+// Rendering produces either marked strings of HTML or instructions for hydration.
+// These directive instructions bubble all the way up to renderPage so that we
+// can ensure they are added only once, and as soon as possible.
+export function stringifyChunk(result: SSRResult, chunk: string | RenderInstruction) {
+ switch ((chunk as any).type) {
+ case 'directive': {
+ const { hydration } = chunk as RenderInstruction;
+ let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
+ let needsDirectiveScript =
+ hydration && determinesIfNeedsDirectiveScript(result, hydration.directive);
+
+ let prescriptType: PrescriptType = needsHydrationScript
+ ? 'both'
+ : needsDirectiveScript
+ ? 'directive'
+ : null;
+ if (prescriptType) {
+ let prescripts = getPrescripts(prescriptType, hydration.directive);
+ return markHTMLString(prescripts);
+ } else {
+ return '';
+ }
+ }
+ default: {
+ return chunk.toString();
+ }
+ }
+}
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
new file mode 100644
index 000000000..38e6add65
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -0,0 +1,350 @@
+import type {
+ AstroComponentMetadata,
+ SSRLoadedRenderer,
+ SSRResult,
+} from '../../../@types/astro';
+import type { RenderInstruction } from './types.js';
+
+import { extractDirectives, generateHydrateScript } from '../hydration.js';
+import { serializeProps } from '../serialize.js';
+import { shorthash } from '../shorthash.js';
+import { Fragment, Renderer } from './common.js';
+import { markHTMLString } from '../escape.js';
+import { renderSlot } from './any.js';
+import { renderToIterable, renderAstroComponent, renderTemplate } from './astro.js';
+import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
+import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
+
+const rendererAliases = new Map([['solid', 'solid-js']]);
+
+function guessRenderers(componentUrl?: string): string[] {
+ const extname = componentUrl?.split('.').pop();
+ switch (extname) {
+ case 'svelte':
+ return ['@astrojs/svelte'];
+ case 'vue':
+ return ['@astrojs/vue'];
+ case 'jsx':
+ case 'tsx':
+ return ['@astrojs/react', '@astrojs/preact'];
+ default:
+ return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue', '@astrojs/svelte'];
+ }
+}
+
+type ComponentType = 'fragment' | 'html' | 'astro-factory' | 'unknown';
+
+function getComponentType(Component: unknown): ComponentType {
+ if (Component === Fragment) {
+ return 'fragment';
+ }
+ if(Component && typeof Component === 'object' && (Component as any)['astro:html']) {
+ return 'html';
+ }
+ if(Component && (Component as any).isAstroComponentFactory) {
+ return 'astro-factory';
+ }
+ return 'unknown';
+}
+
+export async function renderComponent(
+ result: SSRResult,
+ displayName: string,
+ Component: unknown,
+ _props: Record<string | number, any>,
+ slots: any = {}
+): Promise<string | AsyncIterable<string | RenderInstruction>> {
+ Component = await Component;
+
+ switch(getComponentType(Component)) {
+ case 'fragment': {
+ const children = await renderSlot(result, slots?.default);
+ if (children == null) {
+ return children;
+ }
+ return markHTMLString(children);
+ }
+
+ // .html components
+ case 'html': {
+ const children: Record<string, string> = {};
+ if (slots) {
+ await Promise.all(
+ Object.entries(slots).map(([key, value]) =>
+ renderSlot(result, value as string).then((output) => {
+ children[key] = output;
+ })
+ )
+ );
+ }
+ const html = (Component as any).render({ slots: children });
+ return markHTMLString(html);
+ }
+
+ case 'astro-factory': {
+ async function* renderAstroComponentInline(): AsyncGenerator<
+ string | RenderInstruction,
+ void,
+ undefined
+ > {
+ let iterable = await renderToIterable(result, Component as any, displayName, _props, slots);
+ yield* iterable;
+ }
+
+ return renderAstroComponentInline();
+ }
+ }
+
+ if (!Component && !_props['client:only']) {
+ throw new Error(
+ `Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
+ );
+ }
+
+ const { renderers } = result._metadata;
+ const metadata: AstroComponentMetadata = { displayName };
+
+ const { hydration, isPage, props } = extractDirectives(_props);
+ let html = '';
+
+ if (hydration) {
+ metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
+ metadata.hydrateArgs = hydration.value;
+ metadata.componentExport = hydration.componentExport;
+ metadata.componentUrl = hydration.componentUrl;
+ }
+ const probableRendererNames = guessRenderers(metadata.componentUrl);
+
+ if (
+ Array.isArray(renderers) &&
+ renderers.length === 0 &&
+ typeof Component !== 'string' &&
+ !componentIsHTMLElement(Component)
+ ) {
+ const message = `Unable to render ${metadata.displayName}!
+
+There are no \`integrations\` set in your \`astro.config.mjs\` file.
+Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`;
+ throw new Error(message);
+ }
+
+ const children: Record<string, string> = {};
+ if (slots) {
+ await Promise.all(
+ Object.entries(slots).map(([key, value]) =>
+ renderSlot(result, value as string).then((output) => {
+ children[key] = output;
+ })
+ )
+ );
+ }
+
+ // Call the renderers `check` hook to see if any claim this component.
+ let renderer: SSRLoadedRenderer | undefined;
+ if (metadata.hydrate !== 'only') {
+ // If this component ran through `__astro_tag_component__`, we already know
+ // which renderer to match to and can skip the usual `check` calls.
+ // This will help us throw most relevant error message for modules with runtime errors
+ if (Component && (Component as any)[Renderer]) {
+ const rendererName = (Component as any)[Renderer];
+ renderer = renderers.find(({ name }) => name === rendererName);
+ }
+
+ if (!renderer) {
+ let error;
+ for (const r of renderers) {
+ try {
+ if (await r.ssr.check.call({ result }, Component, props, children)) {
+ renderer = r;
+ break;
+ }
+ } catch (e) {
+ error ??= e;
+ }
+ }
+
+ // If no renderer is found and there is an error, throw that error because
+ // it is likely a problem with the component code.
+ if (!renderer && error) {
+ throw error;
+ }
+ }
+
+ if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
+ const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
+
+ return output;
+ }
+ } else {
+ // Attempt: use explicitly passed renderer name
+ if (metadata.hydrateArgs) {
+ const passedName = metadata.hydrateArgs;
+ const rendererName = rendererAliases.has(passedName)
+ ? rendererAliases.get(passedName)
+ : passedName;
+ renderer = renderers.find(
+ ({ name }) => name === `@astrojs/${rendererName}` || name === rendererName
+ );
+ }
+ // Attempt: user only has a single renderer, default to that
+ if (!renderer && renderers.length === 1) {
+ renderer = renderers[0];
+ }
+ // Attempt: can we guess the renderer from the export extension?
+ if (!renderer) {
+ const extname = metadata.componentUrl?.split('.').pop();
+ renderer = renderers.filter(
+ ({ name }) => name === `@astrojs/${extname}` || name === extname
+ )[0];
+ }
+ }
+
+ // If no one claimed the renderer
+ if (!renderer) {
+ if (metadata.hydrate === 'only') {
+ // TODO: improve error message
+ throw new Error(`Unable to render ${metadata.displayName}!
+
+Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.
+Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames
+ .map((r) => r.replace('@astrojs/', ''))
+ .join('|')}" />
+`);
+ } else if (typeof Component !== 'string') {
+ const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name));
+ const plural = renderers.length > 1;
+ if (matchingRenderers.length === 0) {
+ throw new Error(`Unable to render ${metadata.displayName}!
+
+There ${plural ? 'are' : 'is'} ${renderers.length} renderer${
+ plural ? 's' : ''
+ } configured in your \`astro.config.mjs\` file,
+but ${plural ? 'none were' : 'it was not'} able to server-side render ${metadata.displayName}.
+
+Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`);
+ } else if (matchingRenderers.length === 1) {
+ // We already know that renderer.ssr.check() has failed
+ // but this will throw a much more descriptive error!
+ renderer = matchingRenderers[0];
+ ({ html } = await renderer.ssr.renderToStaticMarkup.call(
+ { result },
+ Component,
+ props,
+ children,
+ metadata
+ ));
+ } else {
+ throw new Error(`Unable to render ${metadata.displayName}!
+
+This component likely uses ${formatList(probableRendererNames)},
+but Astro encountered an error during server-side rendering.
+
+Please ensure that ${metadata.displayName}:
+1. Does not unconditionally access browser-specific globals like \`window\` or \`document\`.
+ If this is unavoidable, use the \`client:only\` hydration directive.
+2. Does not conditionally return \`null\` or \`undefined\` when rendered on the server.
+
+If you're still stuck, please open an issue on GitHub or join us at https://astro.build/chat.`);
+ }
+ }
+ } else {
+ if (metadata.hydrate === 'only') {
+ html = await renderSlot(result, slots?.fallback);
+ } else {
+ ({ html } = await renderer.ssr.renderToStaticMarkup.call(
+ { result },
+ Component,
+ props,
+ children,
+ metadata
+ ));
+ }
+ }
+
+ // HACK! The lit renderer doesn't include a clientEntrypoint for custom elements, allow it
+ // to render here until we find a better way to recognize when a client entrypoint isn't required.
+ if (
+ renderer &&
+ !renderer.clientEntrypoint &&
+ renderer.name !== '@astrojs/lit' &&
+ metadata.hydrate
+ ) {
+ throw new Error(
+ `${metadata.displayName} component has a \`client:${metadata.hydrate}\` directive, but no client entrypoint was provided by ${renderer.name}!`
+ );
+ }
+
+ // 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.
+ if (!html && typeof Component === 'string') {
+ const childSlots = Object.values(children).join('');
+ const iterable = renderAstroComponent(
+ await renderTemplate`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
+ childSlots === '' && voidElementNames.test(Component)
+ ? `/>`
+ : `>${childSlots}</${Component}>`
+ )}`
+ );
+ html = '';
+ for await (const chunk of iterable) {
+ html += chunk;
+ }
+ }
+
+ if (!hydration) {
+ if (isPage || renderer?.name === 'astro:jsx') {
+ return html;
+ }
+ return markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
+ }
+
+ // Include componentExport name, componentUrl, and props in hash to dedupe identical islands
+ const astroId = shorthash(
+ `<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serializeProps(
+ props
+ )}`
+ );
+
+ const island = await generateHydrateScript(
+ { renderer: renderer!, result, astroId, props },
+ metadata as Required<AstroComponentMetadata>
+ );
+
+ // Render template if not all astro fragments are provided.
+ let unrenderedSlots: string[] = [];
+ if (html) {
+ if (Object.keys(children).length > 0) {
+ for (const key of Object.keys(children)) {
+ if (!html.includes(key === 'default' ? `<astro-slot>` : `<astro-slot name="${key}">`)) {
+ unrenderedSlots.push(key);
+ }
+ }
+ }
+ } else {
+ unrenderedSlots = Object.keys(children);
+ }
+ const template =
+ unrenderedSlots.length > 0
+ ? unrenderedSlots
+ .map(
+ (key) =>
+ `<template data-astro-template${key !== 'default' ? `="${key}"` : ''}>${
+ children[key]
+ }</template>`
+ )
+ .join('')
+ : '';
+
+ island.children = `${html ?? ''}${template}`;
+
+ if (island.children) {
+ island.props['await-children'] = '';
+ }
+
+ async function* renderAll() {
+ yield { type: 'directive', hydration, result };
+ yield markHTMLString(renderElement('astro-island', island, false));
+ }
+
+ return renderAll();
+}
diff --git a/packages/astro/src/runtime/server/render/dom.ts b/packages/astro/src/runtime/server/render/dom.ts
new file mode 100644
index 000000000..cf6024a88
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/dom.ts
@@ -0,0 +1,42 @@
+import type { SSRResult } from '../../../@types/astro';
+
+import { markHTMLString } from '../escape.js';
+import { renderSlot } from './any.js';
+import { toAttributeString } from './util.js';
+
+export function componentIsHTMLElement(Component: unknown) {
+ return typeof HTMLElement !== 'undefined' && HTMLElement.isPrototypeOf(Component as object);
+}
+
+export async function renderHTMLElement(
+ result: SSRResult,
+ constructor: typeof HTMLElement,
+ props: any,
+ slots: any
+) {
+ const name = getHTMLElementName(constructor);
+
+ let attrHTML = '';
+
+ for (const attr in props) {
+ attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
+ }
+
+ return markHTMLString(
+ `<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`
+ );
+}
+
+function getHTMLElementName(constructor: typeof HTMLElement) {
+ const definedName = (
+ customElements as CustomElementRegistry & { getName(_constructor: typeof HTMLElement): string }
+ ).getName(constructor);
+ if (definedName) return definedName;
+
+ const assignedName = constructor.name
+ .replace(/^HTML|Element$/g, '')
+ .replace(/[A-Z]/g, '-$&')
+ .toLowerCase()
+ .replace(/^-/, 'html-');
+ return assignedName;
+}
diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts
new file mode 100644
index 000000000..bb0fffc2e
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/head.ts
@@ -0,0 +1,43 @@
+import type { SSRResult } from '../../../@types/astro';
+
+import { markHTMLString } from '../escape.js';
+import { renderElement } from './util.js';
+
+// Filter out duplicate elements in our set
+const uniqueElements = (item: any, index: number, all: any[]) => {
+ const props = JSON.stringify(item.props);
+ const children = item.children;
+ return (
+ index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children)
+ );
+};
+
+const alreadyHeadRenderedResults = new WeakSet<SSRResult>();
+export function renderHead(result: SSRResult): Promise<string> {
+ alreadyHeadRenderedResults.add(result);
+ const styles = Array.from(result.styles)
+ .filter(uniqueElements)
+ .map((style) => renderElement('style', style));
+ // Clear result.styles so that any new styles added will be inlined.
+ result.styles.clear();
+ const scripts = Array.from(result.scripts)
+ .filter(uniqueElements)
+ .map((script, i) => {
+ return renderElement('script', script, false);
+ });
+ const links = Array.from(result.links)
+ .filter(uniqueElements)
+ .map((link) => renderElement('link', link, false));
+ return markHTMLString(links.join('\n') + styles.join('\n') + scripts.join('\n'));
+}
+
+// This function is called by Astro components that do not contain a <head> component
+// 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 async function* maybeRenderHead(result: SSRResult): AsyncIterable<string> {
+ if (alreadyHeadRenderedResults.has(result)) {
+ return;
+ }
+ yield renderHead(result);
+}
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
new file mode 100644
index 000000000..e74c3ffb6
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -0,0 +1,17 @@
+import { renderTemplate } from './astro.js';
+
+export type { RenderInstruction } from './types';
+export { renderSlot } from './any.js';
+export { renderTemplate, renderAstroComponent, renderToString } from './astro.js';
+export { stringifyChunk, Fragment, Renderer } from './common.js';
+export { renderComponent } from './component.js';
+export { renderHTMLElement } from './dom.js';
+export { renderHead, maybeRenderHead } from './head.js';
+export { renderPage } from './page.js';
+export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
+
+// The callback passed to to $$createComponent
+export interface AstroComponentFactory {
+ (result: any, props: any, slots: any): ReturnType<typeof renderTemplate> | Response;
+ isAstroComponentFactory?: boolean;
+}
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
new file mode 100644
index 000000000..99c047e57
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -0,0 +1,99 @@
+import type { SSRResult } from '../../../@types/astro';
+import type { AstroComponentFactory } from './index';
+
+import { isAstroComponent, renderAstroComponent } from './astro.js';
+import { stringifyChunk } from './common.js';
+import { renderComponent } from './component.js';
+import { maybeRenderHead } from './head.js';
+import { createResponse } from '../response.js';
+
+const encoder = new TextEncoder();
+
+export async function renderPage(
+ result: SSRResult,
+ componentFactory: AstroComponentFactory,
+ props: any,
+ children: any,
+ streaming: boolean
+): Promise<Response> {
+ if (!componentFactory.isAstroComponentFactory) {
+ const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
+ const output = await renderComponent(
+ result,
+ componentFactory.name,
+ componentFactory,
+ pageProps,
+ null
+ );
+ let html = output.toString();
+ if (!/<!doctype html/i.test(html)) {
+ let rest = html;
+ html = `<!DOCTYPE html>`;
+ for await (let chunk of maybeRenderHead(result)) {
+ html += chunk;
+ }
+ html += rest;
+ }
+ return new Response(html, {
+ headers: new Headers([
+ ['Content-Type', 'text/html; charset=utf-8'],
+ ['Content-Length', Buffer.byteLength(html, 'utf-8').toString()],
+ ]),
+ });
+ }
+ const factoryReturnValue = await componentFactory(result, props, children);
+
+ if (isAstroComponent(factoryReturnValue)) {
+ let iterable = renderAstroComponent(factoryReturnValue);
+ let init = result.response;
+ let headers = new Headers(init.headers);
+ let body: BodyInit;
+
+ if (streaming) {
+ body = new ReadableStream({
+ start(controller) {
+ async function read() {
+ let i = 0;
+ try {
+ for await (const chunk of iterable) {
+ let html = stringifyChunk(result, chunk);
+
+ if (i === 0) {
+ if (!/<!doctype html/i.test(html)) {
+ controller.enqueue(encoder.encode('<!DOCTYPE html>\n'));
+ }
+ }
+ controller.enqueue(encoder.encode(html));
+ i++;
+ }
+ controller.close();
+ } catch (e) {
+ controller.error(e);
+ }
+ }
+ read();
+ },
+ });
+ } else {
+ body = '';
+ let i = 0;
+ for await (const chunk of iterable) {
+ let html = stringifyChunk(result, chunk);
+ if (i === 0) {
+ if (!/<!doctype html/i.test(html)) {
+ body += '<!DOCTYPE html>\n';
+ }
+ }
+ body += html;
+ i++;
+ }
+ const bytes = encoder.encode(body);
+ headers.set('Content-Length', bytes.byteLength.toString());
+ }
+
+ let response = createResponse(body, { ...init, headers });
+ return response;
+ } else {
+ return factoryReturnValue;
+ }
+}
diff --git a/packages/astro/src/runtime/server/render/types.ts b/packages/astro/src/runtime/server/render/types.ts
new file mode 100644
index 000000000..3cc534ac6
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/types.ts
@@ -0,0 +1,8 @@
+import type { SSRResult } from '../../../@types/astro';
+import type { HydrationMetadata } from '../hydration.js';
+
+export interface RenderInstruction {
+ type: 'directive';
+ result: SSRResult;
+ hydration: HydrationMetadata;
+}
diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts
new file mode 100644
index 000000000..d3585fb81
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/util.ts
@@ -0,0 +1,128 @@
+import type { SSRElement } from '../../../@types/astro';
+
+import { markHTMLString, HTMLString } from '../escape.js';
+import { serializeListValue } from '../util.js';
+
+export const voidElementNames =
+ /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
+const htmlBooleanAttributes =
+ /^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i;
+const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
+// Note: SVG is case-sensitive!
+const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
+
+const STATIC_DIRECTIVES = new Set(['set:html', 'set:text']);
+
+// converts (most) arbitrary strings to valid JS identifiers
+const toIdent = (k: string) =>
+ k.trim().replace(/(?:(?<!^)\b\w|\s+|[^\w]+)/g, (match, index) => {
+ if (/[^\w]|\s/.test(match)) return '';
+ return index === 0 ? match : match.toUpperCase();
+ });
+
+export const toAttributeString = (value: any, shouldEscape = true) =>
+ shouldEscape ? String(value).replace(/&/g, '&#38;').replace(/"/g, '&#34;') : value;
+
+const kebab = (k: string) =>
+ k.toLowerCase() === k ? k : k.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
+const toStyleString = (obj: Record<string, any>) =>
+ Object.entries(obj)
+ .map(([k, v]) => `${kebab(k)}:${v}`)
+ .join(';');
+
+// Adds variables to an inline script.
+export function defineScriptVars(vars: Record<any, any>) {
+ let output = '';
+ for (const [key, value] of Object.entries(vars)) {
+ output += `let ${toIdent(key)} = ${JSON.stringify(value)};\n`;
+ }
+ return markHTMLString(output);
+}
+
+export function formatList(values: string[]): string {
+ if (values.length === 1) {
+ return values[0];
+ }
+ return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
+}
+
+// A helper used to turn expressions into attribute key/value
+export function addAttribute(value: any, key: string, shouldEscape = true) {
+ if (value == null) {
+ return '';
+ }
+
+ if (value === false) {
+ if (htmlEnumAttributes.test(key) || svgEnumAttributes.test(key)) {
+ return markHTMLString(` ${key}="false"`);
+ }
+ return '';
+ }
+
+ // compiler directives cannot be applied dynamically, log a warning and ignore.
+ if (STATIC_DIRECTIVES.has(key)) {
+ // eslint-disable-next-line no-console
+ console.warn(`[astro] The "${key}" directive cannot be applied dynamically at runtime. It will not be rendered as an attribute.
+
+Make sure to use the static attribute syntax (\`${key}={value}\`) instead of the dynamic spread syntax (\`{...{ "${key}": value }}\`).`);
+ return '';
+ }
+
+ // support "class" from an expression passed into an element (#782)
+ if (key === 'class:list') {
+ const listValue = toAttributeString(serializeListValue(value));
+ if (listValue === '') {
+ return '';
+ }
+ return markHTMLString(` ${key.slice(0, -5)}="${listValue}"`);
+ }
+
+ // support object styles for better JSX compat
+ if (key === 'style' && !(value instanceof HTMLString) && typeof value === 'object') {
+ return markHTMLString(` ${key}="${toStyleString(value)}"`);
+ }
+
+ // support `className` for better JSX compat
+ if (key === 'className') {
+ return markHTMLString(` class="${toAttributeString(value, shouldEscape)}"`);
+ }
+
+ // Boolean values only need the key
+ if (value === true && (key.startsWith('data-') || htmlBooleanAttributes.test(key))) {
+ return markHTMLString(` ${key}`);
+ } else {
+ return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`);
+ }
+}
+
+// Adds support for `<Component {...value} />
+export function internalSpreadAttributes(values: Record<any, any>, shouldEscape = true) {
+ let output = '';
+ for (const [key, value] of Object.entries(values)) {
+ output += addAttribute(value, key, shouldEscape);
+ }
+ return markHTMLString(output);
+}
+
+export function renderElement(
+ name: string,
+ { props: _props, children = '' }: SSRElement,
+ shouldEscape = true
+) {
+ // Do not print `hoist`, `lang`, `is:global`
+ const { lang: _, 'data-astro-id': astroId, 'define:vars': defineVars, ...props } = _props;
+ if (defineVars) {
+ if (name === 'style') {
+ delete props['is:global'];
+ delete props['is:scoped'];
+ }
+ if (name === 'script') {
+ delete props.hoist;
+ children = defineScriptVars(defineVars) + '\n' + children;
+ }
+ }
+ if ((children == null || children == '') && voidElementNames.test(name)) {
+ return `<${name}${internalSpreadAttributes(props, shouldEscape)} />`;
+ }
+ return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
+}