@reckona/mreact-compat 0.0.153 → 0.0.155

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.
@@ -4,7 +4,6 @@ import {
4
4
  FORWARD_REF_TYPE,
5
5
  Fragment,
6
6
  HOST_CHILDREN_ONLY_PROPS_META,
7
- HOST_OWN_PROPS_META,
8
7
  LAZY_TYPE,
9
8
  MEMO_TYPE,
10
9
  Profiler,
@@ -571,8 +570,8 @@ function getReusableKeyedRowHostFiber(
571
570
  const previousRecord = previousProps as Record<string, unknown>;
572
571
 
573
572
  if (
574
- getHostOwnPropsMeta(previousRecord) !== row.meta ||
575
- getDirectHostTextChild(previousRecord.children) !== row.text
573
+ getDirectHostTextChild(previousRecord.children) !== row.text ||
574
+ !hostOwnPropsEqual(previousRecord, row.element.props)
576
575
  ) {
577
576
  return undefined;
578
577
  }
@@ -591,7 +590,6 @@ interface KeyedRowHostElement {
591
590
  element: ReactCompatElement;
592
591
  key: string;
593
592
  type: string;
594
- meta: number;
595
593
  text: string;
596
594
  }
597
595
 
@@ -600,7 +598,6 @@ function createKeyedRowHostElementScratch(): KeyedRowHostElement {
600
598
  element: undefined as unknown as ReactCompatElement,
601
599
  key: "",
602
600
  type: "",
603
- meta: 0,
604
601
  text: "",
605
602
  };
606
603
  }
@@ -618,18 +615,17 @@ function readKeyedRowHostElement(
618
615
  return false;
619
616
  }
620
617
 
621
- const props = node.props as Record<string, unknown>;
622
- const meta = getHostOwnPropsMeta(props);
623
- const text = meta === undefined ? undefined : getDirectHostTextChild(props.children);
618
+ // Any keyed host row whose children collapse to a single text value
619
+ // qualifies; row props are compared per reuse with hostOwnPropsEqual.
620
+ const text = getDirectHostTextChild((node.props as Record<string, unknown>).children);
624
621
 
625
- if (meta === undefined || text === undefined) {
622
+ if (text === undefined) {
626
623
  return false;
627
624
  }
628
625
 
629
626
  row.element = node;
630
627
  row.key = node.key;
631
628
  row.type = node.type;
632
- row.meta = meta;
633
629
  row.text = text;
634
630
  return true;
635
631
  }
@@ -667,13 +663,9 @@ function createKeyedRowHostFiber(
667
663
  }
668
664
 
669
665
  const previousProps = current.memoizedProps ?? current.pendingProps;
670
- const previousMeta =
671
- typeof previousProps === "object" && previousProps !== null
672
- ? getHostOwnPropsMeta(previousProps as Record<string, unknown>)
673
- : undefined;
674
666
  const previousText = getDirectHostTextChild(hostFiberChildrenProp(previousProps));
675
667
 
676
- if (previousMeta !== row.meta || previousText !== row.text) {
668
+ if (previousText !== row.text || !hostOwnPropsEqual(previousProps, row.element.props)) {
677
669
  fiber.flags |= Update;
678
670
  }
679
671
 
@@ -1642,6 +1634,8 @@ function commitHostDirtyFiber(
1642
1634
  hostPropsAreChildrenOnly(fiber.memoizedProps) &&
1643
1635
  hostPropsAreChildrenOnly(props));
1644
1636
  const textOnlyRowUpdate =
1637
+ !propsAreUnchanged &&
1638
+ !propsAreChildrenOnly &&
1645
1639
  fiber.hydrateExisting !== true &&
1646
1640
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
1647
1641
 
@@ -2032,6 +2026,8 @@ function commitHostFiber(
2032
2026
  hostPropsAreChildrenOnly(fiber.memoizedProps) &&
2033
2027
  hostPropsAreChildrenOnly(props));
2034
2028
  const textOnlyRowUpdate =
2029
+ !propsAreUnchanged &&
2030
+ !propsAreChildrenOnly &&
2035
2031
  fiber.hydrateExisting !== true &&
2036
2032
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
2037
2033
 
@@ -2243,12 +2239,6 @@ function hostOwnPropsEqual(previous: unknown, next: Record<string, unknown>): bo
2243
2239
  }
2244
2240
 
2245
2241
  const previousProps = previous as Record<string, unknown>;
2246
- const previousMeta = getHostOwnPropsMeta(previousProps);
2247
- const nextMeta = getHostOwnPropsMeta(next);
2248
-
2249
- if (previousMeta !== undefined && nextMeta !== undefined) {
2250
- return previousMeta === nextMeta;
2251
- }
2252
2242
 
2253
2243
  let previousCount = 0;
2254
2244
  let nextCount = 0;
@@ -2276,10 +2266,6 @@ function hostOwnPropsEqual(previous: unknown, next: Record<string, unknown>): bo
2276
2266
  return previousCount === nextCount;
2277
2267
  }
