@reckona/mreact-compat 0.0.138 → 0.0.140

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 (49) hide show
  1. package/dist/dom-props.d.ts.map +1 -1
  2. package/dist/dom-props.js +111 -44
  3. package/dist/dom-props.js.map +1 -1
  4. package/dist/element.d.ts +3 -0
  5. package/dist/element.d.ts.map +1 -1
  6. package/dist/element.js +32 -2
  7. package/dist/element.js.map +1 -1
  8. package/dist/event-listeners.d.ts +2 -4
  9. package/dist/event-listeners.d.ts.map +1 -1
  10. package/dist/event-listeners.js +2 -1
  11. package/dist/event-listeners.js.map +1 -1
  12. package/dist/fiber-child.d.ts.map +1 -1
  13. package/dist/fiber-child.js +44 -0
  14. package/dist/fiber-child.js.map +1 -1
  15. package/dist/fiber-commit.d.ts.map +1 -1
  16. package/dist/fiber-commit.js +70 -0
  17. package/dist/fiber-commit.js.map +1 -1
  18. package/dist/hooks.d.ts +4 -2
  19. package/dist/hooks.d.ts.map +1 -1
  20. package/dist/hooks.js +64 -6
  21. package/dist/hooks.js.map +1 -1
  22. package/dist/host-reconciler.d.ts.map +1 -1
  23. package/dist/host-reconciler.js +2 -2
  24. package/dist/host-reconciler.js.map +1 -1
  25. package/dist/jsx-runtime.d.ts.map +1 -1
  26. package/dist/jsx-runtime.js +2 -11
  27. package/dist/jsx-runtime.js.map +1 -1
  28. package/dist/reconciler.d.ts.map +1 -1
  29. package/dist/reconciler.js +2 -2
  30. package/dist/reconciler.js.map +1 -1
  31. package/dist/server-render.d.ts.map +1 -1
  32. package/dist/server-render.js +29 -4
  33. package/dist/server-render.js.map +1 -1
  34. package/dist/url-safety.d.ts +1 -1
  35. package/dist/url-safety.d.ts.map +1 -1
  36. package/dist/url-safety.js +1 -1
  37. package/dist/url-safety.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/dom-props.ts +189 -49
  40. package/src/element.ts +45 -1
  41. package/src/event-listeners.ts +4 -5
  42. package/src/fiber-child.ts +67 -0
  43. package/src/fiber-commit.ts +92 -0
  44. package/src/hooks.ts +88 -8
  45. package/src/host-reconciler.ts +2 -3
  46. package/src/jsx-runtime.ts +2 -18
  47. package/src/reconciler.ts +2 -3
  48. package/src/server-render.ts +43 -4
  49. package/src/url-safety.ts +1 -0
