diff options
author | 2021-04-15 10:55:50 -0500 | |
---|---|---|
committer | 2021-04-15 10:55:50 -0500 | |
commit | 22ca9e0aacf26bf82aa5d0ddd6d1e1d495a1a945 (patch) | |
tree | db56218dd905aca708e39fae6c58d31f99df24dc /src | |
parent | ea33d7b2ab30f6434986bb0d8671e7f681076268 (diff) | |
download | astro-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.ts | 2 | ||||
-rw-r--r-- | src/compiler/codegen/index.ts | 3 | ||||
-rw-r--r-- | src/frontend/SvelteWrapper.svelte | 7 | ||||
-rw-r--r-- | src/frontend/SvelteWrapper.svelte.client.ts | 168 | ||||
-rw-r--r-- | src/frontend/SvelteWrapper.svelte.server.ts | 12 | ||||
-rw-r--r-- | src/frontend/render/preact.ts | 13 | ||||
-rw-r--r-- | src/frontend/render/react.ts | 11 | ||||
-rw-r--r-- | src/frontend/render/renderer.ts | 22 | ||||
-rw-r--r-- | src/frontend/render/svelte.ts | 14 | ||||
-rw-r--r-- | src/frontend/render/utils.ts | 47 | ||||
-rw-r--r-- | src/frontend/render/vue.ts | 40 | ||||
-rw-r--r-- | src/frontend/runtime/svelte.ts | 10 | ||||
-rw-r--r-- | src/runtime.ts | 2 |
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: { |