2278
2268
 
2279
- function getHostOwnPropsMeta(props: Record<string, unknown>): number | undefined {
2280
- return (props as { [HOST_OWN_PROPS_META]?: number })[HOST_OWN_PROPS_META];
2281
- }
2282
-
2283
2269
  function hostDirectTextChildChanged(previous: unknown, next: Record<string, unknown>): boolean {
2284
2270
  const previousText = getDirectHostTextChild(hostFiberChildrenProp(previous));
2285
2271
  const nextText = getDirectHostTextChild(next.children);
@@ -2379,17 +2365,15 @@ function isRowTextOnlyUpdate(previous: unknown, next: Record<string, unknown>):
2379
2365
  }
2380
2366
 
2381
2367
  const previousProps = previous as Record<string, unknown>;
2382
- const previousMeta = getHostOwnPropsMeta(previousProps);
2383
- const nextMeta = getHostOwnPropsMeta(next);
2384
-
2385
- if (previousMeta === undefined || previousMeta !== nextMeta) {
2386
- return false;
2387
- }
2388
-
2389
2368
  const previousText = getDirectHostTextChild(previousProps.children);
2390
2369
  const nextText = getDirectHostTextChild(next.children);
2391
2370
 
2392
- return previousText !== undefined && nextText !== undefined && previousText !== nextText;
2371
+ return (
2372
+ previousText !== undefined &&
2373
+ nextText !== undefined &&
2374
+ previousText !== nextText &&
2375
+ hostOwnPropsEqual(previousProps, next)
2376
+ );
2393
2377
  }
2394
2378
 
2395
2379
  function hostFiberChildrenProp(props: unknown): unknown {
@@ -2404,10 +2388,33 @@ function getDirectHostTextChild(children: unknown): string | undefined {
2404
2388
  : undefined;
2405
2389
  }
2406
2390
 
2391
+ // This package has no Node type dependency; declare the minimal process
2392
+ // shape needed for the literal process.env.NODE_ENV expression below.
2393
+ declare const process: { env: Record<string, string | undefined> };
2394
+
2395
+ type HostFastPathMode = "static-fast" | "dynamic";
2396
+
2397
+ const hostFastPathMode: HostFastPathMode = (() => {
2398
+ try {
2399
+ // The literal process.env.NODE_ENV member expression is what bundler
2400
+ // define rewriting matches; a globalThis.process indirection is never
2401
+ // rewritten and leaves deployed browser bundles without any fast path.
2402
+ return process.env.NODE_ENV === "production" ? "static-fast" : "dynamic";
2403
+ } catch {
2404
+ // No process global at all: an unbundled browser runtime. Treat it as
2405
+ // production rather than running every host update on the slow path.
2406
+ return "static-fast";
2407
+ }
2408
+ })();
2409
+
2407
2410
  function shouldUseDirectHostTextChild(): boolean {
2408
- const globalProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } })
2409
- .process;
2410
- return globalProcess?.env?.NODE_ENV === "production";
2411
+ if (hostFastPathMode === "static-fast") {
2412
+ return true;
2413
+ }
2414
+
2415
+ // Node dev/test environments keep the per-call env read so test harnesses
2416
+ // can flip NODE_ENV (vi.stubEnv) without re-importing this module.
2417
+ return process.env.NODE_ENV === "production";
2411
2418
  }
2412
2419
 
