diff options
Diffstat (limited to 'packages/integrations/react')
-rw-r--r-- | packages/integrations/react/client.js | 13 | ||||
-rw-r--r-- | packages/integrations/react/jsx-runtime.js | 8 | ||||
-rw-r--r-- | packages/integrations/react/package.json | 43 | ||||
-rw-r--r-- | packages/integrations/react/server.js | 67 | ||||
-rw-r--r-- | packages/integrations/react/src/index.ts | 54 | ||||
-rw-r--r-- | packages/integrations/react/static-html.js | 24 | ||||
-rw-r--r-- | packages/integrations/react/tsconfig.json | 10 |
7 files changed, 219 insertions, 0 deletions
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js new file mode 100644 index 000000000..a6bc7d3bc --- /dev/null +++ b/packages/integrations/react/client.js @@ -0,0 +1,13 @@ +import { createElement } from 'react'; +import { hydrate } from 'react-dom'; +import StaticHtml from './static-html.js'; + +export default (element) => (Component, props, children) => + hydrate( + createElement( + Component, + { ...props, suppressHydrationWarning: true }, + children != null ? createElement(StaticHtml, { value: children, suppressHydrationWarning: true }) : children + ), + element + ); diff --git a/packages/integrations/react/jsx-runtime.js b/packages/integrations/react/jsx-runtime.js new file mode 100644 index 000000000..d86f698b9 --- /dev/null +++ b/packages/integrations/react/jsx-runtime.js @@ -0,0 +1,8 @@ +// This module is a simple wrapper around react/jsx-runtime so that +// it can run in Node ESM. 'react' doesn't declare this module as an export map +// So we have to use the .js. The .js is not added via the babel automatic JSX transform +// hence this module as a workaround. +import jsxr from 'react/jsx-runtime.js'; +const { jsx, jsxs, Fragment } = jsxr; + +export { jsx, jsxs, Fragment }; diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json new file mode 100644 index 000000000..022b1d710 --- /dev/null +++ b/packages/integrations/react/package.json @@ -0,0 +1,43 @@ +{ + "name": "@astrojs/react", + "description": "Use React components within Astro", + "version": "0.0.1", + "type": "module", + "types": "./dist/index.d.ts", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/astro.git", + "directory": "packages/integrations/react" + }, + "bugs": "https://github.com/withastro/astro/issues", + "homepage": "https://astro.build", + "exports": { + ".": "./dist/index.js", + "./client.js": "./client.js", + "./server.js": "./server.js", + "./package.json": "./package.json", + "./jsx-runtime": "./jsx-runtime.js" + }, + "scripts": { + "build": "astro-scripts build \"src/**/*.ts\" && tsc", + "dev": "astro-scripts dev \"src/**/*.ts\"" + }, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + }, + "devDependencies": { + "astro": "workspace:*", + "astro-scripts": "workspace:*", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "peerDependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "engines": { + "node": "^14.15.0 || >=16.0.0" + } +} diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js new file mode 100644 index 000000000..1c0c41286 --- /dev/null +++ b/packages/integrations/react/server.js @@ -0,0 +1,67 @@ +import React from 'react'; +import ReactDOM from 'react-dom/server.js'; +import StaticHtml from './static-html.js'; + +const reactTypeof = Symbol.for('react.element'); + +function errorIsComingFromPreactComponent(err) { + return err.message && (err.message.startsWith("Cannot read property '__H'") || err.message.includes("(reading '__H')")); +} + +function check(Component, props, children) { + // Note: there are packages that do some unholy things to create "components". + // Checking the $$typeof property catches most of these patterns. + if (typeof Component === 'object') { + const $$typeof = Component['$$typeof']; + return $$typeof && $$typeof.toString().slice('Symbol('.length).startsWith('react'); + } + if (typeof Component !== 'function') return false; + + if (Component.prototype != null && typeof Component.prototype.render === 'function') { + return React.Component.isPrototypeOf(Component) || React.PureComponent.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) { + if (!errorIsComingFromPreactComponent(err)) { + error = err; + } + } + + return React.createElement('div'); + } + + renderToStaticMarkup(Tester, props, children, {}); + + if (error) { + throw error; + } + return isReactComponent; +} + +function renderToStaticMarkup(Component, props, children, metadata) { + delete props['class']; + const vnode = React.createElement(Component, { + ...props, + children: children != null ? React.createElement(StaticHtml, { value: children }) : undefined, + }); + let html; + if (metadata && metadata.hydrate) { + html = ReactDOM.renderToString(vnode); + } else { + html = ReactDOM.renderToStaticMarkup(vnode); + } + return { html }; +} + +export default { + check, + renderToStaticMarkup, +}; diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts new file mode 100644 index 000000000..128c6406d --- /dev/null +++ b/packages/integrations/react/src/index.ts @@ -0,0 +1,54 @@ +import { AstroIntegration } from 'astro'; + +function getRenderer() { + return { + name: '@astrojs/react', + clientEntrypoint: '@astrojs/react/client.js', + serverEntrypoint: '@astrojs/react/server.js', + jsxImportSource: 'react', + jsxTransformOptions: async () => { + const { + default: { default: jsx }, + // @ts-expect-error types not found + } = await import('@babel/plugin-transform-react-jsx'); + return { + plugins: [ + jsx( + {}, + { + runtime: 'automatic', + importSource: '@astrojs/react', + } + ), + ], + }; + }, + }; +} + +function getViteConfiguration() { + return { + optimizeDeps: { + include: ['@astrojs/react/client.js', 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom'], + exclude: ['@astrojs/react/server.js'], + }, + resolve: { + dedupe: ['react', 'react-dom'], + }, + ssr: { + external: ['react-dom/server.js'], + }, + }; +} + +export default function (): AstroIntegration { + return { + name: '@astrojs/react', + hooks: { + 'astro:config:setup': ({ addRenderer, updateConfig }) => { + addRenderer(getRenderer()); + updateConfig({ vite: getViteConfiguration() }); + }, + }, + }; +} diff --git a/packages/integrations/react/static-html.js b/packages/integrations/react/static-html.js new file mode 100644 index 000000000..47130d786 --- /dev/null +++ b/packages/integrations/react/static-html.js @@ -0,0 +1,24 @@ +import { createElement as h } from 'react'; + +/** + * Astro passes `children` as a string of HTML, so we need + * a wrapper `div` to render that content as VNodes. + * + * As a bonus, we can signal to React that this subtree is + * entirely static and will never change via `shouldComponentUpdate`. + */ +const StaticHtml = ({ value }) => { + if (!value) return null; + return h('astro-fragment', { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: value } }); +}; + +/** + * This tells React to opt-out of re-rendering this subtree, + * In addition to being a performance optimization, + * this also allows other frameworks to attach to `children`. + * + * See https://preactjs.com/guide/v8/external-dom-mutations + */ +StaticHtml.shouldComponentUpdate = () => false; + +export default StaticHtml; diff --git a/packages/integrations/react/tsconfig.json b/packages/integrations/react/tsconfig.json new file mode 100644 index 000000000..44baf375c --- /dev/null +++ b/packages/integrations/react/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "module": "ES2020", + "outDir": "./dist", + "target": "ES2020" + } +} |