summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Nate Moore <natemoo-re@users.noreply.github.com> 2021-04-15 10:55:50 -0500
committerGravatar GitHub <noreply@github.com> 2021-04-15 10:55:50 -0500
commit22ca9e0aacf26bf82aa5d0ddd6d1e1d495a1a945 (patch)
treedb56218dd905aca708e39fae6c58d31f99df24dc /src
parentea33d7b2ab30f6434986bb0d8671e7f681076268 (diff)
downloadastro-22ca9e0aacf26bf82aa5d0ddd6d1e1d495a1a945.tar.gz
astro-22ca9e0aacf26bf82aa5d0ddd6d1e1d495a1a945.tar.zst
astro-22ca9e0aacf26bf82aa5d0ddd6d1e1d495a1a945.zip
Support children inside of components (#72)
* chore(examples): add kitchen-sink * feat: support children in rendered components * feat: add support for rendering children in Svelte * fix: cleanup p/react fragment children * chore: add @ts-nocheck to svelte files * chore: update lockfiles * fix: types * feat: memoize frontend/renderer/utils * fix: disable eslint for compiled SvelteWrapper * fix: add missing dep Co-authored-by: Nate Moore <nate@skypack.dev>
Diffstat (limited to 'src')
-rw-r--r--src/@types/renderer.ts2
-rw-r--r--src/compiler/codegen/index.ts3
-rw-r--r--src/frontend/SvelteWrapper.svelte7
-rw-r--r--src/frontend/SvelteWrapper.svelte.client.ts168
-rw-r--r--src/frontend/SvelteWrapper.svelte.server.ts12
-rw-r--r--src/frontend/render/preact.ts13
-rw-r--r--src/frontend/render/react.ts11
-rw-r--r--src/frontend/render/renderer.ts22
-rw-r--r--src/frontend/render/svelte.ts14
-rw-r--r--src/frontend/render/utils.ts47
-rw-r--r--src/frontend/render/vue.ts40
-rw-r--r--src/frontend/runtime/svelte.ts10
-rw-r--r--src/runtime.ts2
13 files changed, 323 insertions, 28 deletions
diff --git a/src/@types/renderer.ts b/src/@types/renderer.ts
index e8c262b41..f89cb6664 100644
--- a/src/@types/renderer.ts
+++ b/src/@types/renderer.ts
@@ -11,6 +11,8 @@ export interface DynamicRenderContext {
export interface ComponentRenderer<T> {
renderStatic: StaticRendererGenerator<T>;
+ jsxPragma?: (...args: any) => any;
+ jsxPragmaName?: string;
render(context: { root: string; Component: string; props: string; [key: string]: string }): string;
imports?: Record<string, string[]>;
}
diff --git a/src/compiler/codegen/index.ts b/src/compiler/codegen/index.ts
index 13c47117b..d3e8a45dd 100644
--- a/src/compiler/codegen/index.ts
+++ b/src/compiler/codegen/index.ts
@@ -208,6 +208,9 @@ function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo
wrapper: `__svelte_${kind}(${name}, ${JSON.stringify({
componentUrl: getComponentUrl('.svelte.js'),
componentExport: 'default',
+ frameworkUrls: {
+ 'astro/frontend/runtime/svelte': internalImport('runtime/svelte.js'),
+ },
})})`,
wrapperImport: `import {__svelte_${kind}} from '${internalImport('render/svelte.js')}';`,
};
diff --git a/src/frontend/SvelteWrapper.svelte b/src/frontend/SvelteWrapper.svelte
new file mode 100644
index 000000000..eb4cbb7d9
--- /dev/null
+++ b/src/frontend/SvelteWrapper.svelte
@@ -0,0 +1,7 @@
+<script>
+const { __astro_component: Component, __astro_children, ...props } = $$props;
+</script>
+
+<Component {...props}>
+ {@html __astro_children}
+</Component>
diff --git a/src/frontend/SvelteWrapper.svelte.client.ts b/src/frontend/SvelteWrapper.svelte.client.ts
new file mode 100644
index 000000000..9ef0edd92
--- /dev/null
+++ b/src/frontend/SvelteWrapper.svelte.client.ts
@@ -0,0 +1,168 @@
+/* eslint-disable */
+// @ts-nocheck
+// TODO: don't precompile this, but it works for now
+import {
+ HtmlTag,
+ SvelteComponentDev,
+ assign,
+ claim_component,
+ create_component,
+ destroy_component,
+ detach_dev,
+ dispatch_dev,
+ empty,
+ exclude_internal_props,
+ get_spread_object,
+ get_spread_update,
+ init,
+ insert_dev,
+ mount_component,
+ noop,
+ not_equal,
+ transition_in,
+ transition_out,
+ validate_slots
+} from "svelte/internal";
+
+const file = "App.svelte";
+
+// (5:0) <Component {...props}>
+function create_default_slot(ctx) {
+ let html_tag;
+ let html_anchor;
+
+ const block = {
+ c: function create() {
+ html_anchor = empty();
+ this.h();
+ },
+ l: function claim(nodes) {
+ html_anchor = empty();
+ this.h();
+ },
+ h: function hydrate() {
+ html_tag = new HtmlTag(html_anchor);
+ },
+ m: function mount(target, anchor) {
+ html_tag.m(/*__astro_children*/ ctx[1], target, anchor);
+ insert_dev(target, html_anchor, anchor);
+ },
+ p: noop,
+ d: function destroy(detaching) {
+ if (detaching) detach_dev(html_anchor);
+ if (detaching) html_tag.d();
+ }
+ };
+
+ dispatch_dev("SvelteRegisterBlock", {
+ block,
+ id: create_default_slot.name,
+ type: "slot",
+ source: "(5:0) <Component {...props}>",
+ ctx
+ });
+
+ return block;
+}
+
+function create_fragment(ctx) {
+ let component;
+ let current;
+ const component_spread_levels = [/*props*/ ctx[2]];
+
+ let component_props = {
+ $$slots: { default: [create_default_slot] },
+ $$scope: { ctx }
+ };
+
+ for (let i = 0; i < component_spread_levels.length; i += 1) {
+ component_props = assign(component_props, component_spread_levels[i]);
+ }
+
+ component = new /*Component*/ ctx[0]({ props: component_props, $$inline: true });
+
+ const block = {
+ c: function create() {
+ create_component(component.$$.fragment);
+ },
+ l: function claim(nodes) {
+ claim_component(component.$$.fragment, nodes);
+ },
+ m: function mount(target, anchor) {
+ mount_component(component, target, anchor);
+ current = true;
+ },
+ p: function update(ctx, [dirty]) {
+ const component_changes = (dirty & /*props*/ 4)
+ ? get_spread_update(component_spread_levels, [get_spread_object(/*props*/ ctx[2])])
+ : {};
+
+ if (dirty & /*$$scope*/ 16) {
+ component_changes.$$scope = { dirty, ctx };
+ }
+
+ component.$set(component_changes);
+ },
+ i: function intro(local) {
+ if (current) return;
+ transition_in(component.$$.fragment, local);
+ current = true;
+ },
+ o: function outro(local) {
+ transition_out(component.$$.fragment, local);
+ current = false;
+ },
+ d: function destroy(detaching) {
+ destroy_component(component, detaching);
+ }
+ };
+
+ dispatch_dev("SvelteRegisterBlock", {
+ block,
+ id: create_fragment.name,
+ type: "component",
+ source: "",
+ ctx
+ });
+
+ return block;
+}
+
+function instance($$self, $$props, $$invalidate) {
+ let { $$slots: slots = {}, $$scope } = $$props;
+ validate_slots("App", slots, []);
+ const { __astro_component: Component, __astro_children, ...props } = $$props;
+
+ $$self.$$set = $$new_props => {
+ $$invalidate(3, $$props = assign(assign({}, $$props), exclude_internal_props($$new_props)));
+ };
+
+ $$self.$capture_state = () => ({ Component, __astro_children, props });
+
+ $$self.$inject_state = $$new_props => {
+ $$invalidate(3, $$props = assign(assign({}, $$props), $$new_props));
+ };
+
+ if ($$props && "$$inject" in $$props) {
+ $$self.$inject_state($$props.$$inject);
+ }
+
+ $$props = exclude_internal_props($$props);
+ return [Component, __astro_children, props];
+}
+
+class App extends SvelteComponentDev {
+ constructor(options) {
+ super(options);
+ init(this, options, instance, create_fragment, not_equal, {});
+
+ dispatch_dev("SvelteRegisterComponent", {
+ component: this,
+ tagName: "App",
+ options,
+ id: create_fragment.name
+ });
+ }
+}
+
+export default App;
diff --git a/src/frontend/SvelteWrapper.svelte.server.ts b/src/frontend/SvelteWrapper.svelte.server.ts
new file mode 100644
index 000000000..d0e3679f1
--- /dev/null
+++ b/src/frontend/SvelteWrapper.svelte.server.ts
@@ -0,0 +1,12 @@
+/* eslint-disable */
+// @ts-nocheck
+// TODO: don't precompile this, but it works for now
+/* App.svelte generated by Svelte v3.37.0 */
+import { create_ssr_component, validate_component } from "svelte/internal";
+
+const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
+ const { __astro_component: Component, __astro_children, ...props } = $$props;
+ return `${validate_component(Component, "Component").$$render($$result, Object.assign(props), {}, { default: () => `${__astro_children}` })}`;
+});
+
+export default App;
diff --git a/src/frontend/render/preact.ts b/src/frontend/render/preact.ts
index 5f5c03f56..df7c903b0 100644
--- a/src/frontend/render/preact.ts
+++ b/src/frontend/render/preact.ts
@@ -1,5 +1,6 @@
import { h, render, ComponentType } from 'preact';
import { renderToString } from 'preact-render-to-string';
+import { childrenToVnodes } from './utils';
import type { ComponentRenderer } from '../../@types/renderer';
import { createRenderer } from './renderer';
@@ -7,14 +8,18 @@ import { createRenderer } from './renderer';
Function.prototype(render);
const Preact: ComponentRenderer<ComponentType> = {
+ jsxPragma: h,
+ jsxPragmaName: 'h',
renderStatic(Component) {
- return async (props, ...children) => renderToString(h(Component, props, ...children));
+ return async (props, ...children) => {
+ return renderToString(h(Component, props, childrenToVnodes(h, children)));
+ }
},
imports: {
- preact: ['render', 'h'],
+ preact: ['render', 'Fragment', 'h'],
},
- render({ Component, root, props }) {
- return `render(h(${Component}, ${props}), ${root})`;
+ render({ Component, root, props, children }) {
+ return `render(h(${Component}, ${props}, h(Fragment, null, ...${children})), ${root})`;
},
};
diff --git a/src/frontend/render/react.ts b/src/frontend/render/react.ts
index 56c6decb0..ac812e9c1 100644
--- a/src/frontend/render/react.ts
+++ b/src/frontend/render/react.ts
@@ -2,17 +2,22 @@ import type { ComponentRenderer } from '../../@types/renderer';
import React, { ComponentType } from 'react';
import ReactDOMServer from 'react-dom/server';
import { createRenderer } from './renderer';
+import { childrenToVnodes } from './utils';
const ReactRenderer: ComponentRenderer<ComponentType> = {
+ jsxPragma: React.createElement,
+ jsxPragmaName: 'React.createElement',
renderStatic(Component) {
- return async (props, ...children) => ReactDOMServer.renderToString(React.createElement(Component, props, children));
+ return async (props, ...children) => {
+ return ReactDOMServer.renderToString(React.createElement(Component, props, childrenToVnodes(React.createElement, children)));
+ }
},
imports: {
react: ['default: React'],
'react-dom': ['default: ReactDOM'],
},
- render({ Component, root, props }) {
- return `ReactDOM.render(React.createElement(${Component}, ${props}), ${root})`;
+ render({ Component, root, children, props }) {
+ return `ReactDOM.hydrate(React.createElement(${Component}, ${props}, React.createElement(React.Fragment, null, ...${children})), ${root})`;
},
};
diff --git a/src/frontend/render/renderer.ts b/src/frontend/render/renderer.ts
index 02caf145f..25aec3653 100644
--- a/src/frontend/render/renderer.ts
+++ b/src/frontend/render/renderer.ts
@@ -1,4 +1,5 @@
import type { DynamicRenderContext, DynamicRendererGenerator, SupportedComponentRenderer, StaticRendererGenerator } from '../../@types/renderer';
+import { childrenToH } from './utils';
/** Initialize Astro Component renderer for Static and Dynamic components */
export function createRenderer(renderer: SupportedComponentRenderer) {
@@ -16,7 +17,7 @@ export function createRenderer(renderer: SupportedComponentRenderer) {
.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 serializeProps = ({ children: _, ...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' };
@@ -32,10 +33,19 @@ export function createRenderer(renderer: SupportedComponentRenderer) {
}
value = `<div 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>`;
+ const script = `
+ ${typeof wrapperStart === 'function' ? wrapperStart(innerContext) : wrapperStart}
+ ${_imports(renderContext)}
+ ${renderer.render({
+ ...innerContext,
+ props: serializeProps(props),
+ children: `[${childrenToH(renderer, children) ?? ''}]`,
+ childrenAsString: `\`${children}\``
+ })}
+ ${typeof wrapperEnd === 'function' ? wrapperEnd(innerContext) : wrapperEnd}
+ `;
+
+ return [value, `<script type="module">${script.trim()}</script>`].join('\n');
};
};
@@ -45,7 +55,7 @@ export function createRenderer(renderer: SupportedComponentRenderer) {
idle: createDynamicRender('requestIdleCallback(async () => {', '})'),
visible: createDynamicRender(
'const o = new IntersectionObserver(async ([entry]) => { if (!entry.isIntersecting) { return; } o.disconnect();',
- ({ root }) => `}); o.observe(${root})`
+ ({ root }) => `}); Array.from(${root}.item(0).children).forEach(child => o.observe(child))`
),
};
}
diff --git a/src/frontend/render/svelte.ts b/src/frontend/render/svelte.ts
index d3c11638d..7d657c3f7 100644
--- a/src/frontend/render/svelte.ts
+++ b/src/frontend/render/svelte.ts
@@ -1,20 +1,20 @@
import type { ComponentRenderer } from '../../@types/renderer';
import type { SvelteComponent } from 'svelte';
import { createRenderer } from './renderer';
+import SvelteWrapper from '../SvelteWrapper.svelte.server';
const SvelteRenderer: ComponentRenderer<SvelteComponent> = {
renderStatic(Component) {
return async (props, ...children) => {
- const { html } = Component.render(props);
+ const { html } = SvelteWrapper.render({ __astro_component: Component, __astro_children: children.join('\n'), ...props });
return html;
};
},
- render({ Component, root, props }) {
- return `new ${Component}({
- target: ${root},
- props: ${props},
- hydrate: true
- })`;
+ imports: {
+ 'astro/frontend/runtime/svelte': ['default: render']
+ },
+ render({ Component, root, props, childrenAsString }) {
+ return `render(${root}, ${Component}, ${props}, ${childrenAsString});`;
},
};
diff --git a/src/frontend/render/utils.ts b/src/frontend/render/utils.ts
new file mode 100644
index 000000000..7001a7f95
--- /dev/null
+++ b/src/frontend/render/utils.ts
@@ -0,0 +1,47 @@
+import unified from 'unified';
+import parse from 'rehype-parse';
+import toH from 'hast-to-hyperscript';
+import { ComponentRenderer } from '../../@types/renderer';
+import moize from 'moize';
+
+/** @internal */
+function childrenToTree(children: string[]) {
+ return children.map(child => (unified().use(parse, { fragment: true }).parse(child) as any).children.pop());
+}
+
+/**
+ * Converts an HTML fragment string into vnodes for rendering via provided framework
+ * @param h framework's `createElement` function
+ * @param children the HTML string children
+ */
+export const childrenToVnodes = moize.deep(function childrenToVnodes(h: any, children: string[]) {
+ const tree = childrenToTree(children);
+ const vnodes = tree.map(subtree => toH(h, subtree));
+ return vnodes;
+})
+
+/**
+ * Converts an HTML fragment string into h function calls as a string
+ * @param h framework's `createElement` function
+ * @param children the HTML string children
+ */
+export const childrenToH = moize.deep(function childrenToH(renderer: ComponentRenderer<any>, children: string[]): any {
+ if (!renderer.jsxPragma) return;
+ const tree = childrenToTree(children);
+ const innerH = (name: any, attrs: Record<string, any>|null = null, _children: string[]|null = null) => {
+ const vnode = renderer.jsxPragma?.(name, attrs, _children);
+ const childStr = _children ? `, [${_children.map(child => serializeChild(child)).join(',')}]` : '';
+ /* fix(react): avoid hard-coding keys into the serialized tree */
+ if (attrs && attrs.key) attrs.key = undefined;
+ const __SERIALIZED = `${renderer.jsxPragmaName}("${name}", ${attrs ? JSON.stringify(attrs) : 'null'}${childStr})` as string;
+ return { ...vnode, __SERIALIZED }
+ }
+ const serializeChild = (child: unknown) => {
+ if (typeof child === 'string') return `\`${child}\``;
+ if (typeof child === 'number' || typeof child === 'boolean') return `${child}`;
+ if (child === null) return `null`;
+ if ((child as any).__SERIALIZED) return (child as any).__SERIALIZED;
+ return innerH(child).__SERIALIZED;
+ }
+ return tree.map(subtree => toH(innerH, subtree).__SERIALIZED);
+})
diff --git a/src/frontend/render/vue.ts b/src/frontend/render/vue.ts
index 628544775..cf2c203be 100644
--- a/src/frontend/render/vue.ts
+++ b/src/frontend/render/vue.ts
@@ -1,20 +1,45 @@
import type { ComponentRenderer } from '../../@types/renderer';
import type { Component as VueComponent } from 'vue';
import { renderToString } from '@vue/server-renderer';
-import { createSSRApp, h as createElement } from 'vue';
+import { defineComponent, createSSRApp, h as createElement } from 'vue';
import { createRenderer } from './renderer';
+/**
+ * Users might attempt to use :vueAttribute syntax to pass primitive values.
+ * If so, try to JSON.parse them to get the primitives
+ */
+function cleanPropsForVue(obj: Record<string, any>) {
+ let cleaned = {} as any;
+ for (let [key, value] of Object.entries(obj)) {
+ if (key.startsWith(':')) {
+ key = key.slice(1);
+ if (typeof value === 'string') {
+ try {
+ value = JSON.parse(value);
+ } catch (e) {}
+ }
+ }
+ cleaned[key] = value;
+ }
+ return cleaned;
+}
+
const Vue: ComponentRenderer<VueComponent> = {
+ jsxPragma: createElement,
+ jsxPragmaName: 'createElement',
renderStatic(Component) {
return async (props, ...children) => {
- const app = createSSRApp({
+ const App = defineComponent({
components: {
- Component,
+ Component
},
- render() {
- return createElement(Component as any, props);
+ data() {
+ return { props }
},
+ template: `<Component v-bind="props">${children.join('\n')}</Component>`
});
+
+ const app = createSSRApp(App);
const html = await renderToString(app);
return html;
};
@@ -22,8 +47,9 @@ const Vue: ComponentRenderer<VueComponent> = {
imports: {
vue: ['createApp', 'h: createElement'],
},
- render({ Component, root, props }) {
- return `const App = { render() { return createElement(${Component}, ${props} )} };
+ render({ Component, root, props, children }) {
+ const vueProps = cleanPropsForVue(JSON.parse(props));
+ return `const App = { render: () => createElement(${Component}, ${JSON.stringify(vueProps)}, { default: () => ${children} }) };
createApp(App).mount(${root});`;
},
};
diff --git a/src/frontend/runtime/svelte.ts b/src/frontend/runtime/svelte.ts
new file mode 100644
index 000000000..8d7f28b76
--- /dev/null
+++ b/src/frontend/runtime/svelte.ts
@@ -0,0 +1,10 @@
+import SvelteWrapper from '../SvelteWrapper.svelte.client';
+import type { SvelteComponent } from 'svelte';
+
+export default (target: Element, component: SvelteComponent, props: any, children: string) => {
+ new SvelteWrapper({
+ target,
+ props: { __astro_component: component, __astro_children: children, ...props },
+ hydrate: true
+ })
+}
diff --git a/src/runtime.ts b/src/runtime.ts
index 9d441aa34..fd203d2bd 100644
--- a/src/runtime.ts
+++ b/src/runtime.ts
@@ -261,7 +261,7 @@ async function createSnowpack(astroConfig: AstroConfig, env: Record<string, any>
plugins: [
[fileURLToPath(new URL('../snowpack-plugin.cjs', import.meta.url)), astroPlugOptions],
require.resolve('@snowpack/plugin-sass'),
- require.resolve('@snowpack/plugin-svelte'),
+ [require.resolve('@snowpack/plugin-svelte'), { compilerOptions: { hydratable: true }}],
require.resolve('@snowpack/plugin-vue'),
],
devOptions: {