@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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/dist/class-component.d.ts +2 -2
  3. package/dist/class-component.d.ts.map +1 -1
  4. package/dist/class-component.js +29 -3
  5. package/dist/class-component.js.map +1 -1
  6. package/dist/dom-props.d.ts.map +1 -1
  7. package/dist/dom-props.js +0 -21
  8. package/dist/dom-props.js.map +1 -1
  9. package/dist/element.d.ts +0 -1
  10. package/dist/element.d.ts.map +1 -1
  11. package/dist/element.js +2 -41
  12. package/dist/element.js.map +1 -1
  13. package/dist/events.d.ts.map +1 -1
  14. package/dist/events.js +2 -0
  15. package/dist/events.js.map +1 -1
  16. package/dist/fiber-reconciler.js +45 -3
  17. package/dist/fiber-reconciler.js.map +1 -1
  18. package/dist/hooks.d.ts +5 -2
  19. package/dist/hooks.d.ts.map +1 -1
  20. package/dist/hooks.js +38 -15
  21. package/dist/hooks.js.map +1 -1
  22. package/dist/host-reconciler.d.ts.map +1 -1
  23. package/dist/host-reconciler.js +37 -32
  24. package/dist/host-reconciler.js.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/react-default.d.ts +2 -1
  30. package/dist/react-default.d.ts.map +1 -1
  31. package/dist/react-default.js +2 -1
  32. package/dist/react-default.js.map +1 -1
  33. package/dist/server-render.d.ts +1 -0
  34. package/dist/server-render.d.ts.map +1 -1
  35. package/dist/server-render.js +103 -39
  36. package/dist/server-render.js.map +1 -1
  37. package/package.json +3 -3
  38. package/src/class-component.ts +35 -2
  39. package/src/dom-props.ts +0 -30
  40. package/src/element.ts +4 -57
  41. package/src/events.ts +2 -0
  42. package/src/fiber-reconciler.ts +54 -2
  43. package/src/hooks.ts +43 -15
  44. package/src/host-reconciler.ts +43 -36
  45. package/src/index.ts +1 -1
  46. package/src/react-default.ts +2 -1
  47. package/src/server-render.ts +120 -46
@@ -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
- return `<${element.type}${attributes}>${renderNodeToString(element.props.children, runtime, `${path}.children`)}</${element.type}>`;
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 sanitizedProps = sanitizeMetaRefreshProps(props);
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 [name, value] of entries) {
214
- attributes += renderHtmlAttribute(name, value);
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 sanitizeMetaRefreshProps(
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 httpEquiv !== "string" || typeof content !== "string") {
225
- return props;
226
- }
227
- if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
228
- return props;
264
+ if (typeof content !== "string") {
265
+ return false;
229
266
  }
230
267
 
231
- const sanitized = { ...props };
232
- delete sanitized.content;
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
- /^on/i.test(name) ||
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 (/^on/i.test(attributeName)) {
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(name: string): boolean {
392
- const attributeName = toHtmlAttributeName(name).toLowerCase();
393
- return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
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(name: string): boolean {
397
- return toHtmlAttributeName(name).toLowerCase().startsWith("data-");
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
- return Object.entries(value)
459
- .filter(([, propertyValue]) =>
460
- propertyValue !== null &&
461
- propertyValue !== undefined &&
462
- typeof propertyValue !== "boolean" &&
463
- propertyValue !== "",
464
- )
465
- .map(([name, propertyValue]) =>
466
- `${toKebabCase(name)}:${renderCssValue(name, propertyValue)}`,
467
- )
468
- .join(";");
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
- return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
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 {