import opts from 'astro:react:opts'; import React from 'react'; import ReactDOM from 'react-dom/server'; import { incrementId } from './context.js'; import StaticHtml from './static-html.js'; const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); const reactTypeof = Symbol.for('react.element'); function errorIsComingFromPreactComponent(err) { return ( err.message && (err.message.startsWith("Cannot read property '__H'") || err.message.includes("(reading '__H')")) ); } async 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') { return Component['$$typeof'].toString().slice('Symbol('.length).startsWith('react'); } if (typeof Component !== 'function') return false; if (Component.name === 'QwikComponent') return false; // Preact forwarded-ref components can be functions, which React does not support if (typeof Component === 'function' && Component['$$typeof'] === Symbol.for('react.forward_ref')) 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'); } await renderToStaticMarkup(Tester, props, children, {}); if (error) { throw error; } return isReactComponent; } async function getNodeWritable() { let nodeStreamBuiltinModuleName = 'node:stream'; let { Writable } = await import(/* @vite-ignore */ nodeStreamBuiltinModuleName); return Writable; } function needsHydration(metadata) { // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot` return metadata.astroStaticSlot ? !!metadata.hydrate : true; } async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) { let prefix; if (this && this.result) { prefix = incrementId(this.result); } const attrs = { prefix }; delete props['class']; const slots = {}; for (const [key, value] of Object.entries(slotted)) { const name = slotName(key); slots[name] = React.createElement(StaticHtml, { hydrate: needsHydration(metadata), value, name, }); } // Note: create newProps to avoid mutating `props` before they are serialized const newProps = { ...props, ...slots, }; const newChildren = children ?? props.children; if (children && opts.experimentalReactChildren) { attrs['data-react-children'] = true; const convert = await import('./vnode-children.js').then((mod) => mod.default); newProps.children = convert(children); } else if (newChildren != null) { newProps.children = React.createElement(StaticHtml, { hydrate: needsHydration(metadata), value: newChildren, }); } const vnode = React.createElement(Component, newProps); const renderOptions = { identifierPrefix: prefix, }; let html; if (metadata?.hydrate) { if ('renderToReadableStream' in ReactDOM) { html = await renderToReadableStreamAsync(vnode, renderOptions); } else { html = await renderToPipeableStreamAsync(vnode, renderOptions); } } else { if ('renderToReadableStream' in ReactDOM) { html = await renderToReadableStreamAsync(vnode, renderOptions); } else { html = await renderToStaticNodeStreamAsync(vnode, renderOptions); } } return { html, attrs }; } async function renderToPipeableStreamAsync(vnode, options) { const Writable = await getNodeWritable(); let html = ''; return new Promise((resolve, reject) => { let error = undefined; let stream = ReactDOM.renderToPipeableStream(vnode, { ...options, onError(err) { error = err; reject(error); }, onAllReady() { stream.pipe( new Writable({ write(chunk, _encoding, callback) { html += chunk.toString('utf-8'); callback(); }, destroy() { resolve(html); }, }) ); }, }); }); } async function renderToStaticNodeStreamAsync(vnode, options) { const Writable = await getNodeWritable(); let html = ''; return new Promise((resolve, reject) => { let stream = ReactDOM.renderToStaticNodeStream(vnode, options); stream.on('error', (err) => { reject(err); }); stream.pipe( new Writable({ write(chunk, _encoding, callback) { html += chunk.toString('utf-8'); callback(); }, destroy() { resolve(html); }, }) ); }); } /** * Use a while loop instead of "for await" due to cloudflare and Vercel Edge issues * See https://github.com/facebook/react/issues/24169 */ async function readResult(stream) { const reader = stream.getReader(); let result = ''; const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) { if (value) { result += decoder.decode(value); } else { // This closes the decoder decoder.decode(new Uint8Array()); } return result; } result += decoder.decode(value, { stream: true }); } } async function renderToReadableStreamAsync(vnode, options) { return await readResult(await ReactDOM.renderToReadableStream(vnode, options)); } export default { check, renderToStaticMarkup, supportsAstroStaticSlot: true, };