diff --git a/.changeset/happy-peas-type.md b/.changeset/happy-peas-type.md new file mode 100644 index 00000000..fa929705 --- /dev/null +++ b/.changeset/happy-peas-type.md @@ -0,0 +1,5 @@ +--- +'preact-render-to-string': minor +--- + +Insert comment markers for suspended trees, only in renderToStringAsync diff --git a/src/index.js b/src/index.js index ff4c774e..f6530f4f 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,8 @@ const EMPTY_ARR = []; const isArray = Array.isArray; const assign = Object.assign; const EMPTY_STR = ''; +const BEGIN_SUSPENSE_DENOMINATOR = ''; +const END_SUSPENSE_DENOMINATOR = ''; // Global state for the current render pass let beforeDiff, afterDiff, renderHook, ummountHook; @@ -372,7 +374,14 @@ function _renderToString( if (renderHook) renderHook(vnode); - rendered = type.call(component, props, cctx); + try { + rendered = type.call(component, props, cctx); + } catch (e) { + if (asyncMode) { + vnode._suspended = true; + } + throw e; + } } component[DIRTY] = true; } @@ -403,6 +412,7 @@ function _renderToString( selectValue, vnode, asyncMode, + false, renderer ); } catch (err) { @@ -475,6 +485,21 @@ function _renderToString( if (options.unmount) options.unmount(vnode); + if (vnode._suspended) { + if (typeof str === 'string') { + return BEGIN_SUSPENSE_DENOMINATOR + str + END_SUSPENSE_DENOMINATOR; + } else if (isArray(str)) { + str.unshift(BEGIN_SUSPENSE_DENOMINATOR); + str.push(END_SUSPENSE_DENOMINATOR); + return str; + } + + return str.then( + (resolved) => + BEGIN_SUSPENSE_DENOMINATOR + resolved + END_SUSPENSE_DENOMINATOR + ); + } + return str; } catch (error) { if (!asyncMode && renderer && renderer.onError) { @@ -503,7 +528,7 @@ function _renderToString( const renderNestedChildren = () => { try { - return _renderToString( + const result = _renderToString( rendered, context, isSvgMode, @@ -512,22 +537,26 @@ function _renderToString( asyncMode, renderer ); + return vnode._suspended + ? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR + : result; } catch (e) { if (!e || typeof e.then != 'function') throw e; - return e.then( - () => - _renderToString( - rendered, - context, - isSvgMode, - selectValue, - vnode, - asyncMode, - renderer - ), - renderNestedChildren - ); + return e.then(() => { + const result = _renderToString( + rendered, + context, + isSvgMode, + selectValue, + vnode, + asyncMode, + renderer + ); + return vnode._suspended + ? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR + : result; + }, renderNestedChildren); } }; diff --git a/test/compat/async.test.jsx b/test/compat/async.test.jsx index 12e9ac5a..0ca78352 100644 --- a/test/compat/async.test.jsx +++ b/test/compat/async.test.jsx @@ -3,6 +3,7 @@ import { h, Fragment } from 'preact'; import { Suspense, useId, lazy, createContext } from 'preact/compat'; import { expect } from 'chai'; import { createSuspender } from '../utils.jsx'; +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); describe('Async renderToString', () => { it('should render JSX after a suspense boundary', async () => { @@ -16,7 +17,30 @@ describe('Async renderToString', () => { ); - const expected = `
ok
'); + expect(rendered).to.equal('ok
'); }); it('should work with an in-render suspension', async () => { @@ -224,7 +340,10 @@ describe('Async renderToString', () => { ); - expect(rendered).to.equal(`