@reckona/mreact-compat 0.0.65 → 0.0.67
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/package.json +4 -3
- package/src/class-component.ts +386 -0
- package/src/context.ts +140 -0
- package/src/devtools.ts +1275 -0
- package/src/dom-children.ts +34 -0
- package/src/dom-props.ts +408 -0
- package/src/element.ts +317 -0
- package/src/event-listeners.ts +27 -0
- package/src/event-priority.ts +1 -0
- package/src/event-replay.ts +154 -0
- package/src/event-types.ts +18 -0
- package/src/events.ts +384 -0
- package/src/fiber-child.ts +364 -0
- package/src/fiber-commit.ts +83 -0
- package/src/fiber-flags.ts +21 -0
- package/src/fiber-host.ts +1564 -0
- package/src/fiber-lanes.ts +99 -0
- package/src/fiber-reconciler.ts +639 -0
- package/src/fiber-scheduler.ts +435 -0
- package/src/fiber-work-loop.ts +224 -0
- package/src/fiber.ts +148 -0
- package/src/flight-decoder.ts +205 -0
- package/src/flight-element-builder.ts +110 -0
- package/src/flight-parser.ts +698 -0
- package/src/flight-protocol.ts +71 -0
- package/src/flight-types.ts +148 -0
- package/src/flight.ts +162 -0
- package/src/hooks.ts +1940 -0
- package/src/hydration.ts +314 -0
- package/src/index.ts +95 -0
- package/src/internal.ts +7 -0
- package/src/jsx-dev-runtime.ts +40 -0
- package/src/jsx-runtime.ts +119 -0
- package/src/prop-comparison.ts +50 -0
- package/src/reconcile-types.ts +26 -0
- package/src/reconciler.ts +692 -0
- package/src/render.ts +29 -0
- package/src/root.ts +493 -0
- package/src/scheduler.ts +157 -0
- package/src/suspense.ts +317 -0
- package/src/thenable.ts +7 -0
- package/src/url-safety.ts +7 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { collectScopedNodes } from "./hydration.js";
|
|
2
|
+
|
|
3
|
+
export function syncChildNodes(parent: ParentNode, nextNodes: readonly Node[]): void {
|
|
4
|
+
syncScopedChildNodes(parent, null, null, nextNodes);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function syncScopedChildNodes(
|
|
8
|
+
parent: ParentNode,
|
|
9
|
+
before: ChildNode | null,
|
|
10
|
+
after: ChildNode | null,
|
|
11
|
+
nextNodes: readonly Node[],
|
|
12
|
+
): void {
|
|
13
|
+
let cursor = parent.firstChild;
|
|
14
|
+
|
|
15
|
+
if (before !== null) {
|
|
16
|
+
cursor = before.nextSibling;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const node of nextNodes) {
|
|
20
|
+
if (node !== cursor) {
|
|
21
|
+
parent.insertBefore(node, cursor === after ? after : cursor);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
cursor = node.nextSibling;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const nextSet = new Set(nextNodes);
|
|
28
|
+
|
|
29
|
+
for (const child of collectScopedNodes(parent, before, after)) {
|
|
30
|
+
if (!nextSet.has(child)) {
|
|
31
|
+
parent.removeChild(child);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/dom-props.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAppliedProps,
|
|
3
|
+
setAppliedProps,
|
|
4
|
+
type AppliedEventListener,
|
|
5
|
+
type AppliedProps,
|
|
6
|
+
} from "./event-listeners.js";
|
|
7
|
+
import { ensureDelegatedEventListener, toEventNames } from "./events.js";
|
|
8
|
+
import { reportRecoverable, type RenderOptions } from "./hydration.js";
|
|
9
|
+
import type { SyntheticEvent } from "./event-types.js";
|
|
10
|
+
import {
|
|
11
|
+
isDangerousHtmlAttribute,
|
|
12
|
+
isDangerousHtmlOptIn,
|
|
13
|
+
isUnsafeUrlAttribute,
|
|
14
|
+
isUrlAttribute,
|
|
15
|
+
} from "./url-safety.js";
|
|
16
|
+
|
|
17
|
+
export function applyProps(
|
|
18
|
+
element: HTMLElement,
|
|
19
|
+
props: Record<string, unknown>,
|
|
20
|
+
path: string,
|
|
21
|
+
options: RenderOptions,
|
|
22
|
+
): void {
|
|
23
|
+
const previous: AppliedProps = getAppliedProps(element) ?? {
|
|
24
|
+
props: {},
|
|
25
|
+
listeners: new Map<string, AppliedEventListener>(),
|
|
26
|
+
};
|
|
27
|
+
const nextAttributeNames = collectAttributeNames(props);
|
|
28
|
+
const preserveHydrationAttributes = options.preserveHydrationAttributes === true;
|
|
29
|
+
|
|
30
|
+
if (!preserveHydrationAttributes) {
|
|
31
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
32
|
+
if (!nextAttributeNames.has(attribute.name)) {
|
|
33
|
+
reportRecoverable(
|
|
34
|
+
options,
|
|
35
|
+
"attribute",
|
|
36
|
+
path,
|
|
37
|
+
new Error(`Hydration attribute mismatch: ${attribute.name}.`),
|
|
38
|
+
);
|
|
39
|
+
element.removeAttribute(attribute.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [name, appliedListener] of previous.listeners) {
|
|
45
|
+
const nextValue = props[name];
|
|
46
|
+
|
|
47
|
+
if (nextValue !== appliedListener.handler) {
|
|
48
|
+
previous.listeners.delete(name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [name, value] of Object.entries(props)) {
|
|
53
|
+
if (name === "children" || name === "ref" || name === "key") {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (applyFormValueProp(element, name, value, path, options)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (name === "className") {
|
|
62
|
+
applyAttribute(element, "class", value, path, options);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (name === "htmlFor") {
|
|
67
|
+
applyAttribute(element, "for", value, path, options);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (name === "style") {
|
|
72
|
+
applyStyle(element, previous.props[name], value, path, options);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (/^on[A-Z]/.test(name) && typeof value === "function") {
|
|
77
|
+
if (previous.listeners.get(name)?.handler === value) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handler = value as (event: SyntheticEvent) => void;
|
|
82
|
+
for (const eventName of toEventNames(name)) {
|
|
83
|
+
ensureDelegatedEventListener(options.eventRoot ?? element, eventName);
|
|
84
|
+
}
|
|
85
|
+
previous.listeners.set(name, { handler });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof value === "boolean") {
|
|
90
|
+
const attributeName = toDomAttributeName(name);
|
|
91
|
+
if (element.hasAttribute(attributeName) !== value) {
|
|
92
|
+
if (!preserveHydrationAttributes) {
|
|
93
|
+
reportRecoverable(
|
|
94
|
+
options,
|
|
95
|
+
"attribute",
|
|
96
|
+
path,
|
|
97
|
+
new Error(`Hydration attribute mismatch: ${attributeName}.`),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (preserveHydrationAttributes) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
(element as unknown as Record<string, unknown>)[name] = value;
|
|
107
|
+
|
|
108
|
+
if (value) {
|
|
109
|
+
element.setAttribute(attributeName, "");
|
|
110
|
+
} else {
|
|
111
|
+
element.removeAttribute(attributeName);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
applyAttribute(element, toDomAttributeName(name), value, path, options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setAppliedProps(element, { props: { ...props }, listeners: previous.listeners });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function applyPostChildFormProps(
|
|
123
|
+
element: HTMLElement,
|
|
124
|
+
props: Record<string, unknown>,
|
|
125
|
+
): void {
|
|
126
|
+
const value = props.value ?? props.defaultValue;
|
|
127
|
+
|
|
128
|
+
if (value === undefined || value === null) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (element instanceof HTMLInputElement) {
|
|
133
|
+
element.value = String(value);
|
|
134
|
+
element.setAttribute("value", String(value));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
139
|
+
element.value = String(value);
|
|
140
|
+
element.textContent = String(value);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (element instanceof HTMLSelectElement) {
|
|
145
|
+
const nextValue = String(value);
|
|
146
|
+
for (const option of Array.from(element.options)) {
|
|
147
|
+
option.selected = option.value === nextValue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function applyAttribute(
|
|
153
|
+
element: HTMLElement,
|
|
154
|
+
name: string,
|
|
155
|
+
value: unknown,
|
|
156
|
+
path: string,
|
|
157
|
+
options: RenderOptions,
|
|
158
|
+
): void {
|
|
159
|
+
const preserveHydrationAttributes = options.preserveHydrationAttributes === true;
|
|
160
|
+
|
|
161
|
+
// Issue 077: srcdoc and other HTML-bearing attributes require the
|
|
162
|
+
// explicit `{ __html: "..." }` opt-in. A plain value -- string,
|
|
163
|
+
// number, boolean -- is treated as if it were null (drop the
|
|
164
|
+
// attribute and log a recoverable mismatch).
|
|
165
|
+
if (isDangerousHtmlAttribute(name) && !isDangerousHtmlOptIn(value)) {
|
|
166
|
+
if (element.hasAttribute(name) && !preserveHydrationAttributes) {
|
|
167
|
+
reportRecoverable(
|
|
168
|
+
options,
|
|
169
|
+
"attribute",
|
|
170
|
+
path,
|
|
171
|
+
new Error(`Unsafe HTML attribute dropped: ${name}.`),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (!preserveHydrationAttributes) {
|
|
175
|
+
element.removeAttribute(name);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (value === null || value === undefined || value === false) {
|
|
181
|
+
if (element.hasAttribute(name) && !preserveHydrationAttributes) {
|
|
182
|
+
reportRecoverable(
|
|
183
|
+
options,
|
|
184
|
+
"attribute",
|
|
185
|
+
path,
|
|
186
|
+
new Error(`Hydration attribute mismatch: ${name}.`),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!preserveHydrationAttributes) {
|
|
191
|
+
element.removeAttribute(name);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const stringValue = isDangerousHtmlOptIn(value)
|
|
197
|
+
? (value as { __html: string }).__html
|
|
198
|
+
: String(value);
|
|
199
|
+
|
|
200
|
+
// Issue 075: URL attributes are scheme-validated against the same
|
|
201
|
+
// block list used by SSR (packages/server/src/url-safety.ts). If the
|
|
202
|
+
// value is unsafe we treat it as if it were null -- remove the
|
|
203
|
+
// existing attribute, log a recoverable mismatch, and stop. This
|
|
204
|
+
// matches react-dom's sanitizeURL posture.
|
|
205
|
+
if (isUrlAttribute(name) && isUnsafeUrlAttribute(name, stringValue)) {
|
|
206
|
+
if (element.hasAttribute(name) && !preserveHydrationAttributes) {
|
|
207
|
+
reportRecoverable(
|
|
208
|
+
options,
|
|
209
|
+
"attribute",
|
|
210
|
+
path,
|
|
211
|
+
new Error(`Unsafe URL scheme dropped from ${name}.`),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!preserveHydrationAttributes) {
|
|
215
|
+
element.removeAttribute(name);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (element.getAttribute(name) !== stringValue && !preserveHydrationAttributes) {
|
|
221
|
+
reportRecoverable(
|
|
222
|
+
options,
|
|
223
|
+
"attribute",
|
|
224
|
+
path,
|
|
225
|
+
new Error(`Hydration attribute mismatch: ${name}.`),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (preserveHydrationAttributes) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
element.setAttribute(name, stringValue);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyFormValueProp(
|
|
237
|
+
element: HTMLElement,
|
|
238
|
+
name: string,
|
|
239
|
+
value: unknown,
|
|
240
|
+
path: string,
|
|
241
|
+
options: RenderOptions,
|
|
242
|
+
): boolean {
|
|
243
|
+
if (element instanceof HTMLInputElement && (name === "value" || name === "defaultValue")) {
|
|
244
|
+
const nextValue = value === null || value === undefined ? "" : String(value);
|
|
245
|
+
|
|
246
|
+
if (element.value !== nextValue) {
|
|
247
|
+
reportRecoverable(
|
|
248
|
+
options,
|
|
249
|
+
"attribute",
|
|
250
|
+
path,
|
|
251
|
+
new Error("Hydration attribute mismatch: value."),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
element.value = nextValue;
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (element instanceof HTMLInputElement && (name === "checked" || name === "defaultChecked")) {
|
|
260
|
+
const nextChecked = value !== null && value !== undefined && value !== false;
|
|
261
|
+
|
|
262
|
+
if (element.checked !== nextChecked) {
|
|
263
|
+
reportRecoverable(
|
|
264
|
+
options,
|
|
265
|
+
"attribute",
|
|
266
|
+
path,
|
|
267
|
+
new Error("Hydration attribute mismatch: checked."),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
element.checked = nextChecked;
|
|
272
|
+
element.defaultChecked = nextChecked;
|
|
273
|
+
if (nextChecked) {
|
|
274
|
+
element.setAttribute("checked", "");
|
|
275
|
+
} else {
|
|
276
|
+
element.removeAttribute("checked");
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (element instanceof HTMLTextAreaElement && (name === "value" || name === "defaultValue")) {
|
|
282
|
+
const nextValue = value === null || value === undefined ? "" : String(value);
|
|
283
|
+
|
|
284
|
+
if (element.value !== nextValue) {
|
|
285
|
+
reportRecoverable(
|
|
286
|
+
options,
|
|
287
|
+
"attribute",
|
|
288
|
+
path,
|
|
289
|
+
new Error("Hydration attribute mismatch: textarea value."),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
element.value = nextValue;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (element instanceof HTMLSelectElement && (name === "value" || name === "defaultValue")) {
|
|
298
|
+
const nextValue = value === null || value === undefined ? undefined : String(value);
|
|
299
|
+
|
|
300
|
+
for (const option of Array.from(element.options)) {
|
|
301
|
+
option.selected = nextValue !== undefined && option.value === nextValue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function applyStyle(
|
|
311
|
+
element: HTMLElement,
|
|
312
|
+
previousStyle: unknown,
|
|
313
|
+
nextStyle: unknown,
|
|
314
|
+
path: string,
|
|
315
|
+
options: RenderOptions,
|
|
316
|
+
): void {
|
|
317
|
+
if (options.preserveHydrationAttributes === true) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (isStyleObject(previousStyle)) {
|
|
322
|
+
for (const name of Object.keys(previousStyle)) {
|
|
323
|
+
element.style.removeProperty(name);
|
|
324
|
+
}
|
|
325
|
+
} else if (element.hasAttribute("style")) {
|
|
326
|
+
reportRecoverable(
|
|
327
|
+
options,
|
|
328
|
+
"attribute",
|
|
329
|
+
path,
|
|
330
|
+
new Error("Hydration attribute mismatch: style."),
|
|
331
|
+
);
|
|
332
|
+
element.removeAttribute("style");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (isStyleObject(nextStyle)) {
|
|
336
|
+
Object.assign(element.style, nextStyle);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
element.removeAttribute("style");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function collectAttributeNames(props: Record<string, unknown>): Set<string> {
|
|
344
|
+
const names = new Set<string>();
|
|
345
|
+
|
|
346
|
+
for (const [name, value] of Object.entries(props)) {
|
|
347
|
+
if (
|
|
348
|
+
name === "children" ||
|
|
349
|
+
name === "ref" ||
|
|
350
|
+
name === "key" ||
|
|
351
|
+
/^on[A-Z]/.test(name) ||
|
|
352
|
+
value === false ||
|
|
353
|
+
value === null ||
|
|
354
|
+
value === undefined
|
|
355
|
+
) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (name === "defaultValue") {
|
|
360
|
+
names.add("value");
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (name === "defaultChecked") {
|
|
365
|
+
names.add("checked");
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
names.add(toDomAttributeName(name));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return names;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function toDomAttributeName(name: string): string {
|
|
376
|
+
return HTML_ATTRIBUTE_ALIASES[name] ?? name;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const HTML_ATTRIBUTE_ALIASES: Record<string, string> = {
|
|
380
|
+
acceptCharset: "accept-charset",
|
|
381
|
+
autoFocus: "autofocus",
|
|
382
|
+
autoPlay: "autoplay",
|
|
383
|
+
charSet: "charset",
|
|
384
|
+
className: "class",
|
|
385
|
+
colSpan: "colspan",
|
|
386
|
+
contentEditable: "contenteditable",
|
|
387
|
+
crossOrigin: "crossorigin",
|
|
388
|
+
encType: "enctype",
|
|
389
|
+
formAction: "formaction",
|
|
390
|
+
frameBorder: "frameborder",
|
|
391
|
+
htmlFor: "for",
|
|
392
|
+
httpEquiv: "http-equiv",
|
|
393
|
+
maxLength: "maxlength",
|
|
394
|
+
minLength: "minlength",
|
|
395
|
+
noValidate: "novalidate",
|
|
396
|
+
playsInline: "playsinline",
|
|
397
|
+
readOnly: "readonly",
|
|
398
|
+
rowSpan: "rowspan",
|
|
399
|
+
spellCheck: "spellcheck",
|
|
400
|
+
srcDoc: "srcdoc",
|
|
401
|
+
srcSet: "srcset",
|
|
402
|
+
tabIndex: "tabindex",
|
|
403
|
+
useMap: "usemap",
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
function isStyleObject(value: unknown): value is Partial<CSSStyleDeclaration> {
|
|
407
|
+
return typeof value === "object" && value !== null;
|
|
408
|
+
}
|