123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- import React from 'react';
- import ReactDOM from 'react-dom/server';
- import StaticHtml from './static-html.js';
- import { incrementId } from './context.js';
- import opts from 'astro:react:opts';
- 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,
- };
|