@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
package/src/element.ts CHANGED
@@ -11,7 +11,6 @@ export const Suspense = Symbol.for("react.suspense");
11
11
  export const SuspenseList = Symbol.for("react.suspense_list");
12
12
  export const Activity = Symbol.for("react.activity");
13
13
  export const Profiler = Symbol.for("react.profiler");
14
- export const HOST_OWN_PROPS_META = Symbol.for("modular.react.host_own_props_meta");
15
14
  export const HOST_CHILDREN_ONLY_PROPS_META = Symbol.for(
16
15
  "modular.react.host_children_only_props_meta",
17
16
  );
@@ -376,63 +375,11 @@ function isReactCompatContextProviderShorthand(
376
375
  }
377
376
 
378
377
  function setHostOwnPropsMeta(props: Record<string, unknown>): void {
379
- const dataKey = props["data-key"];
380
-
381
- if (typeof dataKey !== "number" || !Number.isSafeInteger(dataKey) || dataKey < 0) {
382
- if (hostPropsAreChildrenOnly(props)) {
383
- (props as { [HOST_CHILDREN_ONLY_PROPS_META]?: true })[
384
- HOST_CHILDREN_ONLY_PROPS_META
385
- ] = true;
386
- }
387
- return;
378
+ if (hostPropsAreChildrenOnly(props)) {
379
+ (props as { [HOST_CHILDREN_ONLY_PROPS_META]?: true })[
380
+ HOST_CHILDREN_ONLY_PROPS_META
381
+ ] = true;
388
382
  }
389
-
390
- let selectedState = 0;
391
-
392
- for (const name in props) {
393
- if (!hasOwnProperty.call(props, name) || name === "children") {
394
- continue;
395
- }
396
-
397
- if (name === "data-key") {
398
- continue;
399
- }
400
-
401
- if (name === "className") {
402
- const value = props[name];
403
-
404
- if (value === undefined) {
405
- continue;
406
- }
407
-
408
- if (value !== "selected") {
409
- return;
410
- }
411
-
412
- selectedState |= 1;
413
- continue;
414
- }
415
-
416
- if (name === "data-selected") {
417
- const value = props[name];
418
-
419
- if (value === undefined) {
420
- continue;
421
- }
422
-
423
- if (value !== "true") {
424
- return;
425
- }
426
-
427
- selectedState |= 2;
428
- continue;
429
- }
430
-
431
- return;
432
- }
433
-
434
- (props as { [HOST_OWN_PROPS_META]?: number })[HOST_OWN_PROPS_META] =
435
- dataKey * 4 + selectedState;
436
383
  }
437
384
 
438
385
  function hostPropsAreChildrenOnly(props: Record<string, unknown>): boolean {
package/src/events.ts CHANGED
@@ -18,6 +18,7 @@ const reactPropToNativeEvent = new Map<string, string[]>([
18
18
  ["onCompositionUpdate", ["compositionupdate"]],
19
19
  ["onContextMenu", ["contextmenu"]],
20
20
  ["onDoubleClick", ["dblclick"]],
21
+ ["onDrag", ["drag"]],
21
22
  ["onDragEnd", ["dragend"]],
22
23
  ["onDragEnter", ["dragenter"]],
23
24
  ["onDragExit", ["dragexit"]],
@@ -57,6 +58,7 @@ const nativeEventToReactProps = new Map<string, string[]>([
57
58
  ["compositionupdate", ["onCompositionUpdate"]],
58
59
  ["contextmenu", ["onContextMenu"]],
59
60
  ["dblclick", ["onDoubleClick"]],
61
+ ["drag", ["onDrag"]],
60
62
  ["dragend", ["onDragEnd"]],
61
63
  ["dragenter", ["onDragEnter"]],
62
64
  ["dragexit", ["onDragExit"]],
@@ -529,7 +529,7 @@ function captureThrownValue(
529
529
  }
530
530
 
531
531
  if (!isThenable(thrownValue) && boundary.tag === "class-component") {
532
- const captured = captureClassErrorBoundary(root, boundary, thrownValue);
532
+ const captured = captureClassErrorBoundary(root, boundary, source, thrownValue);
533
533
 
534
534
  if (captured !== undefined) {
535
535
  return captured;
@@ -574,6 +574,7 @@ function beginClassComponent(
574
574
  function captureClassErrorBoundary(
575
575
  root: FiberRoot,
576
576
  boundary: Fiber,
577
+ source: Fiber,
577
578
  thrownValue: unknown,
578
579
  ): Fiber | undefined {
579
580
  if (!isClassComponentType(boundary.type)) {
@@ -598,7 +599,9 @@ function captureClassErrorBoundary(
598
599
  };
599
600
  }
600
601
 
601
- instance.componentDidCatch?.(error, { componentStack: "" });
602
+ instance.componentDidCatch?.(error, {
603
+ componentStack: componentStackFromFiber(source, boundary),
604
+ });
602
605
  boundary.child = reconcileChildFibers(
603
606
  boundary,
604
607
  boundary.alternate?.child,
@@ -607,6 +610,55 @@ function captureClassErrorBoundary(
607
610
  return boundary.child ?? completeUnitOfWork(root, boundary);
608
611
  }
609
612
 
613
+ function componentStackFromFiber(source: Fiber, boundary: Fiber): string {
614
+ const names: string[] = [];
615
+ let cursor: Fiber | undefined = source;
616
+
617
+ while (cursor !== undefined) {
618
+ const name = componentNameForFiber(cursor);
619
+ if (name !== undefined) {
620
+ names.push(name);
621
+ }
622
+ if (cursor === boundary) {
623
+ break;
624
+ }
625
+ cursor = cursor.return;
626
+ }
627
+
628
+ return names.length === 0
629
+ ? "\n at Anonymous"
630
+ : names.map((name) => `\n at ${name}`).join("");
631
+ }
632
+
633
+ function componentNameForFiber(fiber: Fiber): string | undefined {
634
+ if (fiber.tag === "host-component") {
635
+ return String(fiber.type);
636
+ }
637
+
638
+ if (fiber.tag === "function-component" || fiber.tag === "class-component") {
639
+ return componentNameFromType(fiber.type);
640
+ }
641
+
642
+ if (fiber.tag === "memo" && isMemoType(fiber.type)) {
643
+ return componentNameFromType(fiber.type.type);
644
+ }
645
+
646
+ if (fiber.tag === "forward-ref" && isForwardRefType(fiber.type)) {
647
+ return componentNameFromType(fiber.type.render);
648
+ }
649
+
650
+ return undefined;
651
+ }
652
+
653
+ function componentNameFromType(type: unknown): string {
654
+ const displayName = (type as { displayName?: unknown } | undefined)?.displayName;
655
+ if (typeof displayName === "string" && displayName !== "") {
656
+ return displayName;
657
+ }
658
+ const name = (type as { name?: unknown } | undefined)?.name;
659
+ return typeof name === "string" && name !== "" ? name : "Anonymous";
660
+ }
661
+
610
662
  function isClassErrorBoundary(
611
663
  type: ClassComponentType,
612
664
  instance: ClassComponentInstance,
package/src/hooks.ts CHANGED
@@ -24,6 +24,7 @@ export interface RootRuntime {
24
24
  profilerBaseDurations: Map<string, number>;
25
25
  pendingProfilerCommits: PendingProfilerCommit[];
26
26
  pendingInsertionEffects: PendingEffect[];
27
+ pendingImperativeHandleEffects: PendingEffect[];
27
28
  pendingLayoutEffects: PendingEffect[];
28
29
  pendingEffects: PendingEffect[];
29
30
  externalStoreChecks: ExternalStoreCheck[];
@@ -39,7 +40,7 @@ export interface RootRuntime {
39
40
  strictMemoReplay: { values: readonly unknown[]; index: number } | undefined;
40
41
  strictMemoReplayByHook: ReadonlyMap<string, unknown> | undefined;
41
42
  profilerFlushDepth: number;
42
- effectFlushPhase: "insertion" | "layout" | "normal" | undefined;
43
+ effectFlushPhase: "insertion" | "imperative-handle" | "layout" | "normal" | undefined;
43
44
  externalStoreUpdate: boolean;
44
45
  renderPhaseUpdate: boolean;
45
46
  rerender(priority?: RenderPriority): void;
@@ -147,7 +148,7 @@ type HookSlot =
147
148
  | { kind: "debug"; value: unknown }
148
149
  | {
149
150
  kind: "effect";
150
- effectKind: "insertion" | "layout" | "normal";
151
+ effectKind: "insertion" | "imperative-handle" | "layout" | "normal";
151
152
  callback: EffectCallback;
152
153
  deps?: readonly unknown[];
153
154
  cleanup?: () => void;
@@ -191,7 +192,6 @@ let automaticRerenderScheduled = false;
191
192
  let effectFlushRerenderDepth = 0;
192
193
  let strictMemoOwnerId = 0;
193
194
  const strictMemoObjectOwnerIds = new WeakMap<object, number>();
194
- const strictMemoPrimitiveOwnerIds = new Map<unknown, number>();
195
195
  const queuedTransitionRerenders = new Map<RootRuntime, TransitionContext>();
196
196
  const queuedEventRerenders = new Set<RootRuntime>();
197
197
  export const version = "19.2.6";
@@ -285,6 +285,7 @@ export interface RuntimeSnapshot {
285
285
  portalContainers: Set<Element>;
286
286
  portalNodes: Map<Element, Set<Node>>;
287
287
  pendingInsertionEffectsLength: number;
288
+ pendingImperativeHandleEffectsLength: number;
288
289
  pendingLayoutEffectsLength: number;
289
290
  pendingEffectsLength: number;
290
291
  pendingProfilerCommitsLength: number;
@@ -314,6 +315,7 @@ export function createRootRuntime(
314
315
  profilerBaseDurations: new Map(),
315
316
  pendingProfilerCommits: [],
316
317
  pendingInsertionEffects: [],
318
+ pendingImperativeHandleEffects: [],
317
319
  pendingLayoutEffects: [],
318
320
  pendingEffects: [],
319
321
  externalStoreChecks: [],
@@ -338,6 +340,7 @@ export function createRootRuntime(
338
340
  this.activeProfilerPaths = new Set();
339
341
  this.pendingProfilerCommits = [];
340
342
  this.pendingInsertionEffects = [];
343
+ this.pendingImperativeHandleEffects = [];
341
344
  this.pendingLayoutEffects = [];
342
345
  this.pendingEffects = [];
343
346
  this.externalStoreChecks = [];
@@ -368,6 +371,8 @@ export function createRootRuntime(
368
371
  try {
369
372
  this.effectFlushPhase = "insertion";
370
373
  flushPendingEffects(this.pendingInsertionEffects);
374
+ this.effectFlushPhase = "imperative-handle";
375
+ flushPendingEffects(this.pendingImperativeHandleEffects);
371
376
  this.effectFlushPhase = "layout";
372
377
  const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
373
378
  this.effectFlushPhase = "normal";
@@ -662,6 +667,7 @@ export function takeRuntimeSnapshot(runtime: RootRuntime): RuntimeSnapshot {
662
667
  portalContainers: new Set(runtime.portalContainers),
663
668
  portalNodes: clonePortalNodes(runtime.portalNodes),
664
669
  pendingInsertionEffectsLength: runtime.pendingInsertionEffects.length,
670
+ pendingImperativeHandleEffectsLength: runtime.pendingImperativeHandleEffects.length,
665
671
  pendingLayoutEffectsLength: runtime.pendingLayoutEffects.length,
666
672
  pendingEffectsLength: runtime.pendingEffects.length,
667
673
  pendingProfilerCommitsLength: runtime.pendingProfilerCommits.length,
@@ -684,6 +690,7 @@ export function restoreRuntimeSnapshot(
684
690
  snapshot: RuntimeSnapshot,
685
691
  ): void {
686
692
  runtime.pendingInsertionEffects.length = snapshot.pendingInsertionEffectsLength;
693
+ runtime.pendingImperativeHandleEffects.length = snapshot.pendingImperativeHandleEffectsLength;
687
694
  runtime.pendingLayoutEffects.length = snapshot.pendingLayoutEffectsLength;
688
695
  runtime.pendingEffects.length = snapshot.pendingEffectsLength;
689
696
  runtime.pendingProfilerCommits.length = snapshot.pendingProfilerCommitsLength;
@@ -908,16 +915,20 @@ export function useReducer<TState, TAction, TInitial = TState>(
908
915
  ),
909
916
  );
910
917
  const reducerRef = runWithoutDevToolsHookTracking(() => useRef(reducer));
918
+ const stateRef = runWithoutDevToolsHookTracking(() => useRef(state));
911
919
  const dispatchRef = runWithoutDevToolsHookTracking(() =>
912
920
  useRef<((action: TAction) => void) | undefined>(
913
921
  undefined,
914
922
  )
915
923
  );
916
924
  reducerRef.current = reducer;
925
+ stateRef.current = state;
917
926
 
918
927
  if (dispatchRef.current === undefined) {
919
928
  dispatchRef.current = (action: TAction): void => {
920
- setState((previousState) => reducerRef.current(previousState, action));
929
+ const nextState = reducerRef.current(stateRef.current, action);
930
+ stateRef.current = nextState;
931
+ setState(nextState);
921
932
  };
922
933
  }
923
934
 
@@ -995,7 +1006,7 @@ export function useImperativeHandle<T>(
995
1006
  deps?: readonly unknown[],
996
1007
  ): void {
997
1008
  runWithoutDevToolsHookTracking(() =>
998
- useInsertionEffect(() => {
1009
+ useEffectImpl("imperative-handle", () => {
999
1010
  const handle = create();
1000
1011
  assignRef(ref, handle);
1001
1012
  return () => {
@@ -1076,10 +1087,14 @@ function getStrictMemoHookKey(
1076
1087
  instance: ComponentInstance,
1077
1088
  index: number,
1078
1089
  ): string {
1079
- return `${instance.path}:${getStrictMemoOwnerId(instance.owner)}:${index}`;
1090
+ return `${instance.path}:${getStrictMemoOwnerKey(instance.owner)}:${index}`;
1080
1091
  }
1081
1092
 
1082
- function getStrictMemoOwnerId(owner: unknown): number {
1093
+ export function __getStrictMemoOwnerKeyForTesting(owner: unknown): string {
1094
+ return getStrictMemoOwnerKey(owner);
1095
+ }
1096
+
1097
+ function getStrictMemoOwnerKey(owner: unknown): string {
1083
1098
  if ((typeof owner === "object" && owner !== null) || typeof owner === "function") {
1084
1099
  const objectOwner = owner as object;
1085
1100
  let ownerId = strictMemoObjectOwnerIds.get(objectOwner);
@@ -1087,15 +1102,17 @@ function getStrictMemoOwnerId(owner: unknown): number {
1087
1102
  ownerId = strictMemoOwnerId++;
1088
1103
  strictMemoObjectOwnerIds.set(objectOwner, ownerId);
1089
1104
  }
1090
- return ownerId;
1105
+ return `o:${ownerId}`;
1091
1106
  }
1092
1107
 
1093
- let ownerId = strictMemoPrimitiveOwnerIds.get(owner);
1094
- if (ownerId === undefined) {
1095
- ownerId = strictMemoOwnerId++;
1096
- strictMemoPrimitiveOwnerIds.set(owner, ownerId);
1108
+ if (typeof owner === "symbol") {
1109
+ const globalKey = Symbol.keyFor(owner);
1110
+ return globalKey === undefined
1111
+ ? `p:symbol:${String(owner)}`
1112
+ : `p:symbol-global:${globalKey}`;
1097
1113
  }
1098
- return ownerId;
1114
+
1115
+ return `p:${typeof owner}:${String(owner)}`;
1099
1116
  }
1100
1117
 
1101
1118
  function assignRef<T>(ref: unknown, value: T | null): void {
@@ -1567,6 +1584,7 @@ export function runWithHostCommit<T>(callback: () => T): T {
1567
1584
  }
1568
1585
 
1569
1586
  export function useTransition(): [boolean, StartTransition] {
1587
+ const instance = requireInstance();
1570
1588
  const [pending, setPending] = runWithoutDevToolsHookTracking(() => useState(false));
1571
1589
  const startTransitionWithPending: StartTransition = (scope) => {
1572
1590
  setPending(true);
@@ -1575,6 +1593,10 @@ export function useTransition(): [boolean, StartTransition] {
1575
1593
  transitionVersion: ++transitionVersion,
1576
1594
  };
1577
1595
  scheduleCallback("low", () => {
1596
+ if (instance.disposed === true) {
1597
+ return;
1598
+ }
1599
+
1578
1600
  if (!isTransitionContextCurrent(context)) {
1579
1601
  setPending(false);
1580
1602
  return;
@@ -1582,6 +1604,9 @@ export function useTransition(): [boolean, StartTransition] {
1582
1604
 
1583
1605
  runTransitionScope(() => {
1584
1606
  scope();
1607
+ if (instance.disposed === true) {
1608
+ return;
1609
+ }
1585
1610
  setPending(false);
1586
1611
  }, context);
1587
1612
  });
@@ -1721,7 +1746,7 @@ function isTransitionContextCurrent(context: TransitionContext): boolean {
1721
1746
  }
1722
1747
 
1723
1748
  function useEffectImpl(
1724
- effectKind: "insertion" | "layout" | "normal",
1749
+ effectKind: "insertion" | "imperative-handle" | "layout" | "normal",
1725
1750
  callback: EffectCallback,
1726
1751
  deps?: readonly unknown[],
1727
1752
  ): void {
@@ -1763,12 +1788,15 @@ function useEffectImpl(
1763
1788
 
1764
1789
  slot.strictReplay =
1765
1790
  (runtime.strictModeDepth > 0 || runtime.strictReplayDepth > 0) &&
1766
- effectKind !== "insertion";
1791
+ effectKind !== "insertion" &&
1792
+ effectKind !== "imperative-handle";
1767
1793
 
1768
1794
  if (shouldRun) {
1769
1795
  const queue =
1770
1796
  effectKind === "insertion"
1771
1797
  ? runtime.pendingInsertionEffects
1798
+ : effectKind === "imperative-handle"
1799
+ ? runtime.pendingImperativeHandleEffects
1772
1800
  : effectKind === "layout"
1773
1801
  ? runtime.pendingLayoutEffects
1774
1802
  : runtime.pendingEffects;
@@ -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,