summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Nate Moore <natemoo-re@users.noreply.github.com> 2021-03-26 17:09:28 -0500
committerGravatar GitHub <noreply@github.com> 2021-03-26 17:09:28 -0500
commit9ab1f52a1ca018b7551dc610f7099d300ce5b473 (patch)
tree739ea1e6385c1af866f0c7f5ab154c7d0b68c0ab /src
parent202973291f55bdf21050ab2c1c7db13b2fdb62eb (diff)
downloadastro-9ab1f52a1ca018b7551dc610f7099d300ce5b473.tar.gz
astro-9ab1f52a1ca018b7551dc610f7099d300ce5b473.tar.zst
astro-9ab1f52a1ca018b7551dc610f7099d300ce5b473.zip
New hydration methods (#29)
* WIP: new hydration methods * refactor: genericize load/idle/visible renderers * fix: do not pass "data-astro-id" to component * docs: add hydration section to README * docs: update README Co-authored-by: Nate Moore <nate@skypack.dev>
Diffstat (limited to 'src')
-rw-r--r--src/compiler/codegen.ts96
-rw-r--r--src/frontend/render/preact.ts43
-rw-r--r--src/frontend/render/react.ts35
-rw-r--r--src/frontend/render/renderer.ts63
-rw-r--r--src/frontend/render/svelte.ts41
-rw-r--r--src/frontend/render/vue.ts63
6 files changed, 205 insertions, 136 deletions
diff --git a/src/compiler/codegen.ts b/src/compiler/codegen.ts
index c0d963703..f42968b1a 100644
--- a/src/compiler/codegen.ts
+++ b/src/compiler/codegen.ts
@@ -120,6 +120,8 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
throw new Error(`No supported plugin found for extension ${type}`);
}
+ const getComponentUrl = (ext = '.js') => `new URL(${JSON.stringify(url.replace(/\.[^.]+$/, ext))}, \`http://TEST\${import.meta.url\}\`).pathname.replace(/^\\/\\//, '/_astro/')`;
+
switch (plugin) {
case 'astro': {
if (kind) {
@@ -131,64 +133,78 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
};
}
case 'preact': {
- if (kind === 'dynamic') {
- return {
- wrapper: `__preact_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get(
- 'preact'
- )!}')`,
- wrapperImport: `import {__preact_dynamic} from '${internalImport('render/preact.js')}';`,
- };
- } else {
+ if (['load', 'idle', 'visible'].includes(kind)) {
return {
- wrapper: `__preact_static(${name})`,
- wrapperImport: `import {__preact_static} from '${internalImport('render/preact.js')}';`,
+ wrapper: `__preact_${kind}(${name}, ${JSON.stringify({
+ componentUrl: getComponentUrl(),
+ componentExport: 'default',
+ frameworkUrls: {
+ preact: dynamicImports.get('preact'),
+ },
+ })})`,
+ wrapperImport: `import {__preact_${kind}} from '${internalImport('render/preact.js')}';`,
};
}
+
+ return {
+ wrapper: `__preact_static(${name})`,
+ wrapperImport: `import {__preact_static} from '${internalImport('render/preact.js')}';`,
+ };
}
case 'react': {
- if (kind === 'dynamic') {
+ if (['load', 'idle', 'visible'].includes(kind)) {
return {
- wrapper: `__react_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get(
- 'react'
- )!}', '${dynamicImports.get('react-dom')!}')`,
- wrapperImport: `import {__react_dynamic} from '${internalImport('render/react.js')}';`,
- };
- } else {
- return {
- wrapper: `__react_static(${name})`,
- wrapperImport: `import {__react_static} from '${internalImport('render/react.js')}';`,
+ wrapper: `__preact_${kind}(${name}, ${JSON.stringify({
+ componentUrl: getComponentUrl(),
+ componentExport: 'default',
+ frameworkUrls: {
+ react: dynamicImports.get('react'),
+ 'react-dom': dynamicImports.get('react-dom'),
+ },
+ })})`,
+ wrapperImport: `import {__preact_${kind}} from '${internalImport('render/preact.js')}';`,
};
}
+
+ return {
+ wrapper: `__react_static(${name})`,
+ wrapperImport: `import {__react_static} from '${internalImport('render/react.js')}';`,
+ };
}
case 'svelte': {
- if (kind === 'dynamic') {
+ if (['load', 'idle', 'visible'].includes(kind)) {
return {
- wrapper: `__svelte_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.svelte.js'))}, \`http://TEST\${import.meta.url}\`).pathname)`,
- wrapperImport: `import {__svelte_dynamic} from '${internalImport('render/svelte.js')}';`,
- };
- } else {
- return {
- wrapper: `__svelte_static(${name})`,
- wrapperImport: `import {__svelte_static} from '${internalImport('render/svelte.js')}';`,
+ wrapper: `__svelte_${kind}(${name}, ${JSON.stringify({
+ componentUrl: getComponentUrl('.svelte.js'),
+ componentExport: 'default',
+ })})`,
+ wrapperImport: `import {__svelte_${kind}} from '${internalImport('render/svelte.js')}';`,
};
}
+
+ return {
+ wrapper: `__svelte_static(${name})`,
+ wrapperImport: `import {__svelte_static} from '${internalImport('render/svelte.js')}';`,
+ };
}
case 'vue': {
- if (kind === 'dynamic') {
+ if (['load', 'idle', 'visible'].includes(kind)) {
return {
- wrapper: `__vue_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.vue.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get(
- 'vue'
- )!}')`,
- wrapperImport: `import {__vue_dynamic} from '${internalImport('render/vue.js')}';`,
- };
- } else {
- return {
- wrapper: `__vue_static(${name})`,
- wrapperImport: `
- import {__vue_static} from '${internalImport('render/vue.js')}';
- `,
+ wrapper: `__vue_${kind}(${name}, ${JSON.stringify({
+ componentUrl: getComponentUrl('.vue.js'),
+ componentExport: 'default',
+ frameworkUrls: {
+ vue: dynamicImports.get('vue'),
+ },
+ })})`,
+ wrapperImport: `import {__vue_${kind}} from '${internalImport('render/vue.js')}';`,
};
}
+
+ return {
+ wrapper: `__vue_static(${name})`,
+ wrapperImport: `import {__vue_static} from '${internalImport('render/vue.js')}';`,
+ };
}
default: {
throw new Error(`Unknown component type`);
diff --git a/src/frontend/render/preact.ts b/src/frontend/render/preact.ts
index 50bb9344e..359202372 100644
--- a/src/frontend/render/preact.ts
+++ b/src/frontend/render/preact.ts
@@ -1,30 +1,25 @@
-import renderToString from 'preact-render-to-string';
+import { Renderer, createRenderer } from './renderer';
import { h, render } from 'preact';
-import type { Component } from 'preact';
+import { renderToString } from 'preact-render-to-string';
// This prevents tree-shaking of render.
Function.prototype(render);
-export function __preact_static(PreactComponent: Component) {
- return (attrs: Record<string, any>, ...children: any): string => {
- let html = renderToString(
- h(
- PreactComponent as any, // Preact's types seem wrong...
- attrs,
- children
- )
- );
- return html;
- };
-}
+const Preact: Renderer = {
+ renderStatic(Component) {
+ return (props, ...children) => renderToString(h(Component, props, ...children));
+ },
+ imports: {
+ preact: ['render', 'h'],
+ },
+ render({ Component, root, props }) {
+ return `render(h(${Component}, ${props}), ${root})`;
+ },
+};
-export function __preact_dynamic(PreactComponent: Component, importUrl: string, preactUrl: string) {
- const placeholderId = `placeholder_${String(Math.random())}`;
- return (attrs: Record<string, string>, ...children: any) => {
- return `<div id="${placeholderId}"></div><script type="module">
- import {h, render} from '${preactUrl}';
- import Component from '${importUrl}';
- render(h(Component, ${JSON.stringify(attrs)}), document.getElementById('${placeholderId}'));
- </script>`;
- };
-}
+const renderer = createRenderer(Preact);
+
+export const __preact_static = renderer.static;
+export const __preact_load = renderer.load;
+export const __preact_idle = renderer.idle;
+export const __preact_visible = renderer.visible;
diff --git a/src/frontend/render/react.ts b/src/frontend/render/react.ts
index cd037c35f..8b127cf96 100644
--- a/src/frontend/render/react.ts
+++ b/src/frontend/render/react.ts
@@ -1,22 +1,23 @@
+import { Renderer, createRenderer } from './renderer';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
-export function __react_static(ReactComponent: any) {
- return (attrs: Record<string, any>, ...children: any): string => {
- let html = ReactDOMServer.renderToString(React.createElement(ReactComponent, attrs, children));
- return html;
- };
-}
+const ReactRenderer: Renderer = {
+ renderStatic(Component) {
+ return (props, ...children) => ReactDOMServer.renderToString(React.createElement(Component, props, children));
+ },
+ imports: {
+ react: ['default as React'],
+ 'react-dom': ['default as ReactDOM'],
+ },
+ render({ Component, root, props }) {
+ return `ReactDOM.render(React.createElement(${Component}, ${props}), ${root})`;
+ },
+};
-export function __react_dynamic(ReactComponent: any, importUrl: string, reactUrl: string, reactDomUrl: string) {
- const placeholderId = `placeholder_${String(Math.random())}`;
- return (attrs: Record<string, string>, ...children: any) => {
- return `<div id="${placeholderId}"></div><script type="module">
- import React from '${reactUrl}';
- import ReactDOM from '${reactDomUrl}';
- import Component from '${importUrl}';
+const renderer = createRenderer(ReactRenderer);
- ReactDOM.render(React.createElement(Component, ${JSON.stringify(attrs)}), document.getElementById('${placeholderId}'));
- </script>`;
- };
-}
+export const __react_static = renderer.static;
+export const __react_load = renderer.load;
+export const __react_idle = renderer.idle;
+export const __react_visible = renderer.visible;
diff --git a/src/frontend/render/renderer.ts b/src/frontend/render/renderer.ts
new file mode 100644
index 000000000..ceb460e40
--- /dev/null
+++ b/src/frontend/render/renderer.ts
@@ -0,0 +1,63 @@
+interface DynamicRenderContext {
+ componentUrl: string;
+ componentExport: string;
+ frameworkUrls: string;
+}
+
+export interface Renderer {
+ renderStatic(Component: any): (props: Record<string, string>, ...children: any[]) => string;
+ render(context: { root: string; Component: string; props: string; [key: string]: string }): string;
+ imports?: Record<string, string[]>;
+}
+
+export function createRenderer(renderer: Renderer) {
+ const _static: Renderer['renderStatic'] = (Component: any) => renderer.renderStatic(Component);
+ const _imports = (context: DynamicRenderContext) => {
+ const values = Object.values(renderer.imports ?? {})
+ .reduce((acc, values) => {
+ return [...acc, `{ ${values.join(', ')} }`];
+ }, [])
+ .join(', ');
+ const libs = Object.keys(renderer.imports ?? {})
+ .reduce((acc: string[], lib: string) => {
+ return [...acc, `import("${context.frameworkUrls[lib as any]}")`];
+ }, [])
+ .join(',');
+ return `const [{${context.componentExport}: Component}, ${values}] = await Promise.all([import(${context.componentUrl})${renderer.imports ? ', ' + libs : ''}]);`;
+ };
+ const serializeProps = (props: Record<string, any>) => JSON.stringify(props);
+ const createContext = () => {
+ const astroId = `${Math.floor(Math.random() * 1e16)}`;
+ return { ['data-astro-id']: astroId, root: `document.querySelector('[data-astro-id="${astroId}"]')`, Component: 'Component' };
+ };
+ const createDynamicRender = (
+ wrapperStart: string | ((context: ReturnType<typeof createContext>) => string),
+ wrapperEnd: string | ((context: ReturnType<typeof createContext>) => string)
+ ) => (Component: any, renderContext: DynamicRenderContext) => {
+ const innerContext = createContext();
+ return (props: Record<string, any>, ...children: any[]) => {
+ let value: string;
+ try {
+ value = _static(Component)(props, ...children);
+ } catch (e) {
+ value = '';
+ }
+ value = `<div style="display:contents;" data-astro-id="${innerContext['data-astro-id']}">${value}</div>`;
+
+ return `${value}\n<script type="module">${typeof wrapperStart === 'function' ? wrapperStart(innerContext) : wrapperStart}\n${_imports(renderContext)}\n${renderer.render({
+ ...innerContext,
+ props: serializeProps(props),
+ })}\n${typeof wrapperEnd === 'function' ? wrapperEnd(innerContext) : wrapperEnd}</script>`;
+ };
+ };
+
+ return {
+ static: _static,
+ load: createDynamicRender('(async () => {', '})()'),
+ idle: createDynamicRender('requestIdleCallback(async () => {', '})'),
+ visible: createDynamicRender(
+ 'const o = new IntersectionObserver(async ([entry]) => { if (!entry.isIntersection) { return; } o.disconnect();',
+ ({ root }) => `}); o.observe(${root})`
+ ),
+ };
+}
diff --git a/src/frontend/render/svelte.ts b/src/frontend/render/svelte.ts
index 51cb778db..15676e8c6 100644
--- a/src/frontend/render/svelte.ts
+++ b/src/frontend/render/svelte.ts
@@ -1,24 +1,23 @@
-import { SvelteComponent as Component } from 'svelte';
+import { Renderer, createRenderer } from './renderer';
-export function __svelte_static(SvelteComponent: Component) {
- return (attrs: Record<string, any>, ...children: any): string => {
- // TODO include head and css stuff too...
- const { html } = SvelteComponent.render(attrs);
+const SvelteRenderer: Renderer = {
+ renderStatic(Component) {
+ return (props, ...children) => {
+ const { html } = Component.render(props);
+ return html;
+ };
+ },
+ render({ Component, root, props }) {
+ return `new ${Component}({
+ target: ${root},
+ props: ${props}
+ })`;
+ },
+};
- return html;
- };
-}
+const renderer = createRenderer(SvelteRenderer);
-export function __svelte_dynamic(SvelteComponent: Component, importUrl: string) {
- const placeholderId = `placeholder_${String(Math.random())}`;
- return (attrs: Record<string, string>, ...children: any) => {
- return `<div id="${placeholderId}"></div><script type="module">
- import Component from '${importUrl}';
-
- new Component({
- target: document.getElementById('${placeholderId}'),
- props: ${JSON.stringify(attrs)}
- });
- </script>`;
- };
-}
+export const __svelte_static = renderer.static;
+export const __svelte_load = renderer.load;
+export const __svelte_idle = renderer.idle;
+export const __svelte_visible = renderer.visible;
diff --git a/src/frontend/render/vue.ts b/src/frontend/render/vue.ts
index 6b89aa11e..bcf6b70bd 100644
--- a/src/frontend/render/vue.ts
+++ b/src/frontend/render/vue.ts
@@ -1,39 +1,34 @@
-import type { Component } from 'vue';
-
import { renderToString } from '@vue/server-renderer';
import { createSSRApp, h as createElement } from 'vue';
+import { Renderer, createRenderer } from './renderer';
-export function __vue_static(VueComponent: Component) {
- return async (attrs: Record<string, any>, ...children: any): Promise<string> => {
- const app = createSSRApp({
- components: {
- VueComponent,
- },
- render() {
- return createElement(VueComponent as any, attrs);
- },
- });
-
- const html = await renderToString(app);
-
- return html;
- };
-}
-
-export function __vue_dynamic(VueComponent: Component, importUrl: string, vueUrl: string) {
- const placeholderId = `placeholder_${String(Math.random())}`;
- return (attrs: Record<string, string>, ...children: any) => {
- return `<div id="${placeholderId}"></div><script type="module">
- import Component from '${importUrl}';
- import {createApp, h as createElement} from '${vueUrl}';
+const Vue: Renderer = {
+ renderStatic(Component) {
+ return (props, ...children) => {
+ const app = createSSRApp({
+ components: {
+ Component,
+ },
+ render() {
+ return createElement(Component as any, props);
+ },
+ });
+ // Uh oh, Vue's `renderToString` is async... Does that mean everything needs to be?
+ return renderToString(app) as any;
+ };
+ },
+ imports: {
+ vue: ['createApp', 'h as createElement'],
+ },
+ render({ Component, root, props }) {
+ return `const App = { render() { return createElement(${Component}, ${props} )} };
+createApp(App).mount(${root})`;
+ },
+};
- const App = {
- render() {
- return createElement(Component, ${JSON.stringify(attrs)});
- }
- };
+const renderer = createRenderer(Vue);
- createApp(App).mount(document.getElementById('${placeholderId}'));
- </script>`;
- };
-}
+export const __vue_static = renderer.static;
+export const __vue_load = renderer.load;
+export const __vue_idle = renderer.idle;
+export const __vue_visible = renderer.visible;