preact-render-to-string 6.4.2 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs.js +1 -1
- package/dist/commonjs.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/internal.d.ts +35 -0
- package/dist/jsx-entry.js +1 -1
- package/dist/jsx-entry.js.map +1 -1
- package/dist/jsx.js.map +1 -1
- package/dist/jsx.mjs +1 -1
- package/dist/jsx.mjs.map +1 -1
- package/dist/jsx.module.js +1 -1
- package/dist/jsx.module.js.map +1 -1
- package/dist/jsx.umd.js +1 -1
- package/dist/jsx.umd.js.map +1 -1
- package/dist/stream-node.js +786 -0
- package/dist/stream-node.js.map +1 -0
- package/dist/stream-node.module.js +786 -0
- package/dist/stream-node.module.js.map +1 -0
- package/dist/stream-node.umd.js +790 -0
- package/dist/stream-node.umd.js.map +1 -0
- package/dist/stream.js +2 -0
- package/dist/stream.js.map +1 -0
- package/dist/stream.module.js +2 -0
- package/dist/stream.module.js.map +1 -0
- package/dist/stream.umd.js +2 -0
- package/dist/stream.umd.js.map +1 -0
- package/jsx.d.ts +1 -1
- package/package.json +166 -157
- package/src/index.js +54 -18
- package/src/internal.d.ts +35 -0
- package/src/jsx.js +2 -4
- package/src/lib/chunked.js +97 -0
- package/src/lib/client.js +61 -0
- package/src/{constants.js → lib/constants.js} +3 -0
- package/src/{util.js → lib/util.js} +13 -1
- package/src/pretty.js +13 -13
- package/src/stream-node.js +60 -0
- package/src/stream.js +43 -0
- /package/src/{polyfills.js → lib/polyfills.js} +0 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
import { options, h, Fragment } from 'preact';
|
|
3
|
+
|
|
4
|
+
const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
|
|
5
|
+
const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)([A-Z])/;
|
|
6
|
+
const HTML_LOWER_CASE = /^accessK|^auto[A-Z]|^cell|^ch|^col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|popoverT|readO|rowS|spellC|src[A-Z]|tabI|useM|item[A-Z]/;
|
|
7
|
+
const SVG_CAMEL_CASE = /^ac|^ali|arabic|basel|cap|clipPath$|clipRule$|color|dominant|enable|fill|flood|font|glyph[^R]|horiz|image|letter|lighting|marker[^WUH]|overline|panose|pointe|paint|rendering|shape|stop|strikethrough|stroke|text[^L]|transform|underline|unicode|units|^v[^i]|^w|^xH/; // DOM properties that should NOT have "px" added when numeric
|
|
8
|
+
|
|
9
|
+
const ENCODED_ENTITIES = /["&<]/;
|
|
10
|
+
/** @param {string} str */
|
|
11
|
+
|
|
12
|
+
function encodeEntities(str) {
|
|
13
|
+
// Skip all work for strings with no entities needing encoding:
|
|
14
|
+
if (str.length === 0 || ENCODED_ENTITIES.test(str) === false) return str;
|
|
15
|
+
let last = 0,
|
|
16
|
+
i = 0,
|
|
17
|
+
out = '',
|
|
18
|
+
ch = ''; // Seek forward in str until the next entity char:
|
|
19
|
+
|
|
20
|
+
for (; i < str.length; i++) {
|
|
21
|
+
switch (str.charCodeAt(i)) {
|
|
22
|
+
case 34:
|
|
23
|
+
ch = '"';
|
|
24
|
+
break;
|
|
25
|
+
|
|
26
|
+
case 38:
|
|
27
|
+
ch = '&';
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case 60:
|
|
31
|
+
ch = '<';
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
default:
|
|
35
|
+
continue;
|
|
36
|
+
} // Append skipped/buffered characters and the encoded entity:
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if (i !== last) out += str.slice(last, i);
|
|
40
|
+
out += ch; // Start the next seek/buffer after the entity's offset:
|
|
41
|
+
|
|
42
|
+
last = i + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (i !== last) out += str.slice(last, i);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
const JS_TO_CSS = {};
|
|
49
|
+
const IS_NON_DIMENSIONAL = new Set(['animation-iteration-count', 'border-image-outset', 'border-image-slice', 'border-image-width', 'box-flex', 'box-flex-group', 'box-ordinal-group', 'column-count', 'fill-opacity', 'flex', 'flex-grow', 'flex-negative', 'flex-order', 'flex-positive', 'flex-shrink', 'flood-opacity', 'font-weight', 'grid-column', 'grid-row', 'line-clamp', 'line-height', 'opacity', 'order', 'orphans', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'tab-size', 'widows', 'z-index', 'zoom']);
|
|
50
|
+
const CSS_REGEX = /[A-Z]/g; // Convert an Object style to a CSSText string
|
|
51
|
+
|
|
52
|
+
function styleObjToCss(s) {
|
|
53
|
+
let str = '';
|
|
54
|
+
|
|
55
|
+
for (let prop in s) {
|
|
56
|
+
let val = s[prop];
|
|
57
|
+
|
|
58
|
+
if (val != null && val !== '') {
|
|
59
|
+
const name = prop[0] == '-' ? prop : JS_TO_CSS[prop] || (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase());
|
|
60
|
+
let suffix = ';';
|
|
61
|
+
|
|
62
|
+
if (typeof val === 'number' && // Exclude custom-attributes
|
|
63
|
+
!name.startsWith('--') && !IS_NON_DIMENSIONAL.has(name)) {
|
|
64
|
+
suffix = 'px;';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
str = str + name + ':' + val + suffix;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return str || undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @template T
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
class Deferred {
|
|
78
|
+
constructor() {
|
|
79
|
+
// eslint-disable-next-line lines-around-comment
|
|
80
|
+
|
|
81
|
+
/** @type {Promise<T>} */
|
|
82
|
+
this.promise = new Promise((resolve, reject) => {
|
|
83
|
+
this.resolve = resolve;
|
|
84
|
+
this.reject = reject;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Options hooks
|
|
91
|
+
const DIFF = '__b';
|
|
92
|
+
const RENDER = '__r';
|
|
93
|
+
const DIFFED = 'diffed';
|
|
94
|
+
const COMMIT = '__c';
|
|
95
|
+
const SKIP_EFFECTS = '__s';
|
|
96
|
+
const CATCH_ERROR = '__e'; // VNode properties
|
|
97
|
+
|
|
98
|
+
const COMPONENT = '__c';
|
|
99
|
+
const CHILDREN = '__k';
|
|
100
|
+
const PARENT = '__';
|
|
101
|
+
|
|
102
|
+
const VNODE = '__v';
|
|
103
|
+
const DIRTY = '__d';
|
|
104
|
+
const NEXT_STATE = '__s';
|
|
105
|
+
const CHILD_DID_SUSPEND = '__c';
|
|
106
|
+
|
|
107
|
+
const EMPTY_ARR = [];
|
|
108
|
+
const isArray = Array.isArray;
|
|
109
|
+
const assign = Object.assign; // Global state for the current render pass
|
|
110
|
+
|
|
111
|
+
let beforeDiff, afterDiff, renderHook, ummountHook;
|
|
112
|
+
/**
|
|
113
|
+
* Render Preact JSX + Components to an HTML string.
|
|
114
|
+
* @param {VNode} vnode JSX Element / VNode to render
|
|
115
|
+
* @param {Object} [context={}] Initial root context object
|
|
116
|
+
* @param {RendererState} [_rendererState] for internal use
|
|
117
|
+
* @returns {string} serialized HTML
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
function renderToString(vnode, context, _rendererState) {
|
|
121
|
+
// Performance optimization: `renderToString` is synchronous and we
|
|
122
|
+
// therefore don't execute any effects. To do that we pass an empty
|
|
123
|
+
// array to `options._commit` (`__c`). But we can go one step further
|
|
124
|
+
// and avoid a lot of dirty checks and allocations by setting
|
|
125
|
+
// `options._skipEffects` (`__s`) too.
|
|
126
|
+
const previousSkipEffects = options[SKIP_EFFECTS];
|
|
127
|
+
options[SKIP_EFFECTS] = true; // store options hooks once before each synchronous render call
|
|
128
|
+
|
|
129
|
+
beforeDiff = options[DIFF];
|
|
130
|
+
afterDiff = options[DIFFED];
|
|
131
|
+
renderHook = options[RENDER];
|
|
132
|
+
ummountHook = options.unmount;
|
|
133
|
+
const parent = h(Fragment, null);
|
|
134
|
+
parent[CHILDREN] = [vnode];
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
return _renderToString(vnode, context || EMPTY_OBJ, false, undefined, parent, false, _rendererState);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e.then) {
|
|
140
|
+
throw new Error('Use "renderToStringAsync" for suspenseful rendering.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw e;
|
|
144
|
+
} finally {
|
|
145
|
+
// options._commit, we don't schedule any effects in this library right now,
|
|
146
|
+
// so we can pass an empty queue to this hook.
|
|
147
|
+
if (options[COMMIT]) options[COMMIT](vnode, EMPTY_ARR);
|
|
148
|
+
options[SKIP_EFFECTS] = previousSkipEffects;
|
|
149
|
+
EMPTY_ARR.length = 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function markAsDirty() {
|
|
154
|
+
this.__d = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const EMPTY_OBJ = {};
|
|
158
|
+
/**
|
|
159
|
+
* @param {VNode} vnode
|
|
160
|
+
* @param {Record<string, unknown>} context
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
function renderClassComponent(vnode, context) {
|
|
164
|
+
let type =
|
|
165
|
+
/** @type {import("preact").ComponentClass<typeof vnode.props>} */
|
|
166
|
+
vnode.type;
|
|
167
|
+
let isMounting = true;
|
|
168
|
+
let c;
|
|
169
|
+
|
|
170
|
+
if (vnode[COMPONENT]) {
|
|
171
|
+
isMounting = false;
|
|
172
|
+
c = vnode[COMPONENT];
|
|
173
|
+
c.state = c[NEXT_STATE];
|
|
174
|
+
} else {
|
|
175
|
+
c = new type(vnode.props, context);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
vnode[COMPONENT] = c;
|
|
179
|
+
c[VNODE] = vnode;
|
|
180
|
+
c.props = vnode.props;
|
|
181
|
+
c.context = context; // turn off stateful re-rendering:
|
|
182
|
+
|
|
183
|
+
c[DIRTY] = true;
|
|
184
|
+
if (c.state == null) c.state = EMPTY_OBJ;
|
|
185
|
+
|
|
186
|
+
if (c[NEXT_STATE] == null) {
|
|
187
|
+
c[NEXT_STATE] = c.state;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (type.getDerivedStateFromProps) {
|
|
191
|
+
c.state = assign({}, c.state, type.getDerivedStateFromProps(c.props, c.state));
|
|
192
|
+
} else if (isMounting && c.componentWillMount) {
|
|
193
|
+
c.componentWillMount(); // If the user called setState in cWM we need to flush pending,
|
|
194
|
+
// state updates. This is the same behaviour in React.
|
|
195
|
+
|
|
196
|
+
c.state = c[NEXT_STATE] !== c.state ? c[NEXT_STATE] : c.state;
|
|
197
|
+
} else if (!isMounting && c.componentWillUpdate) {
|
|
198
|
+
c.componentWillUpdate();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (renderHook) renderHook(vnode);
|
|
202
|
+
return c.render(c.props, c.state, context);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Recursively render VNodes to HTML.
|
|
206
|
+
* @param {VNode|any} vnode
|
|
207
|
+
* @param {any} context
|
|
208
|
+
* @param {boolean} isSvgMode
|
|
209
|
+
* @param {any} selectValue
|
|
210
|
+
* @param {VNode} parent
|
|
211
|
+
* @param {boolean} asyncMode
|
|
212
|
+
* @param {RendererState | undefined} [renderer]
|
|
213
|
+
* @returns {string | Promise<string> | (string | Promise<string>)[]}
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
function _renderToString(vnode, context, isSvgMode, selectValue, parent, asyncMode, renderer) {
|
|
218
|
+
// Ignore non-rendered VNodes/values
|
|
219
|
+
if (vnode == null || vnode === true || vnode === false || vnode === '') {
|
|
220
|
+
return '';
|
|
221
|
+
} // Text VNodes: escape as HTML
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if (typeof vnode !== 'object') {
|
|
225
|
+
if (typeof vnode === 'function') return '';
|
|
226
|
+
return encodeEntities(vnode + '');
|
|
227
|
+
} // Recurse into children / Arrays
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if (isArray(vnode)) {
|
|
231
|
+
let rendered = '',
|
|
232
|
+
renderArray;
|
|
233
|
+
parent[CHILDREN] = vnode;
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < vnode.length; i++) {
|
|
236
|
+
let child = vnode[i];
|
|
237
|
+
if (child == null || typeof child === 'boolean') continue;
|
|
238
|
+
|
|
239
|
+
const childRender = _renderToString(child, context, isSvgMode, selectValue, parent, asyncMode, renderer);
|
|
240
|
+
|
|
241
|
+
if (typeof childRender === 'string') {
|
|
242
|
+
rendered += childRender;
|
|
243
|
+
} else {
|
|
244
|
+
renderArray = renderArray || [];
|
|
245
|
+
if (rendered) renderArray.push(rendered);
|
|
246
|
+
rendered = '';
|
|
247
|
+
|
|
248
|
+
if (Array.isArray(childRender)) {
|
|
249
|
+
renderArray.push(...childRender);
|
|
250
|
+
} else {
|
|
251
|
+
renderArray.push(childRender);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (renderArray) {
|
|
257
|
+
if (rendered) renderArray.push(rendered);
|
|
258
|
+
return renderArray;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return rendered;
|
|
262
|
+
} // VNodes have {constructor:undefined} to prevent JSON injection:
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if (vnode.constructor !== undefined) return '';
|
|
266
|
+
vnode[PARENT] = parent;
|
|
267
|
+
if (beforeDiff) beforeDiff(vnode);
|
|
268
|
+
let type = vnode.type,
|
|
269
|
+
props = vnode.props,
|
|
270
|
+
cctx = context,
|
|
271
|
+
contextType,
|
|
272
|
+
rendered,
|
|
273
|
+
component; // Invoke rendering on Components
|
|
274
|
+
|
|
275
|
+
if (typeof type === 'function') {
|
|
276
|
+
if (type === Fragment) {
|
|
277
|
+
// Serialized precompiled JSX.
|
|
278
|
+
if (props.tpl) {
|
|
279
|
+
let out = '';
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < props.tpl.length; i++) {
|
|
282
|
+
out += props.tpl[i];
|
|
283
|
+
|
|
284
|
+
if (props.exprs && i < props.exprs.length) {
|
|
285
|
+
const value = props.exprs[i];
|
|
286
|
+
if (value == null) continue; // Check if we're dealing with a vnode or an array of nodes
|
|
287
|
+
|
|
288
|
+
if (typeof value === 'object' && (value.constructor === undefined || isArray(value))) {
|
|
289
|
+
out += _renderToString(value, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
290
|
+
} else {
|
|
291
|
+
// Values are pre-escaped by the JSX transform
|
|
292
|
+
out += value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return out;
|
|
298
|
+
} else if (props.UNSTABLE_comment) {
|
|
299
|
+
// Fragments are the least used components of core that's why
|
|
300
|
+
// branching here for comments has the least effect on perf.
|
|
301
|
+
return '<!--' + encodeEntities(props.UNSTABLE_comment || '') + '-->';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
rendered = props.children;
|
|
305
|
+
} else {
|
|
306
|
+
contextType = type.contextType;
|
|
307
|
+
|
|
308
|
+
if (contextType != null) {
|
|
309
|
+
let provider = context[contextType.__c];
|
|
310
|
+
cctx = provider ? provider.props.value : contextType.__;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (type.prototype && typeof type.prototype.render === 'function') {
|
|
314
|
+
rendered =
|
|
315
|
+
/**#__NOINLINE__**/
|
|
316
|
+
renderClassComponent(vnode, cctx);
|
|
317
|
+
component = vnode[COMPONENT];
|
|
318
|
+
} else {
|
|
319
|
+
component = {
|
|
320
|
+
__v: vnode,
|
|
321
|
+
props,
|
|
322
|
+
context: cctx,
|
|
323
|
+
// silently drop state updates
|
|
324
|
+
setState: markAsDirty,
|
|
325
|
+
forceUpdate: markAsDirty,
|
|
326
|
+
__d: true,
|
|
327
|
+
// hooks
|
|
328
|
+
__h: []
|
|
329
|
+
};
|
|
330
|
+
vnode[COMPONENT] = component; // If a hook invokes setState() to invalidate the component during rendering,
|
|
331
|
+
// re-render it up to 25 times to allow "settling" of memoized states.
|
|
332
|
+
// Note:
|
|
333
|
+
// This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
|
|
334
|
+
// https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
|
|
335
|
+
|
|
336
|
+
let count = 0;
|
|
337
|
+
|
|
338
|
+
while (component[DIRTY] && count++ < 25) {
|
|
339
|
+
component[DIRTY] = false;
|
|
340
|
+
if (renderHook) renderHook(vnode);
|
|
341
|
+
rendered = type.call(component, props, cctx);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
component[DIRTY] = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (component.getChildContext != null) {
|
|
348
|
+
context = assign({}, context, component.getChildContext());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if ((type.getDerivedStateFromError || component.componentDidCatch) && options.errorBoundaries) {
|
|
352
|
+
let str = ''; // When a component returns a Fragment node we flatten it in core, so we
|
|
353
|
+
// need to mirror that logic here too
|
|
354
|
+
|
|
355
|
+
let isTopLevelFragment = rendered != null && rendered.type === Fragment && rendered.key == null;
|
|
356
|
+
rendered = isTopLevelFragment ? rendered.props.children : rendered;
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
str = _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
360
|
+
return str;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (type.getDerivedStateFromError) {
|
|
363
|
+
component[NEXT_STATE] = type.getDerivedStateFromError(err);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (component.componentDidCatch) {
|
|
367
|
+
component.componentDidCatch(err, {});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (component[DIRTY]) {
|
|
371
|
+
rendered = renderClassComponent(vnode, context);
|
|
372
|
+
component = vnode[COMPONENT];
|
|
373
|
+
|
|
374
|
+
if (component.getChildContext != null) {
|
|
375
|
+
context = assign({}, context, component.getChildContext());
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let isTopLevelFragment = rendered != null && rendered.type === Fragment && rendered.key == null;
|
|
379
|
+
rendered = isTopLevelFragment ? rendered.props.children : rendered;
|
|
380
|
+
str = _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return str;
|
|
384
|
+
} finally {
|
|
385
|
+
if (afterDiff) afterDiff(vnode);
|
|
386
|
+
vnode[PARENT] = null;
|
|
387
|
+
if (ummountHook) ummountHook(vnode);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} // When a component returns a Fragment node we flatten it in core, so we
|
|
391
|
+
// need to mirror that logic here too
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
let isTopLevelFragment = rendered != null && rendered.type === Fragment && rendered.key == null && rendered.props.tpl == null;
|
|
395
|
+
rendered = isTopLevelFragment ? rendered.props.children : rendered;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
// Recurse into children before invoking the after-diff hook
|
|
399
|
+
const str = _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
400
|
+
|
|
401
|
+
if (afterDiff) afterDiff(vnode); // when we are dealing with suspense we can't do this...
|
|
402
|
+
|
|
403
|
+
vnode[PARENT] = null;
|
|
404
|
+
if (options.unmount) options.unmount(vnode);
|
|
405
|
+
return str;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (!asyncMode && renderer && renderer.onError) {
|
|
408
|
+
let res = renderer.onError(error, vnode, child => _renderToString(child, context, isSvgMode, selectValue, vnode, asyncMode, renderer));
|
|
409
|
+
if (res !== undefined) return res;
|
|
410
|
+
let errorHook = options[CATCH_ERROR];
|
|
411
|
+
if (errorHook) errorHook(error, vnode);
|
|
412
|
+
return '';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!asyncMode) throw error;
|
|
416
|
+
if (!error || typeof error.then !== 'function') throw error;
|
|
417
|
+
|
|
418
|
+
const renderNestedChildren = () => {
|
|
419
|
+
try {
|
|
420
|
+
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
if (!e || typeof e.then !== 'function') throw e;
|
|
423
|
+
return e.then(() => _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer), () => renderNestedChildren());
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
return error.then(() => renderNestedChildren());
|
|
428
|
+
}
|
|
429
|
+
} // Serialize Element VNodes to HTML
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
let s = '<' + type,
|
|
433
|
+
html = '',
|
|
434
|
+
children;
|
|
435
|
+
|
|
436
|
+
for (let name in props) {
|
|
437
|
+
let v = props[name];
|
|
438
|
+
|
|
439
|
+
switch (name) {
|
|
440
|
+
case 'children':
|
|
441
|
+
children = v;
|
|
442
|
+
continue;
|
|
443
|
+
// VDOM-specific props
|
|
444
|
+
|
|
445
|
+
case 'key':
|
|
446
|
+
case 'ref':
|
|
447
|
+
case '__self':
|
|
448
|
+
case '__source':
|
|
449
|
+
continue;
|
|
450
|
+
// prefer for/class over htmlFor/className
|
|
451
|
+
|
|
452
|
+
case 'htmlFor':
|
|
453
|
+
if ('for' in props) continue;
|
|
454
|
+
name = 'for';
|
|
455
|
+
break;
|
|
456
|
+
|
|
457
|
+
case 'className':
|
|
458
|
+
if ('class' in props) continue;
|
|
459
|
+
name = 'class';
|
|
460
|
+
break;
|
|
461
|
+
// Form element reflected properties
|
|
462
|
+
|
|
463
|
+
case 'defaultChecked':
|
|
464
|
+
name = 'checked';
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
case 'defaultSelected':
|
|
468
|
+
name = 'selected';
|
|
469
|
+
break;
|
|
470
|
+
// Special value attribute handling
|
|
471
|
+
|
|
472
|
+
case 'defaultValue':
|
|
473
|
+
case 'value':
|
|
474
|
+
name = 'value';
|
|
475
|
+
|
|
476
|
+
switch (type) {
|
|
477
|
+
// <textarea value="a&b"> --> <textarea>a&b</textarea>
|
|
478
|
+
case 'textarea':
|
|
479
|
+
children = v;
|
|
480
|
+
continue;
|
|
481
|
+
// <select value> is serialized as a selected attribute on the matching option child
|
|
482
|
+
|
|
483
|
+
case 'select':
|
|
484
|
+
selectValue = v;
|
|
485
|
+
continue;
|
|
486
|
+
// Add a selected attribute to <option> if its value matches the parent <select> value
|
|
487
|
+
|
|
488
|
+
case 'option':
|
|
489
|
+
if (selectValue == v && !('selected' in props)) {
|
|
490
|
+
s = s + ' selected';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
break;
|
|
497
|
+
|
|
498
|
+
case 'dangerouslySetInnerHTML':
|
|
499
|
+
html = v && v.__html;
|
|
500
|
+
continue;
|
|
501
|
+
// serialize object styles to a CSS string
|
|
502
|
+
|
|
503
|
+
case 'style':
|
|
504
|
+
if (typeof v === 'object') {
|
|
505
|
+
v = styleObjToCss(v);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
break;
|
|
509
|
+
|
|
510
|
+
case 'acceptCharset':
|
|
511
|
+
name = 'accept-charset';
|
|
512
|
+
break;
|
|
513
|
+
|
|
514
|
+
case 'httpEquiv':
|
|
515
|
+
name = 'http-equiv';
|
|
516
|
+
break;
|
|
517
|
+
|
|
518
|
+
default:
|
|
519
|
+
{
|
|
520
|
+
if (NAMESPACE_REPLACE_REGEX.test(name)) {
|
|
521
|
+
name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
|
|
522
|
+
} else if (UNSAFE_NAME.test(name)) {
|
|
523
|
+
continue;
|
|
524
|
+
} else if ((name[4] === '-' || name === 'draggable') && v != null) {
|
|
525
|
+
// serialize boolean aria-xyz or draggable attribute values as strings
|
|
526
|
+
// `draggable` is an enumerated attribute and not Boolean. A value of `true` or `false` is mandatory
|
|
527
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable
|
|
528
|
+
v += '';
|
|
529
|
+
} else if (isSvgMode) {
|
|
530
|
+
if (SVG_CAMEL_CASE.test(name)) {
|
|
531
|
+
name = name === 'panose1' ? 'panose-1' : name.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
532
|
+
}
|
|
533
|
+
} else if (HTML_LOWER_CASE.test(name)) {
|
|
534
|
+
name = name.toLowerCase();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} // write this attribute to the buffer
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if (v != null && v !== false && typeof v !== 'function') {
|
|
541
|
+
if (v === true || v === '') {
|
|
542
|
+
s = s + ' ' + name;
|
|
543
|
+
} else {
|
|
544
|
+
s = s + ' ' + name + '="' + encodeEntities(v + '') + '"';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (UNSAFE_NAME.test(type)) {
|
|
550
|
+
// this seems to performs a lot better than throwing
|
|
551
|
+
// return '<!-- -->';
|
|
552
|
+
throw new Error(`${type} is not a valid HTML tag name in ${s}>`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (html) ; else if (typeof children === 'string') {
|
|
556
|
+
// single text child
|
|
557
|
+
html = encodeEntities(children);
|
|
558
|
+
} else if (children != null && children !== false && children !== true) {
|
|
559
|
+
// recurse into this element VNode's children
|
|
560
|
+
let childSvgMode = type === 'svg' || type !== 'foreignObject' && isSvgMode;
|
|
561
|
+
html = _renderToString(children, context, childSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (afterDiff) afterDiff(vnode); // TODO: this was commented before
|
|
565
|
+
|
|
566
|
+
vnode[PARENT] = null;
|
|
567
|
+
if (ummountHook) ummountHook(vnode); // Emit self-closing tag for empty void elements:
|
|
568
|
+
|
|
569
|
+
if (!html && SELF_CLOSING.has(type)) {
|
|
570
|
+
return s + '/>';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const endTag = '</' + type + '>';
|
|
574
|
+
const startTag = s + '>';
|
|
575
|
+
if (Array.isArray(html)) return [startTag, ...html, endTag];else if (typeof html !== 'string') return [startTag, html, endTag];
|
|
576
|
+
return startTag + html + endTag;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const SELF_CLOSING = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
580
|
+
|
|
581
|
+
/* eslint-disable no-var, key-spacing, object-curly-spacing, prefer-arrow-callback, semi, keyword-spacing */
|
|
582
|
+
function initPreactIslandElement() {
|
|
583
|
+
class PreactIslandElement extends HTMLElement {
|
|
584
|
+
connectedCallback() {
|
|
585
|
+
var d = this;
|
|
586
|
+
if (!d.isConnected) return;
|
|
587
|
+
let i = this.getAttribute('data-target');
|
|
588
|
+
if (!i) return;
|
|
589
|
+
var s,
|
|
590
|
+
e,
|
|
591
|
+
c = document.createNodeIterator(document, 128);
|
|
592
|
+
|
|
593
|
+
while (c.nextNode()) {
|
|
594
|
+
let n = c.referenceNode;
|
|
595
|
+
if (n.data == 'preact-island:' + i) s = n;else if (n.data == '/preact-island:' + i) e = n;
|
|
596
|
+
if (s && e) break;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (s && e) {
|
|
600
|
+
var p = e.previousSibling;
|
|
601
|
+
|
|
602
|
+
while (p != s) {
|
|
603
|
+
if (!p || p == s) break;
|
|
604
|
+
e.parentNode.removeChild(p);
|
|
605
|
+
p = e.previousSibling;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
requestAnimationFrame(() => {
|
|
609
|
+
for (let i = 0; i <= d.children.length; i++) {
|
|
610
|
+
const child = d.children[i];
|
|
611
|
+
e.parentNode.insertBefore(child, e);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
d.parentNode.removeChild(d);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
customElements.define('preact-island', PreactIslandElement);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const fn = initPreactIslandElement.toString();
|
|
625
|
+
const INIT_SCRIPT = fn.slice(fn.indexOf('{') + 1, fn.lastIndexOf('}')).replace(/\n\s+/gm, '');
|
|
626
|
+
function createInitScript() {
|
|
627
|
+
return `<script>(function(){${INIT_SCRIPT}}())</script>`;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* @param {string} id
|
|
631
|
+
* @param {string} content
|
|
632
|
+
* @returns {string}
|
|
633
|
+
*/
|
|
634
|
+
|
|
635
|
+
function createSubtree(id, content) {
|
|
636
|
+
return `<preact-island hidden data-target="${id}">${content}</preact-island>`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* @param {VNode} vnode
|
|
641
|
+
* @param {RenderToChunksOptions} options
|
|
642
|
+
* @returns {Promise<void>}
|
|
643
|
+
*/
|
|
644
|
+
|
|
645
|
+
async function renderToChunks(vnode, {
|
|
646
|
+
context,
|
|
647
|
+
onWrite,
|
|
648
|
+
abortSignal
|
|
649
|
+
}) {
|
|
650
|
+
context = context || {};
|
|
651
|
+
/** @type {RendererState} */
|
|
652
|
+
|
|
653
|
+
const renderer = {
|
|
654
|
+
start: Date.now(),
|
|
655
|
+
abortSignal,
|
|
656
|
+
onWrite,
|
|
657
|
+
onError: handleError,
|
|
658
|
+
suspended: []
|
|
659
|
+
}; // Synchronously render the shell
|
|
660
|
+
// @ts-ignore - using third internal RendererState argument
|
|
661
|
+
|
|
662
|
+
const shell = renderToString(vnode, context, renderer);
|
|
663
|
+
onWrite(shell); // Wait for any suspended sub-trees if there are any
|
|
664
|
+
|
|
665
|
+
const len = renderer.suspended.length;
|
|
666
|
+
|
|
667
|
+
if (len > 0) {
|
|
668
|
+
onWrite('<div hidden>');
|
|
669
|
+
onWrite(createInitScript()); // We should keep checking all promises
|
|
670
|
+
|
|
671
|
+
await forkPromises(renderer);
|
|
672
|
+
onWrite('</div>');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function forkPromises(renderer) {
|
|
677
|
+
if (renderer.suspended.length > 0) {
|
|
678
|
+
const suspensions = [...renderer.suspended];
|
|
679
|
+
await Promise.all(renderer.suspended.map(s => s.promise));
|
|
680
|
+
renderer.suspended = renderer.suspended.filter(s => !suspensions.includes(s));
|
|
681
|
+
await forkPromises(renderer);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/** @type {RendererErrorHandler} */
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
function handleError(error, vnode, renderChild) {
|
|
688
|
+
if (!error || !error.then) return; // walk up to the Suspense boundary
|
|
689
|
+
|
|
690
|
+
while (vnode = vnode[PARENT]) {
|
|
691
|
+
let component = vnode[COMPONENT];
|
|
692
|
+
|
|
693
|
+
if (component && component[CHILD_DID_SUSPEND]) {
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!vnode) return;
|
|
699
|
+
const id = vnode.__v;
|
|
700
|
+
const found = this.suspended.find(x => x.id === id);
|
|
701
|
+
const race = new Deferred();
|
|
702
|
+
const abortSignal = this.abortSignal;
|
|
703
|
+
|
|
704
|
+
if (abortSignal) {
|
|
705
|
+
// @ts-ignore 2554 - implicit undefined arg
|
|
706
|
+
if (abortSignal.aborted) race.resolve();else abortSignal.addEventListener('abort', race.resolve);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const promise = error.then(() => {
|
|
710
|
+
if (abortSignal && abortSignal.aborted) return;
|
|
711
|
+
const child = renderChild(vnode.props.children);
|
|
712
|
+
if (child) this.onWrite(createSubtree(id, child));
|
|
713
|
+
}, // TODO: Abort and send hydration code snippet to client
|
|
714
|
+
// to attempt to recover during hydration
|
|
715
|
+
this.onError);
|
|
716
|
+
this.suspended.push({
|
|
717
|
+
id,
|
|
718
|
+
vnode,
|
|
719
|
+
promise: Promise.race([promise, race.promise])
|
|
720
|
+
});
|
|
721
|
+
const fallback = renderChild(vnode.props.fallback);
|
|
722
|
+
return found ? '' : `<!--preact-island:${id}-->${fallback}<!--/preact-island:${id}-->`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* @typedef {object} RenderToPipeableStreamOptions
|
|
727
|
+
* @property {() => void} [onShellReady]
|
|
728
|
+
* @property {() => void} [onAllReady]
|
|
729
|
+
* @property {(error) => void} [onError]
|
|
730
|
+
*/
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* @param {import('preact').VNode} vnode
|
|
734
|
+
* @param {RenderToPipeableStreamOptions} options
|
|
735
|
+
* @param {any} [context]
|
|
736
|
+
* @returns {{}}
|
|
737
|
+
*/
|
|
738
|
+
|
|
739
|
+
function renderToPipeableStream(vnode, options, context) {
|
|
740
|
+
const encoder = new TextEncoder('utf-8');
|
|
741
|
+
const controller = new AbortController();
|
|
742
|
+
const stream = new PassThrough();
|
|
743
|
+
renderToChunks(vnode, {
|
|
744
|
+
context,
|
|
745
|
+
abortSignal: controller.signal,
|
|
746
|
+
onError: error => {
|
|
747
|
+
if (options.onError) {
|
|
748
|
+
options.onError(error);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
controller.abort(error);
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
onWrite(s) {
|
|
755
|
+
stream.write(encoder.encode(s));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
}).then(() => {
|
|
759
|
+
options.onAllReady && options.onAllReady();
|
|
760
|
+
stream.end();
|
|
761
|
+
}).catch(error => {
|
|
762
|
+
stream.destroy(error);
|
|
763
|
+
});
|
|
764
|
+
Promise.resolve().then(() => {
|
|
765
|
+
options.onShellReady && options.onShellReady();
|
|
766
|
+
});
|
|
767
|
+
return {
|
|
768
|
+
abort() {
|
|
769
|
+
controller.abort();
|
|
770
|
+
stream.destroy(new Error('aborted'));
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* @param {import("stream").Writable} writable
|
|
775
|
+
*/
|
|
776
|
+
pipe(writable) {
|
|
777
|
+
stream.pipe(writable, {
|
|
778
|
+
end: true
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export { renderToPipeableStream };
|
|
786
|
+
//# sourceMappingURL=stream-node.module.js.map
|