server.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import React from 'react';
  2. import ReactDOM from 'react-dom/server';
  3. import StaticHtml from './static-html.js';
  4. import { incrementId } from './context.js';
  5. import opts from 'astro:react:opts';
  6. const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
  7. const reactTypeof = Symbol.for('react.element');
  8. function errorIsComingFromPreactComponent(err) {
  9. return (
  10. err.message &&
  11. (err.message.startsWith("Cannot read property '__H'") ||
  12. err.message.includes("(reading '__H')"))
  13. );
  14. }
  15. async function check(Component, props, children) {
  16. // Note: there are packages that do some unholy things to create "components".
  17. // Checking the $$typeof property catches most of these patterns.
  18. if (typeof Component === 'object') {
  19. return Component['$$typeof'].toString().slice('Symbol('.length).startsWith('react');
  20. }
  21. if (typeof Component !== 'function') return false;
  22. if (Component.name === 'QwikComponent') return false;
  23. // Preact forwarded-ref components can be functions, which React does not support
  24. if (typeof Component === 'function' && Component['$$typeof'] === Symbol.for('react.forward_ref'))
  25. return false;
  26. if (Component.prototype != null && typeof Component.prototype.render === 'function') {
  27. return React.Component.isPrototypeOf(Component) || React.PureComponent.isPrototypeOf(Component);
  28. }
  29. let error = null;
  30. let isReactComponent = false;
  31. function Tester(...args) {
  32. try {
  33. const vnode = Component(...args);
  34. if (vnode && vnode['$$typeof'] === reactTypeof) {
  35. isReactComponent = true;
  36. }
  37. } catch (err) {
  38. if (!errorIsComingFromPreactComponent(err)) {
  39. error = err;
  40. }
  41. }
  42. return React.createElement('div');
  43. }
  44. await renderToStaticMarkup(Tester, props, children, {});
  45. if (error) {
  46. throw error;
  47. }
  48. return isReactComponent;
  49. }
  50. async function getNodeWritable() {
  51. let nodeStreamBuiltinModuleName = 'node:stream';
  52. let { Writable } = await import(/* @vite-ignore */ nodeStreamBuiltinModuleName);
  53. return Writable;
  54. }
  55. function needsHydration(metadata) {
  56. // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
  57. return metadata.astroStaticSlot ? !!metadata.hydrate : true;
  58. }
  59. async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
  60. let prefix;
  61. if (this && this.result) {
  62. prefix = incrementId(this.result);
  63. }
  64. const attrs = { prefix };
  65. delete props['class'];
  66. const slots = {};
  67. for (const [key, value] of Object.entries(slotted)) {
  68. const name = slotName(key);
  69. slots[name] = React.createElement(StaticHtml, {
  70. hydrate: needsHydration(metadata),
  71. value,
  72. name,
  73. });
  74. }
  75. // Note: create newProps to avoid mutating `props` before they are serialized
  76. const newProps = {
  77. ...props,
  78. ...slots,
  79. };
  80. const newChildren = children ?? props.children;
  81. if (children && opts.experimentalReactChildren) {
  82. attrs['data-react-children'] = true;
  83. const convert = await import('./vnode-children.js').then((mod) => mod.default);
  84. newProps.children = convert(children);
  85. } else if (newChildren != null) {
  86. newProps.children = React.createElement(StaticHtml, {
  87. hydrate: needsHydration(metadata),
  88. value: newChildren,
  89. });
  90. }
  91. const vnode = React.createElement(Component, newProps);
  92. const renderOptions = {
  93. identifierPrefix: prefix,
  94. };
  95. let html;
  96. if (metadata?.hydrate) {
  97. if ('renderToReadableStream' in ReactDOM) {
  98. html = await renderToReadableStreamAsync(vnode, renderOptions);
  99. } else {
  100. html = await renderToPipeableStreamAsync(vnode, renderOptions);
  101. }
  102. } else {
  103. if ('renderToReadableStream' in ReactDOM) {
  104. html = await renderToReadableStreamAsync(vnode, renderOptions);
  105. } else {
  106. html = await renderToStaticNodeStreamAsync(vnode, renderOptions);
  107. }
  108. }
  109. return { html, attrs };
  110. }
  111. async function renderToPipeableStreamAsync(vnode, options) {
  112. const Writable = await getNodeWritable();
  113. let html = '';
  114. return new Promise((resolve, reject) => {
  115. let error = undefined;
  116. let stream = ReactDOM.renderToPipeableStream(vnode, {
  117. ...options,
  118. onError(err) {
  119. error = err;
  120. reject(error);
  121. },
  122. onAllReady() {
  123. stream.pipe(
  124. new Writable({
  125. write(chunk, _encoding, callback) {
  126. html += chunk.toString('utf-8');
  127. callback();
  128. },
  129. destroy() {
  130. resolve(html);
  131. },
  132. })
  133. );
  134. },
  135. });
  136. });
  137. }
  138. async function renderToStaticNodeStreamAsync(vnode, options) {
  139. const Writable = await getNodeWritable();
  140. let html = '';
  141. return new Promise((resolve, reject) => {
  142. let stream = ReactDOM.renderToStaticNodeStream(vnode, options);
  143. stream.on('error', (err) => {
  144. reject(err);
  145. });
  146. stream.pipe(
  147. new Writable({
  148. write(chunk, _encoding, callback) {
  149. html += chunk.toString('utf-8');
  150. callback();
  151. },
  152. destroy() {
  153. resolve(html);
  154. },
  155. })
  156. );
  157. });
  158. }
  159. /**
  160. * Use a while loop instead of "for await" due to cloudflare and Vercel Edge issues
  161. * See https://github.com/facebook/react/issues/24169
  162. */
  163. async function readResult(stream) {
  164. const reader = stream.getReader();
  165. let result = '';
  166. const decoder = new TextDecoder('utf-8');
  167. while (true) {
  168. const { done, value } = await reader.read();
  169. if (done) {
  170. if (value) {
  171. result += decoder.decode(value);
  172. } else {
  173. // This closes the decoder
  174. decoder.decode(new Uint8Array());
  175. }
  176. return result;
  177. }
  178. result += decoder.decode(value, { stream: true });
  179. }
  180. }
  181. async function renderToReadableStreamAsync(vnode, options) {
  182. return await readResult(await ReactDOM.renderToReadableStream(vnode, options));
  183. }
  184. export default {
  185. check,
  186. renderToStaticMarkup,
  187. supportsAstroStaticSlot: true,
  188. };