package/src/hooks.ts CHANGED
@@ -16,6 +16,7 @@ import { isThenable } from "./thenable.js";
16
16
  export interface RootRuntime {
17
17
  currentElement?: unknown;
18
18
  instances: Map<string, ComponentInstance>;
19
+ instanceKeysByPrefix: Map<string, Set<string>>;
19
20
  activeInstanceKeys: Set<string> | undefined;
20
21
  activeProfilerPaths: Set<string> | undefined;
21
22
  mountedProfilerPaths: Set<string>;
@@ -55,8 +56,8 @@ interface ComponentInstance {
55
56
  dirty: boolean;
56
57
  disposed?: boolean;
57
58
  contextDependencies?: Map<ReactCompatContextLike<unknown>, unknown>;
58
- devToolsHooks: DevToolsHookValue[];
59
- devToolsHookTypes: string[];
59
+ devToolsHooks?: DevToolsHookValue[];
60
+ devToolsHookTypes?: string[];
60
61
  devToolsHookSuppressionDepth: number;
61
62
  }
62
63
 
@@ -297,6 +298,7 @@ export function createRootRuntime(
297
298
  ): RootRuntime {
298
299
  return {
299
300
  instances: new Map(),
301
+ instanceKeysByPrefix: new Map(),
300
302
  activeInstanceKeys: undefined,
301
303
  activeProfilerPaths: undefined,
302
304
  mountedProfilerPaths: new Set(),
@@ -491,20 +493,24 @@ export function renderWithRootRuntime<T>(
491
493
  hooks: [],
492
494
  hookIndex: 0,
493
495
  dirty: false,
494
- devToolsHooks: [],
495
- devToolsHookTypes: [],
496
496
  devToolsHookSuppressionDepth: 0,
497
497
  };
498
498
  instance.owner = owner;
499
499
  instance.path = path;
500
500
  runtime.instances.set(path, instance);
501
+ indexInstanceKey(runtime, path);
501
502
  runtime.activeInstanceKeys?.add(path);
502
503
  instance.hookIndex = 0;
503
504
  instance.dirty = false;
504
505
  instance.disposed = false;
505
506
  delete instance.contextDependencies;
506
- instance.devToolsHooks = [];
507
- instance.devToolsHookTypes = [];
507
+ if (hasInstalledDevToolsHook()) {
508
+ instance.devToolsHooks = [];
509
+ instance.devToolsHookTypes = [];
510
+ } else {
511
+ delete instance.devToolsHooks;
512
+ delete instance.devToolsHookTypes;
513
+ }
508
514
  instance.devToolsHookSuppressionDepth = 0;
509
515
  hookRenderState.currentRuntime = runtime;
510
516
  hookRenderState.currentInstance = instance;
@@ -547,13 +553,35 @@ export function hasContextDependency(
547
553
  return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
548
554
  }
549
555
 
556
+ export function collectRuntimeInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
557
+ const keys = runtime.instanceKeysByPrefix.get(prefix);
558
+
559
+ if (keys === undefined) {
560
+ return [];
561
+ }
562
+
563
+ const activeKeys: string[] = [];
564
+
565
+ for (const key of keys) {
566
+ if (runtime.instances.has(key)) {
567
+ activeKeys.push(key);
568
+ }
569
+ }
570
+
571
+ return activeKeys;
572
+ }
573
+
550
574
  export function getDevToolsHookState(
551
575
  runtime: RootRuntime,
552
576
  path: string,
553
577
  ): DevToolsHookState | undefined {
554
578
  const instance = runtime.instances.get(path);
555
579
 
556
- if (instance === undefined) {
580
+ if (
581
+ instance === undefined ||
582
+ instance.devToolsHooks === undefined ||
583
+ instance.devToolsHookTypes === undefined
584
+ ) {
557
585
  return undefined;
558
586
  }
559
587
 
@@ -966,7 +994,12 @@ function assignRef<T>(ref: unknown, value: T | null): void {
966
994
  function recordDevToolsHook(type: string, value: DevToolsHookValue): void {
967
995
  const instance = hookRenderState.currentInstance;
968
996
 
969
- if (instance === undefined || instance.devToolsHookSuppressionDepth > 0) {
997
+ if (
998
+ instance === undefined ||
999
+ instance.devToolsHookSuppressionDepth > 0 ||
1000
+ instance.devToolsHooks === undefined ||
1001
+ instance.devToolsHookTypes === undefined
1002
+ ) {
970
1003
  return;
971
1004
  }
972
1005
 
@@ -974,6 +1007,12 @@ function recordDevToolsHook(type: string, value: DevToolsHookValue): void {
974
1007
  instance.devToolsHooks.push(value);
975
1008
  }
976
1009
 
1010
+ function hasInstalledDevToolsHook(): boolean {
1011
+ return typeof (globalThis as {
1012
+ __REACT_DEVTOOLS_GLOBAL_HOOK__?: { inject?: unknown } | undefined;
1013
+ }).__REACT_DEVTOOLS_GLOBAL_HOOK__?.inject === "function";
1014
+ }
1015
+
977
1016
  function runWithoutDevToolsHookTracking<T>(callback: () => T): T {
978
1017
  const instance = requireInstance();
979
1018
  instance.devToolsHookSuppressionDepth += 1;
@@ -1941,10 +1980,51 @@ function cleanupInactiveInstances(runtime: RootRuntime): void {
1941
1980
  if (!activeInstanceKeys.has(key)) {
1942
1981
  cleanupInstance(instance);
1943
1982
  runtime.instances.delete(key);
1983
+ removeInstanceKeyFromIndex(runtime, key);
1984
+ }
1985
+ }
1986
+ }
1987
+
1988
+ function indexInstanceKey(runtime: RootRuntime, key: string): void {
1989
+ for (const prefix of instanceKeyPrefixes(key)) {
1990
+ let keys = runtime.instanceKeysByPrefix.get(prefix);
1991
+
1992
+ if (keys === undefined) {
1993
+ keys = new Set();
1994
+ runtime.instanceKeysByPrefix.set(prefix, keys);
1944
1995
  }
1996
+
1997
+ keys.add(key);
1945
1998
  }
1946
1999
  }
1947
2000
 
2001
+ function removeInstanceKeyFromIndex(runtime: RootRuntime, key: string): void {
2002
+ for (const prefix of instanceKeyPrefixes(key)) {
2003
+ const keys = runtime.instanceKeysByPrefix.get(prefix);
2004
+
2005
+ if (keys === undefined) {
2006
+ continue;
2007
+ }
2008
+
2009
+ keys.delete(key);
2010
+
2011
+ if (keys.size === 0) {
2012
+ runtime.instanceKeysByPrefix.delete(prefix);
2013
+ }
2014
+ }
2015
+ }
2016
+
2017
+ function instanceKeyPrefixes(key: string): string[] {
2018
+ const parts = key.split(".");
2019
+ const prefixes: string[] = [];
2020
+
2021
+ for (let index = 1; index <= parts.length; index += 1) {
2022
+ prefixes.push(parts.slice(0, index).join("."));
2023
+ }
2024
+
2025
+ return prefixes;
2026
+ }
2027
+
1948
2028
  function cleanupInstance(instance: ComponentInstance): void {
1949
2029
  instance.disposed = true;
1950
2030
  for (const slot of instance.hooks) {
@@ -45,6 +45,7 @@ import {
45
45
  restoreRuntimeSnapshot,
46
46
  takeRuntimeSnapshot,
47
47
  getDevToolsHookState,
48
+ collectRuntimeInstanceKeys,
48
49
  hasContextDependency,
49
50
  hasChangedContextDependency,
50
51
  type RootRuntime,
@@ -3074,9 +3075,7 @@ function isLazyType(
3074
3075
  }
3075
3076
 
3076
3077
  function collectInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
3077
- return Array.from(runtime.instances.keys()).filter(
3078
- (key) => key === prefix || key.startsWith(`${prefix}.`),
3079
- );
3078
+ return collectRuntimeInstanceKeys(runtime, prefix);
3080
3079
  }
3081
3080
 
3082
3081
  function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]): void {
@@ -1,4 +1,4 @@
1
- import { createElement, Fragment } from "./element.js";
1
+ import { createElementFromJsxConfig, Fragment } from "./element.js";
2
2
  import type {
3
3
  ElementType,
4
4
  ReactCompatElement,
@@ -99,21 +99,5 @@ function createElementFromJsx<P extends Record<string, unknown>>(
99
99
  props: (P & { children?: ReactCompatNode; key?: unknown; ref?: unknown }) | null,
100
100
  key: unknown,
101
101
  ): ReactCompatElement<P> {
102
- const config = { ...props } as P & {
103
- children?: ReactCompatNode;
104
- key?: unknown;
105
- ref?: unknown;
106
- };
107
- const hasChildren = Object.hasOwn(config, "children");
108
- const children = config.children;
109
-
110
- if (key !== undefined) {
111
- config.key = key;
112
- }
113
-
114
- delete config.children;
115
-
116
- return hasChildren
117
- ? createElement(type, config, children)
118
- : createElement(type, config);
102
+ return createElementFromJsxConfig(type, props, key);
119
103
  }
package/src/reconciler.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  } from "./context.js";
25
25
  import {
26
26
  clearRuntimePortalNodes,
27
+ collectRuntimeInstanceKeys,
27
28
  hasStableExternalStores,
28
29
  restoreRuntimeSnapshot,
29
30
  renderWithProfiler,
@@ -684,9 +685,7 @@ function getMemoRenderStates(runtime: RootRuntime): Map<string, MemoRenderState>
684
685
  }
685
686
 
686
687
  function collectInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
687
- return Array.from(runtime.instances.keys()).filter((key) =>
688
- key === prefix || key.startsWith(`${prefix}.`),
689
- );
688
+ return collectRuntimeInstanceKeys(runtime, prefix);
690
689
  }
691
690
 
692
691
  function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]): void {
@@ -25,7 +25,12 @@ import {
25
25
  type RootRuntime,
26
26
  type RootRuntimeOptions,
27
27
  } from "./hooks.js";
28
- import { isDangerousHtmlAttribute, isDangerousHtmlOptIn } from "./url-safety.js";
28
+ import {
29
+ isDangerousHtmlAttribute,
30
+ isDangerousHtmlOptIn,
31
+ isUnsafeMetaRefreshContent,
32
+ isUnsafeUrlAttribute,
33
+ } from "./url-safety.js";
29
34
  import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
30
35
  import { isVoidHtmlElement } from "@reckona/mreact-shared";
31
36
 
@@ -195,7 +200,8 @@ function renderElementToString(
195
200
  }
196
201
 
197
202
  function renderAttributesToString(props: Record<string, unknown>): string {
198
- const entries = Object.entries(props);
203
+ const sanitizedProps = sanitizeMetaRefreshProps(props);
204
+ const entries = Object.entries(sanitizedProps);
199
205
  if (
200
206
  entries.length === 0 ||
201
207
  (entries.length === 1 && entries[0]?.[0] === "children")
@@ -210,6 +216,23 @@ function renderAttributesToString(props: Record<string, unknown>): string {
210
216
  return attributes;
211
217
  }
212
218
 
219
+ function sanitizeMetaRefreshProps(
220
+ props: Record<string, unknown>,
221
+ ): Record<string, unknown> {
222
+ const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
223
+ const content = props.content;
224
+ if (typeof httpEquiv !== "string" || typeof content !== "string") {
225
+ return props;
226
+ }
227
+ if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
228
+ return props;
229
+ }
230
+
231
+ const sanitized = { ...props };
232
+ delete sanitized.content;
233
+ return sanitized;
234
+ }
235
+
213
236
  function isClassComponentType(
214
237
  value: unknown,
215
238
  ): value is new (props: Record<string, unknown>) => { render(): ReactCompatNode } {
@@ -305,7 +328,7 @@ function renderHtmlAttribute(name: string, value: unknown): string {
305
328
  name === "children" ||
306
329
  name === "key" ||
307
330
  name === "ref" ||
308
- /^on[A-Z]/.test(name) ||
331
+ /^on/i.test(name) ||
309
332
  value === null ||
310
333
  value === undefined ||
311
334
  typeof value === "function"
@@ -320,6 +343,14 @@ function renderHtmlAttribute(name: string, value: unknown): string {
320
343
 
321
344
  const attributeName = toHtmlAttributeName(name);
322
345
 
346
+ if (!VALID_ATTRIBUTE_NAME.test(attributeName)) {
347
+ return "";
348
+ }
349
+
350
+ if (/^on/i.test(attributeName)) {
351
+ return "";
352
+ }
353
+
323
354
  if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
324
355
  return ` ${attributeName}="${value ? "true" : "false"}"`;
325
356
  }
@@ -346,9 +377,17 @@ function renderHtmlAttribute(name: string, value: unknown): string {
346
377
  return ` ${attributeName}=""`;
347
378
  }
348
379
 
349
- return ` ${attributeName}="${escapeHtml(value)}"`;
380
+ const stringValue = String(value);
381
+
382
+ if (isUnsafeUrlAttribute(attributeName, stringValue)) {
383
+ return "";
384
+ }
385
+
386
+ return ` ${attributeName}="${escapeHtml(stringValue)}"`;
350
387
  }
351
388
 
389
+ const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
390
+
352
391
  function isBooleanishStringAttribute(name: string): boolean {
353
392
  const attributeName = toHtmlAttributeName(name).toLowerCase();
354
393
  return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
package/src/url-safety.ts CHANGED
@@ -2,6 +2,7 @@ export {
2
2
  isDangerousHtmlAttribute,
3
3
  isDangerousHtmlOptIn,
4
4
  isSrcsetAttribute,
5
+ isUnsafeMetaRefreshContent,
5
6
  isUnsafeUrlAttribute,
6
7
  isUrlAttribute,
7
8
  } from "@reckona/mreact-shared/url-safety";