@@ -60,8 +60,74 @@ export function renderToString(vnode, context) {
6060 context || EMPTY_OBJ ,
6161 false ,
6262 undefined ,
63- parent
63+ parent ,
64+ false
6465 ) ;
66+ } catch ( e ) {
67+ if ( e . then ) {
68+ throw new Error ( 'Use "renderToStringAsync" for suspenseful rendering.' ) ;
69+ }
70+
71+ throw e ;
72+ } finally {
73+ // options._commit, we don't schedule any effects in this library right now,
74+ // so we can pass an empty queue to this hook.
75+ if ( options [ COMMIT ] ) options [ COMMIT ] ( vnode , EMPTY_ARR ) ;
76+ options [ SKIP_EFFECTS ] = previousSkipEffects ;
77+ EMPTY_ARR . length = 0 ;
78+ }
79+ }
80+
81+ /**
82+ * Render Preact JSX + Components to an HTML string.
83+ * @param {VNode } vnode JSX Element / VNode to render
84+ * @param {Object } [context={}] Initial root context object
85+ * @returns {string } serialized HTML
86+ */
87+ export async function renderToStringAsync ( vnode , context ) {
88+ // Performance optimization: `renderToString` is synchronous and we
89+ // therefore don't execute any effects. To do that we pass an empty
90+ // array to `options._commit` (`__c`). But we can go one step further
91+ // and avoid a lot of dirty checks and allocations by setting
92+ // `options._skipEffects` (`__s`) too.
93+ const previousSkipEffects = options [ SKIP_EFFECTS ] ;
94+ options [ SKIP_EFFECTS ] = true ;
95+
96+ // store options hooks once before each synchronous render call
97+ beforeDiff = options [ DIFF ] ;
98+ afterDiff = options [ DIFFED ] ;
99+ renderHook = options [ RENDER ] ;
100+ ummountHook = options . unmount ;
101+
102+ const parent = h ( Fragment , null ) ;
103+ parent [ CHILDREN ] = [ vnode ] ;
104+
105+ try {
106+ const rendered = _renderToString (
107+ vnode ,
108+ context || EMPTY_OBJ ,
109+ false ,
110+ undefined ,
111+ parent ,
112+ true
113+ ) ;
114+
115+ if ( Array . isArray ( rendered ) ) {
116+ let count = 0 ;
117+ let resolved = rendered ;
118+
119+ // Resolving nested Promises with a maximum depth of 25
120+ while (
121+ resolved . some ( ( element ) => typeof element . then === 'function' ) &&
122+ count ++ < 25
123+ ) {
124+ resolved = ( await Promise . all ( resolved ) ) . flat ( ) ;
125+ }
126+
127+ return resolved . join ( '' ) ;
128+ }
129+
130+ return rendered ;
65131 } finally {
66132 // options._commit, we don't schedule any effects in this library right now,
67133 // so we can pass an empty queue to this hook.
@@ -137,9 +203,17 @@ function renderClassComponent(vnode, context) {
137203 * @param {boolean } isSvgMode
138204 * @param {any } selectValue
139205 * @param {VNode } parent
140- * @returns {string }
206+ * @param {boolean } asyncMode
207+ * @returns {string | Promise<string> | (string | Promise<string>)[] }
141208 */
142- function _renderToString ( vnode , context , isSvgMode , selectValue , parent ) {
209+ function _renderToString (
210+ vnode ,
211+ context ,
212+ isSvgMode ,
213+ selectValue ,
214+ parent ,
215+ asyncMode
216+ ) {
143217 // Ignore non-rendered VNodes/values
144218 if ( vnode == null || vnode === true || vnode === false || vnode === '' ) {
145219 return '' ;
@@ -153,16 +227,44 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
153227
154228 // Recurse into children / Arrays
155229 if ( isArray ( vnode ) ) {
156- let rendered = '' ;
230+ let rendered = '' ,
231+ renderArray ;
157232 parent [ CHILDREN ] = vnode ;
158233 for ( let i = 0 ; i < vnode . length ; i ++ ) {
159234 let child = vnode [ i ] ;
160235 if ( child == null || typeof child === 'boolean' ) continue ;
161236
162- rendered =
163- rendered +
164- _renderToString ( child , context , isSvgMode , selectValue , parent ) ;
237+ const childRender = _renderToString (
238+ child ,
239+ context ,
240+ isSvgMode ,
241+ selectValue ,
242+ parent ,
243+ asyncMode
244+ ) ;
245+
246+ if ( typeof childRender === 'string' ) {
247+ rendered += childRender ;
248+ } else {
249+ renderArray = renderArray || [ ] ;
250+
251+ if ( rendered ) renderArray . push ( rendered ) ;
252+
253+ rendered = '' ;
254+
255+ if ( Array . isArray ( childRender ) ) {
256+ renderArray . push ( ...childRender ) ;
257+ } else {
258+ renderArray . push ( childRender ) ;
259+ }
260+ }
261+ }
262+
263+ if ( renderArray ) {
264+ if ( rendered ) renderArray . push ( rendered ) ;
265+ return renderArray ;
165266 }
267+
166268 return rendered ;
167269 }
168270
@@ -202,7 +304,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
202304 context ,
203305 isSvgMode ,
204306 selectValue ,
205- vnode
307+ vnode ,
308+ asyncMode
206309 ) ;
207310 } else {
208311 // Values are pre-escaped by the JSX transform
@@ -282,7 +385,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
282385 context ,
283386 isSvgMode ,
284387 selectValue ,
285- vnode
388+ vnode ,
389+ asyncMode
286390 ) ;
287391 return str ;
288392 } catch ( err ) {
@@ -313,7 +417,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
313417 context ,
314418 isSvgMode ,
315419 selectValue ,
316- vnode
420+ vnode ,
421+ asyncMode
317422 ) ;
318423 }
319424
@@ -333,20 +438,44 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
333438 rendered != null && rendered . type === Fragment && rendered . key == null ;
334439 rendered = isTopLevelFragment ? rendered . props . children : rendered ;
335440
336- // Recurse into children before invoking the after-diff hook
337- const str = _renderToString (
338- rendered ,
339- context ,
340- isSvgMode ,
341- selectValue ,
342- vnode
343- ) ;
344- if ( afterDiff ) afterDiff ( vnode ) ;
345- vnode [ PARENT ] = undefined ;
441+ const renderChildren = ( ) =>
442+ _renderToString (
443+ rendered ,
444+ context ,
445+ isSvgMode ,
446+ selectValue ,
447+ vnode ,
448+ asyncMode
449+ ) ;
450+
451+ try {
452+ // Recurse into children before invoking the after-diff hook
453+ const str = renderChildren ( ) ;
454+
455+ if ( afterDiff ) afterDiff ( vnode ) ;
456+ vnode [ PARENT ] = undefined ;
346457
347- if ( ummountHook ) ummountHook ( vnode ) ;
458+ if ( ummountHook ) ummountHook ( vnode ) ;
459+
460+ return str ;
461+ } catch ( error ) {
462+ if ( ! asyncMode ) throw error ;
463+
464+ if ( ! error || typeof error . then !== 'function' ) throw error ;
465+
466+ const renderNestedChildren = ( ) => {
467+ try {
468+ return renderChildren ( ) ;
469+ } catch ( e ) {
470+ return e . then (
471+ ( ) => renderChildren ( ) ,
472+ ( ) => renderNestedChildren ( )
473+ ) ;
474+ }
475+ } ;
348476
349- return str ;
477+ return error . then ( ( ) => renderNestedChildren ( ) ) ;
478+ }
350479 }
351480
352481 // Serialize Element VNodes to HTML
@@ -476,7 +605,14 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
476605 // recurse into this element VNode's children
477606 let childSvgMode =
478607 type === 'svg' || ( type !== 'foreignObject' && isSvgMode ) ;
479- html = _renderToString ( children , context , childSvgMode , selectValue , vnode ) ;
608+ html = _renderToString (
609+ children ,
610+ context ,
611+ childSvgMode ,
612+ selectValue ,
613+ vnode ,
614+ asyncMode
615+ ) ;
480616 }
481617
482618 if ( afterDiff ) afterDiff ( vnode ) ;
@@ -488,7 +624,13 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
488624 return s + '/>' ;
489625 }
490626
491- return s + '>' + html + '</' + type + '>' ;
627+ const endTag = '</' + type + '>' ;
628+ const startTag = s + '>' ;
629+
630+ if ( Array . isArray ( html ) ) return [ startTag , ...html , endTag ] ;
631+ else if ( typeof html !== 'string' ) return [ startTag , html , endTag ] ;
632+
633+ return startTag + html + endTag ;
492634}
493635
494636const SELF_CLOSING = new Set ( [
0 commit comments