Skip to content

Commit 33b8d6a

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Make constructors of internal DOM, MutationObserver, and IntersectionObserver classes throw (facebook#57046)
Summary: Pull Request resolved: facebook#57046 Several Web API classes exposed globally by React Native are not meant to be constructed from userland — either because they correspond to web spec interfaces whose constructors are illegal (e.g. `Node`, `Element`, `HTMLCollection`, `NodeList`, `DOMRectList`, `IntersectionObserverEntry`, `MutationRecord`), or because their React Native implementations take internal arguments that callers cannot meaningfully supply (e.g. `Document`, `Text`, `CharacterData`, `HTMLElement`). Apply the same `_public` pattern already in use under `setUpPerformanceModern` to these classes: export a constructor stub that throws `TypeError: Failed to construct '<Name>': Illegal constructor`, with its `prototype` aliased to the real class so that `instanceof` checks against the global still work. Internal code continues to use the real class via the default export. The following classes get a `_public` export and are now polyfilled through it in their corresponding setup file: - `setUpDOM`: `DOMRectList`, `HTMLCollection`, `NodeList`, `Node` (`ReadOnlyNode`), `Document` (`ReactNativeDocument`), `CharacterData` (`ReadOnlyCharacterData`), `Text` (`ReadOnlyText`), `Element` (`ReadOnlyElement`), `HTMLElement` (`ReactNativeElement`). - `setUpMutationObserver`: `MutationRecord`. - `setUpIntersectionObserver`: `IntersectionObserverEntry` (also newly added to the setup file — it was not being exposed globally before). For `Node`, the public stub also copies the static node-type and document-position constants (`ELEMENT_NODE`, `TEXT_NODE`, `DOCUMENT_POSITION_*`, etc.) so that consumers can keep reading them from the global (`Node.ELEMENT_NODE`, …). Classes whose web-spec constructors are legitimately public (`DOMRect`, `DOMRectReadOnly`, `Event`, `EventTarget`, `CustomEvent`, `MutationObserver`, `IntersectionObserver`) are unchanged. For the three collection classes whose public Flow types live in a `.js.flow` declaration file (`DOMRectList`, `HTMLCollection`, `NodeList`), the new `_public` export is also declared in the `.js.flow` companion so Flow can see it. Changelog: [General][Fixed] - Throw "Illegal constructor" when constructing `Node`, `Document`, `Element`, `HTMLElement`, `CharacterData`, `Text`, `HTMLCollection`, `NodeList`, `DOMRectList`, `MutationRecord`, and `IntersectionObserverEntry` from JavaScript. Reviewed By: huntie Differential Revision: D107227212
1 parent 3278f30 commit 33b8d6a

27 files changed

Lines changed: 395 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @fantom_flags enableIntersectionObserverByDefault:*
8+
* @flow strict-local
9+
* @format
10+
* @oncall react_native
11+
*/
12+
13+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
14+
15+
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
16+
17+
declare var IntersectionObserverEntry: unknown;
18+
19+
// TODO: Merge into `setUpDefaultReactNativeEnvironment-Globals-itest.js` once
20+
// the `enableIntersectionObserverByDefault` feature flag is cleaned up and the
21+
// IntersectionObserver globals are exposed unconditionally.
22+
describe('setUpDefaultReactNativeEnvironment (IntersectionObserver globals)', () => {
23+
if (ReactNativeFeatureFlags.enableIntersectionObserverByDefault()) {
24+
describe('when enableIntersectionObserverByDefault is enabled', () => {
25+
it('should provide IntersectionObserver', () => {
26+
expect(typeof IntersectionObserver).toBe('function');
27+
});
28+
29+
it('should provide IntersectionObserverEntry', () => {
30+
expect(typeof IntersectionObserverEntry).toBe('function');
31+
});
32+
});
33+
} else {
34+
describe('when enableIntersectionObserverByDefault is disabled', () => {
35+
it('should not provide IntersectionObserver', () => {
36+
expect(typeof IntersectionObserver).toBe('undefined');
37+
});
38+
39+
it('should not provide IntersectionObserverEntry', () => {
40+
expect(typeof IntersectionObserverEntry).toBe('undefined');
41+
});
42+
});
43+
}
44+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @fantom_flags enableMutationObserverByDefault:*
8+
* @flow strict-local
9+
* @format
10+
* @oncall react_native
11+
*/
12+
13+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
14+
15+
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
16+
17+
declare var MutationRecord: unknown;
18+
19+
// TODO: Merge into `setUpDefaultReactNativeEnvironment-Globals-itest.js` once
20+
// the `enableMutationObserverByDefault` feature flag is cleaned up and the
21+
// MutationObserver globals are exposed unconditionally.
22+
describe('setUpDefaultReactNativeEnvironment (MutationObserver globals)', () => {
23+
if (ReactNativeFeatureFlags.enableMutationObserverByDefault()) {
24+
describe('when enableMutationObserverByDefault is enabled', () => {
25+
it('should provide MutationObserver', () => {
26+
expect(typeof MutationObserver).toBe('function');
27+
});
28+
29+
it('should provide MutationRecord', () => {
30+
expect(typeof MutationRecord).toBe('function');
31+
});
32+
});
33+
} else {
34+
describe('when enableMutationObserverByDefault is disabled', () => {
35+
it('should not provide MutationObserver', () => {
36+
expect(typeof MutationObserver).toBe('undefined');
37+
});
38+
39+
it('should not provide MutationRecord', () => {
40+
expect(typeof MutationRecord).toBe('undefined');
41+
});
42+
});
43+
}
44+
});

packages/react-native/src/private/setup/setUpDOM.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,47 +31,57 @@ export default function setUpDOM() {
3131

3232
polyfillGlobal(
3333
'DOMRectList',
34-
() => require('../webapis/geometry/DOMRectList').default,
34+
() => require('../webapis/geometry/DOMRectList').DOMRectList_public,
3535
);
3636

3737
polyfillGlobal(
3838
'HTMLCollection',
39-
() => require('../webapis/dom/oldstylecollections/HTMLCollection').default,
39+
() =>
40+
require('../webapis/dom/oldstylecollections/HTMLCollection')
41+
.HTMLCollection_public,
4042
);
4143

4244
polyfillGlobal(
4345
'NodeList',
44-
() => require('../webapis/dom/oldstylecollections/NodeList').default,
46+
() =>
47+
require('../webapis/dom/oldstylecollections/NodeList').NodeList_public,
4548
);
4649

4750
polyfillGlobal(
4851
'Node',
49-
() => require('../webapis/dom/nodes/ReadOnlyNode').default,
52+
() => require('../webapis/dom/nodes/ReadOnlyNode').ReadOnlyNode_public,
5053
);
5154

5255
polyfillGlobal(
5356
'Document',
54-
() => require('../webapis/dom/nodes/ReactNativeDocument').default,
57+
() =>
58+
require('../webapis/dom/nodes/ReactNativeDocument')
59+
.ReactNativeDocument_public,
5560
);
5661

5762
polyfillGlobal(
5863
'CharacterData',
59-
() => require('../webapis/dom/nodes/ReadOnlyCharacterData').default,
64+
() =>
65+
require('../webapis/dom/nodes/ReadOnlyCharacterData')
66+
.ReadOnlyCharacterData_public,
6067
);
6168

6269
polyfillGlobal(
6370
'Text',
64-
() => require('../webapis/dom/nodes/ReadOnlyText').default,
71+
() => require('../webapis/dom/nodes/ReadOnlyText').ReadOnlyText_public,
6572
);
6673

6774
polyfillGlobal(
6875
'Element',
69-
() => require('../webapis/dom/nodes/ReadOnlyElement').default,
76+
() =>
77+
require('../webapis/dom/nodes/ReadOnlyElement').ReadOnlyElement_public,
7078
);
7179

7280
polyfillGlobal(
7381
'HTMLElement',
74-
() => require('../webapis/dom/nodes/ReactNativeElement').default,
82+
() =>
83+
require('../webapis/dom/nodes/ReactNativeElement')
84+
.ReactNativeElement_public,
7585
);
7686

7787
polyfillGlobal('Event', () => require('../webapis/dom/events/Event').default);

packages/react-native/src/private/setup/setUpIntersectionObserver.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,11 @@ export default function setUpIntersectionObserver() {
2424
() =>
2525
require('../webapis/intersectionobserver/IntersectionObserver').default,
2626
);
27+
28+
polyfillGlobal(
29+
'IntersectionObserverEntry',
30+
() =>
31+
require('../webapis/intersectionobserver/IntersectionObserverEntry')
32+
.IntersectionObserverEntry_public,
33+
);
2734
}

packages/react-native/src/private/setup/setUpMutationObserver.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export default function setUpMutationObserver() {
2626

2727
polyfillGlobal(
2828
'MutationRecord',
29-
() => require('../webapis/mutationobserver/MutationRecord').default,
29+
() =>
30+
require('../webapis/mutationobserver/MutationRecord')
31+
.MutationRecord_public,
3032
);
3133
}

packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,14 @@ export function createReactNativeDocument(
139139
const document = new ReactNativeDocument(rootTag, instanceHandle);
140140
return document;
141141
}
142+
143+
export const ReactNativeDocument_public: typeof ReactNativeDocument =
144+
// $FlowExpectedError[incompatible-type]
145+
function Document() {
146+
throw new TypeError(
147+
"Failed to construct 'Document': Nodes cannot be imperatively created in React Native",
148+
);
149+
};
150+
151+
// $FlowExpectedError[prop-missing]
152+
ReactNativeDocument_public.prototype = ReactNativeDocument.prototype;

packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,14 @@ function replaceConstructorWithoutSuper(
292292
export default replaceConstructorWithoutSuper(
293293
ReactNativeElement,
294294
) as typeof ReactNativeElement;
295+
296+
export const ReactNativeElement_public: typeof ReactNativeElement =
297+
// $FlowExpectedError[incompatible-type]
298+
function HTMLElement() {
299+
throw new TypeError(
300+
"Failed to construct 'HTMLElement': Nodes cannot be imperatively created in React Native",
301+
);
302+
};
303+
304+
// $FlowExpectedError[prop-missing]
305+
ReactNativeElement_public.prototype = ReactNativeElement.prototype;

packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,14 @@ export default class ReadOnlyCharacterData extends ReadOnlyNode {
7070
return data.slice(offset, offset + adjustedCount);
7171
}
7272
}
73+
74+
export const ReadOnlyCharacterData_public: typeof ReadOnlyCharacterData =
75+
// $FlowExpectedError[incompatible-type]
76+
function CharacterData() {
77+
throw new TypeError(
78+
"Failed to construct 'CharacterData': Illegal constructor",
79+
);
80+
};
81+
82+
// $FlowExpectedError[prop-missing]
83+
ReadOnlyCharacterData_public.prototype = ReadOnlyCharacterData.prototype;

packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,12 @@ export function getBoundingClientRect(
245245
// Empty rect if any of the above failed
246246
return new DOMRect(0, 0, 0, 0);
247247
}
248+
249+
export const ReadOnlyElement_public: typeof ReadOnlyElement =
250+
// $FlowExpectedError[incompatible-type]
251+
function Element() {
252+
throw new TypeError("Failed to construct 'Element': Illegal constructor");
253+
};
254+
255+
// $FlowExpectedError[prop-missing]
256+
ReadOnlyElement_public.prototype = ReadOnlyElement.prototype;

packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,21 @@ export default replaceConstructorWithoutSuper(
371371
ReadOnlyNode,
372372
) as typeof ReadOnlyNode;
373373

374+
export const ReadOnlyNode_public: typeof ReadOnlyNode =
375+
// $FlowExpectedError[incompatible-type]
376+
function Node() {
377+
throw new TypeError("Failed to construct 'Node': Illegal constructor");
378+
};
379+
380+
// $FlowExpectedError[prop-missing]
381+
ReadOnlyNode_public.prototype = ReadOnlyNode.prototype;
382+
// Copy static properties (ELEMENT_NODE, DOCUMENT_NODE, TEXT_NODE,
383+
// DOCUMENT_POSITION_*, etc.) so that callers accessing them via the public
384+
// constructor (e.g. `Node.ELEMENT_NODE`) still work.
385+
// $FlowFixMe[unsafe-object-assign]
386+
// $FlowFixMe[not-an-object]
387+
Object.assign(ReadOnlyNode_public, ReadOnlyNode);
388+
374389
// Temporary type until we ship ReadOnlyNode extending EventTarget ungated.
375390
export type ReadOnlyNodeWithEventTarget = ReadOnlyNode & EventTarget;
376391

0 commit comments

Comments
 (0)