Update base for Update on "[compiler][fixtures] test repros: codegen, alignScope, phis"

The AlignReactiveScope bug should be simplest to fix, but it's also caught by an invariant assertion. I think a fix could be either keeping track of "active" block-fallthrough pairs (`retainWhere(pair => pair.range.end > current.instr[0].id)`) or following the approach in `assertValidBlockNesting`.
I'm tempted to pull the value-block aligning logic out into its own pass (using the current `node` tree traversal), then align to non-value blocks with the `assertValidBlockNesting` approach. Happy to hear feedback on this though!

The other two are likely bigger issues, as they're not caught by static invariants.

Update:
- removed bug-phi-reference-effect as it's been patched by josephsavona
- added bug-array-concat-should-capture

[ghstack-poisoned]
This commit is contained in:
Mofei Zhang
2024-06-13 12:47:42 -04:00
28 changed files with 260 additions and 604 deletions
+6
View File
@@ -14,6 +14,12 @@ module.exports = {
parser: 'flow',
arrowParens: 'avoid',
overrides: [
{
files: ['*.code-workspace'],
options: {
parser: 'json-stringify',
},
},
{
files: esNextPaths,
options: {
@@ -1668,6 +1668,15 @@ function lowerExpression(
const left = lowerExpressionToTemporary(builder, leftPath);
const right = lowerExpressionToTemporary(builder, expr.get("right"));
const operator = expr.node.operator;
if (operator === "|>") {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
return {
kind: "BinaryExpression",
operator,
@@ -1893,7 +1902,9 @@ function lowerExpression(
);
}
const operators: { [key: string]: t.BinaryExpression["operator"] } = {
const operators: {
[key: string]: Exclude<t.BinaryExpression["operator"], "|>">;
} = {
"+=": "+",
"-=": "-",
"/=": "/",
@@ -2307,6 +2318,20 @@ function lowerExpression(
});
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
}
} else if (expr.node.operator === "throw") {
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
description: "Remove this line",
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
});
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
} else {
return {
kind: "UnaryExpression",
@@ -866,7 +866,7 @@ export type InstructionValue =
| JSXText
| {
kind: "BinaryExpression";
operator: t.BinaryExpression["operator"];
operator: Exclude<t.BinaryExpression["operator"], "|>">;
left: Place;
right: Place;
loc: SourceLocation;
@@ -881,7 +881,7 @@ export type InstructionValue =
| MethodCall
| {
kind: "UnaryExpression";
operator: t.UnaryExpression["operator"];
operator: Exclude<t.UnaryExpression["operator"], "throw" | "delete">;
value: Place;
loc: SourceLocation;
}
@@ -856,7 +856,7 @@ export function mapTerminalSuccessors(
const block = fn(terminal.block);
const fallthrough = fn(terminal.fallthrough);
return {
kind: "scope",
kind: terminal.kind,
scope: terminal.scope,
block,
fallthrough,
@@ -201,7 +201,7 @@ export default function inferReferenceEffects(
let queuedState = queuedStates.get(blockId);
if (queuedState != null) {
// merge the queued states for this block
state = queuedState.merge(state) ?? state;
state = queuedState.merge(state) ?? queuedState;
queuedStates.set(blockId, state);
} else {
/*
@@ -765,7 +765,7 @@ class InferenceState {
result.values[id] = { kind, value: printMixedHIR(value) };
}
for (const [variable, values] of this.#variables) {
result.variables[variable] = [...values].map(identify);
result.variables[`$${variable}`] = [...values].map(identify);
}
return result;
}
@@ -0,0 +1,61 @@
## Input
```javascript
import { arrayPush } from "shared-runtime";
function Foo(cond) {
let x = null;
if (cond) {
x = [];
} else {
}
// Here, x = phi(x$null, x$[]) should receive a ValueKind of Mutable
arrayPush(x, 2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush } from "shared-runtime";
function Foo(cond) {
const $ = _c(2);
let x;
if ($[0] !== cond) {
x = null;
if (cond) {
x = [];
}
arrayPush(x, 2);
$[0] = cond;
$[1] = x;
} else {
x = $[1];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
```
### Eval output
(kind: ok) [2]
[2]
@@ -0,0 +1,19 @@
import { arrayPush } from "shared-runtime";
function Foo(cond) {
let x = null;
if (cond) {
x = [];
} else {
}
// Here, x = phi(x$null, x$[]) should receive a ValueKind of Mutable
arrayPush(x, 2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
+8 -8
View File
@@ -79,11 +79,11 @@ brace-expansion@^1.1.7:
concat-map "0.0.1"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
browserslist@^4.18.1:
version "4.21.7"
@@ -265,10 +265,10 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
+1
View File
@@ -280,6 +280,7 @@ function initialize(socket: WebSocket) {
store = new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
supportsClickToInspect: true,
});
log('Connected');
+2 -1
View File
@@ -97,7 +97,8 @@ function createBridgeAndStore() {
// At this time, the timeline can only parse Chrome performance profiles.
supportsTimeline: __IS_CHROME__,
supportsTraceUpdates: true,
supportsNativeInspection: true,
supportsInspectMatchingDOMElement: true,
supportsClickToInspect: true,
});
if (!isProfiling) {
+1
View File
@@ -37,6 +37,7 @@ export function createStore(bridge: FrontendBridge, config?: Config): Store {
return new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
supportsClickToInspect: true,
...config,
});
}
+17 -7
View File
@@ -71,7 +71,8 @@ type ErrorAndWarningTuples = Array<{id: number, index: number}>;
export type Config = {
checkBridgeProtocolCompatibility?: boolean,
isProfiling?: boolean,
supportsNativeInspection?: boolean,
supportsInspectMatchingDOMElement?: boolean,
supportsClickToInspect?: boolean,
supportsReloadAndProfile?: boolean,
supportsTimeline?: boolean,
supportsTraceUpdates?: boolean,
@@ -172,7 +173,8 @@ export default class Store extends EventEmitter<{
_rootIDToRendererID: Map<number, number> = new Map();
// These options may be initially set by a configuration option when constructing the Store.
_supportsNativeInspection: boolean = false;
_supportsInspectMatchingDOMElement: boolean = false;
_supportsClickToInspect: boolean = false;
_supportsReloadAndProfile: boolean = false;
_supportsTimeline: boolean = false;
_supportsTraceUpdates: boolean = false;
@@ -211,13 +213,17 @@ export default class Store extends EventEmitter<{
isProfiling = config.isProfiling === true;
const {
supportsNativeInspection,
supportsInspectMatchingDOMElement,
supportsClickToInspect,
supportsReloadAndProfile,
supportsTimeline,
supportsTraceUpdates,
} = config;
if (supportsNativeInspection) {
this._supportsNativeInspection = true;
if (supportsInspectMatchingDOMElement) {
this._supportsInspectMatchingDOMElement = true;
}
if (supportsClickToInspect) {
this._supportsClickToInspect = true;
}
if (supportsReloadAndProfile) {
this._supportsReloadAndProfile = true;
@@ -437,8 +443,12 @@ export default class Store extends EventEmitter<{
return this._rootSupportsTimelineProfiling;
}
get supportsNativeInspection(): boolean {
return this._supportsNativeInspection;
get supportsInspectMatchingDOMElement(): boolean {
return this._supportsInspectMatchingDOMElement;
}
get supportsClickToInspect(): boolean {
return this._supportsClickToInspect;
}
get supportsNativeStyleEditor(): boolean {
@@ -296,7 +296,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
<ButtonIcon type="suspend" />
</Toggle>
)}
{store.supportsNativeInspection && (
{store.supportsInspectMatchingDOMElement && (
<Button
onClick={highlightElement}
title="Inspect the matching DOM element">
@@ -361,7 +361,7 @@ export default function Tree(props: Props): React.Node {
<TreeFocusedContext.Provider value={treeFocused}>
<div className={styles.Tree} ref={treeRef}>
<div className={styles.SearchInput}>
{store.supportsNativeInspection && (
{store.supportsClickToInspect && (
<Fragment>
<InspectHostNodesToggle />
<div className={styles.VRule} />
@@ -52,6 +52,7 @@ import {
enableLegacyFBSupport,
enableCreateEventHandleAPI,
enableScopeAPI,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
import {
@@ -70,6 +71,8 @@ import * as FormActionEventPlugin from './plugins/FormActionEventPlugin';
import reportGlobalError from 'shared/reportGlobalError';
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
type DispatchListener = {
instance: null | Fiber,
listener: Function,
@@ -255,7 +258,17 @@ function processDispatchQueueItemsInOrder(
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(
instance,
executeDispatch,
event,
listener,
currentTarget,
);
} else {
executeDispatch(event, listener, currentTarget);
}
previousInstance = instance;
}
} else {
@@ -264,7 +277,17 @@ function processDispatchQueueItemsInOrder(
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(
instance,
executeDispatch,
event,
listener,
currentTarget,
);
} else {
executeDispatch(event, listener, currentTarget);
}
previousInstance = instance;
}
}
+2 -2
View File
@@ -63,9 +63,9 @@
"bun": "./server.bun.js",
"deno": "./server.browser.js",
"worker": "./server.browser.js",
"browser": "./server.browser.js",
"node": "./server.node.js",
"edge-light": "./server.edge.js",
"browser": "./server.browser.js",
"default": "./server.node.js"
},
"./server.browser": {
@@ -89,9 +89,9 @@
"workerd": "./static.edge.js",
"deno": "./static.browser.js",
"worker": "./static.browser.js",
"browser": "./static.browser.js",
"node": "./static.node.js",
"edge-light": "./static.edge.js",
"browser": "./static.browser.js",
"default": "./static.node.js"
},
"./static.browser": {
-1
View File
@@ -43,6 +43,5 @@ export {
render,
unstable_batchedUpdates,
findDOMNode,
unstable_renderSubtreeIntoContainer,
unmountComponentAtNode,
} from './client/ReactDOMRootFB';
@@ -1,353 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const React = require('react');
const PropTypes = require('prop-types');
const ReactDOM = require('react-dom');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
const renderSubtreeIntoContainer =
require('react-dom').unstable_renderSubtreeIntoContainer;
describe('renderSubtreeIntoContainer', () => {
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should pass context when rendering subtree elsewhere', () => {
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.foo}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};
getChildContext() {
return {
foo: 'bar',
};
}
render() {
return null;
}
componentDidMount() {
expect(
function () {
renderSubtreeIntoContainer(this, <Component />, portal);
}.bind(this),
).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
expect(portal.firstChild.innerHTML).toBe('bar');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update context if it changes due to setState', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
render() {
return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
state = {
bar: 'initial',
};
getChildContext() {
return {
foo: this.state.bar,
getFoo: () => this.state.bar,
};
}
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
componentDidUpdate() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const root = ReactDOMClient.createRoot(container);
const parentRef = React.createRef();
await act(async () => {
root.render(<Parent ref={parentRef} />);
});
const instance = parentRef.current;
expect(portal.firstChild.innerHTML).toBe('initial-initial');
await act(async () => {
instance.setState({bar: 'changed'});
});
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update context if it changes due to re-render', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
render() {
return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
getChildContext() {
return {
foo: this.props.bar,
getFoo: () => this.props.bar,
};
}
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
componentDidUpdate() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent bar="initial" />);
});
expect(portal.firstChild.innerHTML).toBe('initial-initial');
await act(async () => {
root.render(<Parent bar="changed" />);
});
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
// @gate !disableLegacyMode
it('should render portal with non-context-provider parent', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Parent extends React.Component {
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <div>hello</div>, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent bar="initial" />);
});
expect(portal.firstChild.innerHTML).toBe('hello');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should get context through non-context-provider parent', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Parent extends React.Component {
render() {
return <Middle />;
}
getChildContext() {
return {value: this.props.value};
}
static childContextTypes = {
value: PropTypes.string.isRequired,
};
}
class Middle extends React.Component {
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Child />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
class Child extends React.Component {
static contextTypes = {
value: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.value}</div>;
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent value="foo" />);
});
expect(portal.textContent).toBe('foo');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should get context through middle non-context-provider layer', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal1 = document.createElement('div');
const portal2 = document.createElement('div');
class Parent extends React.Component {
render() {
return null;
}
getChildContext() {
return {value: this.props.value};
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Middle />, portal1);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
static childContextTypes = {
value: PropTypes.string.isRequired,
};
}
class Middle extends React.Component {
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Child />, portal2);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
class Child extends React.Component {
static contextTypes = {
value: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.value}</div>;
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent value="foo" />);
});
expect(portal2.textContent).toBe('foo');
});
// @gate !disableLegacyMode
it('legacy test: fails gracefully when mixing React 15 and 16', () => {
class C extends React.Component {
render() {
return <div />;
}
}
const c = ReactDOM.render(<C />, document.createElement('div'));
// React 15 calls this:
// https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479
expect(() => {
c._reactInternalInstance._processChildContext({});
}).toThrow(
__DEV__
? '_processChildContext is not available in React 16+. This likely ' +
'means you have multiple copies of React and are attempting to nest ' +
'a React 15 tree inside a React 16 tree using ' +
"unstable_renderSubtreeIntoContainer, which isn't supported. Try to " +
'make sure you have only one copy of React (and ideally, switch to ' +
'ReactDOM.createPortal).'
: "Cannot read property '_processChildContext' of undefined",
);
});
});
-41
View File
@@ -59,7 +59,6 @@ import {
} from 'react-reconciler/src/ReactFiberReconciler';
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {has as hasInstance} from 'shared/ReactInstanceMap';
import {
current as currentOwner,
@@ -420,46 +419,6 @@ export function render(
);
}
export function unstable_renderSubtreeIntoContainer(
parentComponent: React$Component<any, any>,
element: React$Element<any>,
containerNode: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (disableLegacyMode) {
if (__DEV__) {
console.error(
'ReactDOM.unstable_renderSubtreeIntoContainer() was removed in React 19. Consider using a portal instead.',
);
}
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
}
if (__DEV__) {
console.error(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported ' +
'since React 18. Consider using a portal instead. Until you switch to ' +
"the createRoot API, your app will behave as if it's running React " +
'17. Learn more: https://react.dev/link/switch-to-createroot',
);
}
if (!isValidContainerLegacy(containerNode)) {
throw new Error('Target container is not a DOM element.');
}
if (parentComponent == null || !hasInstance(parentComponent)) {
throw new Error('parentComponent must be a valid React Component');
}
return legacyRenderSubtreeIntoContainer(
parentComponent,
element,
containerNode,
false,
callback,
);
}
export function unmountComponentAtNode(container: Container): boolean {
if (disableLegacyMode) {
if (__DEV__) {
@@ -7,6 +7,10 @@
import isArray from 'shared/isArray';
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
let hasError = false;
let caughtError = null;
@@ -93,10 +97,22 @@ export function executeDispatchesInOrder(event) {
break;
}
// Listeners and Instances are two parallel arrays that are always in sync.
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
const listener = dispatchListeners[i];
const instance = dispatchInstances[i];
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(instance, executeDispatch, event, listener, instance);
} else {
executeDispatch(event, listener, instance);
}
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances);
const listener = dispatchListeners;
const instance = dispatchInstances;
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(instance, executeDispatch, event, listener, instance);
} else {
executeDispatch(event, listener, instance);
}
}
event._dispatchListeners = null;
event._dispatchInstances = null;
+1 -21
View File
@@ -73,9 +73,7 @@ import {
setIsStrictModeForDevtools,
} from './ReactFiberDevToolsHook';
const fakeInternalInstance: {
_processChildContext?: () => empty,
} = {};
const fakeInternalInstance = {};
let didWarnAboutStateAssignmentForComponent;
let didWarnAboutUninitializedState;
@@ -98,24 +96,6 @@ if (__DEV__) {
didWarnAboutInvalidateContextType = new Set<string>();
didWarnOnInvalidCallback = new Set<string>();
// This is so gross but it's at least non-critical and can be removed if
// it causes problems. This is meant to give a nicer error message for
// ReactDOM15.unstable_renderSubtreeIntoContainer(reactDOM16Component,
// ...)) which otherwise throws a "_processChildContext is not a function"
// exception.
Object.defineProperty(fakeInternalInstance, '_processChildContext', {
enumerable: false,
value: function (): empty {
throw new Error(
'_processChildContext is not available in React 16+. This likely ' +
'means you have multiple copies of React and are attempting to nest ' +
'a React 15 tree inside a React 16 tree using ' +
"unstable_renderSubtreeIntoContainer, which isn't supported. Try " +
'to make sure you have only one copy of React (and ideally, switch ' +
'to ReactDOM.createPortal).',
);
},
});
Object.freeze(fakeInternalInstance);
}
@@ -1,99 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from '../ReactServerStreamConfigFB';
import type {
PrecomputedChunk,
Chunk,
BinaryChunk,
} from '../ReactServerStreamConfigFB';
let byteLengthImpl: null | ((chunk: Chunk | PrecomputedChunk) => number) = null;
export function setByteLengthOfChunkImplementation(
impl: (chunk: Chunk | PrecomputedChunk) => number,
): void {
byteLengthImpl = impl;
}
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
if (byteLengthImpl == null) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);',
);
}
return byteLengthImpl(chunk);
}
export interface Destination {
beginWriting(): void;
write(chunk: Chunk | PrecomputedChunk | BinaryChunk): void;
completeWriting(): void;
flushBuffered(): void;
close(): void;
onError(error: mixed): void;
}
function handleErrorInNextTick(error: any) {
setTimeout(() => {
throw error;
});
}
const LocalPromise = Promise;
/**
* Since this environment doesn't have a way to schedule tasks from JS we schedule
* using a microtask instead. This isn't necessarily ideal since we would like to give
* other IO a chance to run before performing work typically but it's the best we can
* do in this environment
*/
export function scheduleWork(callback: () => void) {
LocalPromise.resolve().then(callback).catch(handleErrorInNextTick);
}
export const scheduleMicrotask: (callback: () => void) => void = scheduleWork;
export function beginWriting(destination: Destination) {
destination.beginWriting();
}
export function writeChunk(
destination: Destination,
chunk: Chunk | PrecomputedChunk | BinaryChunk,
): void {
destination.write(chunk);
}
export function writeChunkAndReturn(
destination: Destination,
chunk: Chunk | PrecomputedChunk | BinaryChunk,
): boolean {
destination.write(chunk);
return true;
}
export function completeWriting(destination: Destination) {
destination.completeWriting();
}
export function flushBuffered(destination: Destination) {
destination.flushBuffered();
}
export function close(destination: Destination) {
destination.close();
}
export function closeWithError(destination: Destination, error: mixed): void {
destination.onError(error);
destination.close();
}
-13
View File
@@ -15,23 +15,10 @@
* If this becomes an actual Map, that will break.
*/
/**
* This API should be called `delete` but we'd have to make sure to always
* transform these to strings for IE support. When this transform is fully
* supported we can rename it.
*/
export function remove(key) {
key._reactInternals = undefined;
}
export function get(key) {
return key._reactInternals;
}
export function has(key) {
return key._reactInternals !== undefined;
}
export function set(key, value) {
key._reactInternals = value;
}
@@ -20,7 +20,7 @@ let useState;
let useEffect;
let useLayoutEffect;
let assertLog;
let originalError;
let assertConsoleErrorDev;
// This tests shared behavior between the built-in and shim implementations of
// of useSyncExternalStore.
@@ -50,9 +50,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
: 'react-dom-17/umd/react-dom.production.min.js',
),
);
// Because React 17 prints extra logs we need to ignore them.
originalError = console.error;
console.error = jest.fn();
}
React = require('react');
ReactDOM = require('react-dom');
@@ -63,6 +60,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useLayoutEffect = React.useLayoutEffect;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
const internalAct = require('internal-test-utils').act;
// The internal act implementation doesn't batch updates by default, since
@@ -85,11 +83,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useSyncExternalStoreWithSelector =
require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
});
afterEach(() => {
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
console.error = originalError;
}
});
function Text({text}) {
Scheduler.log(text);
return text;
@@ -630,36 +623,30 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const container = document.createElement('div');
const root = createRoot(container);
await expect(async () => {
await expect(async () => {
await act(() => {
ReactDOM.flushSync(async () =>
root.render(React.createElement(App, null)),
);
});
}).rejects.toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
}).toErrorDev(
await act(() => {
ReactDOM.flushSync(async () =>
root.render(React.createElement(App, null)),
);
});
}).rejects.toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
assertConsoleErrorDev(
gate(flags => flags.enableUseSyncExternalStoreShim)
? [
'Maximum update depth exceeded. ',
'The result of getSnapshot should be cached to avoid an infinite loop',
'The above error occurred in the',
[
'The result of getSnapshot should be cached to avoid an infinite loop',
{withoutStack: true},
],
'Error: Maximum update depth exceeded',
'The above error occurred i',
]
: [
'The result of getSnapshot should be cached to avoid an infinite loop',
],
{
withoutStack: gate(flags => {
if (flags.enableUseSyncExternalStoreShim) {
// Stacks don't work when mixing the source and the npm package.
return flags.source ? 1 : 0;
}
return false;
}),
},
);
});
it('getSnapshot can return NaN without infinite loop warning', async () => {
@@ -850,10 +837,9 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// client. To avoid this server mismatch warning, user must account for
// this themselves and return the correct value inside `getSnapshot`.
await act(() => {
expect(() =>
ReactDOM.hydrate(React.createElement(App, null), container),
).toErrorDev('Text content did not match');
ReactDOM.hydrate(React.createElement(App, null), container);
});
assertConsoleErrorDev(['Text content did not match']);
assertLog(['client', 'Passive effect: client']);
}
expect(container.textContent).toEqual('client');
@@ -16,7 +16,10 @@ import * as React from 'react';
export const useSyncExternalStore = React.useSyncExternalStore;
if (__DEV__) {
console.error(
// Avoid transforming the `console.error` call as it would cause the built artifact
// to access React internals, which exist under different paths depending on the
// React version.
console['error'](
"The main 'use-sync-external-store' entry point is not supported; all it " +
"does is re-export useSyncExternalStore from the 'react' package, so " +
'it only works with React 18+.' +
@@ -40,7 +40,10 @@ export function useSyncExternalStore<T>(
if (!didWarnOld18Alpha) {
if (React.startTransition !== undefined) {
didWarnOld18Alpha = true;
console.error(
// Avoid transforming the `console.error` call as it would cause the built artifact
// to access React internals, which exist under different paths depending on the
// React version.
console['error'](
'You are using an outdated, pre-release alpha of React 18 that ' +
'does not support useSyncExternalStore. The ' +
'use-sync-external-store shim will not work correctly. Upgrade ' +
@@ -59,7 +62,10 @@ export function useSyncExternalStore<T>(
if (!didWarnUncachedGetSnapshot) {
const cachedValue = getSnapshot();
if (!is(value, cachedValue)) {
console.error(
// Avoid transforming the `console.error` call as it would cause the built artifact
// to access React internals, which exist under different paths depending on the
// React version.
console['error'](
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
+30
View File
@@ -0,0 +1,30 @@
{
"folders": [
{
"path": "."
}
],
"extensions": {
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"flowtype.flow-for-vscode"
]
},
"settings": {
"search.exclude": {
"**/dist/**": true,
"**/build/**": true,
"**/out/**": true,
"*.map": true,
"*.log": true
},
"javascript.validate.enable": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"flow.pathToFlow": "${workspaceFolder}/node_modules/.bin/flow",
"prettier.configPath": "",
"prettier.ignorePath": ""
}
}
+2 -7
View File
@@ -95,13 +95,8 @@ function getTestFlags() {
// This is used by useSyncExternalStoresShared-test.js to decide whether
// to test the shim or the native implementation of useSES.
// TODO: It's disabled when enableRefAsProp is on because the JSX
// runtime used by our tests is not compatible with older versions of
// React. If we want to keep testing this shim after enableRefIsProp is
// on everywhere, we'll need to find some other workaround. Maybe by
// only using createElement instead of JSX in that test module.
enableUseSyncExternalStoreShim:
!__VARIANT__ && !featureFlags.enableRefAsProp,
enableUseSyncExternalStoreShim: !__VARIANT__,
// If there's a naming conflict between scheduler and React feature flags, the
// React ones take precedence.