@reckona/mreact-compat 0.0.152 → 0.0.154
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/README.md +1 -0
- package/dist/class-component.d.ts +2 -2
- package/dist/class-component.d.ts.map +1 -1
- package/dist/class-component.js +29 -3
- package/dist/class-component.js.map +1 -1
- package/dist/dom-props.d.ts.map +1 -1
- package/dist/dom-props.js +0 -21
- package/dist/dom-props.js.map +1 -1
- package/dist/element.d.ts +0 -1
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +2 -41
- package/dist/element.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -1
- package/dist/fiber-reconciler.js +45 -3
- package/dist/fiber-reconciler.js.map +1 -1
- package/dist/hooks.d.ts +5 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +38 -15
- package/dist/hooks.js.map +1 -1
- package/dist/host-reconciler.d.ts.map +1 -1
- package/dist/host-reconciler.js +37 -32
- package/dist/host-reconciler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-default.d.ts +2 -1
- package/dist/react-default.d.ts.map +1 -1
- package/dist/react-default.js +2 -1
- package/dist/react-default.js.map +1 -1
- package/dist/server-render.d.ts +1 -0
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +103 -39
- package/dist/server-render.js.map +1 -1
- package/package.json +3 -3
- package/src/class-component.ts +35 -2
- package/src/dom-props.ts +0 -30
- package/src/element.ts +4 -57
- package/src/events.ts +2 -0
- package/src/fiber-reconciler.ts +54 -2
- package/src/hooks.ts +43 -15
- package/src/host-reconciler.ts +43 -36
- package/src/index.ts +1 -1
- package/src/react-default.ts +2 -1
- package/src/server-render.ts +120 -46
package/src/server-render.ts
CHANGED
|
@@ -59,12 +59,52 @@ export function renderToString<TProps>(
|
|
|
59
59
|
return typeof rendered === "string"
|
|
60
60
|
? rendered
|
|
61
61
|
: renderNodeToString(rendered, runtime, "0.0");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isThenable(error)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"renderToString does not support Suspense. Use a streaming server renderer for components that suspend.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw error;
|
|
62
70
|
} finally {
|
|
63
71
|
runtime.dispose();
|
|
64
72
|
}
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
// Renders a single child value the way the interpreter renders expression
|
|
77
|
+
// children: primitives escape, null/undefined/boolean render nothing, and
|
|
78
|
+
// react nodes fall back to the interpreter. Compiled compat pages call this
|
|
79
|
+
// for expression children whose runtime type is unknown.
|
|
80
|
+
export function renderChildToString(value: unknown): string {
|
|
81
|
+
if (value === null || value === undefined || typeof value === "boolean") {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
86
|
+
return escapeHtml(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const runtime = createRootRuntime(() => undefined, { idMode: "server" });
|
|
90
|
+
|
|
91
|
+
return runWithCacheScope(createCacheScope(), () => {
|
|
92
|
+
try {
|
|
93
|
+
return renderNodeToString(value as ReactCompatNode, runtime, "0.0");
|
|
94
|
+
} finally {
|
|
95
|
+
runtime.dispose();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
101
|
+
return (
|
|
102
|
+
typeof value === "object" &&
|
|
103
|
+
value !== null &&
|
|
104
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
68
108
|
function renderNodeToString(
|
|
69
109
|
node: ReactCompatNode,
|
|
70
110
|
runtime: RootRuntime,
|
|
@@ -115,7 +155,14 @@ function renderElementToString(
|
|
|
115
155
|
return `<${element.type}${attributes}/>`;
|
|
116
156
|
}
|
|
117
157
|
|
|
118
|
-
|
|
158
|
+
// Primitive children dominate real markup; serializing them inline skips
|
|
159
|
+
// one recursive call and one child-path allocation per text leaf.
|
|
160
|
+
const children = element.props.children;
|
|
161
|
+
if (typeof children === "string" || typeof children === "number") {
|
|
162
|
+
return `<${element.type}${attributes}>${escapeHtml(children)}</${element.type}>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return `<${element.type}${attributes}>${renderNodeToString(children, runtime, `${path}.children`)}</${element.type}>`;
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
if (element.type === Fragment) {
|
|
@@ -200,37 +247,26 @@ function renderElementToString(
|
|
|
200
247
|
}
|
|
201
248
|
|
|
202
249
|
function renderAttributesToString(props: Record<string, unknown>): string {
|
|
203
|
-
const
|
|
204
|
-
const entries = Object.entries(sanitizedProps);
|
|
205
|
-
if (
|
|
206
|
-
entries.length === 0 ||
|
|
207
|
-
(entries.length === 1 && entries[0]?.[0] === "children")
|
|
208
|
-
) {
|
|
209
|
-
return "";
|
|
210
|
-
}
|
|
250
|
+
const skipUnsafeMetaRefreshContent = hasUnsafeMetaRefreshProps(props);
|
|
211
251
|
|
|
212
252
|
let attributes = "";
|
|
213
|
-
for (const
|
|
214
|
-
|
|
253
|
+
for (const name in props) {
|
|
254
|
+
if (skipUnsafeMetaRefreshContent && name === "content") {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
attributes += renderHtmlAttribute(name, props[name]);
|
|
215
258
|
}
|
|
216
259
|
return attributes;
|
|
217
260
|
}
|
|
218
261
|
|
|
219
|
-
function
|
|
220
|
-
props: Record<string, unknown>,
|
|
221
|
-
): Record<string, unknown> {
|
|
222
|
-
const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
|
|
262
|
+
function hasUnsafeMetaRefreshProps(props: Record<string, unknown>): boolean {
|
|
223
263
|
const content = props.content;
|
|
224
|
-
if (typeof
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
|
|
228
|
-
return props;
|
|
264
|
+
if (typeof content !== "string") {
|
|
265
|
+
return false;
|
|
229
266
|
}
|
|
230
267
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
return sanitized;
|
|
268
|
+
const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
|
|
269
|
+
return typeof httpEquiv === "string" && isUnsafeMetaRefreshContent(httpEquiv, content);
|
|
234
270
|
}
|
|
235
271
|
|
|
236
272
|
function isClassComponentType(
|
|
@@ -323,15 +359,24 @@ function renderInputAttributesToString(props: Record<string, unknown>): string {
|
|
|
323
359
|
.join("");
|
|
324
360
|
}
|
|
325
361
|
|
|
362
|
+
// Matches the /^on/i prefix without allocating a fresh regex per attribute.
|
|
363
|
+
function isEventHandlerName(name: string): boolean {
|
|
364
|
+
return (
|
|
365
|
+
name.length > 1 &&
|
|
366
|
+
(name.charCodeAt(0) | 32) === 111 &&
|
|
367
|
+
(name.charCodeAt(1) | 32) === 110
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
326
371
|
function renderHtmlAttribute(name: string, value: unknown): string {
|
|
327
372
|
if (
|
|
373
|
+
value === null ||
|
|
374
|
+
value === undefined ||
|
|
375
|
+
typeof value === "function" ||
|
|
328
376
|
name === "children" ||
|
|
329
377
|
name === "key" ||
|
|
330
378
|
name === "ref" ||
|
|
331
|
-
|
|
332
|
-
value === null ||
|
|
333
|
-
value === undefined ||
|
|
334
|
-
typeof value === "function"
|
|
379
|
+
isEventHandlerName(name)
|
|
335
380
|
) {
|
|
336
381
|
return "";
|
|
337
382
|
}
|
|
@@ -347,7 +392,7 @@ function renderHtmlAttribute(name: string, value: unknown): string {
|
|
|
347
392
|
return "";
|
|
348
393
|
}
|
|
349
394
|
|
|
350
|
-
if (
|
|
395
|
+
if (isEventHandlerName(attributeName)) {
|
|
351
396
|
return "";
|
|
352
397
|
}
|
|
353
398
|
|
|
@@ -388,13 +433,14 @@ function renderHtmlAttribute(name: string, value: unknown): string {
|
|
|
388
433
|
|
|
389
434
|
const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
|
|
390
435
|
|
|
391
|
-
function isBooleanishStringAttribute(
|
|
392
|
-
|
|
393
|
-
|
|
436
|
+
function isBooleanishStringAttribute(attributeName: string): boolean {
|
|
437
|
+
// Callers pass the already-mapped HTML attribute name.
|
|
438
|
+
const lowerCased = attributeName.toLowerCase();
|
|
439
|
+
return lowerCased.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(lowerCased);
|
|
394
440
|
}
|
|
395
441
|
|
|
396
|
-
function isDataAttribute(
|
|
397
|
-
return
|
|
442
|
+
function isDataAttribute(attributeName: string): boolean {
|
|
443
|
+
return attributeName.toLowerCase().startsWith("data-");
|
|
398
444
|
}
|
|
399
445
|
|
|
400
446
|
const BOOLEANISH_STRING_ATTRIBUTES = new Set<string>([
|
|
@@ -441,7 +487,6 @@ const HTML_ATTRIBUTE_ALIASES: Record<string, string> = {
|
|
|
441
487
|
minLength: "minlength",
|
|
442
488
|
noValidate: "novalidate",
|
|
443
489
|
playsInline: "playsinline",
|
|
444
|
-
readOnly: "readOnly",
|
|
445
490
|
rowSpan: "rowspan",
|
|
446
491
|
spellCheck: "spellcheck",
|
|
447
492
|
srcDoc: "srcdoc",
|
|
@@ -455,17 +500,24 @@ function renderStyleAttribute(value: unknown): string {
|
|
|
455
500
|
return "";
|
|
456
501
|
}
|
|
457
502
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
propertyValue
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
)
|
|
468
|
-
|
|
503
|
+
const styleProps = value as Record<string, unknown>;
|
|
504
|
+
let css = "";
|
|
505
|
+
for (const name in styleProps) {
|
|
506
|
+
const propertyValue = styleProps[name];
|
|
507
|
+
if (
|
|
508
|
+
propertyValue === null ||
|
|
509
|
+
propertyValue === undefined ||
|
|
510
|
+
typeof propertyValue === "boolean" ||
|
|
511
|
+
propertyValue === ""
|
|
512
|
+
) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
css += css === ""
|
|
517
|
+
? `${toKebabCase(name)}:${renderCssValue(name, propertyValue)}`
|
|
518
|
+
: `;${toKebabCase(name)}:${renderCssValue(name, propertyValue)}`;
|
|
519
|
+
}
|
|
520
|
+
return css;
|
|
469
521
|
}
|
|
470
522
|
|
|
471
523
|
function renderCssValue(name: string, value: unknown): string {
|
|
@@ -476,8 +528,30 @@ function renderCssValue(name: string, value: unknown): string {
|
|
|
476
528
|
return `${value}px`;
|
|
477
529
|
}
|
|
478
530
|
|
|
531
|
+
const UPPERCASE_LETTER = /[A-Z]/;
|
|
532
|
+
const UPPERCASE_LETTER_GLOBAL = /[A-Z]/g;
|
|
533
|
+
// Distinct camelCase style names per app are few; the cap only guards
|
|
534
|
+
// pathological dynamically-generated property names.
|
|
535
|
+
const KEBAB_CASE_CACHE = new Map<string, string>();
|
|
536
|
+
const KEBAB_CASE_CACHE_LIMIT = 512;
|
|
537
|
+
|
|
538
|
+
function kebabReplace(letter: string): string {
|
|
539
|
+
return `-${letter.toLowerCase()}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
479
542
|
function toKebabCase(value: string): string {
|
|
480
|
-
|
|
543
|
+
if (!UPPERCASE_LETTER.test(value)) {
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let cached = KEBAB_CASE_CACHE.get(value);
|
|
548
|
+
if (cached === undefined) {
|
|
549
|
+
cached = value.replace(UPPERCASE_LETTER_GLOBAL, kebabReplace);
|
|
550
|
+
if (KEBAB_CASE_CACHE.size < KEBAB_CASE_CACHE_LIMIT) {
|
|
551
|
+
KEBAB_CASE_CACHE.set(value, cached);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return cached;
|
|
481
555
|
}
|
|
482
556
|
|
|
483
557
|
function isUnitlessCssProperty(name: string): boolean {
|