@reckona/mreact-compat 0.0.137 → 0.0.139
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/dist/dom-props.d.ts.map +1 -1
- package/dist/dom-props.js +67 -18
- package/dist/dom-props.js.map +1 -1
- package/dist/element.d.ts +3 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +32 -2
- package/dist/element.js.map +1 -1
- package/dist/event-listeners.d.ts +1 -0
- package/dist/event-listeners.d.ts.map +1 -1
- package/dist/event-listeners.js.map +1 -1
- package/dist/hooks.d.ts +4 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +64 -6
- package/dist/hooks.js.map +1 -1
- package/dist/host-reconciler.d.ts.map +1 -1
- package/dist/host-reconciler.js +2 -2
- package/dist/host-reconciler.js.map +1 -1
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +2 -11
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/reconciler.d.ts.map +1 -1
- package/dist/reconciler.js +2 -2
- package/dist/reconciler.js.map +1 -1
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +29 -4
- package/dist/server-render.js.map +1 -1
- package/dist/url-safety.d.ts +1 -1
- package/dist/url-safety.d.ts.map +1 -1
- package/dist/url-safety.js +1 -1
- package/dist/url-safety.js.map +1 -1
- package/package.json +3 -3
- package/src/dom-props.ts +85 -18
- package/src/element.ts +45 -1
- package/src/event-listeners.ts +1 -0
- package/src/hooks.ts +88 -8
- package/src/host-reconciler.ts +2 -3
- package/src/jsx-runtime.ts +2 -18
- package/src/reconciler.ts +2 -3
- package/src/server-render.ts +43 -4
- package/src/url-safety.ts +1 -0
package/src/dom-props.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type { SyntheticEvent } from "./event-types.js";
|
|
|
12
12
|
import {
|
|
13
13
|
isDangerousHtmlAttribute,
|
|
14
14
|
isDangerousHtmlOptIn,
|
|
15
|
+
isSrcsetAttribute,
|
|
16
|
+
isUnsafeMetaRefreshContent,
|
|
15
17
|
isUnsafeUrlAttribute,
|
|
16
18
|
isUrlAttribute,
|
|
17
19
|
} from "./url-safety.js";
|
|
@@ -29,28 +31,34 @@ export function applyProps(
|
|
|
29
31
|
): void {
|
|
30
32
|
const preserveHydrationAttributes = options.preserveHydrationAttributes === true;
|
|
31
33
|
const previous = getAppliedProps(element);
|
|
34
|
+
const nextProps = sanitizeMetaRefreshElementProps(element, props);
|
|
32
35
|
|
|
33
36
|
if (previous === undefined && !preserveHydrationAttributes) {
|
|
34
|
-
if (applyInitialRowProps(element,
|
|
35
|
-
setAppliedProps(element, {
|
|
37
|
+
if (applyInitialRowProps(element, nextProps)) {
|
|
38
|
+
setAppliedProps(element, {
|
|
39
|
+
attributeNames: collectAttributeNames(nextProps),
|
|
40
|
+
props: nextProps,
|
|
41
|
+
});
|
|
36
42
|
return;
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
const attributeNames = collectAttributeNames(nextProps);
|
|
39
46
|
setAppliedProps(element, {
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
attributeNames,
|
|
48
|
+
props: nextProps,
|
|
49
|
+
...applyInitialProps(element, nextProps, path, options),
|
|
42
50
|
});
|
|
43
51
|
return;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
const previousProps = previous?.props ?? {};
|
|
47
55
|
let listeners = previous?.listeners;
|
|
48
|
-
const previousAttributeNames = collectAttributeNames(previousProps);
|
|
49
|
-
const nextAttributeNames = collectAttributeNames(
|
|
56
|
+
const previousAttributeNames = previous?.attributeNames ?? collectAttributeNames(previousProps);
|
|
57
|
+
const nextAttributeNames = collectAttributeNames(nextProps);
|
|
50
58
|
|
|
51
59
|
if (!preserveHydrationAttributes) {
|
|
52
60
|
for (const attributeName of previousAttributeNames) {
|
|
53
|
-
if (!nextAttributeNames.
|
|
61
|
+
if (!nextAttributeNames.includes(attributeName)) {
|
|
54
62
|
if (attributeName === "style") {
|
|
55
63
|
removePreviousStyle(element, previousProps.style, path, options);
|
|
56
64
|
continue;
|
|
@@ -71,7 +79,7 @@ export function applyProps(
|
|
|
71
79
|
|
|
72
80
|
if (listeners !== undefined) {
|
|
73
81
|
for (const [name, appliedListener] of listeners) {
|
|
74
|
-
const nextValue =
|
|
82
|
+
const nextValue = nextProps[name];
|
|
75
83
|
|
|
76
84
|
if (nextValue !== appliedListener.handler) {
|
|
77
85
|
listeners.delete(name);
|
|
@@ -79,7 +87,13 @@ export function applyProps(
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
for (const
|
|
90
|
+
for (const name in nextProps) {
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(nextProps, name)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const value = nextProps[name];
|
|
96
|
+
|
|
83
97
|
if (name === "children" || name === "ref" || name === "key") {
|
|
84
98
|
continue;
|
|
85
99
|
}
|
|
@@ -117,6 +131,10 @@ export function applyProps(
|
|
|
117
131
|
continue;
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
if (/^on/i.test(name)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
const attributeName = toDomAttributeName(name);
|
|
121
139
|
|
|
122
140
|
if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
|
|
@@ -163,7 +181,8 @@ export function applyProps(
|
|
|
163
181
|
}
|
|
164
182
|
|
|
165
183
|
setAppliedProps(element, {
|
|
166
|
-
|
|
184
|
+
attributeNames: nextAttributeNames,
|
|
185
|
+
props: nextProps,
|
|
167
186
|
...(listeners === undefined ? {} : { listeners }),
|
|
168
187
|
});
|
|
169
188
|
}
|
|
@@ -220,6 +239,10 @@ function applyInitialProps(
|
|
|
220
239
|
continue;
|
|
221
240
|
}
|
|
222
241
|
|
|
242
|
+
if (/^on/i.test(name)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
const attributeName = toDomAttributeName(name);
|
|
224
247
|
|
|
225
248
|
if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
|
|
@@ -338,6 +361,13 @@ function applyAttribute(
|
|
|
338
361
|
return;
|
|
339
362
|
}
|
|
340
363
|
|
|
364
|
+
if (/^on/i.test(name)) {
|
|
365
|
+
if (element.hasAttribute(name) && !preserveHydrationAttributes) {
|
|
366
|
+
element.removeAttribute(name);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
341
371
|
if (value === null || value === undefined || value === false) {
|
|
342
372
|
if (element.hasAttribute(name) && !preserveHydrationAttributes) {
|
|
343
373
|
reportRecoverable(
|
|
@@ -363,7 +393,10 @@ function applyAttribute(
|
|
|
363
393
|
// value is unsafe we treat it as if it were null -- remove the
|
|
364
394
|
// existing attribute, log a recoverable mismatch, and stop. This
|
|
365
395
|
// matches react-dom's sanitizeURL posture.
|
|
366
|
-
if (
|
|
396
|
+
if (
|
|
397
|
+
(isUrlAttribute(name) || isSrcsetAttribute(name)) &&
|
|
398
|
+
isUnsafeUrlAttribute(name, stringValue)
|
|
399
|
+
) {
|
|
367
400
|
if (element.hasAttribute(name) && !preserveHydrationAttributes) {
|
|
368
401
|
reportRecoverable(
|
|
369
402
|
options,
|
|
@@ -530,15 +563,21 @@ function removePreviousStyle(
|
|
|
530
563
|
}
|
|
531
564
|
}
|
|
532
565
|
|
|
533
|
-
function collectAttributeNames(props: Record<string, unknown>):
|
|
534
|
-
const names =
|
|
566
|
+
function collectAttributeNames(props: Record<string, unknown>): string[] {
|
|
567
|
+
const names: string[] = [];
|
|
568
|
+
|
|
569
|
+
for (const name in props) {
|
|
570
|
+
if (!Object.prototype.hasOwnProperty.call(props, name)) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const value = props[name];
|
|
535
575
|
|
|
536
|
-
for (const [name, value] of Object.entries(props)) {
|
|
537
576
|
if (
|
|
538
577
|
name === "children" ||
|
|
539
578
|
name === "ref" ||
|
|
540
579
|
name === "key" ||
|
|
541
|
-
/^on
|
|
580
|
+
/^on/i.test(name) ||
|
|
542
581
|
value === null ||
|
|
543
582
|
value === undefined
|
|
544
583
|
) {
|
|
@@ -556,21 +595,49 @@ function collectAttributeNames(props: Record<string, unknown>): Set<string> {
|
|
|
556
595
|
}
|
|
557
596
|
|
|
558
597
|
if (name === "defaultValue") {
|
|
559
|
-
names
|
|
598
|
+
pushUniqueAttributeName(names, "value");
|
|
560
599
|
continue;
|
|
561
600
|
}
|
|
562
601
|
|
|
563
602
|
if (name === "defaultChecked") {
|
|
564
|
-
names
|
|
603
|
+
pushUniqueAttributeName(names, "checked");
|
|
565
604
|
continue;
|
|
566
605
|
}
|
|
567
606
|
|
|
568
|
-
names
|
|
607
|
+
pushUniqueAttributeName(names, attributeName);
|
|
569
608
|
}
|
|
570
609
|
|
|
571
610
|
return names;
|
|
572
611
|
}
|
|
573
612
|
|
|
613
|
+
function pushUniqueAttributeName(names: string[], name: string): void {
|
|
614
|
+
if (!names.includes(name)) {
|
|
615
|
+
names.push(name);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function sanitizeMetaRefreshElementProps(
|
|
620
|
+
element: Element,
|
|
621
|
+
props: Record<string, unknown>,
|
|
622
|
+
): Record<string, unknown> {
|
|
623
|
+
if (element.tagName.toLowerCase() !== "meta") {
|
|
624
|
+
return props;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const httpEquiv = props["http-equiv"] ?? props.httpEquiv ?? element.getAttribute("http-equiv");
|
|
628
|
+
const content = props.content;
|
|
629
|
+
if (typeof httpEquiv !== "string" || typeof content !== "string") {
|
|
630
|
+
return props;
|
|
631
|
+
}
|
|
632
|
+
if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
|
|
633
|
+
return props;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const sanitized = { ...props };
|
|
637
|
+
delete sanitized.content;
|
|
638
|
+
return sanitized;
|
|
639
|
+
}
|
|
640
|
+
|
|
574
641
|
function isBooleanishStringAttribute(name: string): boolean {
|
|
575
642
|
const attributeName = toDomAttributeName(name).toLowerCase();
|
|
576
643
|
return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
|
package/src/element.ts
CHANGED
|
@@ -120,6 +120,43 @@ export function createElement<P extends Record<string, unknown>>(
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function createElementFromJsxConfig<P extends Record<string, unknown>>(
|
|
124
|
+
type: ElementType<P>,
|
|
125
|
+
config: (P & ReactReservedProps & { children?: ReactCompatNode }) | null,
|
|
126
|
+
keyArgument?: unknown,
|
|
127
|
+
): ReactCompatElement<P> {
|
|
128
|
+
const normalizedType =
|
|
129
|
+
typeof type === "object" && type !== null ? normalizeElementType(type) : type;
|
|
130
|
+
const key = keyArgument !== undefined
|
|
131
|
+
? String(keyArgument)
|
|
132
|
+
: config?.key === undefined ? null : String(config.key);
|
|
133
|
+
const ref = config?.ref ?? null;
|
|
134
|
+
const hasChildren = config !== null && config !== undefined && hasOwnProperty.call(config, "children");
|
|
135
|
+
const children = config?.children;
|
|
136
|
+
const copiedProps = copyElementProps(config, undefined, true);
|
|
137
|
+
const props = (typeof normalizedType === "string"
|
|
138
|
+
? copiedProps
|
|
139
|
+
: applyDefaultProps(normalizedType, copiedProps)) as P & {
|
|
140
|
+
children?: ReactCompatNode;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (hasChildren) {
|
|
144
|
+
props.children = children;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof normalizedType === "string") {
|
|
148
|
+
setHostOwnPropsMeta(props);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
$$typeof: REACT_COMPAT_ELEMENT_TYPE,
|
|
153
|
+
type: normalizedType as ElementType<P>,
|
|
154
|
+
key,
|
|
155
|
+
ref,
|
|
156
|
+
props,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
123
160
|
export function isReactCompatElement(
|
|
124
161
|
value: unknown,
|
|
125
162
|
): value is ReactCompatElement {
|
|
@@ -232,6 +269,7 @@ export function cloneElement<P extends Record<string, unknown>>(
|
|
|
232
269
|
function copyElementProps(
|
|
233
270
|
source: Record<string, unknown> | null | undefined,
|
|
234
271
|
base?: Record<string, unknown>,
|
|
272
|
+
omitChildren = false,
|
|
235
273
|
): Record<string, unknown> {
|
|
236
274
|
const props: Record<string, unknown> = base === undefined ? {} : { ...base };
|
|
237
275
|
|
|
@@ -244,7 +282,13 @@ function copyElementProps(
|
|
|
244
282
|
continue;
|
|
245
283
|
}
|
|
246
284
|
|
|
247
|
-
if (
|
|
285
|
+
if (
|
|
286
|
+
name !== "key" &&
|
|
287
|
+
name !== "ref" &&
|
|
288
|
+
name !== "__self" &&
|
|
289
|
+
name !== "__source" &&
|
|
290
|
+
(!omitChildren || name !== "children")
|
|
291
|
+
) {
|
|
248
292
|
props[name] = source[name];
|
|
249
293
|
}
|
|
250
294
|
}
|
package/src/event-listeners.ts
CHANGED
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
|
|
59
|
-
devToolsHookTypes
|
|
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
|
-
|
|
507
|
-
|
|
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 (
|
|
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 (
|
|
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) {
|
package/src/host-reconciler.ts
CHANGED
|
@@ -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
|
|
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 {
|
package/src/jsx-runtime.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
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 {
|
package/src/server-render.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
type RootRuntime,
|
|
26
26
|
type RootRuntimeOptions,
|
|
27
27
|
} from "./hooks.js";
|
|
28
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|