@@ -60,8 +60,74 @@ export function renderToString(vnode, context) {
60
60
context || EMPTY_OBJ ,
61
61
false ,
62
62
undefined ,
63
- parent
63
+ parent ,
64
+ false
64
65
) ;
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 ;
65
131
} finally {
66
132
// options._commit, we don't schedule any effects in this library right now,
67
133
// so we can pass an empty queue to this hook.
@@ -137,9 +203,17 @@ function renderClassComponent(vnode, context) {
137
203
* @param {boolean } isSvgMode
138
204
* @param {any } selectValue
139
205
* @param {VNode } parent
140
- * @returns {string }
206
+ * @param {boolean } asyncMode
207
+ * @returns {string | Promise<string> | (string | Promise<string>)[] }
141
208
*/
142
- function _renderToString ( vnode , context , isSvgMode , selectValue , parent ) {
209
+ function _renderToString (
210
+ vnode ,
211
+ context ,
212
+ isSvgMode ,
213
+ selectValue ,
214
+ parent ,
215
+ asyncMode
216
+ ) {
143
217
// Ignore non-rendered VNodes/values
144
218
if ( vnode == null || vnode === true || vnode === false || vnode === '' ) {
145
219
return '' ;
@@ -153,16 +227,44 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
153
227
154
228
// Recurse into children / Arrays
155
229
if ( isArray ( vnode ) ) {
156
- let rendered = '' ;
230
+ let rendered = '' ,
231
+ renderArray ;
157
232
parent [ CHILDREN ] = vnode ;
158
233
for ( let i = 0 ; i < vnode . length ; i ++ ) {
159
234
let child = vnode [ i ] ;
160
235
if ( child == null || typeof child === 'boolean' ) continue ;
161
236
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 ;
165
266
}
267
+
166
268
return rendered ;
167
269
}
168
270
@@ -202,7 +304,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
202
304
context ,
203
305
isSvgMode ,
204
306
selectValue ,
205
- vnode
307
+ vnode ,
308
+ asyncMode
206
309
) ;
207
310
} else {
208
311
// Values are pre-escaped by the JSX transform
@@ -282,7 +385,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
282
385
context ,
283
386
isSvgMode ,
284
387
selectValue ,
285
- vnode
388
+ vnode ,
389
+ asyncMode
286
390
) ;
287
391
return str ;
288
392
} catch ( err ) {
@@ -313,7 +417,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
313
417
context ,
314
418
isSvgMode ,
315
419
selectValue ,
316
- vnode
420
+ vnode ,
421
+ asyncMode
317
422
) ;
318
423
}
319
424
@@ -333,20 +438,44 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
333
438
rendered != null && rendered . type === Fragment && rendered . key == null ;
334
439
rendered = isTopLevelFragment ? rendered . props . children : rendered ;
335
440
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 ;
346
457
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
+ } ;
348
476
349
- return str ;
477
+ return error . then ( ( ) => renderNestedChildren ( ) ) ;
478
+ }
350
479
}
351
480
352
481
// Serialize Element VNodes to HTML
@@ -476,7 +605,14 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
476
605
// recurse into this element VNode's children
477
606
let childSvgMode =
478
607
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
+ ) ;
480
616
}
481
617
482
618
if ( afterDiff ) afterDiff ( vnode ) ;
@@ -488,7 +624,13 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
488
624
return s + '/>' ;
489
625
}
490
626
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 ;
492
634
}
493
635
494
636
const SELF_CLOSING = new Set ( [
0 commit comments