summaryrefslogtreecommitdiff
path: root/packages/astro/src/internal/__astro_component.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src/internal/__astro_component.ts')
-rw-r--r--packages/astro/src/internal/__astro_component.ts107
1 files changed, 78 insertions, 29 deletions
diff --git a/packages/astro/src/internal/__astro_component.ts b/packages/astro/src/internal/__astro_component.ts
index 1e0a75c16..2e98d55dc 100644
--- a/packages/astro/src/internal/__astro_component.ts
+++ b/packages/astro/src/internal/__astro_component.ts
@@ -3,38 +3,64 @@ import hash from 'shorthash';
import { valueToEstree, Value } from 'estree-util-value-to-estree';
import { generate } from 'astring';
import * as astro from './renderer-astro';
+import * as astroHtml from './renderer-html';
// A more robust version alternative to `JSON.stringify` that can handle most values
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
const serialize = (value: Value) => generate(valueToEstree(value));
-let rendererSources: string[] = [];
-let renderers: Renderer[] = [];
+export interface RendererInstance {
+ source: string | null;
+ renderer: Renderer;
+ options: any;
+ polyfills: string[];
+}
+
+const astroRendererInstance: RendererInstance = {
+ source: '',
+ renderer: astro as Renderer,
+ options: null,
+ polyfills: []
+};
+
+const astroHtmlRendererInstance: RendererInstance = {
+ source: '',
+ renderer: astroHtml as Renderer,
+ options: null,
+ polyfills: []
+};
-export function setRenderers(_rendererSources: string[], _renderers: Renderer[]) {
- rendererSources = [''].concat(_rendererSources);
- renderers = [astro as Renderer].concat(_renderers);
+let rendererInstances: RendererInstance[] = [];
+
+export function setRenderers(_rendererInstances: RendererInstance[]) {
+ rendererInstances = [astroRendererInstance].concat(_rendererInstances);
+}
+
+function isCustomElementTag(name: string | Function) {
+ return typeof name === 'string' && /-/.test(name);
}
-const rendererCache = new WeakMap();
+const rendererCache = new Map<any, RendererInstance>();
/** For a given component, resolve the renderer. Results are cached if this instance is encountered again */
-async function resolveRenderer(Component: any, props: any = {}, children?: string) {
+async function resolveRenderer(Component: any, props: any = {}, children?: string): Promise<RendererInstance | undefined> {
if (rendererCache.has(Component)) {
- return rendererCache.get(Component);
+ return rendererCache.get(Component)!;
}
const errors: Error[] = [];
- for (const __renderer of renderers) {
+ for (const instance of rendererInstances) {
+ const { renderer, options } = instance;
+
// Yes, we do want to `await` inside of this loop!
// __renderer.check can't be run in parallel, it
// returns the first match and skips any subsequent checks
try {
- const shouldUse: boolean = await __renderer.check(Component, props, children);
+ const shouldUse: boolean = await renderer.check(Component, props, children, options);
if (shouldUse) {
- rendererCache.set(Component, __renderer);
- return __renderer;
+ rendererCache.set(Component, instance);
+ return instance;
}
} catch (err) {
errors.push(err);
@@ -47,26 +73,39 @@ async function resolveRenderer(Component: any, props: any = {}, children?: strin
}
}
-interface AstroComponentProps {
+export interface AstroComponentProps {
displayName: string;
hydrate?: 'load' | 'idle' | 'visible';
componentUrl?: string;
componentExport?: { value: string; namespace?: boolean };
}
+interface HydrateScriptOptions {
+ instance: RendererInstance;
+ astroId: string;
+ props: any;
+}
+
/** For hydrated components, generate a <script type="module"> to load the component */
-async function generateHydrateScript({ renderer, astroId, props }: any, { hydrate, componentUrl, componentExport }: Required<AstroComponentProps>) {
- const rendererSource = rendererSources[renderers.findIndex((r) => r === renderer)];
+async function generateHydrateScript({ instance, astroId, props }: HydrateScriptOptions, { hydrate, componentUrl, componentExport }: Required<AstroComponentProps>) {
+ const { source } = instance;
+
+ const hydrationSource = source ? `
+ const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${source}")]);
+ return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
+`.trim() : `
+ await import("${componentUrl}");
+ return () => {};
+`.trim()
- const script = `<script type="module">
+ const hydrationScript = `<script type="module">
import setup from '/_astro_frontend/hydrate/${hydrate}.js';
setup("${astroId}", async () => {
- const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${rendererSource}")]);
- return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
+ ${hydrationSource}
});
</script>`;
- return script;
+ return hydrationScript;
}
const getComponentName = (Component: any, componentProps: any) => {
@@ -85,25 +124,35 @@ const getComponentName = (Component: any, componentProps: any) => {
export const __astro_component = (Component: any, componentProps: AstroComponentProps = {} as any) => {
if (Component == null) {
throw new Error(`Unable to render ${componentProps.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
- } else if (typeof Component === 'string') {
+ } else if (typeof Component === 'string' && !isCustomElementTag(Component)) {
throw new Error(`Astro is unable to render ${componentProps.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`);
}
return async (props: any, ..._children: string[]) => {
const children = _children.join('\n');
- let renderer = await resolveRenderer(Component, props, children);
-
- if (!renderer) {
- // If the user only specifies a single renderer, but the check failed
- // for some reason... just default to their preferred renderer.
- renderer = rendererSources.length === 2 ? renderers[1] : null;
+ let instance = await resolveRenderer(Component, props, children);
+
+ if (!instance) {
+ if(isCustomElementTag(Component)) {
+ instance = astroHtmlRendererInstance;
+ } else {
+ // If the user only specifies a single renderer, but the check failed
+ // for some reason... just default to their preferred renderer.
+ instance = rendererInstances.length === 2 ? rendererInstances[1] : undefined;
+ }
- if (!renderer) {
+ if (!instance) {
const name = getComponentName(Component, componentProps);
throw new Error(`No renderer found for ${name}! Did you forget to add a renderer to your Astro config?`);
}
}
- const { html } = await renderer.renderToStaticMarkup(Component, props, children);
+ let { html } = await instance.renderer.renderToStaticMarkup(Component, props, children, instance.options);
+
+ if(instance.polyfills.length) {
+ let polyfillScripts = instance.polyfills.map(src => `<script type="module" src="${src}"></script>`).join('');
+ html = html + polyfillScripts;
+ }
+
// If we're NOT hydrating this component, just return the HTML
if (!componentProps.hydrate) {
// It's safe to remove <astro-fragment>, static content doesn't need the wrapper
@@ -112,7 +161,7 @@ export const __astro_component = (Component: any, componentProps: AstroComponent
// If we ARE hydrating this component, let's generate the hydration script
const astroId = hash.unique(html);
- const script = await generateHydrateScript({ renderer, astroId, props }, componentProps as Required<AstroComponentProps>);
+ const script = await generateHydrateScript({ instance, astroId, props }, componentProps as Required<AstroComponentProps>);
const astroRoot = `<astro-root uid="${astroId}">${html}</astro-root>`;
return [astroRoot, script].join('\n');
};