2413
2420
  function syncDirectHostTextChild(element: Element, text: string): Text {
package/src/index.ts CHANGED
@@ -91,6 +91,6 @@ export {
91
91
  useTransition,
92
92
  version,
93
93
  } from "./hooks.js";
94
- export { renderToString } from "./server-render.js";
94
+ export { renderChildToString, renderToString } from "./server-render.js";
95
95
  export type { StartTransition, TransitionScope } from "./hooks.js";
96
96
  export { default } from "./react-default.js";
@@ -66,7 +66,7 @@ import {
66
66
  useTransition,
67
67
  version,
68
68
  } from "./hooks.js";
69
- import { renderToString } from "./server-render.js";
69
+ import { renderChildToString, renderToString } from "./server-render.js";
70
70
 
71
71
  const ReactCompat = {
72
72
  Component,
@@ -123,6 +123,7 @@ const ReactCompat = {
123
123
  cache,
124
124
  cacheSignal,
125
125
  captureOwnerStack,
126
+ renderChildToString,
126
127
  renderToString,
127
128
  startTransition,
128
129
  unstable_useCacheRefresh,
@@ -73,6 +73,30 @@ export function renderToString<TProps>(
73
73
  });
74
74
  }
75
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
+
76
100
  function isThenable(value: unknown): value is PromiseLike<unknown> {
77
101
  return (
78
102
  typeof value === "object" &&
@@ -131,7 +155,14 @@ function renderElementToString(
131
155
  return `<${element.type}${attributes}/>`;
132
156
  }
133
157
 
134
- 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}>`;
135
166
  }
136
167
 
137
168
  if (element.type === Fragment) {
@@ -216,37 +247,26 @@ function renderElementToString(
216
247
  }
217
248
 
218
249
  function renderAttributesToString(props: Record<string, unknown>): string {
219
- const sanitizedProps = sanitizeMetaRefreshProps(props);
220
- const entries = Object.entries(sanitizedProps);
221
- if (
222
- entries.length === 0 ||
223
- (entries.length === 1 && entries[0]?.[0] === "children")
224
- ) {
225
- return "";
226
- }
250
+ const skipUnsafeMetaRefreshContent = hasUnsafeMetaRefreshProps(props);
227
251
 
228
252
  let attributes = "";
229
- for (const [name, value] of entries) {
230
- attributes += renderHtmlAttribute(name, value);
253
+ for (const name in props) {
254
+ if (skipUnsafeMetaRefreshContent && name === "content") {
255
+ continue;
256
+ }
257
+ attributes += renderHtmlAttribute(name, props[name]);
231
258
  }
232
259
  return attributes;
233
260
  }
234
261
 
235
- function sanitizeMetaRefreshProps(
236
- props: Record<string, unknown>,
237
- ): Record<string, unknown> {
238
- const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
262
+ function hasUnsafeMetaRefreshProps(props: Record<string, unknown>): boolean {
239
263
  const content = props.content;
240
- if (typeof httpEquiv !== "string" || typeof content !== "string") {
241
- return props;
242
- }
243
- if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
244
- return props;
264
+ if (typeof content !== "string") {
265
+ return false;
245
266
  }
246
267
 
247
- const sanitized = { ...props };
248
- delete sanitized.content;
249
- return sanitized;
268
+ const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
269
+ return typeof httpEquiv === "string" && isUnsafeMetaRefreshContent(httpEquiv, content);
250
270
  }
251
271
 
252
272
  function isClassComponentType(
@@ -339,15 +359,24 @@ function renderInputAttributesToString(props: Record<string, unknown>): string {
339
359
  .join("");
340
360
  }
341
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
+
342
371
  function renderHtmlAttribute(name: string, value: unknown): string {
343
372
  if (
373
+ value === null ||
374
+ value === undefined ||
375
+ typeof value === "function" ||
344
376
  name === "children" ||
345
377
  name === "key" ||
346
378
  name === "ref" ||
347
- /^on/i.test(name) ||
348
- value === null ||
349
- value === undefined ||
350
- typeof value === "function"
379
+ isEventHandlerName(name)
351
380
  ) {
352
381
  return "";
353
382
  }
@@ -363,7 +392,7 @@ function renderHtmlAttribute(name: string, value: unknown): string {
363
392
  return "";
364
393
  }
365
394
 
366
- if (/^on/i.test(attributeName)) {
395
+ if (isEventHandlerName(attributeName)) {
367
396
  return "";
368
397
  }
369
398
 
@@ -404,13 +433,14 @@ function renderHtmlAttribute(name: string, value: unknown): string {
404
433
 
405
434
  const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
406
435
 
407
- function isBooleanishStringAttribute(name: string): boolean {
408
- const attributeName = toHtmlAttributeName(name).toLowerCase();
409
- 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);
410
440
  }
411
441
 
412
- function isDataAttribute(name: string): boolean {
413
- return toHtmlAttributeName(name).toLowerCase().startsWith("data-");
442
+ function isDataAttribute(attributeName: string): boolean {
443
+ return attributeName.toLowerCase().startsWith("data-");
414
444
  }
415
445
 
416
446
  const BOOLEANISH_STRING_ATTRIBUTES = new Set<string>([
@@ -470,17 +500,24 @@ function renderStyleAttribute(value: unknown): string {
470
500
  return "";
471
501
  }
472
502
 
473
- return Object.entries(value)
474
- .filter(([, propertyValue]) =>
475
- propertyValue !== null &&
476
- propertyValue !== undefined &&
477
- typeof propertyValue !== "boolean" &&
478
- propertyValue !== "",
479
- )
480
- .map(([name, propertyValue]) =>
481
- `${toKebabCase(name)}:${renderCssValue(name, propertyValue)}`,
482
- )
483
- .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;
484
521
  }
485
522
 
486
523
  function renderCssValue(name: string, value: unknown): string {
@@ -491,8 +528,30 @@ function renderCssValue(name: string, value: unknown): string {
491
528
  return `${value}px`;
492
529
  }
493
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
+
494
542
  function toKebabCase(value: string): string {
495
- 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;
496
555
  }
497
556
 
498
557
  function isUnitlessCssProperty(name: string): boolean {