@reckona/mreact-compat 0.0.183 → 0.0.184

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/src/root.ts CHANGED
@@ -32,6 +32,10 @@ import {
32
32
  enqueueRootRender,
33
33
  } from "./fiber-work-loop.js";
34
34
  import { commitFiberRoot, detachFiberRefs } from "./fiber-commit.js";
35
+ import {
36
+ withBatchedDelegatedRootReleases,
37
+ withDeferredDelegatedEventPromotions,
38
+ } from "@reckona/mreact-reactive-dom";
35
39
  import {
36
40
  canRenderHostFiber,
37
41
  commitHydratingHostFiberRoot,
@@ -40,7 +44,6 @@ import {
40
44
  renderHostFiberRoot,
41
45
  } from "./fiber-host.js";
42
46
  import type { Fiber, FiberRoot } from "./fiber.js";
43
- import { renderIntoContainer } from "./reconciler.js";
44
47
 
45
48
  /** Root controller returned by createRoot and hydrateRoot. */
46
49
  export interface Root {
@@ -113,7 +116,7 @@ export function createRoot(
113
116
  );
114
117
  }
115
118
 
116
- renderIntoContainer(container, runtime.currentElement, runtime);
119
+ throwUnsupportedRootNode();
117
120
  });
118
121
  }
119
122
  }, options);
@@ -126,17 +129,19 @@ export function createRoot(
126
129
  return renderHostFiberIntoContainer(container, fiberRoot, runtime, element);
127
130
  }
128
131
 
129
- renderIntoContainer(container, element, runtime);
132
+ throwUnsupportedRootNode();
130
133
  });
131
134
  },
132
135
  unmount() {
133
- runtime.currentElement = undefined;
134
- runtime.dispose();
135
- detachFiberRefs(fiberRoot.current);
136
- disposeHostFiberResources(fiberRoot.current);
137
- runtime.instances.clear();
138
- unmountDevToolsRoot(container);
139
- clearElementChildren(container);
136
+ withBatchedDelegatedRootReleases(() => {
137
+ runtime.currentElement = undefined;
138
+ runtime.dispose();
139
+ detachFiberRefs(fiberRoot.current);
140
+ disposeHostFiberResources(fiberRoot.current);
141
+ runtime.instances.clear();
142
+ unmountDevToolsRoot(container);
143
+ clearElementChildren(container);
144
+ });
140
145
  },
141
146
  };
142
147
  }
@@ -153,7 +158,10 @@ function renderHostFiberIntoContainer(
153
158
  let committed = false;
154
159
 
155
160
  try {
156
- const finishedWork = renderHostFiberRoot(fiberRoot, element, runtime);
161
+ const deferred = withDeferredDelegatedEventPromotions(() =>
162
+ renderHostFiberRoot(fiberRoot, element, runtime)
163
+ );
164
+ const finishedWork = deferred.value;
157
165
 
158
166
  if (runtime.renderPhaseUpdate) {
159
167
  runtime.renderPhaseUpdate = false;
@@ -165,9 +173,10 @@ function renderHostFiberIntoContainer(
165
173
  }
166
174
 
167
175
  fiberRoot.finishedWork = finishedWork;
168
- commitFiberRoot(fiberRoot);
176
+ withBatchedDelegatedRootReleases(() => commitFiberRoot(fiberRoot));
169
177
  collectPortalNodes(fiberRoot.current, runtime, portalSnapshot);
170
178
  removeStalePortalNodes(portalSnapshot, runtime);
179
+ deferred.promote?.();
171
180
  commitDevToolsRoot(container, fiberRoot);
172
181
  runtime.idMode = "client";
173
182
  committed = true;
@@ -203,13 +212,16 @@ function renderHydratingHostFiberIntoContainer(
203
212
 
204
213
  try {
205
214
  const scope = getHydrationScope(container, options.resumeId);
206
- const finishedWork = renderHydratingHostFiberRoot(
207
- fiberRoot,
208
- element,
209
- runtime,
210
- scope,
211
- options,
215
+ const deferred = withDeferredDelegatedEventPromotions(() =>
216
+ renderHydratingHostFiberRoot(
217
+ fiberRoot,
218
+ element,
219
+ runtime,
220
+ scope,
221
+ options,
222
+ )
212
223
  );
224
+ const finishedWork = deferred.value;
213
225
 
214
226
  if (runtime.renderPhaseUpdate) {
215
227
  runtime.renderPhaseUpdate = false;
@@ -220,7 +232,9 @@ function renderHydratingHostFiberIntoContainer(
220
232
  continue;
221
233
  }
222
234
 
223
- commitHydratingHostFiberRoot(fiberRoot, finishedWork, scope, options);
235
+ withBatchedDelegatedRootReleases(() =>
236
+ commitHydratingHostFiberRoot(fiberRoot, finishedWork, scope, options)
237
+ );
224
238
  fiberRoot.current = finishedWork;
225
239
  fiberRoot.current.stateNode = fiberRoot;
226
240
  fiberRoot.finishedWork = undefined;
@@ -228,6 +242,7 @@ function renderHydratingHostFiberIntoContainer(
228
242
  fiberRoot.workInProgressRootRenderLanes = 0;
229
243
  collectPortalNodes(fiberRoot.current, runtime, portalSnapshot);
230
244
  removeStalePortalNodes(portalSnapshot, runtime);
245
+ deferred.promote?.();
231
246
  commitDevToolsRoot(container, fiberRoot);
232
247
  runtime.idMode = "client";
233
248
  committed = true;
@@ -302,12 +317,7 @@ export function hydrateRoot(
302
317
  );
303
318
  }
304
319
 
305
- renderIntoContainer(
306
- container,
307
- runtime.currentElement,
308
- runtime,
309
- useHydratingRerender ? renderOptions : {},
310
- );
320
+ throwUnsupportedRootNode();
311
321
  });
312
322
  }
313
323
  }, {
@@ -325,17 +335,19 @@ export function hydrateRoot(
325
335
  return renderHostFiberIntoContainer(container, fiberRoot, runtime, nextElement);
326
336
  }
327
337
 
328
- renderIntoContainer(container, nextElement, runtime);
338
+ throwUnsupportedRootNode();
329
339
  });
330
340
  },
331
341
  unmount() {
332
- runtime.currentElement = undefined;
333
- runtime.dispose();
334
- detachFiberRefs(fiberRoot.current);
335
- disposeHostFiberResources(fiberRoot.current);
336
- runtime.instances.clear();
337
- unmountDevToolsRoot(container);
338
- clearElementChildren(container);
342
+ withBatchedDelegatedRootReleases(() => {
343
+ runtime.currentElement = undefined;
344
+ runtime.dispose();
345
+ detachFiberRefs(fiberRoot.current);
346
+ disposeHostFiberResources(fiberRoot.current);
347
+ runtime.instances.clear();
348
+ unmountDevToolsRoot(container);
349
+ clearElementChildren(container);
350
+ });
339
351
  },
340
352
  };
