diff --git a/packages/astro/src/internal/__astro_component.ts b/packages/astro/src/internal/__astro_component.ts index 191b05cc7..10844db93 100644 --- a/packages/astro/src/internal/__astro_component.ts +++ b/packages/astro/src/internal/__astro_component.ts @@ -32,16 +32,27 @@ async function resolveRenderer(Component: any, props: any = {}, children?: strin return rendererCache.get(Component); } + const errors: Error[] = []; for (const __renderer of __renderers) { // 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 - const shouldUse = await __renderer.check(Component, props, children); - if (shouldUse) { - rendererCache.set(Component, __renderer); - return __renderer; + try { + const shouldUse: boolean = await __renderer.check(Component, props, children); + + if(shouldUse) { + rendererCache.set(Component, __renderer); + return __renderer; + } + } catch(err) { + errors.push(err); } } + + if(errors.length) { + // For now just throw the first error we encounter. + throw errors[0]; + } } interface AstroComponentProps { diff --git a/packages/astro/test/fixtures/preact-component/snowpack.config.json b/packages/astro/test/fixtures/preact-component/snowpack.config.json new file mode 100644 index 000000000..8f034781d --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/snowpack.config.json @@ -0,0 +1,3 @@ +{ + "workspaceRoot": "../../../../../" +} diff --git a/packages/astro/test/fixtures/preact-component/src/components/Class.jsx b/packages/astro/test/fixtures/preact-component/src/components/Class.jsx new file mode 100644 index 000000000..7cf07c736 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/Class.jsx @@ -0,0 +1,7 @@ +import { h, Component } from 'preact'; + +export default class extends Component { + render() { + return
+ } +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/preact-component/src/components/Function.jsx b/packages/astro/test/fixtures/preact-component/src/components/Function.jsx new file mode 100644 index 000000000..002ce9b51 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/Function.jsx @@ -0,0 +1,5 @@ +import { h, Component } from 'preact'; + +export default function() { + return
; +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/preact-component/src/components/Hooks.jsx b/packages/astro/test/fixtures/preact-component/src/components/Hooks.jsx new file mode 100644 index 000000000..53b7473b1 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/Hooks.jsx @@ -0,0 +1,7 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; + +export default function() { + const [val] = useState('world'); + return
; +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/preact-component/src/pages/class.astro b/packages/astro/test/fixtures/preact-component/src/pages/class.astro new file mode 100644 index 000000000..fae5e846f --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/pages/class.astro @@ -0,0 +1,12 @@ +--- +import ClassComponent from '../components/Class.jsx'; +--- + + + + Preact class component + + + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/preact-component/src/pages/fn.astro b/packages/astro/test/fixtures/preact-component/src/pages/fn.astro new file mode 100644 index 000000000..07a2c7e3e --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/pages/fn.astro @@ -0,0 +1,12 @@ +--- +import FunctionComponent from '../components/Function.jsx'; +--- + + + + Preact function component + + + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/preact-component/src/pages/hooks.astro b/packages/astro/test/fixtures/preact-component/src/pages/hooks.astro new file mode 100644 index 000000000..7c9d94b93 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/pages/hooks.astro @@ -0,0 +1,11 @@ +--- +import Hooks from '../components/Hooks.jsx'; +--- + + + Preact hooks + + + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx b/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx new file mode 100644 index 000000000..d7dfc29f7 --- /dev/null +++ b/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx @@ -0,0 +1,5 @@ + + +export default function ({}) { + return

oops

; +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/src/pages/forgot-import.astro b/packages/astro/test/fixtures/react-component/src/pages/forgot-import.astro new file mode 100644 index 000000000..de5d319d9 --- /dev/null +++ b/packages/astro/test/fixtures/react-component/src/pages/forgot-import.astro @@ -0,0 +1,12 @@ +--- +import ForgotImport from '../components/ForgotImport.jsx'; +--- + + + + Here we are + + + + + \ No newline at end of file diff --git a/packages/astro/test/preact-component.test.js b/packages/astro/test/preact-component.test.js new file mode 100644 index 000000000..2ff4bbfdc --- /dev/null +++ b/packages/astro/test/preact-component.test.js @@ -0,0 +1,34 @@ +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; +import { doc } from './test-utils.js'; +import { setup } from './helpers.js'; + +const PreactComponent = suite('Preact component test'); + +setup(PreactComponent, './fixtures/preact-component'); + +PreactComponent('Can load class component', async ({ runtime }) => { + const result = await runtime.load('/class'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + assert.equal($('#class-component').length, 1, 'Can use class components'); +}); + +PreactComponent('Can load function component', async ({ runtime }) => { + const result = await runtime.load('/fn'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + assert.equal($('#fn-component').length, 1, 'Can use function components'); +}); + +PreactComponent('Can use hooks', async ({ runtime }) => { + const result = await runtime.load('/hooks'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + assert.equal($('#world').length, 1); +}); + +PreactComponent.run(); diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js index 10b44120c..a638bd1c4 100644 --- a/packages/astro/test/react-component.test.js +++ b/packages/astro/test/react-component.test.js @@ -49,4 +49,11 @@ React('Can load Vue', async () => { assert.equal($('#vue-h2').text(), 'Hasta la vista, baby'); }); +React('Get good error message when react import is forgotten', async () => { + const result = await runtime.load('/forgot-import'); + + assert.ok(result.error instanceof ReferenceError); + assert.equal(result.error.message, 'React is not defined'); +}); + React.run(); diff --git a/packages/renderers/renderer-preact/server.js b/packages/renderers/renderer-preact/server.js index 8fd4ccf2f..989e2d385 100644 --- a/packages/renderers/renderer-preact/server.js +++ b/packages/renderers/renderer-preact/server.js @@ -1,13 +1,16 @@ -import { h } from 'preact'; +import { h, Component as BaseComponent } from 'preact'; import { renderToString } from 'preact-render-to-string'; import StaticHtml from './static-html.js'; function check(Component, props, children) { - try { - const { html } = renderToStaticMarkup(Component, props, children); - return Boolean(html); - } catch (e) {} - return false; + if(typeof Component !== 'function') return false; + + if(typeof Component.prototype.render === 'function') { + return BaseComponent.isPrototypeOf(Component); + } + + const { html } = renderToStaticMarkup(Component, props, children); + return Boolean(html); } function renderToStaticMarkup(Component, props, children) { diff --git a/packages/renderers/renderer-react/server.js b/packages/renderers/renderer-react/server.js index 56f2f5aa8..8ac177c7c 100644 --- a/packages/renderers/renderer-react/server.js +++ b/packages/renderers/renderer-react/server.js @@ -1,13 +1,37 @@ -import { createElement as h } from 'react'; +import { Component as BaseComponent, createElement as h } from 'react'; import { renderToStaticMarkup as renderToString } from 'react-dom/server.js'; import StaticHtml from './static-html.js'; +const reactTypeof = Symbol.for('react.element'); + function check(Component, props, children) { - try { - const { html } = renderToStaticMarkup(Component, props, children); - return Boolean(html); - } catch (e) {} - return false; + if(typeof Component !== 'function') return false; + + if(typeof Component.prototype.render === 'function') { + return BaseComponent.isPrototypeOf(Component); + } + + let error = null; + let isReactComponent = false; + function Tester(...args) { + try { + const vnode = Component(...args); + if(vnode && vnode['$$typeof'] === reactTypeof) { + isReactComponent = true; + } + } catch(err) { + error = err; + } + + return h('div'); + } + + renderToStaticMarkup(Tester, props, children); + + if(error) { + throw error; + } + return isReactComponent; } function renderToStaticMarkup(Component, props, children) {