341
353
 
@@ -348,15 +360,19 @@ export function hydrateRoot(
348
360
  runtime,
349
361
  element,
350
362
  renderOptions,
351
- );
352
- }
363
+ );
364
+ }
353
365
 
354
- renderIntoContainer(container, element, runtime, renderOptions);
366
+ throwUnsupportedRootNode();
355
367
  });
356
368
  replayQueuedHydrationEvents(container);
357
369
  return root;
358
370
  }
359
371
 
372
+ function throwUnsupportedRootNode(): never {
373
+ throw new Error("Unsupported react-compat root node. Pass a valid React-compatible element, portal, fragment, primitive, array, or nullish value.");
374
+ }
375
+
360
376
  function laneForRenderPriority(priority: RenderPriority): Lane {
361
377
  if (priority === "transition") {
362
378
  return TransitionLane;
@@ -369,39 +369,61 @@ function isEventHandlerName(name: string): boolean {
369
369
  );
370
370
  }
371
371
 
372
+ type AttributeNameClassification =
373
+ | { kind: "skip" }
374
+ | { kind: "style" }
375
+ | {
376
+ kind: "attribute";
377
+ attributeName: string;
378
+ booleanishString: boolean;
379
+ dataAttribute: boolean;
380
+ dangerousHtml: boolean;
381
+ };
382
+
383
+ const ATTRIBUTE_CLASSIFICATION_CACHE = new Map<string, AttributeNameClassification>();
384
+ const ATTRIBUTE_CLASSIFICATION_CACHE_LIMIT = 1024;
385
+ let attributeClassificationCacheMissCount = 0;
386
+
387
+ export const __serverRenderAttributeCacheForTesting = {
388
+ clear() {
389
+ ATTRIBUTE_CLASSIFICATION_CACHE.clear();
390
+ attributeClassificationCacheMissCount = 0;
391
+ },
392
+ missCount() {
393
+ return attributeClassificationCacheMissCount;
394
+ },
395
+ size() {
396
+ return ATTRIBUTE_CLASSIFICATION_CACHE.size;
397
+ },
398
+ };
399
+
372
400
  function renderHtmlAttribute(name: string, value: unknown): string {
373
401
  if (
374
402
  value === null ||
375
403
  value === undefined ||
376
- typeof value === "function" ||
377
- name === "children" ||
378
- name === "key" ||
379
- name === "ref" ||
380
- isEventHandlerName(name)
404
+ typeof value === "function"
381
405
  ) {
382
406
  return "";
383
407
  }
384
408
 
385
- if (name === "style") {
386
- const style = renderStyleAttribute(value);
387
- return style === "" ? "" : ` style="${escapeHtml(style)}"`;
388
- }
389
-
390
- const attributeName = toHtmlAttributeName(name);
409
+ const classification = classifyAttributeName(name);
391
410
 
392
- if (!VALID_ATTRIBUTE_NAME.test(attributeName)) {
411
+ if (classification.kind === "skip") {
393
412
  return "";
394
413
  }
395
414
 
396
- if (isEventHandlerName(attributeName)) {
397
- return "";
415
+ if (classification.kind === "style") {
416
+ const style = renderStyleAttribute(value);
417
+ return style === "" ? "" : ` style="${escapeHtml(style)}"`;
398
418
  }
399
419
 
400
- if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
420
+ const { attributeName } = classification;
421
+
422
+ if (typeof value === "boolean" && classification.booleanishString) {
401
423
  return ` ${attributeName}="${value ? "true" : "false"}"`;
402
424
  }
403
425
 
404
- if (typeof value === "boolean" && isDataAttribute(attributeName)) {
426
+ if (typeof value === "boolean" && classification.dataAttribute) {
405
427
  return ` ${attributeName}="${value ? "true" : "false"}"`;
406
428
  }
407
429
 
@@ -409,7 +431,7 @@ function renderHtmlAttribute(name: string, value: unknown): string {
409
431
  return "";
410
432
  }
411
433
 
412
- if (isDangerousHtmlAttribute(attributeName)) {
434
+ if (classification.dangerousHtml) {
413
435
  return isDangerousHtmlOptIn(value)
414
436
  ? ` ${attributeName}="${escapeHtml(value.__html)}"`
415
437
  : "";
@@ -434,6 +456,50 @@ function renderHtmlAttribute(name: string, value: unknown): string {
434
456
 
435
457
  const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
436
458
 
459
+ function classifyAttributeName(name: string): AttributeNameClassification {
460
+ const cached = ATTRIBUTE_CLASSIFICATION_CACHE.get(name);
461
+ if (cached !== undefined) {
462
+ return cached;
463
+ }
464
+
465
+ attributeClassificationCacheMissCount += 1;
466
+
467
+ const classification = createAttributeNameClassification(name);
468
+ if (ATTRIBUTE_CLASSIFICATION_CACHE.size < ATTRIBUTE_CLASSIFICATION_CACHE_LIMIT) {
469
+ ATTRIBUTE_CLASSIFICATION_CACHE.set(name, classification);
470
+ }
471
+ return classification;
472
+ }
473
+
474
+ function createAttributeNameClassification(name: string): AttributeNameClassification {
475
+ if (
476
+ name === "children" ||
477
+ name === "key" ||
478
+ name === "ref" ||
479
+ isEventHandlerName(name)
480
+ ) {
481
+ return { kind: "skip" };
482
+ }
483
+
484
+ if (name === "style") {
485
+ return { kind: "style" };
486
+ }
487
+
488
+ const attributeName = toHtmlAttributeName(name);
489
+
490
+ if (!VALID_ATTRIBUTE_NAME.test(attributeName) || isEventHandlerName(attributeName)) {
491
+ return { kind: "skip" };
492
+ }
493
+
494
+ return {
495
+ kind: "attribute",
496
+ attributeName,
497
+ booleanishString: isBooleanishStringAttribute(attributeName),
498
+ dataAttribute: isDataAttribute(attributeName),
499
+ dangerousHtml: isDangerousHtmlAttribute(attributeName),
500
+ };
501
+ }
502
+
437
503
  function isBooleanishStringAttribute(attributeName: string): boolean {
438
504
  // Callers pass the already-mapped HTML attribute name.
439
505
  const lowerCased = attributeName.toLowerCase();
package/src/server.ts ADDED
@@ -0,0 +1,62 @@
1
+ export {
2
+ Component,
3
+ PureComponent,
4
+ } from "./class-component.js";
5
+ export {
6
+ Fragment,
7
+ Activity,
8
+ Profiler,
9
+ Suspense,
10
+ SuspenseList,
11
+ StrictMode,
12
+ Children,
13
+ cloneElement,
14
+ createElement,
15
+ createErrorBoundary,
16
+ createRef,
17
+ forwardRef,
18
+ isValidElement,
19
+ lazy,
20
+ memo,
21
+ } from "./element.js";
22
+ export type {
23
+ ErrorBoundaryOptions,
24
+ ElementType,
25
+ ReactCompatElement,
26
+ ReactCompatNode,
27
+ ReactCompatRenderableElement,
28
+ } from "./element.js";
29
+ export {
30
+ createContext,
31
+ renderContextConsumerToString,
32
+ renderContextProviderToString,
33
+ useContext,
34
+ } from "./context.js";
35
+ export {
36
+ useCallback,
37
+ useDebugValue,
38
+ useDeferredValue,
39
+ useEffectEvent,
40
+ useEffect,
41
+ useId,
42
+ useImperativeHandle,
43
+ useInsertionEffect,
44
+ useLayoutEffect,
45
+ useMemo,
46
+ useOptimistic,
47
+ useReducer,
48
+ useRef,
49
+ useState,
50
+ useSyncExternalStore,
51
+ use,
52
+ useActionState,
53
+ cache,
54
+ cacheSignal,
55
+ captureOwnerStack,
56
+ startTransition,
57
+ unstable_useCacheRefresh,
58
+ useTransition,
59
+ version,
60
+ } from "./hooks.js";
61
+ export { renderChildToString, renderToString } from "./server-render.js";
62
+ export type { StartTransition, TransitionScope } from "./hooks.js";