From 920f30ef7732e87045ae6652c464b58267991b8f Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 18 Apr 2018 16:36:31 -0700 Subject: [PATCH 001/277] Add forwardRef DEV warning for prop-types on render function (#12644) --- .../src/__tests__/forwardRef-test.internal.js | 21 +++++++++++++++++++ packages/react/src/forwardRef.js | 8 +++++++ 2 files changed, 29 insertions(+) diff --git a/packages/react/src/__tests__/forwardRef-test.internal.js b/packages/react/src/__tests__/forwardRef-test.internal.js index 068448c437..5780dbd054 100644 --- a/packages/react/src/__tests__/forwardRef-test.internal.js +++ b/packages/react/src/__tests__/forwardRef-test.internal.js @@ -249,4 +249,25 @@ describe('forwardRef', () => { 'forwardRef requires a render function but was given undefined.', ); }); + + it('should warn if the render function provided has propTypes or defaultProps attributes', () => { + function renderWithPropTypes() { + return null; + } + renderWithPropTypes.propTypes = {}; + + function renderWithDefaultProps() { + return null; + } + renderWithDefaultProps.defaultProps = {}; + + expect(() => React.forwardRef(renderWithPropTypes)).toWarnDev( + 'forwardRef render functions do not support propTypes or defaultProps. ' + + 'Did you accidentally pass a React component?', + ); + expect(() => React.forwardRef(renderWithDefaultProps)).toWarnDev( + 'forwardRef render functions do not support propTypes or defaultProps. ' + + 'Did you accidentally pass a React component?', + ); + }); }); diff --git a/packages/react/src/forwardRef.js b/packages/react/src/forwardRef.js index 6a923be127..6c3c1a4343 100644 --- a/packages/react/src/forwardRef.js +++ b/packages/react/src/forwardRef.js @@ -18,6 +18,14 @@ export default function forwardRef( 'forwardRef requires a render function but was given %s.', render === null ? 'null' : typeof render, ); + + if (render != null) { + warning( + render.defaultProps == null && render.propTypes == null, + 'forwardRef render functions do not support propTypes or defaultProps. ' + + 'Did you accidentally pass a React component?', + ); + } } return { From f80bbf88e5278cab9aca7206a1f241e33f3f674b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 19 Apr 2018 09:08:44 -0700 Subject: [PATCH 002/277] StrictMode should not warn about polyfilled getSnapshotBeforeUpdate (#12647) * Installed 3.x release of react-lifecycles-compat * Updated ReactComponentLifeCycle-test and ReactDOMServerLifecycles-test to cover both polyfilled lifecycles in StrictMode * Updated StrictMode warnings to not warn about polyfilled getSnapshotBeforeUpdate --- package.json | 2 +- .../ReactComponentLifeCycle-test.internal.js | 49 ++++++++++--------- .../ReactDOMServerLifecycles-test.internal.js | 34 +++++++++++-- .../src/ReactStrictModeWarnings.js | 20 +++----- yarn.lock | 6 +-- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 86cff63303..01f0a2781f 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "prettier": "1.11.1", "prop-types": "^15.6.0", "random-seed": "^0.3.0", - "react-lifecycles-compat": "^1.0.2", + "react-lifecycles-compat": "^3.0.2", "rimraf": "^2.6.1", "rollup": "^0.52.1", "rollup-plugin-babel": "^3.0.1", diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js index 5b1b1732a1..651463bf1c 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js @@ -63,28 +63,9 @@ describe('ReactComponentLifeCycle', () => { }); describe('react-lifecycles-compat', () => { - const polyfill = require('react-lifecycles-compat'); - - it('should not warn about deprecated cWM/cWRP for polyfilled components', () => { - class PolyfilledComponent extends React.Component { - state = {}; - static getDerivedStateFromProps() { - return null; - } - render() { - return null; - } - } - - polyfill(PolyfilledComponent); - - const container = document.createElement('div'); - ReactDOM.render(, container); - }); - - it('should not warn about unsafe lifecycles within "strict" tree for polyfilled components', () => { - const {StrictMode} = React; + const {polyfill} = require('react-lifecycles-compat'); + it('should not warn for components with polyfilled getDerivedStateFromProps', () => { class PolyfilledComponent extends React.Component { state = {}; static getDerivedStateFromProps() { @@ -99,9 +80,31 @@ describe('ReactComponentLifeCycle', () => { const container = document.createElement('div'); ReactDOM.render( - + - , + , + container, + ); + }); + + it('should not warn for components with polyfilled getSnapshotBeforeUpdate', () => { + class PolyfilledComponent extends React.Component { + getSnapshotBeforeUpdate() { + return null; + } + componentDidUpdate() {} + render() { + return null; + } + } + + polyfill(PolyfilledComponent); + + const container = document.createElement('div'); + ReactDOM.render( + + + , container, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js index 69bedb3de1..bbd502e2ab 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js @@ -69,9 +69,9 @@ describe('ReactDOMServerLifecycles', () => { }); describe('react-lifecycles-compat', () => { - const polyfill = require('react-lifecycles-compat'); + const {polyfill} = require('react-lifecycles-compat'); - it('should not warn about deprecated cWM/cWRP for polyfilled components', () => { + it('should not warn for components with polyfilled getDerivedStateFromProps', () => { class PolyfilledComponent extends React.Component { state = {}; static getDerivedStateFromProps() { @@ -84,7 +84,35 @@ describe('ReactDOMServerLifecycles', () => { polyfill(PolyfilledComponent); - ReactDOMServer.renderToString(); + const container = document.createElement('div'); + ReactDOMServer.renderToString( + + + , + container, + ); + }); + + it('should not warn for components with polyfilled getSnapshotBeforeUpdate', () => { + class PolyfilledComponent extends React.Component { + getSnapshotBeforeUpdate() { + return null; + } + componentDidUpdate() {} + render() { + return null; + } + } + + polyfill(PolyfilledComponent); + + const container = document.createElement('div'); + ReactDOMServer.renderToString( + + + , + container, + ); }); }); }); diff --git a/packages/react-reconciler/src/ReactStrictModeWarnings.js b/packages/react-reconciler/src/ReactStrictModeWarnings.js index 9b7b6c1dd2..0f39b09371 100644 --- a/packages/react-reconciler/src/ReactStrictModeWarnings.js +++ b/packages/react-reconciler/src/ReactStrictModeWarnings.js @@ -236,16 +236,6 @@ if (__DEV__) { return; } - // Don't warn about react-lifecycles-compat polyfilled components. - // Note that it is sufficient to check for the presence of a - // single lifecycle, componentWillMount, with the polyfill flag. - if ( - typeof instance.componentWillMount === 'function' && - instance.componentWillMount.__suppressDeprecationWarning === true - ) { - return; - } - let warningsForRoot; if (!pendingUnsafeLifecycleWarnings.has(strictRoot)) { warningsForRoot = { @@ -261,19 +251,23 @@ if (__DEV__) { const unsafeLifecycles = []; if ( - typeof instance.componentWillMount === 'function' || + (typeof instance.componentWillMount === 'function' && + instance.componentWillMount.__suppressDeprecationWarning !== true) || typeof instance.UNSAFE_componentWillMount === 'function' ) { unsafeLifecycles.push('UNSAFE_componentWillMount'); } if ( - typeof instance.componentWillReceiveProps === 'function' || + (typeof instance.componentWillReceiveProps === 'function' && + instance.componentWillReceiveProps.__suppressDeprecationWarning !== + true) || typeof instance.UNSAFE_componentWillReceiveProps === 'function' ) { unsafeLifecycles.push('UNSAFE_componentWillReceiveProps'); } if ( - typeof instance.componentWillUpdate === 'function' || + (typeof instance.componentWillUpdate === 'function' && + instance.componentWillUpdate.__suppressDeprecationWarning !== true) || typeof instance.UNSAFE_componentWillUpdate === 'function' ) { unsafeLifecycles.push('UNSAFE_componentWillUpdate'); diff --git a/yarn.lock b/yarn.lock index 495fa024f6..0c9feb48b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4434,9 +4434,9 @@ react-dom@15.5.4: object-assign "^4.1.0" prop-types "~15.5.7" -react-lifecycles-compat@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-1.0.2.tgz#551d8b1d156346e5fcf30ffac9b32ce3f78b8850" +react-lifecycles-compat@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz#7279047275bd727a912e25f734c0559527e84eff" react@15.5.4: version "15.5.4" From 999b656ed1c94b00fcfd043f54e18ade7553dee0 Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Thu, 19 Apr 2018 09:29:08 -0700 Subject: [PATCH 003/277] Initial commit (#12624) This is the first step - pulling the ReactDOMFrameScheduling module out into a separate package. Co-authored-by: Brandon Dail --- packages/react-art/src/ReactART.js | 6 ++-- .../react-dom/src/__tests__/ReactDOM-test.js | 35 +++++++++++++++++++ packages/react-dom/src/client/ReactDOM.js | 8 ++--- packages/react-scheduler/README.md | 4 +++ packages/react-scheduler/index.js | 12 +++++++ packages/react-scheduler/npm/index.js | 7 ++++ packages/react-scheduler/package.json | 23 ++++++++++++ .../src/ReactScheduler.js} | 15 ++++++++ scripts/rollup/bundles.js | 10 ++++++ 9 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 packages/react-scheduler/README.md create mode 100644 packages/react-scheduler/index.js create mode 100644 packages/react-scheduler/npm/index.js create mode 100644 packages/react-scheduler/package.json rename packages/{shared/ReactDOMFrameScheduling.js => react-scheduler/src/ReactScheduler.js} (94%) diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index 670e2a349b..04478434e8 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -7,7 +7,7 @@ import React from 'react'; import ReactFiberReconciler from 'react-reconciler'; -import * as ReactDOMFrameScheduling from 'shared/ReactDOMFrameScheduling'; +import * as ReactScheduler from 'react-scheduler'; import Mode from 'art/modes/current'; import FastNoSideEffects from 'art/modes/fast-noSideEffects'; import Transform from 'art/core/transform'; @@ -468,7 +468,7 @@ const ARTRenderer = ReactFiberReconciler({ return emptyObject; }, - scheduleDeferredCallback: ReactDOMFrameScheduling.rIC, + scheduleDeferredCallback: ReactScheduler.rIC, shouldSetTextContent(type, props) { return ( @@ -476,7 +476,7 @@ const ARTRenderer = ReactFiberReconciler({ ); }, - now: ReactDOMFrameScheduling.now, + now: ReactScheduler.now, mutation: { appendChild(parentInstance, child) { diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index 46b9f5cabe..5e7d2772e3 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -439,4 +439,39 @@ describe('ReactDOM', () => { Object.defineProperty(global, 'document', documentDescriptor); } }); + + it('warns when requestAnimationFrame is not polyfilled in the browser', () => { + const previousRAF = global.requestAnimationFrame; + try { + global.requestAnimationFrame = undefined; + jest.resetModules(); + expect(() => require('react-dom')).toWarnDev( + 'React depends on requestAnimationFrame.', + ); + } finally { + global.requestAnimationFrame = previousRAF; + } + }); + + // We're just testing importing, not using it. + // It is important because even isomorphic components may import it. + it('can import findDOMNode in Node environment', () => { + const previousRAF = global.requestAnimationFrame; + const previousRIC = global.requestIdleCallback; + const prevWindow = global.window; + try { + global.requestAnimationFrame = undefined; + global.requestIdleCallback = undefined; + // Simulate the Node environment: + delete global.window; + jest.resetModules(); + expect(() => { + require('react-dom'); + }).not.toThrow(); + } finally { + global.requestAnimationFrame = previousRAF; + global.requestIdleCallback = previousRIC; + global.window = prevWindow; + } + }); }); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 1bb0c86ed2..82032f1d1d 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -29,7 +29,7 @@ import * as EventPluginRegistry from 'events/EventPluginRegistry'; import * as EventPropagators from 'events/EventPropagators'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; import ReactVersion from 'shared/ReactVersion'; -import * as ReactDOMFrameScheduling from 'shared/ReactDOMFrameScheduling'; +import * as ReactScheduler from 'react-scheduler'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; @@ -688,7 +688,7 @@ const DOMRenderer = ReactFiberReconciler({ return textNode; }, - now: ReactDOMFrameScheduling.now, + now: ReactScheduler.now, mutation: { commitMount( @@ -984,8 +984,8 @@ const DOMRenderer = ReactFiberReconciler({ }, }, - scheduleDeferredCallback: ReactDOMFrameScheduling.rIC, - cancelDeferredCallback: ReactDOMFrameScheduling.cIC, + scheduleDeferredCallback: ReactScheduler.rIC, + cancelDeferredCallback: ReactScheduler.cIC, }); ReactGenericBatching.injection.injectRenderer(DOMRenderer); diff --git a/packages/react-scheduler/README.md b/packages/react-scheduler/README.md new file mode 100644 index 0000000000..481b6f8f1a --- /dev/null +++ b/packages/react-scheduler/README.md @@ -0,0 +1,4 @@ +# React Scheduler + +This is a work in progress - we are building a utility to better coordinate +React and other JavaScript work. diff --git a/packages/react-scheduler/index.js b/packages/react-scheduler/index.js new file mode 100644 index 0000000000..9eceea5966 --- /dev/null +++ b/packages/react-scheduler/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +export * from './src/ReactScheduler'; diff --git a/packages/react-scheduler/npm/index.js b/packages/react-scheduler/npm/index.js new file mode 100644 index 0000000000..777fe2f80d --- /dev/null +++ b/packages/react-scheduler/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-scheduler.production.min.js'); +} else { + module.exports = require('./cjs/react-scheduler.development.js'); +} diff --git a/packages/react-scheduler/package.json b/packages/react-scheduler/package.json new file mode 100644 index 0000000000..b16a07e99b --- /dev/null +++ b/packages/react-scheduler/package.json @@ -0,0 +1,23 @@ +{ + "name": "react-scheduler", + "version": "0.1.0-alpha-1", + "private": true, + "description": "unstable scheduling helper for coordinating React and other JS libraries", + "main": "index.js", + "repository": "facebook/react", + "license": "MIT", + "keywords": [ + "react" + ], + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://reactjs.org/", + "files": [ + "LICENSE", + "README.md", + "index.js", + "cjs/", + "umd/" + ] +} diff --git a/packages/shared/ReactDOMFrameScheduling.js b/packages/react-scheduler/src/ReactScheduler.js similarity index 94% rename from packages/shared/ReactDOMFrameScheduling.js rename to packages/react-scheduler/src/ReactScheduler.js index 514ca07c22..a7983dfeaf 100644 --- a/packages/shared/ReactDOMFrameScheduling.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -7,6 +7,21 @@ * @flow */ +'use strict'; + +/** + * A scheduling library to allow scheduling work with more granular priority and + * control than requestAnimationFrame and requestIdleCallback. + * Current TODO items: + * X- Pull out the rIC polyfill built into React + * - Initial test coverage + * - Support for multiple callbacks + * - Support for two priorities; serial and deferred + * - Better test coverage + * - Better docblock + * - Polish documentation, API + */ + // This is a built-in polyfill for requestIdleCallback. It works by scheduling // a requestAnimationFrame, storing the time for the start of the frame, then // scheduling a postMessage which gets scheduled after paint. Within the diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index e22aca11b5..f6681c6467 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -351,6 +351,16 @@ const bundles = [ global: 'createSubscription', externals: ['react'], }, + + /******* React Scheduler (experimental) *******/ + { + label: 'react-scheduler', + bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], + moduleType: ISOMORPHIC, + entry: 'react-scheduler', + global: 'ReactScheduler', + externals: [], + }, ]; // Based on deep-freeze by substack (public domain) From c040bcbea8e4393fbab549cc195162ac183625ed Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 21 Apr 2018 21:21:05 +0100 Subject: [PATCH 004/277] Add server integration tests for new context (#12654) * Add server integration tests for new context * Pretty please * Remove unused --- ...DOMServerIntegrationLegacyContext-test.js} | 2 +- ...eactDOMServerIntegrationNewContext-test.js | 195 ++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) rename packages/react-dom/src/__tests__/{ReactDOMServerIntegrationContext-test.js => ReactDOMServerIntegrationLegacyContext-test.js} (99%) create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js similarity index 99% rename from packages/react-dom/src/__tests__/ReactDOMServerIntegrationContext-test.js rename to packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js index 4ccf8a303c..32b38b3e0f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationContext-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js @@ -42,7 +42,7 @@ describe('ReactDOMServerIntegration', () => { resetModules(); }); - describe('context', function() { + describe('legacy context', function() { let PurpleContext, RedContext; beforeEach(() => { class Parent extends React.Component { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js new file mode 100644 index 0000000000..c3c08eb6bf --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * 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 ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); + +let React; +let ReactDOM; +let ReactDOMServer; + +function initModules() { + // Reset warning cache. + jest.resetModuleRegistry(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMServer = require('react-dom/server'); + + // Make them available to the helpers. + return { + ReactDOM, + ReactDOMServer, + }; +} + +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); + +describe('ReactDOMServerIntegration', () => { + beforeEach(() => { + resetModules(); + }); + + describe('context', function() { + let PurpleContext, RedContext, Consumer; + beforeEach(() => { + let Context = React.createContext('none'); + + class Parent extends React.Component { + render() { + return ( + + {this.props.children} + + ); + } + } + Consumer = Context.Consumer; + PurpleContext = props => {props.children}; + RedContext = props => {props.children}; + }); + + itRenders('class child with context', async render => { + class ClassChildWithContext extends React.Component { + render() { + return ( +
+ {text => text} +
+ ); + } + } + + const e = await render( + + + , + ); + expect(e.textContent).toBe('purple'); + }); + + itRenders('stateless child with context', async render => { + function StatelessChildWithContext(props) { + return {text => text}; + } + + const e = await render( + + + , + ); + expect(e.textContent).toBe('purple'); + }); + + itRenders('class child with default context', async render => { + class ClassChildWithWrongContext extends React.Component { + render() { + return ( +
+ {text => text} +
+ ); + } + } + + const e = await render(); + expect(e.textContent).toBe('none'); + }); + + itRenders('stateless child with wrong context', async render => { + function StatelessChildWithWrongContext(props) { + return ( +
+ {text => text} +
+ ); + } + + const e = await render(); + expect(e.textContent).toBe('none'); + }); + + itRenders('with context passed through to a grandchild', async render => { + function Grandchild(props) { + return ( +
+ {text => text} +
+ ); + } + + const Child = props => ; + + const e = await render( + + + , + ); + expect(e.textContent).toBe('purple'); + }); + + itRenders('a child context overriding a parent context', async render => { + const Grandchild = props => { + return ( +
+ {text => text} +
+ ); + }; + + const e = await render( + + + + + , + ); + expect(e.textContent).toBe('red'); + }); + + itRenders('multiple contexts', async render => { + const Theme = React.createContext('dark'); + const Language = React.createContext('french'); + class Parent extends React.Component { + render() { + return ( + + + + ); + } + } + + function Child() { + return ( + + + + ); + } + + const Grandchild = props => { + return ( +
+ + {theme =>
{theme}
} +
+ + {language =>
{language}
} +
+
+ ); + }; + + const e = await render(); + expect(e.querySelector('#theme').textContent).toBe('light'); + expect(e.querySelector('#language').textContent).toBe('english'); + }); + }); +}); From 5dcf93d146ccde90af2c442d6cb32b29efddb46b Mon Sep 17 00:00:00 2001 From: Nicole Levy Date: Sun, 22 Apr 2018 08:39:38 -0400 Subject: [PATCH 005/277] Validate props on context providers (#12658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * checkPropTypes in updateContextProvider * invalid “prop” * `type not `types` .. :l * test * don’t need extra check with no spelling mistake (: * change error message to specifically address provider * don’t need class, add extra render to make sure good props go through * nitpicky rename * prettier * switch to `Context.Provider` * add stack to warning, add extra undefined check * separate dev check * add stack to test * more efficient * remove unused function * prettier * const to top --- .../src/ReactFiberBeginWork.js | 17 ++++++++++++++ .../__tests__/ReactContextValidator-test.js | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6de5e54877..f2d93b0962 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -16,6 +16,7 @@ import type {NewContext} from './ReactFiberNewContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import checkPropTypes from 'prop-types/checkPropTypes'; import { IndeterminateComponent, @@ -63,6 +64,8 @@ import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; +const {getCurrentFiberStackAddendum} = ReactDebugCurrentFiber; + let didWarnAboutBadClass; let didWarnAboutGetDerivedStateOnFunctionalComponent; let didWarnAboutStatelessRefs; @@ -885,6 +888,20 @@ export default function( const newValue = newProps.value; workInProgress.memoizedProps = newProps; + if (__DEV__) { + const providerPropTypes = workInProgress.type.propTypes; + + if (providerPropTypes) { + checkPropTypes( + providerPropTypes, + newProps, + 'prop', + 'Context.Provider', + getCurrentFiberStackAddendum, + ); + } + } + let changedBits: number; if (oldProps === null) { // Initial render diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index d3b9e5b240..b4f500d930 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -225,6 +225,28 @@ describe('ReactContextValidator', () => { ReactTestUtils.renderIntoDocument(); }); + it('warns of incorrect prop types on context provider', () => { + const TestContext = React.createContext(); + + TestContext.Provider.propTypes = { + value: PropTypes.string.isRequired, + }; + + ReactTestUtils.renderIntoDocument(); + + class Component extends React.Component { + render() { + return ; + } + } + + expect(() => ReactTestUtils.renderIntoDocument()).toWarnDev( + 'Warning: Failed prop type: The prop `value` is marked as required in ' + + '`Context.Provider`, but its value is `undefined`.\n' + + ' in Component (at **)', + ); + }); + // TODO (bvaughn) Remove this test and the associated behavior in the future. // It has only been added in Fiber to match the (unintentional) behavior in Stack. it('should warn (but not error) if getChildContext method is missing', () => { From b548b3cd640dbd515f5d67dafc0216bb7ee0d796 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 22 Apr 2018 23:05:28 -0700 Subject: [PATCH 006/277] Decouple update queue from Fiber type (#12600) * Decouple update queue from Fiber type The update queue is in need of a refactor. Recent bugfixes (#12528) have exposed some flaws in how it's modeled. Upcoming features like Suspense and [redacted] also rely on the update queue in ways that weren't anticipated in the original design. Major changes: - Instead of boolean flags for `isReplace` and `isForceUpdate`, updates have a `tag` field (like Fiber). This lowers the cost for adding new types of updates. - Render phase updates are special cased. Updates scheduled during the render phase are dropped if the work-in-progress does not commit. This is used for `getDerivedStateFrom{Props,Catch}`. - `callbackList` has been replaced with a generic effect list. Aside from callbacks, this is also used for `componentDidCatch`. * Remove first class UpdateQueue types and use closures instead I tried to avoid this at first, since we avoid it everywhere else in the Fiber codebase, but since updates are not in a hot path, the trade off with file size seems worth it. * Store captured errors on a separate part of the update queue This way they can be reused independently of updates like getDerivedStateFromProps. This will be important for resuming. * Revert back to storing hasForceUpdate on the update queue Instead of using the effect tag. Ideally, this would be part of the return type of processUpdateQueue. * Rename UpdateQueue effect type back to Callback I don't love this name either, but it's less confusing than UpdateQueue I suppose. Conceptually, this is usually a callback: setState callbacks, componentDidCatch. The only case that feels a bit weird is Timeouts, which use this effect to attach a promise listener. I guess that kinda fits, too. * Call getDerivedStateFromProps every render, even if props did not change Rather than enqueue a new setState updater for every props change, we can skip the update queue entirely and merge the result into state at the end. This makes more sense, since "receiving props" is not an event that should be observed. It's still a bit weird, since eventually we do persist the derived state (in other words, it accumulates). * Store captured effects on separate list from "own" effects (callbacks) For resuming, we need the ability to discard the "own" effects while reusing the captured effects. * Optimize for class components Change `process` and `callback` to match the expected payload types for class components. I had intended for the update queue to be reusable for both class components and a future React API, but we'll likely have to fork anyway. * Only double-invoke render phase lifecycles functions in DEV * Use global state to track currently processing queue in DEV --- .../createSubscription-test.internal.js | 14 +- packages/react-noop-renderer/src/ReactNoop.js | 16 +- packages/react-reconciler/src/ReactFiber.js | 2 +- .../src/ReactFiberBeginWork.js | 78 +-- .../src/ReactFiberClassComponent.js | 505 +++++--------- .../src/ReactFiberCommitWork.js | 91 +-- .../src/ReactFiberCompleteWork.js | 27 +- .../src/ReactFiberReconciler.js | 22 +- .../src/ReactFiberScheduler.js | 89 ++- .../src/ReactFiberUnwindWork.js | 105 ++- .../src/ReactFiberUpdateQueue.js | 394 ----------- .../react-reconciler/src/ReactUpdateQueue.js | 640 ++++++++++++++++++ .../ReactIncremental-test.internal.js | 11 +- .../ReactIncrementalTriangle-test.internal.js | 2 + ...ReactIncrementalPerf-test.internal.js.snap | 8 +- .../ReactStrictMode-test.internal.js | 82 ++- packages/shared/ReactTypeOfSideEffect.js | 29 +- 17 files changed, 1111 insertions(+), 1004 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberUpdateQueue.js create mode 100644 packages/react-reconciler/src/ReactUpdateQueue.js diff --git a/packages/create-subscription/src/__tests__/createSubscription-test.internal.js b/packages/create-subscription/src/__tests__/createSubscription-test.internal.js index 2cc81b696d..d96f57ba51 100644 --- a/packages/create-subscription/src/__tests__/createSubscription-test.internal.js +++ b/packages/create-subscription/src/__tests__/createSubscription-test.internal.js @@ -264,7 +264,6 @@ describe('createSubscription', () => { it('should ignore values emitted by a new subscribable until the commit phase', () => { const log = []; - let parentInstance; function Child({value}) { ReactNoop.yield('Child: ' + value); @@ -301,8 +300,6 @@ describe('createSubscription', () => { } render() { - parentInstance = this; - return ( {(value = 'default') => { @@ -331,8 +328,8 @@ describe('createSubscription', () => { observableB.next('b-2'); observableB.next('b-3'); - // Mimic a higher-priority interruption - parentInstance.setState({observed: observableA}); + // Update again + ReactNoop.render(); // Flush everything and ensure that the correct subscribable is used // We expect the last emitted update to be rendered (because of the commit phase value check) @@ -354,7 +351,6 @@ describe('createSubscription', () => { it('should not drop values emitted between updates', () => { const log = []; - let parentInstance; function Child({value}) { ReactNoop.yield('Child: ' + value); @@ -391,8 +387,6 @@ describe('createSubscription', () => { } render() { - parentInstance = this; - return ( {(value = 'default') => { @@ -420,8 +414,8 @@ describe('createSubscription', () => { observableA.next('a-1'); observableA.next('a-2'); - // Mimic a higher-priority interruption - parentInstance.setState({observed: observableA}); + // Update again + ReactNoop.render(); // Flush everything and ensure that the correct subscribable is used // We expect the new subscribable to finish rendering, diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 7b7bb61d0d..5cd6df0fbd 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -15,7 +15,7 @@ */ import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {UpdateQueue} from 'react-reconciler/src/ReactFiberUpdateQueue'; +import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; import type {ReactNodeList} from 'shared/ReactTypes'; import ReactFiberReconciler from 'react-reconciler'; import {enablePersistentReconciler} from 'shared/ReactFeatureFlags'; @@ -526,23 +526,15 @@ const ReactNoop = { function logUpdateQueue(updateQueue: UpdateQueue, depth) { log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); - const firstUpdate = updateQueue.first; + const firstUpdate = updateQueue.firstUpdate; if (!firstUpdate) { return; } - log( - ' '.repeat(depth + 1) + '~', - firstUpdate && firstUpdate.partialState, - firstUpdate.callback ? 'with callback' : '', - '[' + firstUpdate.expirationTime + ']', - ); - let next; - while ((next = firstUpdate.next)) { + log(' '.repeat(depth + 1) + '~', '[' + firstUpdate.expirationTime + ']'); + while (firstUpdate.next) { log( ' '.repeat(depth + 1) + '~', - next.partialState, - next.callback ? 'with callback' : '', '[' + firstUpdate.expirationTime + ']', ); } diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 1ffa168b11..393cecb527 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -12,7 +12,7 @@ import type {TypeOfWork} from 'shared/ReactTypeOfWork'; import type {TypeOfMode} from './ReactTypeOfMode'; import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {UpdateQueue} from './ReactFiberUpdateQueue'; +import type {UpdateQueue} from './ReactUpdateQueue'; import invariant from 'fbjs/lib/invariant'; import {NoEffect} from 'shared/ReactTypeOfSideEffect'; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index f2d93b0962..f2a8ad1f2f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -36,10 +36,12 @@ import { ContextConsumer, } from 'shared/ReactTypeOfWork'; import { + NoEffect, PerformedWork, Placement, ContentReset, Ref, + DidCapture, } from 'shared/ReactTypeOfSideEffect'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import { @@ -53,13 +55,15 @@ import warning from 'fbjs/lib/warning'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {cancelWorkTimer} from './ReactDebugFiberPerf'; -import ReactFiberClassComponent from './ReactFiberClassComponent'; +import ReactFiberClassComponent, { + applyDerivedStateFromProps, +} from './ReactFiberClassComponent'; import { mountChildFibers, reconcileChildFibers, cloneChildFibers, } from './ReactChildFiber'; -import {processUpdateQueue} from './ReactFiberUpdateQueue'; +import {processUpdateQueue} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -108,7 +112,6 @@ export default function( const { adoptClassInstance, - callGetDerivedStateFromProps, constructClassInstance, mountClassInstance, resumeMountClassInstance, @@ -263,7 +266,11 @@ export default function( if (current === null) { if (workInProgress.stateNode === null) { // In the initial pass we might need to construct the instance. - constructClassInstance(workInProgress, workInProgress.pendingProps); + constructClassInstance( + workInProgress, + workInProgress.pendingProps, + renderExpirationTime, + ); mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; @@ -281,22 +288,11 @@ export default function( renderExpirationTime, ); } - - // We processed the update queue inside updateClassInstance. It may have - // included some errors that were dispatched during the commit phase. - // TODO: Refactor class components so this is less awkward. - let didCaptureError = false; - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - shouldUpdate = true; - didCaptureError = true; - } return finishClassComponent( current, workInProgress, shouldUpdate, hasContext, - didCaptureError, renderExpirationTime, ); } @@ -306,12 +302,14 @@ export default function( workInProgress: Fiber, shouldUpdate: boolean, hasContext: boolean, - didCaptureError: boolean, renderExpirationTime: ExpirationTime, ) { // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); + const didCaptureError = + (workInProgress.effectTag & DidCapture) !== NoEffect; + if (!shouldUpdate && !didCaptureError) { // Context providers should defer to sCU for rendering if (hasContext) { @@ -351,13 +349,6 @@ export default function( } ReactDebugCurrentFiber.setCurrentPhase(null); } else { - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - instance.render(); - } nextChildren = instance.render(); } } @@ -416,29 +407,24 @@ export default function( pushHostRootContext(workInProgress); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { + const nextProps = workInProgress.pendingProps; const prevState = workInProgress.memoizedState; - const state = processUpdateQueue( - current, + const prevChildren = prevState !== null ? prevState.children : null; + processUpdateQueue( workInProgress, updateQueue, - null, + nextProps, null, renderExpirationTime, ); - memoizeState(workInProgress, state); - updateQueue = workInProgress.updateQueue; + const nextState = workInProgress.memoizedState; + const nextChildren = nextState.children; - let element; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - // There's an uncaught error. Unmount the whole root. - element = null; - } else if (prevState === state) { + if (nextChildren === prevChildren) { // If the state is the same as before, that's a bailout because we had // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current, workInProgress); - } else { - element = state.element; } const root: FiberRoot = workInProgress.stateNode; if ( @@ -463,16 +449,15 @@ export default function( workInProgress.child = mountChildFibers( workInProgress, null, - element, + nextChildren, renderExpirationTime, ); } else { // Otherwise reset hydration state in case we aborted and resumed another // root. resetHydrationState(); - reconcileChildren(current, workInProgress, element); + reconcileChildren(current, workInProgress, nextChildren); } - memoizeState(workInProgress, state); return workInProgress.child; } resetHydrationState(); @@ -610,21 +595,13 @@ export default function( workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null; - if (typeof Component.getDerivedStateFromProps === 'function') { - const partialState = callGetDerivedStateFromProps( + const getDerivedStateFromProps = Component.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( workInProgress, - value, + getDerivedStateFromProps, props, - workInProgress.memoizedState, ); - - if (partialState !== null && partialState !== undefined) { - workInProgress.memoizedState = Object.assign( - {}, - workInProgress.memoizedState, - partialState, - ); - } } // Push context providers early to prevent context stack mismatches. @@ -638,7 +615,6 @@ export default function( workInProgress, true, hasContext, - false, renderExpirationTime, ); } else { diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 3f811c1108..444d1070e6 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -10,11 +10,9 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {LegacyContext} from './ReactFiberContext'; -import type {CapturedValue} from './ReactCapturedValue'; import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect'; import { - enableGetDerivedStateFromCatch, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, @@ -31,26 +29,31 @@ import warning from 'fbjs/lib/warning'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; import { - insertUpdateIntoFiber, + enqueueUpdate, processUpdateQueue, -} from './ReactFiberUpdateQueue'; + createUpdate, + ReplaceState, + ForceUpdate, +} from './ReactUpdateQueue'; +import {NoWork} from './ReactFiberExpirationTime'; const fakeInternalInstance = {}; const isArray = Array.isArray; let didWarnAboutStateAssignmentForComponent; -let didWarnAboutUndefinedDerivedState; let didWarnAboutUninitializedState; let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; let didWarnAboutLegacyLifecyclesAndDerivedState; +let didWarnAboutUndefinedDerivedState; +let warnOnUndefinedDerivedState; let warnOnInvalidCallback; if (__DEV__) { didWarnAboutStateAssignmentForComponent = new Set(); - didWarnAboutUndefinedDerivedState = new Set(); didWarnAboutUninitializedState = new Set(); didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); + didWarnAboutUndefinedDerivedState = new Set(); const didWarnOnInvalidCallback = new Set(); @@ -71,6 +74,21 @@ if (__DEV__) { } }; + warnOnUndefinedDerivedState = function(workInProgress, partialState) { + if (partialState === undefined) { + const componentName = getComponentName(workInProgress) || 'Component'; + if (!didWarnAboutUndefinedDerivedState.has(componentName)) { + didWarnAboutUndefinedDerivedState.add(componentName); + warning( + false, + '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + + 'You have returned undefined.', + componentName, + ); + } + } + }; + // 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, @@ -92,17 +110,43 @@ if (__DEV__) { }); Object.freeze(fakeInternalInstance); } -function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array) { - const resultState = {}; - for (let i = 0; i < capturedValues.length; i++) { - const capturedValue: CapturedValue = (capturedValues[i]: any); - const error = capturedValue.value; - const partialState = ctor.getDerivedStateFromCatch.call(null, error); - if (partialState !== null && partialState !== undefined) { - Object.assign(resultState, partialState); + +export function applyDerivedStateFromProps( + workInProgress: Fiber, + getDerivedStateFromProps: (props: any, state: any) => any, + nextProps: any, +) { + const prevState = workInProgress.memoizedState; + + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromProps(nextProps, prevState); } } - return resultState; + + const partialState = getDerivedStateFromProps(nextProps, prevState); + + if (__DEV__) { + warnOnUndefinedDerivedState(workInProgress, partialState); + } + // Merge the partial state and the previous state. + const memoizedState = + partialState === null || partialState === undefined + ? prevState + : Object.assign({}, prevState, partialState); + workInProgress.memoizedState = memoizedState; + + // Once the update queue is empty, persist the derived state onto the + // base state. + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null && updateQueue.expirationTime === NoWork) { + updateQueue.baseState = memoizedState; + } } export default function( @@ -120,64 +164,57 @@ export default function( hasContextChanged, } = legacyContext; - // Class component state updater - const updater = { + const classComponentUpdater = { isMounted, - enqueueSetState(instance, partialState, callback) { - const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); - } + enqueueSetState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createUpdate(expirationTime); + update.payload = payload; + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, - enqueueReplaceState(instance, state, callback) { - const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'replaceState'); - } + enqueueReplaceState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createUpdate(expirationTime); + update.tag = ReplaceState; + update.payload = payload; + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'replaceState'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, - enqueueForceUpdate(instance, callback) { - const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'forceUpdate'); - } + enqueueForceUpdate(inst, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createUpdate(expirationTime); + update.tag = ForceUpdate; + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'forceUpdate'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, }; @@ -191,11 +228,10 @@ export default function( newContext, ) { if ( - oldProps === null || - (workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate) + workInProgress.updateQueue !== null && + workInProgress.updateQueue.hasForceUpdate ) { - // If the workInProgress already has an Update effect, return true + // If forceUpdate was called, disregard sCU. return true; } @@ -420,13 +456,8 @@ export default function( } } - function resetInputPointers(workInProgress: Fiber, instance: any) { - instance.props = workInProgress.memoizedProps; - instance.state = workInProgress.memoizedState; - } - function adoptClassInstance(workInProgress: Fiber, instance: any): void { - instance.updater = updater; + instance.updater = classComponentUpdater; workInProgress.stateNode = instance; // The instance needs access to the fiber so that it can schedule updates ReactInstanceMap.set(instance, workInProgress); @@ -435,7 +466,11 @@ export default function( } } - function constructClassInstance(workInProgress: Fiber, props: any): any { + function constructClassInstance( + workInProgress: Fiber, + props: any, + renderExpirationTime: ExpirationTime, + ): any { const ctor = workInProgress.type; const unmaskedContext = getUnmaskedContext(workInProgress); const needsContext = isContextConsumer(workInProgress); @@ -444,19 +479,21 @@ export default function( : emptyObject; // Instantiate twice to help detect side-effects. - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - new ctor(props, context); // eslint-disable-line no-new + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + new ctor(props, context); // eslint-disable-line no-new + } } const instance = new ctor(props, context); - const state = + const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state - : null; + : null); adoptClassInstance(workInProgress, instance); if (__DEV__) { @@ -545,26 +582,6 @@ export default function( } } - workInProgress.memoizedState = state; - - const partialState = callGetDerivedStateFromProps( - workInProgress, - instance, - props, - state, - ); - - if (partialState !== null && partialState !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - workInProgress.memoizedState = Object.assign( - {}, - workInProgress.memoizedState, - partialState, - ); - } - // Cache unmasked context so we can avoid recreating masked context unless necessary. // ReactFiberContext usually updates this cache but can't for newly-created instances. if (needsContext) { @@ -597,7 +614,7 @@ export default function( getComponentName(workInProgress) || 'Component', ); } - updater.enqueueReplaceState(instance, instance.state, null); + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } } @@ -631,50 +648,7 @@ export default function( ); } } - updater.enqueueReplaceState(instance, instance.state, null); - } - } - - function callGetDerivedStateFromProps( - workInProgress: Fiber, - instance: any, - nextProps: any, - prevState: any, - ) { - const {type} = workInProgress; - - if (typeof type.getDerivedStateFromProps === 'function') { - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - // Invoke method an extra time to help detect side-effects. - type.getDerivedStateFromProps.call(null, nextProps, prevState); - } - - const partialState = type.getDerivedStateFromProps.call( - null, - nextProps, - prevState, - ); - - if (__DEV__) { - if (partialState === undefined) { - const componentName = getComponentName(workInProgress) || 'Component'; - if (!didWarnAboutUndefinedDerivedState.has(componentName)) { - didWarnAboutUndefinedDerivedState.add(componentName); - warning( - false, - '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + - 'You have returned undefined.', - componentName, - ); - } - } - } - - return partialState; + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } } @@ -684,7 +658,6 @@ export default function( renderExpirationTime: ExpirationTime, ): void { const ctor = workInProgress.type; - const current = workInProgress.alternate; if (__DEV__) { checkClassInstance(workInProgress); @@ -715,6 +688,29 @@ export default function( } } + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( + workInProgress, + updateQueue, + props, + instance, + renderExpirationTime, + ); + instance.state = workInProgress.memoizedState; + } + + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + props, + ); + instance.state = workInProgress.memoizedState; + } + // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( @@ -726,18 +722,19 @@ export default function( callComponentWillMount(workInProgress, instance); // If we had additional state updates during this life-cycle, let's // process them now. - const updateQueue = workInProgress.updateQueue; + updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - instance.state = processUpdateQueue( - current, + processUpdateQueue( workInProgress, updateQueue, - instance, props, + instance, renderExpirationTime, ); + instance.state = workInProgress.memoizedState; } } + if (typeof instance.componentDidMount === 'function') { workInProgress.effectTag |= Update; } @@ -749,16 +746,18 @@ export default function( ): boolean { const ctor = workInProgress.type; const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); const oldProps = workInProgress.memoizedProps; const newProps = workInProgress.pendingProps; + instance.props = oldProps; + const oldContext = instance.context; const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = - typeof ctor.getDerivedStateFromProps === 'function' || + typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function'; // Note: During these life-cycles, instance.props/instance.state are what @@ -782,93 +781,27 @@ export default function( } } - // Compute the next state using the memoized state and the update queue. const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - let derivedStateFromCatch; - if (workInProgress.updateQueue !== null) { - newState = processUpdateQueue( - null, + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( workInProgress, - workInProgress.updateQueue, - instance, + updateQueue, newProps, + instance, renderExpirationTime, ); - - let updateQueue = workInProgress.updateQueue; - if ( - updateQueue !== null && - updateQueue.capturedValues !== null && - (enableGetDerivedStateFromCatch && - typeof ctor.getDerivedStateFromCatch === 'function') - ) { - const capturedValues = updateQueue.capturedValues; - // Don't remove these from the update queue yet. We need them in - // finishClassComponent. Do the reset there. - // TODO: This is awkward. Refactor class components. - // updateQueue.capturedValues = null; - derivedStateFromCatch = callGetDerivedStateFromCatch( - ctor, - capturedValues, - ); - } - } else { - newState = oldState; + newState = workInProgress.memoizedState; } - let derivedStateFromProps; - if (oldProps !== newProps) { - // The prevState parameter should be the partially updated state. - // Otherwise, spreading state in return values could override updates. - derivedStateFromProps = callGetDerivedStateFromProps( + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( workInProgress, - instance, + getDerivedStateFromProps, newProps, - newState, ); - } - - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromProps - : Object.assign({}, newState, derivedStateFromProps); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromProps, - ); - } - } - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromCatch - : Object.assign({}, newState, derivedStateFromCatch); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromCatch, - ); - } + newState = workInProgress.memoizedState; } if ( @@ -925,9 +858,9 @@ export default function( } // If shouldComponentUpdate returned false, we should still update the - // memoized props/state to indicate that this work can be reused. - memoizeProps(workInProgress, newProps); - memoizeState(workInProgress, newState); + // memoized state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; } // Update the existing instance's state, props, and context pointers even @@ -947,16 +880,18 @@ export default function( ): boolean { const ctor = workInProgress.type; const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); const oldProps = workInProgress.memoizedProps; const newProps = workInProgress.pendingProps; + instance.props = oldProps; + const oldContext = instance.context; const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = - typeof ctor.getDerivedStateFromProps === 'function' || + typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function'; // Note: During these life-cycles, instance.props/instance.state are what @@ -980,94 +915,27 @@ export default function( } } - // Compute the next state using the memoized state and the update queue. const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - let derivedStateFromCatch; - - if (workInProgress.updateQueue !== null) { - newState = processUpdateQueue( - current, + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( workInProgress, - workInProgress.updateQueue, - instance, + updateQueue, newProps, + instance, renderExpirationTime, ); - - let updateQueue = workInProgress.updateQueue; - if ( - updateQueue !== null && - updateQueue.capturedValues !== null && - (enableGetDerivedStateFromCatch && - typeof ctor.getDerivedStateFromCatch === 'function') - ) { - const capturedValues = updateQueue.capturedValues; - // Don't remove these from the update queue yet. We need them in - // finishClassComponent. Do the reset there. - // TODO: This is awkward. Refactor class components. - // updateQueue.capturedValues = null; - derivedStateFromCatch = callGetDerivedStateFromCatch( - ctor, - capturedValues, - ); - } - } else { - newState = oldState; + newState = workInProgress.memoizedState; } - let derivedStateFromProps; - if (oldProps !== newProps) { - // The prevState parameter should be the partially updated state. - // Otherwise, spreading state in return values could override updates. - derivedStateFromProps = callGetDerivedStateFromProps( + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( workInProgress, - instance, + getDerivedStateFromProps, newProps, - newState, ); - } - - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromProps - : Object.assign({}, newState, derivedStateFromProps); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromProps, - ); - } - } - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromCatch - : Object.assign({}, newState, derivedStateFromCatch); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromCatch, - ); - } + newState = workInProgress.memoizedState; } if ( @@ -1154,8 +1022,8 @@ export default function( // If shouldComponentUpdate returned false, we should still update the // memoized props/state to indicate that this work can be reused. - memoizeProps(workInProgress, newProps); - memoizeState(workInProgress, newState); + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; } // Update the existing instance's state, props, and context pointers even @@ -1169,7 +1037,6 @@ export default function( return { adoptClassInstance, - callGetDerivedStateFromProps, constructClassInstance, mountClassInstance, resumeMountClassInstance, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index afa5b46d0b..a375490297 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -33,15 +33,15 @@ import { ContentReset, Snapshot, } from 'shared/ReactTypeOfSideEffect'; +import {commitUpdateQueue} from './ReactUpdateQueue'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; -import {commitCallbacks} from './ReactFiberUpdateQueue'; import {onCommitUnmount} from './ReactFiberDevToolsHook'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; -import {logCapturedError} from './ReactFiberErrorLogger'; import getComponentName from 'shared/getComponentName'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; +import {logCapturedError} from './ReactFiberErrorLogger'; const { invokeGuardedCallback, @@ -54,7 +54,7 @@ if (__DEV__) { didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); } -function logError(boundary: Fiber, errorInfo: CapturedValue) { +export function logError(boundary: Fiber, errorInfo: CapturedValue) { const source = errorInfo.source; let stack = errorInfo.stack; if (stack === null) { @@ -251,7 +251,14 @@ export default function( } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitCallbacks(updateQueue, instance); + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } return; } @@ -269,7 +276,12 @@ export default function( break; } } - commitCallbacks(updateQueue, instance); + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } return; } @@ -306,73 +318,6 @@ export default function( } } - function commitErrorLogging( - finishedWork: Fiber, - onUncaughtError: (error: Error) => void, - ) { - switch (finishedWork.tag) { - case ClassComponent: - { - const ctor = finishedWork.type; - const instance = finishedWork.stateNode; - const updateQueue = finishedWork.updateQueue; - invariant( - updateQueue !== null && updateQueue.capturedValues !== null, - 'An error logging effect should not have been scheduled if no errors ' + - 'were captured. This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); - const capturedErrors = updateQueue.capturedValues; - updateQueue.capturedValues = null; - - if (typeof ctor.getDerivedStateFromCatch !== 'function') { - // To preserve the preexisting retry behavior of error boundaries, - // we keep track of which ones already failed during this batch. - // This gets reset before we yield back to the browser. - // TODO: Warn in strict mode if getDerivedStateFromCatch is - // not defined. - markLegacyErrorBoundaryAsFailed(instance); - } - - instance.props = finishedWork.memoizedProps; - instance.state = finishedWork.memoizedState; - for (let i = 0; i < capturedErrors.length; i++) { - const errorInfo = capturedErrors[i]; - const error = errorInfo.value; - const stack = errorInfo.stack; - logError(finishedWork, errorInfo); - instance.componentDidCatch(error, { - componentStack: stack !== null ? stack : '', - }); - } - } - break; - case HostRoot: { - const updateQueue = finishedWork.updateQueue; - invariant( - updateQueue !== null && updateQueue.capturedValues !== null, - 'An error logging effect should not have been scheduled if no errors ' + - 'were captured. This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); - const capturedErrors = updateQueue.capturedValues; - updateQueue.capturedValues = null; - for (let i = 0; i < capturedErrors.length; i++) { - const errorInfo = capturedErrors[i]; - logError(finishedWork, errorInfo); - onUncaughtError(errorInfo.value); - } - break; - } - default: - invariant( - false, - 'This unit of work tag cannot capture errors. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - } - } - function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { @@ -564,7 +509,6 @@ export default function( }, commitLifeCycles, commitBeforeMutationLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, }; @@ -892,7 +836,6 @@ export default function( commitDeletion, commitWork, commitLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, }; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 02779db8b6..aec3bc17c6 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -38,13 +38,7 @@ import { Fragment, Mode, } from 'shared/ReactTypeOfWork'; -import { - Placement, - Ref, - Update, - ErrLog, - DidCapture, -} from 'shared/ReactTypeOfSideEffect'; +import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; import {reconcileChildFibers} from './ReactChildFiber'; @@ -416,20 +410,6 @@ export default function( case ClassComponent: { // We are leaving this subtree, so pop context if any. popLegacyContextProvider(workInProgress); - - // If this component caught an error, schedule an error log effect. - const instance = workInProgress.stateNode; - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - workInProgress.effectTag &= ~DidCapture; - if (typeof instance.componentDidCatch === 'function') { - workInProgress.effectTag |= ErrLog; - } else { - // Normally we clear this in the commit phase, but since we did not - // schedule an effect, we need to reset it here. - updateQueue.capturedValues = null; - } - } return null; } case HostRoot: { @@ -449,11 +429,6 @@ export default function( workInProgress.effectTag &= ~Placement; } updateHostContainer(workInProgress); - - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - workInProgress.effectTag |= ErrLog; - } return null; } case HostComponent: { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 8118675232..519003f177 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,7 +26,7 @@ import warning from 'fbjs/lib/warning'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import ReactFiberScheduler from './ReactFiberScheduler'; -import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue'; +import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -339,28 +339,22 @@ export default function( } } + const update = createUpdate(expirationTime); + update.payload = {children: element}; + callback = callback === undefined ? null : callback; - if (__DEV__) { + if (callback !== null) { warning( - callback === null || typeof callback === 'function', + typeof callback === 'function', 'render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback, ); + update.callback = callback; } + enqueueUpdate(current, update, expirationTime); - const update = { - expirationTime, - partialState: {element}, - callback, - isReplace: false, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(current, update); scheduleWork(current, expirationTime); - return expirationTime; } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 46cef6cec6..1a9846b0bd 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -31,7 +31,6 @@ import { Ref, Incomplete, HostEffectMask, - ErrLog, } from 'shared/ReactTypeOfSideEffect'; import { HostRoot, @@ -89,10 +88,7 @@ import { import {AsyncMode} from './ReactTypeOfMode'; import ReactFiberLegacyContext from './ReactFiberContext'; import ReactFiberNewContext from './ReactFiberNewContext'; -import { - getUpdateExpirationTime, - insertUpdateIntoFiber, -} from './ReactFiberUpdateQueue'; +import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; @@ -195,12 +191,16 @@ export default function( throwException, unwindWork, unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, } = ReactFiberUnwindWork( hostContext, legacyContext, newContext, scheduleWork, + markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, + onUncaughtError, ); const { commitBeforeMutationLifeCycles, @@ -209,7 +209,6 @@ export default function( commitDeletion, commitWork, commitLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, } = ReactFiberCommitWork( @@ -447,10 +446,6 @@ export default function( ); } - if (effectTag & ErrLog) { - commitErrorLogging(nextEffect, onUncaughtError); - } - if (effectTag & Ref) { recordEffect(); commitAttachRef(nextEffect); @@ -681,7 +676,16 @@ export default function( } // Check for pending updates. - let newExpirationTime = getUpdateExpirationTime(workInProgress); + let newExpirationTime = NoWork; + switch (workInProgress.tag) { + case HostRoot: + case ClassComponent: { + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + newExpirationTime = updateQueue.expirationTime; + } + } + } // TODO: Calls need to visit stateNode @@ -956,6 +960,12 @@ export default function( break; } + if (__DEV__) { + // Reset global debug state + // We assume this is defined in DEV + (resetCurrentlyProcessingQueue: any)(); + } + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { const failedUnitOfWork = nextUnitOfWork; replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync); @@ -974,7 +984,12 @@ export default function( onUncaughtError(thrownValue); break; } - throwException(returnFiber, sourceFiber, thrownValue); + throwException( + returnFiber, + sourceFiber, + thrownValue, + nextRenderExpirationTime, + ); nextUnitOfWork = completeUnitOfWork(sourceFiber); } break; @@ -1022,22 +1037,6 @@ export default function( } } - function scheduleCapture(sourceFiber, boundaryFiber, value, expirationTime) { - // TODO: We only support dispatching errors. - const capturedValue = createCapturedValue(value, sourceFiber); - const update = { - expirationTime, - partialState: null, - callback: null, - isReplace: false, - isForced: false, - capturedValue, - next: null, - }; - insertUpdateIntoFiber(boundaryFiber, update); - scheduleWork(boundaryFiber, expirationTime); - } - function dispatch( sourceFiber: Fiber, value: mixed, @@ -1048,8 +1047,6 @@ export default function( 'dispatch: Cannot dispatch during the render phase.', ); - // TODO: Handle arrays - let fiber = sourceFiber.return; while (fiber !== null) { switch (fiber.tag) { @@ -1061,14 +1058,28 @@ export default function( (typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance)) ) { - scheduleCapture(sourceFiber, fiber, value, expirationTime); + const errorInfo = createCapturedValue(value, sourceFiber); + const update = createClassErrorUpdate( + fiber, + errorInfo, + expirationTime, + ); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); return; } break; - // TODO: Handle async boundaries - case HostRoot: - scheduleCapture(sourceFiber, fiber, value, expirationTime); + case HostRoot: { + const errorInfo = createCapturedValue(value, sourceFiber); + const update = createRootErrorUpdate( + fiber, + errorInfo, + expirationTime, + ); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); return; + } } fiber = fiber.return; } @@ -1076,7 +1087,15 @@ export default function( if (sourceFiber.tag === HostRoot) { // Error was thrown at the root. There is no parent, so the root // itself should capture it. - scheduleCapture(sourceFiber, sourceFiber, value, expirationTime); + const rootFiber = sourceFiber; + const errorInfo = createCapturedValue(value, rootFiber); + const update = createRootErrorUpdate( + rootFiber, + errorInfo, + expirationTime, + ); + enqueueUpdate(rootFiber, update, expirationTime); + scheduleWork(rootFiber, expirationTime); } } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 6565e4888d..e59324c3c4 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -12,10 +12,16 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; import type {LegacyContext} from './ReactFiberContext'; import type {NewContext} from './ReactFiberNewContext'; -import type {UpdateQueue} from './ReactFiberUpdateQueue'; +import type {CapturedValue} from './ReactCapturedValue'; +import type {Update} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {ensureUpdateQueues} from './ReactFiberUpdateQueue'; +import { + enqueueCapturedUpdate, + createUpdate, + CaptureUpdate, +} from './ReactUpdateQueue'; +import {logError} from './ReactFiberCommitWork'; import { ClassComponent, @@ -42,7 +48,9 @@ export default function( startTime: ExpirationTime, expirationTime: ExpirationTime, ) => void, + markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean, + onUncaughtError: (error: mixed) => void, ) { const {popHostContainer, popHostContext} = hostContext; const { @@ -51,10 +59,71 @@ export default function( } = legacyContext; const {popProvider} = newContext; + function createRootErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, + ): Update { + const update = createUpdate(expirationTime); + // Unmount the root by rendering null. + update.tag = CaptureUpdate; + update.payload = {children: null}; + const error = errorInfo.value; + update.callback = () => { + onUncaughtError(error); + logError(fiber, errorInfo); + }; + return update; + } + + function createClassErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, + ): Update { + const update = createUpdate(expirationTime); + update.tag = CaptureUpdate; + const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch; + if ( + enableGetDerivedStateFromCatch && + typeof getDerivedStateFromCatch === 'function' + ) { + const error = errorInfo.value; + update.payload = () => { + return getDerivedStateFromCatch(error); + }; + } + + const inst = fiber.stateNode; + if (inst !== null && typeof inst.componentDidCatch === 'function') { + update.callback = function callback() { + if ( + !enableGetDerivedStateFromCatch || + getDerivedStateFromCatch !== 'function' + ) { + // To preserve the preexisting retry behavior of error boundaries, + // we keep track of which ones already failed during this batch. + // This gets reset before we yield back to the browser. + // TODO: Warn in strict mode if getDerivedStateFromCatch is + // not defined. + markLegacyErrorBoundaryAsFailed(this); + } + const error = errorInfo.value; + const stack = errorInfo.stack; + logError(fiber, errorInfo); + this.componentDidCatch(error, { + componentStack: stack !== null ? stack : '', + }); + }; + } + return update; + } + function throwException( returnFiber: Fiber, sourceFiber: Fiber, rawValue: mixed, + renderExpirationTime: ExpirationTime, ) { // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; @@ -67,18 +136,19 @@ export default function( do { switch (workInProgress.tag) { case HostRoot: { - // Uncaught error const errorInfo = value; - ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue< - any, - > = (workInProgress.updateQueue: any); - updateQueue.capturedValues = [errorInfo]; workInProgress.effectTag |= ShouldCapture; + const update = createRootErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } case ClassComponent: // Capture and retry + const errorInfo = value; const ctor = workInProgress.type; const instance = workInProgress.stateNode; if ( @@ -89,17 +159,14 @@ export default function( typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance))) ) { - ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue< - any, - > = (workInProgress.updateQueue: any); - const capturedValues = updateQueue.capturedValues; - if (capturedValues === null) { - updateQueue.capturedValues = [value]; - } else { - capturedValues.push(value); - } workInProgress.effectTag |= ShouldCapture; + // Schedule the error boundary to re-render using updated state + const update = createClassErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } break; @@ -176,5 +243,7 @@ export default function( throwException, unwindWork, unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, }; } diff --git a/packages/react-reconciler/src/ReactFiberUpdateQueue.js b/packages/react-reconciler/src/ReactFiberUpdateQueue.js deleted file mode 100644 index df66807dce..0000000000 --- a/packages/react-reconciler/src/ReactFiberUpdateQueue.js +++ /dev/null @@ -1,394 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {Fiber} from './ReactFiber'; -import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {CapturedValue} from './ReactCapturedValue'; - -import { - debugRenderPhaseSideEffects, - debugRenderPhaseSideEffectsForStrictMode, -} from 'shared/ReactFeatureFlags'; -import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect'; -import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; -import invariant from 'fbjs/lib/invariant'; -import warning from 'fbjs/lib/warning'; -import {StrictMode} from './ReactTypeOfMode'; - -import {NoWork} from './ReactFiberExpirationTime'; - -let didWarnUpdateInsideUpdate; - -if (__DEV__) { - didWarnUpdateInsideUpdate = false; -} - -type PartialState = - | $Subtype - | ((prevState: State, props: Props) => $Subtype); - -// Callbacks are not validated until invocation -type Callback = mixed; - -export type Update = { - expirationTime: ExpirationTime, - partialState: PartialState, - callback: Callback | null, - isReplace: boolean, - isForced: boolean, - capturedValue: CapturedValue | null, - next: Update | null, -}; - -// Singly linked-list of updates. When an update is scheduled, it is added to -// the queue of the current fiber and the work-in-progress fiber. The two queues -// are separate but they share a persistent structure. -// -// During reconciliation, updates are removed from the work-in-progress fiber, -// but they remain on the current fiber. That ensures that if a work-in-progress -// is aborted, the aborted updates are recovered by cloning from current. -// -// The work-in-progress queue is always a subset of the current queue. -// -// When the tree is committed, the work-in-progress becomes the current. -export type UpdateQueue = { - // A processed update is not removed from the queue if there are any - // unprocessed updates that came before it. In that case, we need to keep - // track of the base state, which represents the base state of the first - // unprocessed update, which is the same as the first update in the list. - baseState: State, - // For the same reason, we keep track of the remaining expiration time. - expirationTime: ExpirationTime, - first: Update | null, - last: Update | null, - callbackList: Array> | null, - hasForceUpdate: boolean, - isInitialized: boolean, - capturedValues: Array> | null, - - // Dev only - isProcessing?: boolean, -}; - -function createUpdateQueue(baseState: State): UpdateQueue { - const queue: UpdateQueue = { - baseState, - expirationTime: NoWork, - first: null, - last: null, - callbackList: null, - hasForceUpdate: false, - isInitialized: false, - capturedValues: null, - }; - if (__DEV__) { - queue.isProcessing = false; - } - return queue; -} - -export function insertUpdateIntoQueue( - queue: UpdateQueue, - update: Update, -): void { - // Append the update to the end of the list. - if (queue.last === null) { - // Queue is empty - queue.first = queue.last = update; - } else { - queue.last.next = update; - queue.last = update; - } - if ( - queue.expirationTime === NoWork || - queue.expirationTime > update.expirationTime - ) { - queue.expirationTime = update.expirationTime; - } -} - -let q1; -let q2; -export function ensureUpdateQueues(fiber: Fiber) { - q1 = q2 = null; - // We'll have at least one and at most two distinct update queues. - const alternateFiber = fiber.alternate; - let queue1 = fiber.updateQueue; - if (queue1 === null) { - // TODO: We don't know what the base state will be until we begin work. - // It depends on which fiber is the next current. Initialize with an empty - // base state, then set to the memoizedState when rendering. Not super - // happy with this approach. - queue1 = fiber.updateQueue = createUpdateQueue((null: any)); - } - - let queue2; - if (alternateFiber !== null) { - queue2 = alternateFiber.updateQueue; - if (queue2 === null) { - queue2 = alternateFiber.updateQueue = createUpdateQueue((null: any)); - } - } else { - queue2 = null; - } - queue2 = queue2 !== queue1 ? queue2 : null; - - // Use module variables instead of returning a tuple - q1 = queue1; - q2 = queue2; -} - -export function insertUpdateIntoFiber( - fiber: Fiber, - update: Update, -): void { - ensureUpdateQueues(fiber); - const queue1: Fiber = (q1: any); - const queue2: Fiber | null = (q2: any); - - // Warn if an update is scheduled from inside an updater function. - if (__DEV__) { - if ( - (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && - !didWarnUpdateInsideUpdate - ) { - warning( - false, - 'An update (setState, replaceState, or forceUpdate) was scheduled ' + - 'from inside an update function. Update functions should be pure, ' + - 'with zero side-effects. Consider using componentDidUpdate or a ' + - 'callback.', - ); - didWarnUpdateInsideUpdate = true; - } - } - - // If there's only one queue, add the update to that queue and exit. - if (queue2 === null) { - insertUpdateIntoQueue(queue1, update); - return; - } - - // If either queue is empty, we need to add to both queues. - if (queue1.last === null || queue2.last === null) { - insertUpdateIntoQueue(queue1, update); - insertUpdateIntoQueue(queue2, update); - return; - } - - // If both lists are not empty, the last update is the same for both lists - // because of structural sharing. So, we should only append to one of - // the lists. - insertUpdateIntoQueue(queue1, update); - // But we still need to update the `last` pointer of queue2. - queue2.last = update; -} - -export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { - switch (fiber.tag) { - case HostRoot: - case ClassComponent: - const updateQueue = fiber.updateQueue; - if (updateQueue === null) { - return NoWork; - } - return updateQueue.expirationTime; - default: - return NoWork; - } -} - -function getStateFromUpdate(update, instance, prevState, props) { - const partialState = update.partialState; - if (typeof partialState === 'function') { - return partialState.call(instance, prevState, props); - } else { - return partialState; - } -} - -export function processUpdateQueue( - current: Fiber | null, - workInProgress: Fiber, - queue: UpdateQueue, - instance: any, - props: any, - renderExpirationTime: ExpirationTime, -): State { - if (current !== null && current.updateQueue === queue) { - // We need to create a work-in-progress queue, by cloning the current queue. - const currentQueue = queue; - queue = workInProgress.updateQueue = { - baseState: currentQueue.baseState, - expirationTime: currentQueue.expirationTime, - first: currentQueue.first, - last: currentQueue.last, - isInitialized: currentQueue.isInitialized, - capturedValues: currentQueue.capturedValues, - // These fields are no longer valid because they were already committed. - // Reset them. - callbackList: null, - hasForceUpdate: false, - }; - } - - if (__DEV__) { - // Set this flag so we can warn if setState is called inside the update - // function of another setState. - queue.isProcessing = true; - } - - // Reset the remaining expiration time. If we skip over any updates, we'll - // increase this accordingly. - queue.expirationTime = NoWork; - - // TODO: We don't know what the base state will be until we begin work. - // It depends on which fiber is the next current. Initialize with an empty - // base state, then set to the memoizedState when rendering. Not super - // happy with this approach. - let state; - if (queue.isInitialized) { - state = queue.baseState; - } else { - state = queue.baseState = workInProgress.memoizedState; - queue.isInitialized = true; - } - let dontMutatePrevState = true; - let update = queue.first; - let didSkip = false; - while (update !== null) { - const updateExpirationTime = update.expirationTime; - if (updateExpirationTime > renderExpirationTime) { - // This update does not have sufficient priority. Skip it. - const remainingExpirationTime = queue.expirationTime; - if ( - remainingExpirationTime === NoWork || - remainingExpirationTime > updateExpirationTime - ) { - // Update the remaining expiration time. - queue.expirationTime = updateExpirationTime; - } - if (!didSkip) { - didSkip = true; - queue.baseState = state; - } - // Continue to the next update. - update = update.next; - continue; - } - - // This update does have sufficient priority. - - // If no previous updates were skipped, drop this update from the queue by - // advancing the head of the list. - if (!didSkip) { - queue.first = update.next; - if (queue.first === null) { - queue.last = null; - } - } - - // Invoke setState callback an extra time to help detect side-effects. - // Ignore the return value in this case. - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - getStateFromUpdate(update, instance, state, props); - } - - // Process the update - let partialState; - if (update.isReplace) { - state = getStateFromUpdate(update, instance, state, props); - dontMutatePrevState = true; - } else { - partialState = getStateFromUpdate(update, instance, state, props); - if (partialState) { - if (dontMutatePrevState) { - // $FlowFixMe: Idk how to type this properly. - state = Object.assign({}, state, partialState); - } else { - state = Object.assign(state, partialState); - } - dontMutatePrevState = false; - } - } - if (update.isForced) { - queue.hasForceUpdate = true; - } - if (update.callback !== null) { - // Append to list of callbacks. - let callbackList = queue.callbackList; - if (callbackList === null) { - callbackList = queue.callbackList = []; - } - callbackList.push(update); - } - if (update.capturedValue !== null) { - let capturedValues = queue.capturedValues; - if (capturedValues === null) { - queue.capturedValues = [update.capturedValue]; - } else { - capturedValues.push(update.capturedValue); - } - } - update = update.next; - } - - if (queue.callbackList !== null) { - workInProgress.effectTag |= CallbackEffect; - } else if ( - queue.first === null && - !queue.hasForceUpdate && - queue.capturedValues === null - ) { - // The queue is empty. We can reset it. - workInProgress.updateQueue = null; - } - - if (!didSkip) { - didSkip = true; - queue.baseState = state; - } - - if (__DEV__) { - // No longer processing. - queue.isProcessing = false; - } - - return state; -} - -export function commitCallbacks( - queue: UpdateQueue, - context: any, -) { - const callbackList = queue.callbackList; - if (callbackList === null) { - return; - } - // Set the list to null to make sure they don't get called more than once. - queue.callbackList = null; - for (let i = 0; i < callbackList.length; i++) { - const update = callbackList[i]; - const callback = update.callback; - // This update might be processed again. Clear the callback so it's only - // called once. - update.callback = null; - invariant( - typeof callback === 'function', - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: %s', - callback, - ); - callback.call(context); - } -} diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js new file mode 100644 index 0000000000..574a3c0740 --- /dev/null +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -0,0 +1,640 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// UpdateQueue is a linked list of prioritized updates. +// +// Like fibers, update queues come in pairs: a current queue, which represents +// the visible state of the screen, and a work-in-progress queue, which is +// can be mutated and processed asynchronously before it is committed — a form +// of double buffering. If a work-in-progress render is discarded before +// finishing, we create a new work-in-progress by cloning the current queue. +// +// Both queues share a persistent, singly-linked list structure. To schedule an +// update, we append it to the end of both queues. Each queue maintains a +// pointer to first update in the persistent list that hasn't been processed. +// The work-in-progress pointer always has a position equal to or greater than +// the current queue, since we always work on that one. The current queue's +// pointer is only updated during the commit phase, when we swap in the +// work-in-progress. +// +// For example: +// +// Current pointer: A - B - C - D - E - F +// Work-in-progress pointer: D - E - F +// ^ +// The work-in-progress queue has +// processed more updates than current. +// +// The reason we append to both queues is because otherwise we might drop +// updates without ever processing them. For example, if we only add updates to +// the work-in-progress queue, some updates could be lost whenever a work-in +// -progress render restarts by cloning from current. Similarly, if we only add +// updates to the current queue, the updates will be lost whenever an already +// in-progress queue commits and swaps with the current queue. However, by +// adding to both queues, we guarantee that the update will be part of the next +// work-in-progress. (And because the work-in-progress queue becomes the +// current queue once it commits, there's no danger of applying the same +// update twice.) +// +// Prioritization +// -------------- +// +// Updates are not sorted by priority, but by insertion; new updates are always +// appended to the end of the list. +// +// The priority is still important, though. When processing the update queue +// during the render phase, only the updates with sufficient priority are +// included in the result. If we skip an update because it has insufficient +// priority, it remains in the queue to be processed later, during a lower +// priority render. Crucially, all updates subsequent to a skipped update also +// remain in the queue *regardless of their priority*. That means high priority +// updates are sometimes processed twice, at two separate priorities. We also +// keep track of a base state, that represents the state before the first +// update in the queue is applied. +// +// For example: +// +// Given a base state of '', and the following queue of updates +// +// A1 - B2 - C1 - D2 +// +// where the number indicates the priority, and the update is applied to the +// previous state by appending a letter, React will process these updates as +// two separate renders, one per distinct priority level: +// +// First render, at priority 1: +// Base state: '' +// Updates: [A1, C1] +// Result state: 'AC' +// +// Second render, at priority 2: +// Base state: 'A' <- The base state does not include C1, +// because B2 was skipped. +// Updates: [B2, C1, D2] <- C1 was rebased on top of B2 +// Result state: 'ABCD' +// +// Because we process updates in insertion order, and rebase high priority +// updates when preceding updates are skipped, the final result is deterministic +// regardless of priority. Intermediate state may vary according to system +// resources, but the final state is always the same. + +import type {Fiber} from './ReactFiber'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +import {NoWork} from './ReactFiberExpirationTime'; +import { + Callback, + ShouldCapture, + DidCapture, +} from 'shared/ReactTypeOfSideEffect'; +import {ClassComponent} from 'shared/ReactTypeOfWork'; + +import { + debugRenderPhaseSideEffects, + debugRenderPhaseSideEffectsForStrictMode, +} from 'shared/ReactFeatureFlags'; + +import {StrictMode} from './ReactTypeOfMode'; + +import invariant from 'fbjs/lib/invariant'; +import warning from 'fbjs/lib/warning'; + +export type Update = { + expirationTime: ExpirationTime, + + tag: 0 | 1 | 2 | 3, + payload: any, + callback: (() => mixed) | null, + + next: Update | null, + nextEffect: Update | null, +}; + +export type UpdateQueue = { + expirationTime: ExpirationTime, + baseState: State, + + firstUpdate: Update | null, + lastUpdate: Update | null, + + firstCapturedUpdate: Update | null, + lastCapturedUpdate: Update | null, + + firstEffect: Update | null, + lastEffect: Update | null, + + firstCapturedEffect: Update | null, + lastCapturedEffect: Update | null, + + // TODO: Workaround for lack of tuples. Could use global state instead. + hasForceUpdate: boolean, +}; + +export const UpdateState = 0; +export const ReplaceState = 1; +export const ForceUpdate = 2; +export const CaptureUpdate = 3; + +let didWarnUpdateInsideUpdate; +let currentlyProcessingQueue; +export let resetCurrentlyProcessingQueue; +if (__DEV__) { + didWarnUpdateInsideUpdate = false; + currentlyProcessingQueue = null; + resetCurrentlyProcessingQueue = () => { + currentlyProcessingQueue = null; + }; +} + +export function createUpdateQueue(baseState: State): UpdateQueue { + const queue: UpdateQueue = { + expirationTime: NoWork, + baseState, + firstUpdate: null, + lastUpdate: null, + firstCapturedUpdate: null, + lastCapturedUpdate: null, + firstEffect: null, + lastEffect: null, + firstCapturedEffect: null, + lastCapturedEffect: null, + hasForceUpdate: false, + }; + return queue; +} + +function cloneUpdateQueue( + currentQueue: UpdateQueue, +): UpdateQueue { + const queue: UpdateQueue = { + expirationTime: currentQueue.expirationTime, + baseState: currentQueue.baseState, + firstUpdate: currentQueue.firstUpdate, + lastUpdate: currentQueue.lastUpdate, + + // TODO: With resuming, if we bail out and resuse the child tree, we should + // keep these effects. + firstCapturedUpdate: null, + lastCapturedUpdate: null, + + hasForceUpdate: false, + + firstEffect: null, + lastEffect: null, + + firstCapturedEffect: null, + lastCapturedEffect: null, + }; + return queue; +} + +export function createUpdate(expirationTime: ExpirationTime): Update<*> { + return { + expirationTime: expirationTime, + + tag: UpdateState, + payload: null, + callback: null, + + next: null, + nextEffect: null, + }; +} + +function appendUpdateToQueue( + queue: UpdateQueue, + update: Update, + expirationTime: ExpirationTime, +) { + // Append the update to the end of the list. + if (queue.lastUpdate === null) { + // Queue is empty + queue.firstUpdate = queue.lastUpdate = update; + } else { + queue.lastUpdate.next = update; + queue.lastUpdate = update; + } + if ( + queue.expirationTime === NoWork || + queue.expirationTime > expirationTime + ) { + // The incoming update has the earliest expiration of any update in the + // queue. Update the queue's expiration time. + queue.expirationTime = expirationTime; + } +} + +export function enqueueUpdate( + fiber: Fiber, + update: Update, + expirationTime: ExpirationTime, +) { + // Update queues are created lazily. + const alternate = fiber.alternate; + let queue1; + let queue2; + if (alternate === null) { + // There's only one fiber. + queue1 = fiber.updateQueue; + queue2 = null; + if (queue1 === null) { + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); + } + } else { + // There are two owners. + queue1 = fiber.updateQueue; + queue2 = alternate.updateQueue; + if (queue1 === null) { + if (queue2 === null) { + // Neither fiber has an update queue. Create new ones. + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); + queue2 = alternate.updateQueue = createUpdateQueue( + alternate.memoizedState, + ); + } else { + // Only one fiber has an update queue. Clone to create a new one. + queue1 = fiber.updateQueue = cloneUpdateQueue(queue2); + } + } else { + if (queue2 === null) { + // Only one fiber has an update queue. Clone to create a new one. + queue2 = alternate.updateQueue = cloneUpdateQueue(queue1); + } else { + // Both owners have an update queue. + } + } + } + if (queue2 === null || queue1 === queue2) { + // There's only a single queue. + appendUpdateToQueue(queue1, update, expirationTime); + } else { + // There are two queues. We need to append the update to both queues, + // while accounting for the persistent structure of the list — we don't + // want the same update to be added multiple times. + if (queue1.lastUpdate === null || queue2.lastUpdate === null) { + // One of the queues is not empty. We must add the update to both queues. + appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue2, update, expirationTime); + } else { + // Both queues are non-empty. The last update is the same in both lists, + // because of structural sharing. So, only append to one of the lists. + appendUpdateToQueue(queue1, update, expirationTime); + // But we still need to update the `lastUpdate` pointer of queue2. + queue2.lastUpdate = update; + } + } + + if (__DEV__) { + if ( + fiber.tag === ClassComponent && + (currentlyProcessingQueue === queue1 || + (queue2 !== null && currentlyProcessingQueue === queue2)) && + !didWarnUpdateInsideUpdate + ) { + warning( + false, + 'An update (setState, replaceState, or forceUpdate) was scheduled ' + + 'from inside an update function. Update functions should be pure, ' + + 'with zero side-effects. Consider using componentDidUpdate or a ' + + 'callback.', + ); + didWarnUpdateInsideUpdate = true; + } + } +} + +export function enqueueCapturedUpdate( + workInProgress: Fiber, + update: Update, + renderExpirationTime: ExpirationTime, +) { + // Captured updates go into a separate list, and only on the work-in- + // progress queue. + let workInProgressQueue = workInProgress.updateQueue; + if (workInProgressQueue === null) { + workInProgressQueue = workInProgress.updateQueue = createUpdateQueue( + workInProgress.memoizedState, + ); + } else { + // TODO: I put this here rather than createWorkInProgress so that we don't + // clone the queue unnecessarily. There's probably a better way to + // structure this. + workInProgressQueue = ensureWorkInProgressQueueIsAClone( + workInProgress, + workInProgressQueue, + ); + } + + // Append the update to the end of the list. + if (workInProgressQueue.lastCapturedUpdate === null) { + // This is the first render phase update + workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update; + } else { + workInProgressQueue.lastCapturedUpdate.next = update; + workInProgressQueue.lastCapturedUpdate = update; + } + if ( + workInProgressQueue.expirationTime === NoWork || + workInProgressQueue.expirationTime > renderExpirationTime + ) { + // The incoming update has the earliest expiration of any update in the + // queue. Update the queue's expiration time. + workInProgressQueue.expirationTime = renderExpirationTime; + } +} + +function ensureWorkInProgressQueueIsAClone( + workInProgress: Fiber, + queue: UpdateQueue, +): UpdateQueue { + const current = workInProgress.alternate; + if (current !== null) { + // If the work-in-progress queue is equal to the current queue, + // we need to clone it first. + if (queue === current.updateQueue) { + queue = workInProgress.updateQueue = cloneUpdateQueue(queue); + } + } + return queue; +} + +function getStateFromUpdate( + workInProgress: Fiber, + queue: UpdateQueue, + update: Update, + prevState: State, + nextProps: any, + instance: any, +): any { + switch (update.tag) { + case ReplaceState: { + const payload = update.payload; + if (typeof payload === 'function') { + // Updater function + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + payload.call(instance, prevState, nextProps); + } + } + return payload.call(instance, prevState, nextProps); + } + // State object + return payload; + } + case CaptureUpdate: { + workInProgress.effectTag = + (workInProgress.effectTag & ~ShouldCapture) | DidCapture; + } + // Intentional fallthrough + case UpdateState: { + const payload = update.payload; + let partialState; + if (typeof payload === 'function') { + // Updater function + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + payload.call(instance, prevState, nextProps); + } + } + partialState = payload.call(instance, prevState, nextProps); + } else { + // Partial state object + partialState = payload; + } + if (partialState === null || partialState === undefined) { + // Null and undefined are treated as no-ops. + return prevState; + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } + case ForceUpdate: { + queue.hasForceUpdate = true; + return prevState; + } + } + return prevState; +} + +export function processUpdateQueue( + workInProgress: Fiber, + queue: UpdateQueue, + props: any, + instance: any, + renderExpirationTime: ExpirationTime, +): void { + if ( + queue.expirationTime === NoWork || + queue.expirationTime > renderExpirationTime + ) { + // Insufficient priority. Bailout. + return; + } + + queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); + + if (__DEV__) { + currentlyProcessingQueue = queue; + } + + // These values may change as we process the queue. + let newBaseState = queue.baseState; + let newFirstUpdate = null; + let newExpirationTime = NoWork; + + // Iterate through the list of updates to compute the result. + let update = queue.firstUpdate; + let resultState = newBaseState; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstUpdate === null) { + // This is the first skipped update. It will be the first update in + // the new list. + newFirstUpdate = update; + // Since this is the first update that was skipped, the current result + // is the new base state. + newBaseState = resultState; + } + // Since this update will remain in the list, update the remaining + // expiration time. + if ( + newExpirationTime === NoWork || + newExpirationTime > updateExpirationTime + ) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. Process it and compute + // a new result. + resultState = getStateFromUpdate( + workInProgress, + queue, + update, + resultState, + props, + instance, + ); + const callback = update.callback; + if (callback !== null) { + workInProgress.effectTag |= Callback; + // Set this to null, in case it was mutated during an aborted render. + update.nextEffect = null; + if (queue.lastEffect === null) { + queue.firstEffect = queue.lastEffect = update; + } else { + queue.lastEffect.nextEffect = update; + queue.lastEffect = update; + } + } + } + // Continue to the next update. + update = update.next; + } + + // Separately, iterate though the list of captured updates. + let newFirstCapturedUpdate = null; + update = queue.firstCapturedUpdate; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstCapturedUpdate === null) { + // This is the first skipped captured update. It will be the first + // update in the new list. + newFirstCapturedUpdate = update; + // If this is the first update that was skipped, the current result is + // the new base state. + if (newFirstUpdate === null) { + newBaseState = resultState; + } + } + // Since this update will remain in the list, update the remaining + // expiration time. + if ( + newExpirationTime === NoWork || + newExpirationTime > updateExpirationTime + ) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. Process it and compute + // a new result. + resultState = getStateFromUpdate( + workInProgress, + queue, + update, + resultState, + props, + instance, + ); + const callback = update.callback; + if (callback !== null) { + workInProgress.effectTag |= Callback; + // Set this to null, in case it was mutated during an aborted render. + update.nextEffect = null; + if (queue.lastCapturedEffect === null) { + queue.firstCapturedEffect = queue.lastCapturedEffect = update; + } else { + queue.lastCapturedEffect.nextEffect = update; + queue.lastCapturedEffect = update; + } + } + } + update = update.next; + } + + if (newFirstUpdate === null) { + queue.lastUpdate = null; + } + if (newFirstCapturedUpdate === null) { + queue.lastCapturedUpdate = null; + } else { + workInProgress.effectTag |= Callback; + } + if (newFirstUpdate === null && newFirstCapturedUpdate === null) { + // We processed every update, without skipping. That means the new base + // state is the same as the result state. + newBaseState = resultState; + } + + queue.baseState = newBaseState; + queue.firstUpdate = newFirstUpdate; + queue.firstCapturedUpdate = newFirstCapturedUpdate; + queue.expirationTime = newExpirationTime; + + workInProgress.memoizedState = resultState; + + if (__DEV__) { + currentlyProcessingQueue = null; + } +} + +function callCallback(callback, context) { + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(context); +} + +export function commitUpdateQueue( + finishedWork: Fiber, + finishedQueue: UpdateQueue, + instance: any, + renderExpirationTime: ExpirationTime, +): void { + // If the finished render included captured updates, and there are still + // lower priority updates left over, we need to keep the captured updates + // in the queue so that they are rebased and not dropped once we process the + // queue again at the lower priority. + if (finishedQueue.firstCapturedUpdate !== null) { + // Join the captured update list to the end of the normal list. + if (finishedQueue.lastUpdate !== null) { + finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate; + finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate; + } + // Clear the list of captured updates. + finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null; + } + + // Commit the effects + let effect = finishedQueue.firstEffect; + finishedQueue.firstEffect = finishedQueue.lastEffect = null; + while (effect !== null) { + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); + } + effect = effect.nextEffect; + } + + effect = finishedQueue.firstCapturedEffect; + finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null; + while (effect !== null) { + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); + } + effect = effect.nextEffect; + } +} diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index 91de8d285d..a8b66373d8 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1003,6 +1003,7 @@ describe('ReactIncremental', () => { instance.setState(updater); ReactNoop.flush(); expect(instance.state.num).toEqual(2); + instance.setState(updater); ReactNoop.render(); ReactNoop.flush(); @@ -1421,7 +1422,7 @@ describe('ReactIncremental', () => { ]); }); - it('does not call static getDerivedStateFromProps for state-only updates', () => { + it('calls getDerivedStateFromProps even for state-only updates', () => { let ops = []; let instance; @@ -1455,8 +1456,12 @@ describe('ReactIncremental', () => { instance.changeState(); ReactNoop.flush(); - expect(ops).toEqual(['render', 'componentDidUpdate']); - expect(instance.state).toEqual({foo: 'bar'}); + expect(ops).toEqual([ + 'getDerivedStateFromProps', + 'render', + 'componentDidUpdate', + ]); + expect(instance.state).toEqual({foo: 'foo'}); }); xit('does not call componentWillReceiveProps for state-only updates', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js index 628e1c3880..731865da65 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js @@ -427,6 +427,8 @@ describe('ReactIncrementalTriangle', () => { function simulate(...actions) { const gen = simulateAndYield(); + // Call this once to prepare the generator + gen.next(); // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (let action of actions) { gen.next(action); diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index 40e37fe439..ffe5706d1c 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -298,7 +298,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update ⚛ (Committing Snapshot Effects: 0 Total) ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) ⚛ (React Tree Reconciliation: Completed Root) ⚛ Boundary [update] @@ -324,7 +324,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` ⚛ (Committing Changes) ⚛ (Committing Snapshot Effects: 0 Total) ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) ⚛ (Waiting for async callback... will force flush in 5230 ms) @@ -406,8 +406,8 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = ⚛ (Committing Changes) ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + ⚛ (Committing Host Effects: 0 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js index 5a5a799bf8..9236ae7754 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js @@ -57,38 +57,64 @@ describe('ReactStrictMode', () => { const component = ReactTestRenderer.create(); - expect(log).toEqual([ - 'constructor', - 'constructor', - 'getDerivedStateFromProps', - 'getDerivedStateFromProps', - 'render', - 'render', - 'componentDidMount', - ]); + if (__DEV__) { + expect(log).toEqual([ + 'constructor', + 'constructor', + 'getDerivedStateFromProps', + 'getDerivedStateFromProps', + 'render', + 'render', + 'componentDidMount', + ]); + } else { + expect(log).toEqual([ + 'constructor', + 'getDerivedStateFromProps', + 'render', + 'componentDidMount', + ]); + } log = []; shouldComponentUpdate = true; component.update(); - expect(log).toEqual([ - 'getDerivedStateFromProps', - 'getDerivedStateFromProps', - 'shouldComponentUpdate', - 'render', - 'render', - 'componentDidUpdate', - ]); + if (__DEV__) { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + 'render', + 'render', + 'componentDidUpdate', + ]); + } else { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + 'render', + 'componentDidUpdate', + ]); + } log = []; shouldComponentUpdate = false; component.update(); - expect(log).toEqual([ - 'getDerivedStateFromProps', - 'getDerivedStateFromProps', - 'shouldComponentUpdate', - ]); + + if (__DEV__) { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + ]); + } else { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + ]); + } }); it('should invoke setState callbacks twice', () => { @@ -112,8 +138,8 @@ describe('ReactStrictMode', () => { }; }); - // Callback should be invoked twice - expect(setStateCount).toBe(2); + // Callback should be invoked twice in DEV + expect(setStateCount).toBe(__DEV__ ? 2 : 1); // But each time `state` should be the previous value expect(instance.state.count).toBe(2); }); @@ -174,7 +200,7 @@ describe('ReactStrictMode', () => { const component = ReactTestRenderer.create(); - if (debugRenderPhaseSideEffectsForStrictMode) { + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) { expect(log).toEqual([ 'constructor', 'constructor', @@ -197,7 +223,7 @@ describe('ReactStrictMode', () => { shouldComponentUpdate = true; component.update(); - if (debugRenderPhaseSideEffectsForStrictMode) { + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) { expect(log).toEqual([ 'getDerivedStateFromProps', 'getDerivedStateFromProps', @@ -219,7 +245,7 @@ describe('ReactStrictMode', () => { shouldComponentUpdate = false; component.update(); - if (debugRenderPhaseSideEffectsForStrictMode) { + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) { expect(log).toEqual([ 'getDerivedStateFromProps', 'getDerivedStateFromProps', @@ -263,7 +289,7 @@ describe('ReactStrictMode', () => { // Callback should be invoked twice (in DEV) expect(setStateCount).toBe( - debugRenderPhaseSideEffectsForStrictMode ? 2 : 1, + __DEV__ && debugRenderPhaseSideEffectsForStrictMode ? 2 : 1, ); // But each time `state` should be the previous value expect(instance.state.count).toBe(2); diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 82e8c3342f..27d6aa6090 100644 --- a/packages/shared/ReactTypeOfSideEffect.js +++ b/packages/shared/ReactTypeOfSideEffect.js @@ -10,23 +10,22 @@ export type TypeOfSideEffect = number; // Don't change these two values. They're used by React Dev Tools. -export const NoEffect = /* */ 0b000000000000; -export const PerformedWork = /* */ 0b000000000001; +export const NoEffect = /* */ 0b00000000000; +export const PerformedWork = /* */ 0b00000000001; // You can change the rest (and add more). -export const Placement = /* */ 0b000000000010; -export const Update = /* */ 0b000000000100; -export const PlacementAndUpdate = /* */ 0b000000000110; -export const Deletion = /* */ 0b000000001000; -export const ContentReset = /* */ 0b000000010000; -export const Callback = /* */ 0b000000100000; -export const DidCapture = /* */ 0b000001000000; -export const Ref = /* */ 0b000010000000; -export const ErrLog = /* */ 0b000100000000; -export const Snapshot = /* */ 0b100000000000; +export const Placement = /* */ 0b00000000010; +export const Update = /* */ 0b00000000100; +export const PlacementAndUpdate = /* */ 0b00000000110; +export const Deletion = /* */ 0b00000001000; +export const ContentReset = /* */ 0b00000010000; +export const Callback = /* */ 0b00000100000; +export const DidCapture = /* */ 0b00001000000; +export const Ref = /* */ 0b00010000000; +export const Snapshot = /* */ 0b00100000000; // Union of all host effects -export const HostEffectMask = /* */ 0b100111111111; +export const HostEffectMask = /* */ 0b00111111111; -export const Incomplete = /* */ 0b001000000000; -export const ShouldCapture = /* */ 0b010000000000; +export const Incomplete = /* */ 0b01000000000; +export const ShouldCapture = /* */ 0b10000000000; From 149a34f735b47f225e3d26ce2478b3181e4ff8fe Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 23 Apr 2018 10:27:39 -0700 Subject: [PATCH 007/277] Exposed flushSync on the test renderer (#12672) --- .../src/ReactTestRenderer.js | 1 + .../__tests__/ReactTestRendererAsync-test.js | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index aa58411a7d..977a1f5c68 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -736,6 +736,7 @@ const ReactTestRendererFiber = { } return TestRenderer.getPublicRootInstance(root); }, + unstable_flushSync: TestRenderer.flushSync, }; Object.defineProperty( diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js index b9574012bd..26a3de6746 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js @@ -94,4 +94,44 @@ describe('ReactTestRendererAsync', () => { expect(renderer.unstable_flushAll()).toEqual(['C:1']); expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']); }); + + it('supports high priority interruptions', () => { + function Child(props) { + renderer.unstable_yield(props.children); + return props.children; + } + + class Example extends React.Component { + componentDidMount() { + expect(this.props.step).toEqual(2); + } + componentDidUpdate() { + throw Error('Unexpected update'); + } + render() { + return ( + + {'A:' + this.props.step} + {'B:' + this.props.step} + + ); + } + } + + const renderer = ReactTestRenderer.create(, { + unstable_isAsync: true, + }); + + // Flush the some of the changes, but don't commit + expect(renderer.unstable_flushThrough(['A:1'])).toEqual(['A:1']); + expect(renderer.toJSON()).toEqual(null); + + // Interrupt with higher priority properties + renderer.unstable_flushSync(() => { + renderer.update(); + }); + + // Only the higher priority properties have been committed + expect(renderer.toJSON()).toEqual(['A:2', 'B:2']); + }); }); From 1e3cd332a015e312149efa36eb81c7523411cc2d Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Mon, 23 Apr 2018 15:25:46 -0700 Subject: [PATCH 008/277] Remove the 'alwaysUseRequestIdleCallbackPolyfill' feature flag (#12648) * Remove the 'alwaysUseRequestIdleCallbackPolyfill' feature flag **what is the change?:** Removes the feature flag 'alwaysUseRequestIdleCallbackPolyfill', such that we **always** use the polyfill for requestIdleCallback. **why make this change?:** We have been testing this feature flag at 100% for some time internally, and determined it works better for React than the native implementation. Looks like RN was overriding the flag to use the native when possible, but since no RN products are using 'async' mode it should be safe to switch this flag over for RN as well. **test plan:** We have already been testing this internally for some time. **issue:** internal task t28128480 * fix mistaken conditional * Add mocking of rAF, postMessage, and initial test for ReactScheduler **what is the change?:** - In all tests where we previously mocked rIC or relied on native mocking which no longer works, we are now mocking rAF and postMessage. - Also adds a basic initial test for ReactScheduler. NOTE -> we do plan to write headless browser tests for ReactScheduler! This is just an initial test, to verify that it works with the mocked out browser APIs as expected. **why make this change?:** We need to mock out the browser APIs more completely for the new 'ReactScheduler' to work in our tests. Many tests are depending on it, since it's used at a low level. By mocking the browser APIs rather than the 'react-scheduler' module, we enable testing the production bundles. This approach is trading isolation for accuracy. These tests will be closer to a real use. **test plan:** run the tests :) **issue:** internal task T28128480 --- .../ReactDOMFiberAsync-test.internal.js | 25 +++++ .../src/__tests__/ReactDOMRoot-test.js | 105 +++++++++--------- .../ChangeEventPlugin-test.internal.js | 25 +++++ .../SimpleEventPlugin-test.internal.js | 25 +++++ .../react-scheduler/src/ReactScheduler.js | 13 +-- .../src/__tests__/ReactScheduler-test.js | 56 ++++++++++ .../src/__tests__/test_page.html | 10 ++ packages/shared/ReactFeatureFlags.js | 2 - .../ReactFeatureFlags.native-fabric-fb.js | 1 - .../ReactFeatureFlags.native-fabric-oss.js | 1 - .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.persistent.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 1 - 15 files changed, 196 insertions(+), 72 deletions(-) create mode 100644 packages/react-scheduler/src/__tests__/ReactScheduler-test.js create mode 100644 packages/react-scheduler/src/__tests__/test_page.html diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js index e0c256d4a3..51b901f1dd 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js @@ -20,6 +20,31 @@ describe('ReactDOMFiberAsync', () => { let container; beforeEach(() => { + // TODO pull this into helper method, reduce repetition. + // mock the browser APIs which are used in react-scheduler: + // - requestAnimationFrame should pass the DOMHighResTimeStamp argument + // - calling 'window.postMessage' should actually fire postmessage handlers + global.requestAnimationFrame = function(cb) { + return setTimeout(() => { + cb(Date.now()); + }); + }; + const originalAddEventListener = global.addEventListener; + let postMessageCallback; + global.addEventListener = function(eventName, callback, useCapture) { + if (eventName === 'message') { + postMessageCallback = callback; + } else { + originalAddEventListener(eventName, callback, useCapture); + } + }; + global.postMessage = function(messageKey, targetOrigin) { + const postMessageEvent = {source: window, data: messageKey}; + if (postMessageCallback) { + postMessageCallback(postMessageEvent); + } + }; + jest.resetModules(); container = document.createElement('div'); ReactDOM = require('react-dom'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index f86d3b0e06..2255d1cb04 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -17,47 +17,46 @@ let AsyncMode = React.unstable_AsyncMode; describe('ReactDOMRoot', () => { let container; - let scheduledCallback; - let flush; - let now; - let expire; + let advanceCurrentTime; beforeEach(() => { container = document.createElement('div'); - - // Override requestIdleCallback - scheduledCallback = null; - flush = function(units = Infinity) { - if (scheduledCallback !== null) { - let didStop = false; - while (scheduledCallback !== null && !didStop) { - const cb = scheduledCallback; - scheduledCallback = null; - cb({ - timeRemaining() { - if (units > 0) { - return 999; - } - didStop = true; - return 0; - }, - }); - units--; - } + // TODO pull this into helper method, reduce repetition. + // mock the browser APIs which are used in react-scheduler: + // - requestAnimationFrame should pass the DOMHighResTimeStamp argument + // - calling 'window.postMessage' should actually fire postmessage handlers + // - must allow artificially changing time returned by Date.now + // Performance.now is not supported in the test environment + const originalDateNow = Date.now; + let advancedTime = null; + global.Date.now = function() { + if (advancedTime) { + return originalDateNow() + advancedTime; + } + return originalDateNow(); + }; + advanceCurrentTime = function(amount) { + advancedTime = amount; + }; + global.requestAnimationFrame = function(cb) { + return setTimeout(() => { + cb(Date.now()); + }); + }; + const originalAddEventListener = global.addEventListener; + let postMessageCallback; + global.addEventListener = function(eventName, callback, useCapture) { + if (eventName === 'message') { + postMessageCallback = callback; + } else { + originalAddEventListener(eventName, callback, useCapture); } }; - global.performance = { - now() { - return now; - }, - }; - global.requestIdleCallback = function(cb) { - scheduledCallback = cb; - }; - - now = 0; - expire = function(ms) { - now += ms; + global.postMessage = function(messageKey, targetOrigin) { + const postMessageEvent = {source: window, data: messageKey}; + if (postMessageCallback) { + postMessageCallback(postMessageEvent); + } }; jest.resetModules(); @@ -70,17 +69,17 @@ describe('ReactDOMRoot', () => { it('renders children', () => { const root = ReactDOM.unstable_createRoot(container); root.render(
Hi
); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual('Hi'); }); it('unmounts children', () => { const root = ReactDOM.unstable_createRoot(container); root.render(
Hi
); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual('Hi'); root.unmount(); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual(''); }); @@ -92,7 +91,7 @@ describe('ReactDOMRoot', () => { ops.push('inside callback: ' + container.textContent); }); ops.push('before committing: ' + container.textContent); - flush(); + jest.runAllTimers(); ops.push('after committing: ' + container.textContent); expect(ops).toEqual([ 'before committing: ', @@ -105,7 +104,7 @@ describe('ReactDOMRoot', () => { it('resolves `work.then` callback synchronously if the work already committed', () => { const root = ReactDOM.unstable_createRoot(container); const work = root.render(Hi); - flush(); + jest.runAllTimers(); let ops = []; work.then(() => { ops.push('inside callback'); @@ -133,7 +132,7 @@ describe('ReactDOMRoot', () => { , ); - flush(); + jest.runAllTimers(); // Accepts `hydrate` option const container2 = document.createElement('div'); @@ -144,7 +143,7 @@ describe('ReactDOMRoot', () => { , ); - expect(flush).toWarnDev('Extra attributes'); + expect(jest.runAllTimers).toWarnDev('Extra attributes'); }); it('does not clear existing children', async () => { @@ -156,7 +155,7 @@ describe('ReactDOMRoot', () => { d , ); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual('abcd'); root.render(
@@ -164,7 +163,7 @@ describe('ReactDOMRoot', () => { c
, ); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual('abdc'); }); @@ -200,7 +199,7 @@ describe('ReactDOMRoot', () => { , ); - flush(); + jest.runAllTimers(); // Hasn't updated yet expect(container.textContent).toEqual(''); @@ -229,7 +228,7 @@ describe('ReactDOMRoot', () => { const batch = root.createBatch(); batch.render(Hi); // Flush all async work. - flush(); + jest.runAllTimers(); // Root should complete without committing. expect(ops).toEqual(['Foo']); expect(container.textContent).toEqual(''); @@ -247,7 +246,7 @@ describe('ReactDOMRoot', () => { const batch = root.createBatch(); batch.render(Foo); - flush(); + jest.runAllTimers(); // Hasn't updated yet expect(container.textContent).toEqual(''); @@ -287,7 +286,7 @@ describe('ReactDOMRoot', () => { const root = ReactDOM.unstable_createRoot(container); root.render(1); - expire(2000); + advanceCurrentTime(2000); // This batch has a later expiration time than the earlier update. const batch = root.createBatch(); @@ -295,7 +294,7 @@ describe('ReactDOMRoot', () => { batch.commit(); expect(container.textContent).toEqual(''); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual('1'); }); @@ -322,7 +321,7 @@ describe('ReactDOMRoot', () => { batch1.render(1); // This batch has a later expiration time - expire(2000); + advanceCurrentTime(2000); const batch2 = root.createBatch(); batch2.render(2); @@ -341,7 +340,7 @@ describe('ReactDOMRoot', () => { batch1.render(1); // This batch has a later expiration time - expire(2000); + advanceCurrentTime(2000); const batch2 = root.createBatch(); batch2.render(2); @@ -351,7 +350,7 @@ describe('ReactDOMRoot', () => { expect(container.textContent).toEqual('2'); batch1.commit(); - flush(); + jest.runAllTimers(); expect(container.textContent).toEqual('1'); }); diff --git a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js index e5f8969ba1..570ecf3238 100644 --- a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js +++ b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js @@ -32,6 +32,31 @@ describe('ChangeEventPlugin', () => { let container; beforeEach(() => { + // TODO pull this into helper method, reduce repetition. + // mock the browser APIs which are used in react-scheduler: + // - requestAnimationFrame should pass the DOMHighResTimeStamp argument + // - calling 'window.postMessage' should actually fire postmessage handlers + global.requestAnimationFrame = function(cb) { + return setTimeout(() => { + cb(Date.now()); + }); + }; + const originalAddEventListener = global.addEventListener; + let postMessageCallback; + global.addEventListener = function(eventName, callback, useCapture) { + if (eventName === 'message') { + postMessageCallback = callback; + } else { + originalAddEventListener(eventName, callback, useCapture); + } + }; + global.postMessage = function(messageKey, targetOrigin) { + const postMessageEvent = {source: window, data: messageKey}; + if (postMessageCallback) { + postMessageCallback(postMessageEvent); + } + }; + jest.resetModules(); container = document.createElement('div'); document.body.appendChild(container); }); diff --git a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js index ad90110818..1b39dd1e47 100644 --- a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js +++ b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js @@ -33,6 +33,31 @@ describe('SimpleEventPlugin', function() { } beforeEach(function() { + // TODO pull this into helper method, reduce repetition. + // mock the browser APIs which are used in react-scheduler: + // - requestAnimationFrame should pass the DOMHighResTimeStamp argument + // - calling 'window.postMessage' should actually fire postmessage handlers + global.requestAnimationFrame = function(cb) { + return setTimeout(() => { + cb(Date.now()); + }); + }; + const originalAddEventListener = global.addEventListener; + let postMessageCallback; + global.addEventListener = function(eventName, callback, useCapture) { + if (eventName === 'message') { + postMessageCallback = callback; + } else { + originalAddEventListener(eventName, callback, useCapture); + } + }; + global.postMessage = function(messageKey, targetOrigin) { + const postMessageEvent = {source: window, data: messageKey}; + if (postMessageCallback) { + postMessageCallback(postMessageEvent); + } + }; + jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils'); diff --git a/packages/react-scheduler/src/ReactScheduler.js b/packages/react-scheduler/src/ReactScheduler.js index a7983dfeaf..49169c6396 100644 --- a/packages/react-scheduler/src/ReactScheduler.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -32,7 +32,6 @@ import type {Deadline} from 'react-reconciler'; -import {alwaysUseRequestIdleCallbackPolyfill} from 'shared/ReactFeatureFlags'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import warning from 'fbjs/lib/warning'; @@ -85,12 +84,8 @@ if (!ExecutionEnvironment.canUseDOM) { cIC = function(timeoutID: number) { clearTimeout(timeoutID); }; -} else if ( - alwaysUseRequestIdleCallbackPolyfill || - typeof requestIdleCallback !== 'function' || - typeof cancelIdleCallback !== 'function' -) { - // Polyfill requestIdleCallback and cancelIdleCallback +} else { + // Always polyfill requestIdleCallback and cancelIdleCallback let scheduledRICCallback = null; let isIdleScheduled = false; @@ -175,6 +170,7 @@ if (!ExecutionEnvironment.canUseDOM) { window.addEventListener('message', idleTick, false); const animationTick = function(rafTime) { + console.log('animationTick called and rafTime is ', rafTime); isAnimationFrameScheduled = false; let nextFrameTime = rafTime - frameDeadline + activeFrameTime; if ( @@ -231,9 +227,6 @@ if (!ExecutionEnvironment.canUseDOM) { isIdleScheduled = false; timeoutTime = -1; }; -} else { - rIC = window.requestIdleCallback; - cIC = window.cancelIdleCallback; } export {now, rIC, cIC}; diff --git a/packages/react-scheduler/src/__tests__/ReactScheduler-test.js b/packages/react-scheduler/src/__tests__/ReactScheduler-test.js new file mode 100644 index 0000000000..1ed60b0fb3 --- /dev/null +++ b/packages/react-scheduler/src/__tests__/ReactScheduler-test.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * 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'; + +let ReactScheduler; + +describe('ReactScheduler', () => { + beforeEach(() => { + // TODO pull this into helper method, reduce repetition. + // mock the browser APIs which are used in react-scheduler: + // - requestAnimationFrame should pass the DOMHighResTimeStamp argument + // - calling 'window.postMessage' should actually fire postmessage handlers + global.requestAnimationFrame = function(cb) { + return setTimeout(() => { + cb(Date.now()); + }); + }; + const originalAddEventListener = global.addEventListener; + let postMessageCallback; + global.addEventListener = function(eventName, callback, useCapture) { + if (eventName === 'message') { + postMessageCallback = callback; + } else { + originalAddEventListener(eventName, callback, useCapture); + } + }; + global.postMessage = function(messageKey, targetOrigin) { + const postMessageEvent = {source: window, data: messageKey}; + if (postMessageCallback) { + postMessageCallback(postMessageEvent); + } + }; + jest.resetModules(); + ReactScheduler = require('react-scheduler'); + }); + + it('rIC calls the callback within the frame when not blocked', () => { + const {rIC} = ReactScheduler; + const cb = jest.fn(); + rIC(cb); + jest.runAllTimers(); + expect(cb.mock.calls.length).toBe(1); + // should have ... TODO details on what we expect + expect(cb.mock.calls[0][0].didTimeout).toBe(false); + expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number'); + }); + + // TODO: test cIC and now +}); diff --git a/packages/react-scheduler/src/__tests__/test_page.html b/packages/react-scheduler/src/__tests__/test_page.html new file mode 100644 index 0000000000..3b5af08b39 --- /dev/null +++ b/packages/react-scheduler/src/__tests__/test_page.html @@ -0,0 +1,10 @@ + + + + + React Scheduler test page + + +

Hello World

+ + diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index fc0a50083d..955668d13e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -37,8 +37,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; // Warn about deprecated, async-unsafe lifecycles; relates to RFC #6: export const warnAboutDeprecatedLifecycles = false; -export const alwaysUseRequestIdleCallbackPolyfill = false; - // Only used in www builds. export function addUserTimingListener() { invariant(false, 'Not implemented.'); diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js index 4e61717d9c..e2fd437999 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js @@ -23,7 +23,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableMutatingReconciler = false; export const enableNoopReconciler = false; export const enablePersistentReconciler = true; -export const alwaysUseRequestIdleCallbackPolyfill = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js index 0948deff81..c6a4862d85 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js @@ -23,7 +23,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableMutatingReconciler = false; export const enableNoopReconciler = false; export const enablePersistentReconciler = true; -export const alwaysUseRequestIdleCallbackPolyfill = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index aeed05a9b3..3026a69fc6 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -26,7 +26,6 @@ export const enableUserTimingAPI = __DEV__; export const enableMutatingReconciler = true; export const enableNoopReconciler = false; export const enablePersistentReconciler = false; -export const alwaysUseRequestIdleCallbackPolyfill = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 381fe7b39d..a238ca1688 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -12,7 +12,6 @@ import invariant from 'fbjs/lib/invariant'; import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-oss'; -export const alwaysUseRequestIdleCallbackPolyfill = false; export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableGetDerivedStateFromCatch = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 6ae31a7ce5..57d7c6bf53 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -24,7 +24,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableMutatingReconciler = false; export const enableNoopReconciler = false; export const enablePersistentReconciler = true; -export const alwaysUseRequestIdleCallbackPolyfill = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 03642454dd..000f950a4f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -21,7 +21,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; export const enableMutatingReconciler = true; export const enableNoopReconciler = false; export const enablePersistentReconciler = false; -export const alwaysUseRequestIdleCallbackPolyfill = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 407a0a662c..9153819ce5 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -17,7 +17,6 @@ export const { debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, replayFailedUnitOfWorkWithInvokeGuardedCallback, - alwaysUseRequestIdleCallbackPolyfill, } = require('ReactFeatureFlags'); // The rest of the flags are static for better dead code elimination. From 1673485720d9ac172fcf1f8628a004a4990bf4f4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 23 Apr 2018 18:44:14 -0700 Subject: [PATCH 009/277] Revert stray console.log --- packages/react-scheduler/src/ReactScheduler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-scheduler/src/ReactScheduler.js b/packages/react-scheduler/src/ReactScheduler.js index 49169c6396..362a3f34b7 100644 --- a/packages/react-scheduler/src/ReactScheduler.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -170,7 +170,6 @@ if (!ExecutionEnvironment.canUseDOM) { window.addEventListener('message', idleTick, false); const animationTick = function(rafTime) { - console.log('animationTick called and rafTime is ', rafTime); isAnimationFrameScheduled = false; let nextFrameTime = rafTime - frameDeadline + activeFrameTime; if ( From 09a14eacd45993cfc9fb623a9ee14a924f3ce483 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 23 Apr 2018 19:38:07 -0700 Subject: [PATCH 010/277] Update bundle sizes --- scripts/rollup/results.json | 289 +++++++++++++++++++++++++++++------- 1 file changed, 239 insertions(+), 50 deletions(-) diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 0383f623b4..67107645c5 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -4,8 +4,8 @@ "filename": "react.development.js", "bundleType": "UMD_DEV", "packageName": "react", - "size": 56545, - "gzip": 15535 + "size": 56795, + "gzip": 15603 }, { "filename": "react.production.min.js", @@ -18,8 +18,8 @@ "filename": "react.development.js", "bundleType": "NODE_DEV", "packageName": "react", - "size": 46962, - "gzip": 13128 + "size": 47210, + "gzip": 13192 }, { "filename": "react.production.min.js", @@ -46,29 +46,29 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 623901, - "gzip": 143779 + "size": 626353, + "gzip": 144739 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 102609, - "gzip": 32741 + "size": 102821, + "gzip": 32649 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 607898, - "gzip": 139520 + "size": 610354, + "gzip": 140504 }, { "filename": "react-dom.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 101020, - "gzip": 31842 + "size": 101220, + "gzip": 31815 }, { "filename": "ReactDOM-dev.js", @@ -88,7 +88,7 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 41729, + "size": 41728, "gzip": 11969 }, { @@ -102,8 +102,8 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 36466, - "gzip": 10518 + "size": 36465, + "gzip": 10517 }, { "filename": "react-dom-test-utils.production.min.js", @@ -165,8 +165,8 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 103764, - "gzip": 27110 + "size": 103762, + "gzip": 27108 }, { "filename": "react-dom-server.browser.production.min.js", @@ -179,8 +179,8 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 92808, - "gzip": 24792 + "size": 92806, + "gzip": 24789 }, { "filename": "react-dom-server.browser.production.min.js", @@ -207,8 +207,8 @@ "filename": "react-dom-server.node.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 94776, - "gzip": 25351 + "size": 94774, + "gzip": 25349 }, { "filename": "react-dom-server.node.production.min.js", @@ -221,29 +221,29 @@ "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 422274, - "gzip": 91610 + "size": 424539, + "gzip": 92383 }, { "filename": "react-art.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-art", - "size": 92654, - "gzip": 28286 + "size": 92908, + "gzip": 28133 }, { "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 346329, - "gzip": 72375 + "size": 348598, + "gzip": 73299 }, { "filename": "react-art.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-art", - "size": 56356, - "gzip": 17279 + "size": 56579, + "gzip": 17175 }, { "filename": "ReactART-dev.js", @@ -291,29 +291,29 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 354126, - "gzip": 74269 + "size": 356387, + "gzip": 74965 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 56085, - "gzip": 17115 + "size": 56480, + "gzip": 17088 }, { "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 344731, - "gzip": 71384 + "size": 346996, + "gzip": 72227 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 55313, - "gzip": 16741 + "size": 55696, + "gzip": 16727 }, { "filename": "ReactTestRenderer-dev.js", @@ -361,49 +361,49 @@ "filename": "react-noop-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 18834, - "gzip": 5201 + "size": 18663, + "gzip": 5148 }, { "filename": "react-noop-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 6618, - "gzip": 2600 + "size": 6521, + "gzip": 2566 }, { "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 324645, - "gzip": 66734 + "size": 326858, + "gzip": 67568 }, { "filename": "react-reconciler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 48036, - "gzip": 14640 + "size": 48375, + "gzip": 14567 }, { "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 323964, - "gzip": 66489 + "size": 326178, + "gzip": 67326 }, { "filename": "react-reconciler-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 46925, - "gzip": 14421 + "size": 47268, + "gzip": 14446 }, { "filename": "react-reconciler-reflection.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 11327, + "size": 11326, "gzip": 3505 }, { @@ -496,6 +496,195 @@ "packageName": "create-subscription", "size": 2599, "gzip": 1240 + }, + { + "filename": "React-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react", + "size": 47216, + "gzip": 12859 + }, + { + "filename": "React-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react", + "size": 13749, + "gzip": 3815 + }, + { + "filename": "ReactDOM-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-dom", + "size": 635369, + "gzip": 143277 + }, + { + "filename": "ReactDOM-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-dom", + "size": 291114, + "gzip": 53216 + }, + { + "filename": "ReactTestUtils-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-dom", + "size": 37779, + "gzip": 10710 + }, + { + "filename": "ReactDOMUnstableNativeDependencies-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-dom", + "size": 58465, + "gzip": 14911 + }, + { + "filename": "ReactDOMUnstableNativeDependencies-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-dom", + "size": 26974, + "gzip": 5507 + }, + { + "filename": "ReactDOMServer-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-dom", + "size": 96360, + "gzip": 24600 + }, + { + "filename": "ReactDOMServer-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-dom", + "size": 32376, + "gzip": 7965 + }, + { + "filename": "ReactART-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-art", + "size": 357056, + "gzip": 72841 + }, + { + "filename": "ReactART-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-art", + "size": 171040, + "gzip": 28102 + }, + { + "filename": "ReactNativeRenderer-dev.js", + "bundleType": "RN_FB_DEV", + "packageName": "react-native-renderer", + "size": 470028, + "gzip": 100556 + }, + { + "filename": "ReactNativeRenderer-prod.js", + "bundleType": "RN_FB_PROD", + "packageName": "react-native-renderer", + "size": 222916, + "gzip": 37259 + }, + { + "filename": "ReactNativeRenderer-dev.js", + "bundleType": "RN_OSS_DEV", + "packageName": "react-native-renderer", + "size": 469772, + "gzip": 100493 + }, + { + "filename": "ReactNativeRenderer-prod.js", + "bundleType": "RN_OSS_PROD", + "packageName": "react-native-renderer", + "size": 222116, + "gzip": 37129 + }, + { + "filename": "ReactFabric-dev.js", + "bundleType": "RN_FB_DEV", + "packageName": "react-native-renderer", + "size": 452022, + "gzip": 96052 + }, + { + "filename": "ReactFabric-prod.js", + "bundleType": "RN_FB_PROD", + "packageName": "react-native-renderer", + "size": 207365, + "gzip": 34455 + }, + { + "filename": "ReactFabric-dev.js", + "bundleType": "RN_OSS_DEV", + "packageName": "react-native-renderer", + "size": 452057, + "gzip": 96067 + }, + { + "filename": "ReactFabric-prod.js", + "bundleType": "RN_OSS_PROD", + "packageName": "react-native-renderer", + "size": 207401, + "gzip": 34473 + }, + { + "filename": "ReactTestRenderer-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-test-renderer", + "size": 355733, + "gzip": 71836 + }, + { + "filename": "ReactShallowRenderer-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-test-renderer", + "size": 14759, + "gzip": 3631 + }, + { + "filename": "ReactIs-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-is", + "size": 4263, + "gzip": 1220 + }, + { + "filename": "ReactIs-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-is", + "size": 3414, + "gzip": 953 + }, + { + "filename": "react-scheduler.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-scheduler", + "size": 10937, + "gzip": 3771 + }, + { + "filename": "react-scheduler.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-scheduler", + "size": 1721, + "gzip": 873 + }, + { + "filename": "react-scheduler.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-scheduler", + "size": 10741, + "gzip": 3720 + }, + { + "filename": "react-scheduler.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-scheduler", + "size": 1792, + "gzip": 889 } ] } \ No newline at end of file From 9c77ffb444598c32c8f92c8d79e406959a10445b Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Tue, 24 Apr 2018 08:54:31 -0700 Subject: [PATCH 011/277] Dedup conditional in ReactScheduler (#12680) **what is the change?:** We had a condition to set either 'performance.now' or 'Date.now' as the 'now' function. Then later we had another conditional checking again if 'performance.now' was supported, and using it if so, otherwise falling back to 'Date.now'. More efficient to just use the 'now' shortcut defined above. **why make this change?:** Fewer lines, clearer code. **test plan:** Now that we have tests we can run them :) --- .../react-scheduler/src/ReactScheduler.js | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/react-scheduler/src/ReactScheduler.js b/packages/react-scheduler/src/ReactScheduler.js index 362a3f34b7..b15e5f4428 100644 --- a/packages/react-scheduler/src/ReactScheduler.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -100,27 +100,13 @@ if (!ExecutionEnvironment.canUseDOM) { let previousFrameTime = 33; let activeFrameTime = 33; - let frameDeadlineObject; - if (hasNativePerformanceNow) { - frameDeadlineObject = { - didTimeout: false, - timeRemaining() { - // We assume that if we have a performance timer that the rAF callback - // gets a performance timer value. Not sure if this is always true. - const remaining = frameDeadline - performance.now(); - return remaining > 0 ? remaining : 0; - }, - }; - } else { - frameDeadlineObject = { - didTimeout: false, - timeRemaining() { - // Fallback to Date.now() - const remaining = frameDeadline - Date.now(); - return remaining > 0 ? remaining : 0; - }, - }; - } + const frameDeadlineObject = { + didTimeout: false, + timeRemaining() { + const remaining = frameDeadline - now(); + return remaining > 0 ? remaining : 0; + }, + }; // We use the postMessage trick to defer idle work until after the repaint. const messageKey = From ec57d2994156ba5fcf794100968764bf25206629 Mon Sep 17 00:00:00 2001 From: Heaven Date: Fri, 27 Apr 2018 01:39:11 +0800 Subject: [PATCH 012/277] Remove redundant feature flag in the test due to https://github.com/facebook/react/pull/12117 (#12696) --- .../src/events/__tests__/ChangeEventPlugin-test.internal.js | 1 - .../src/events/__tests__/SimpleEventPlugin-test.internal.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js index 570ecf3238..51eb86eedb 100644 --- a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js +++ b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js @@ -475,7 +475,6 @@ describe('ChangeEventPlugin', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableAsyncSubtreeAPI = true; ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactDOM = require('react-dom'); diff --git a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js index 1b39dd1e47..dc6087e1d3 100644 --- a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js +++ b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js @@ -246,7 +246,6 @@ describe('SimpleEventPlugin', function() { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableAsyncSubtreeAPI = true; ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactDOM = require('react-dom'); }); From d883d59863483a1fb9f3dd0455022e4c6e1a41d5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 26 Apr 2018 19:47:34 +0100 Subject: [PATCH 013/277] forwardRef() components should not re-render on deep setState() (#12690) * Add a failing test for forwardRef memoization * Memoize forwardRef props and bail out on strict equality * Bail out only when ref matches the current ref --- .../src/ReactFiberBeginWork.js | 18 +++++++--- .../src/__tests__/forwardRef-test.internal.js | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index f2a8ad1f2f..3c20dceacf 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -169,12 +169,20 @@ export default function( function updateForwardRef(current, workInProgress) { const render = workInProgress.type.render; - const nextChildren = render( - workInProgress.pendingProps, - workInProgress.ref, - ); + const nextProps = workInProgress.pendingProps; + const ref = workInProgress.ref; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (workInProgress.memoizedProps === nextProps) { + const currentRef = current !== null ? current.ref : null; + if (ref === currentRef) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + } + const nextChildren = render(nextProps, ref); reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); return workInProgress.child; } diff --git a/packages/react/src/__tests__/forwardRef-test.internal.js b/packages/react/src/__tests__/forwardRef-test.internal.js index 5780dbd054..a578ae7368 100644 --- a/packages/react/src/__tests__/forwardRef-test.internal.js +++ b/packages/react/src/__tests__/forwardRef-test.internal.js @@ -232,6 +232,39 @@ describe('forwardRef', () => { expect(ref.current).toBe(null); }); + it('should not re-run the render callback on a deep setState', () => { + let inst; + + class Inner extends React.Component { + render() { + ReactNoop.yield('Inner'); + inst = this; + return
; + } + } + + function Middle(props) { + ReactNoop.yield('Middle'); + return ; + } + + const Forward = React.forwardRef((props, ref) => { + ReactNoop.yield('Forward'); + return ; + }); + + function App() { + ReactNoop.yield('App'); + return ; + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['App', 'Forward', 'Middle', 'Inner']); + + inst.setState({}); + expect(ReactNoop.flush()).toEqual(['Inner']); + }); + it('should warn if not provided a callback during creation', () => { expect(() => React.forwardRef(undefined)).toWarnDev( 'forwardRef requires a render function but was given undefined.', From 7c3932857195d0cfba2379875b65f1bd1fae39fd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 26 Apr 2018 20:59:17 +0100 Subject: [PATCH 014/277] Don't bail on new context Provider if a legacy provider rendered above (#12586) * Don't bail on new context Provider if a legacy provider rendered above * Avoid an extra variable --- .../src/ReactFiberBeginWork.js | 8 ++- .../ReactNewContext-test.internal.js | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 3c20dceacf..527e6787d1 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -859,8 +859,10 @@ export default function( const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; + let canBailOnProps = true; if (hasLegacyContextChanged()) { + canBailOnProps = false; // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. } else if (oldProps === newProps) { @@ -893,7 +895,7 @@ export default function( } else { if (oldProps.value === newProps.value) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children) { + if (oldProps.children === newProps.children && canBailOnProps) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -910,7 +912,7 @@ export default function( (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare ) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children) { + if (oldProps.children === newProps.children && canBailOnProps) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -933,7 +935,7 @@ export default function( if (changedBits === 0) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children) { + if (oldProps.children === newProps.children && canBailOnProps) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 0a4763a4fe..db2703d3a7 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -847,6 +847,72 @@ describe('ReactNewContext', () => { expect(ReactNoop.getChildren()).toEqual([span('Child')]); }); + it('provider does not bail out if legacy context changed above', () => { + const Context = React.createContext(0); + + function Child() { + ReactNoop.yield('Child'); + return ; + } + + const children = ; + + class LegacyProvider extends React.Component { + static childContextTypes = { + legacyValue: () => {}, + }; + state = {legacyValue: 1}; + getChildContext() { + return {legacyValue: this.state.legacyValue}; + } + render() { + ReactNoop.yield('LegacyProvider'); + return this.props.children; + } + } + + class App extends React.Component { + state = {value: 1}; + render() { + ReactNoop.yield('App'); + return ( + + {this.props.children} + + ); + } + } + + const legacyProviderRef = React.createRef(); + const appRef = React.createRef(); + + // Initial mount + ReactNoop.render( + + + {children} + + , + ); + expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update App with same value (should bail out) + appRef.current.setState({value: 1}); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update LegacyProvider (should not bail out) + legacyProviderRef.current.setState({value: 1}); + expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update App with same value (should bail out) + appRef.current.setState({value: 1}); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + }); + it('consumer bails out if value is unchanged and something above bailed out', () => { const Context = React.createContext(0); From 045d4f166db6023550b76da6a83fe595a6cdf2f2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 28 Apr 2018 01:52:48 +0100 Subject: [PATCH 015/277] Fix a context propagation bug (#12708) * Fix a context propagation bug * Add a regression test --- .../src/ReactFiberBeginWork.js | 2 + .../ReactNewContext-test.internal.js | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 527e6787d1..76f93beac0 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -838,6 +838,8 @@ export default function( } let sibling = nextFiber.sibling; if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + sibling.return = nextFiber.return; nextFiber = sibling; break; } diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index db2703d3a7..480b4b0021 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -1055,6 +1055,82 @@ describe('ReactNewContext', () => { ReactNoop.flush(); }); + // This is a regression case for https://github.com/facebook/react/issues/12686 + it('does not skip some siblings', () => { + const Context = React.createContext(0); + + class App extends React.Component { + state = { + step: 0, + }; + + render() { + ReactNoop.yield('App'); + return ( + + + {this.state.step > 0 && } + + ); + } + } + + class StaticContent extends React.PureComponent { + render() { + return ( + + + + + + + ); + } + } + + class Indirection extends React.PureComponent { + render() { + return ; + } + } + + function Consumer() { + return ( + + {value => { + ReactNoop.yield('Consumer'); + return ; + }} + + ); + } + + // Initial mount + let inst; + ReactNoop.render( (inst = ref)} />); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([ + span('static 1'), + span('static 2'), + ]); + // Update the first time + inst.setState({step: 1}); + expect(ReactNoop.flush()).toEqual(['App', 'Consumer']); + expect(ReactNoop.getChildren()).toEqual([ + span('static 1'), + span('static 2'), + span(1), + ]); + // Update the second time + inst.setState({step: 2}); + expect(ReactNoop.flush()).toEqual(['App', 'Consumer']); + expect(ReactNoop.getChildren()).toEqual([ + span('static 1'), + span('static 2'), + span(2), + ]); + }); + describe('fuzz test', () => { const Fragment = React.Fragment; const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; From dcc854bcc3c940ca583565ce25200ca618c05bf0 Mon Sep 17 00:00:00 2001 From: Airam Date: Sat, 28 Apr 2018 21:52:30 +0200 Subject: [PATCH 016/277] prevent removing attributes on custom component tags (#12702) --- .../react-dom/src/__tests__/DOMPropertyOperations-test.js | 6 ++++++ packages/react-dom/src/shared/DOMProperty.js | 3 +++ 2 files changed, 9 insertions(+) diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js index aabb3e995f..08874222d5 100644 --- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js @@ -155,5 +155,11 @@ describe('DOMPropertyOperations', () => { expect(container.firstChild.getAttribute('value')).toBe('foo'); expect(container.firstChild.value).toBe('foo'); }); + + it('should not remove attributes for custom component tag', () => { + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(container.firstChild.getAttribute('size')).toBe('5px'); + }); }); }); diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js index dd2c8f6e3a..b01c069dba 100644 --- a/packages/react-dom/src/shared/DOMProperty.js +++ b/packages/react-dom/src/shared/DOMProperty.js @@ -157,6 +157,9 @@ export function shouldRemoveAttribute( ) { return true; } + if (isCustomComponentTag) { + return false; + } if (propertyInfo !== null) { switch (propertyInfo.type) { case BOOLEAN: From 9a9f54720fda4c69e35503b64a215e6235cfe07d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 30 Apr 2018 14:30:37 +0100 Subject: [PATCH 017/277] Remove ES3-specific transforms (#12716) --- .babelrc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.babelrc b/.babelrc index f2c5329116..3879b06aee 100644 --- a/.babelrc +++ b/.babelrc @@ -18,8 +18,6 @@ ["transform-es2015-spread", { "loose": true }], "transform-es2015-parameters", ["transform-es2015-destructuring", { "loose": true }], - ["transform-es2015-block-scoping", { "throwIfClosureRequired": true }], - "transform-es3-member-expression-literals", - "transform-es3-property-literals" + ["transform-es2015-block-scoping", { "throwIfClosureRequired": true }] ] } From 7dd4ca2911d742f9b5c457ce2d010432fa2d6578 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 1 May 2018 01:04:40 +0900 Subject: [PATCH 018/277] Call getDerivedStateFromProps even for setState of ShallowRenderer (#12676) --- packages/react-test-renderer/src/ReactShallowRenderer.js | 3 +-- .../src/__tests__/ReactShallowRenderer-test.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index 131db2d417..49be844e8b 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -186,9 +186,8 @@ class ReactShallowRenderer { this._instance.UNSAFE_componentWillReceiveProps(props, context); } } - - this._updateStateFromStaticLifecycle(props); } + this._updateStateFromStaticLifecycle(props); // Read state after cWRP in case it calls setState const state = this._newState || oldState; diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index 7e60bde003..59ea0d9c69 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -94,7 +94,7 @@ describe('ReactShallowRenderer', () => { const instance = shallowRenderer.getMountedInstance(); instance.setState({}); - expect(logs).toEqual(['shouldComponentUpdate']); + expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); logs.splice(0); From e0ca51a85d7c5b01c1efa7edb40080770d508cad Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 1 May 2018 19:55:06 +0100 Subject: [PATCH 019/277] Make React.forwardRef() components discoverable by TestRenderer traversal (#12725) --- .../react-test-renderer/src/ReactTestRenderer.js | 3 ++- .../__tests__/ReactTestRendererTraversal-test.js | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 977a1f5c68..d3ef7aff4f 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -411,6 +411,7 @@ const validWrapperTypes = new Set([ FunctionalComponent, ClassComponent, HostComponent, + ForwardRef, ]); class ReactTestInstance { @@ -475,6 +476,7 @@ class ReactTestInstance { case FunctionalComponent: case ClassComponent: case HostComponent: + case ForwardRef: children.push(wrapFiber(node)); break; case HostText: @@ -484,7 +486,6 @@ class ReactTestInstance { case ContextProvider: case ContextConsumer: case Mode: - case ForwardRef: descend = true; break; default: diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js index 587a094f8a..e6f3909b0b 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js @@ -37,6 +37,7 @@ describe('ReactTestRendererTraversal', () => { + ); @@ -48,13 +49,17 @@ describe('ReactTestRendererTraversal', () => { const ExampleFn = props => ; const ExampleNull = props => null; + const ExampleForwardRef = React.forwardRef((props, ref) => ( + + )); + it('initializes', () => { const render = ReactTestRenderer.create(); const hasFooProp = node => node.props.hasOwnProperty('foo'); // assert .props, .type and .parent attributes const foo = render.root.find(hasFooProp); - expect(foo.props.children).toHaveLength(7); + expect(foo.props.children).toHaveLength(8); expect(foo.type).toBe(View); expect(render.root.parent).toBe(null); expect(foo.children[0].parent).toBe(foo); @@ -116,14 +121,16 @@ describe('ReactTestRendererTraversal', () => { expect(() => render.root.findByType(ExampleFn)).not.toThrow(); // 1 match expect(() => render.root.findByType(View)).not.toThrow(); // 1 match + expect(() => render.root.findByType(ExampleForwardRef)).not.toThrow(); // 1 match // note: there are clearly multiple in general, but there // is only one being rendered at root node level expect(() => render.root.findByType(ExampleNull)).toThrow(); // 2 matches expect(render.root.findAllByType(ExampleFn)).toHaveLength(1); expect(render.root.findAllByType(View, {deep: false})).toHaveLength(1); - expect(render.root.findAllByType(View)).toHaveLength(7); + expect(render.root.findAllByType(View)).toHaveLength(8); expect(render.root.findAllByType(ExampleNull)).toHaveLength(2); + expect(render.root.findAllByType(ExampleForwardRef)).toHaveLength(1); const nulls = render.root.findAllByType(ExampleNull); expect(nulls[0].findAllByType(View)).toHaveLength(0); @@ -138,17 +145,21 @@ describe('ReactTestRendererTraversal', () => { const foo = 'foo'; const bar = 'bar'; const baz = 'baz'; + const qux = 'qux'; expect(() => render.root.findByProps({foo})).not.toThrow(); // 1 match expect(() => render.root.findByProps({bar})).toThrow(); // >1 matches expect(() => render.root.findByProps({baz})).toThrow(); // >1 matches + expect(() => render.root.findByProps({qux})).not.toThrow(); // 1 match expect(render.root.findAllByProps({foo}, {deep: false})).toHaveLength(1); expect(render.root.findAllByProps({bar}, {deep: false})).toHaveLength(5); expect(render.root.findAllByProps({baz}, {deep: false})).toHaveLength(2); + expect(render.root.findAllByProps({qux}, {deep: false})).toHaveLength(1); expect(render.root.findAllByProps({foo})).toHaveLength(2); expect(render.root.findAllByProps({bar})).toHaveLength(9); expect(render.root.findAllByProps({baz})).toHaveLength(4); + expect(render.root.findAllByProps({qux})).toHaveLength(3); }); }); From 200357596aed51d5a71cac0996e3a0510e507156 Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Tue, 1 May 2018 12:46:17 -0700 Subject: [PATCH 020/277] Add error when running jest directly (#12726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` $ jest FAIL scripts/jest/dont-run-jest-directly.js ● Test suite failed to run Don't run `jest` directly. Run `yarn test` instead. > 1 | throw new Error("Don't run `jest` directly. Run `yarn test` instead."); 2 | at Object. (scripts/jest/dont-run-jest-directly.js:1:96) Test Suites: 1 failed, 1 total Tests: 0 total Snapshots: 0 total Time: 0.866s Ran all test suites. ``` --- package.json | 3 +++ scripts/jest/dont-run-jest-directly.js | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 scripts/jest/dont-run-jest-directly.js diff --git a/package.json b/package.json index 01f0a2781f..9b625ea397 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,9 @@ "devEngines": { "node": "8.x || 9.x" }, + "jest": { + "testRegex": "/scripts/jest/dont-run-jest-directly\\.js$" + }, "scripts": { "build": "npm run version-check && node ./scripts/rollup/build.js", "flow-coverage": "flow-coverage-report --config ./.flowcoverage", diff --git a/scripts/jest/dont-run-jest-directly.js b/scripts/jest/dont-run-jest-directly.js new file mode 100644 index 0000000000..672d07caec --- /dev/null +++ b/scripts/jest/dont-run-jest-directly.js @@ -0,0 +1,3 @@ +'use strict'; + +throw new Error("Don't run `jest` directly. Run `yarn test` instead."); From ad7cd686670d8e519789fb226d8ff9175fb69370 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 1 May 2018 21:04:20 +0100 Subject: [PATCH 021/277] Rename internal property to fix React DevTools (#12727) --- packages/react-reconciler/src/ReactFiberBeginWork.js | 6 ++++-- packages/react-reconciler/src/ReactFiberReconciler.js | 4 +++- packages/react-reconciler/src/ReactFiberUnwindWork.js | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 76f93beac0..ce93bccfdd 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -417,7 +417,7 @@ export default function( if (updateQueue !== null) { const nextProps = workInProgress.pendingProps; const prevState = workInProgress.memoizedState; - const prevChildren = prevState !== null ? prevState.children : null; + const prevChildren = prevState !== null ? prevState.element : null; processUpdateQueue( workInProgress, updateQueue, @@ -426,7 +426,9 @@ export default function( renderExpirationTime, ); const nextState = workInProgress.memoizedState; - const nextChildren = nextState.children; + // Caution: React DevTools currently depends on this property + // being called "element". + const nextChildren = nextState.element; if (nextChildren === prevChildren) { // If the state is the same as before, that's a bailout because we had diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 519003f177..a3f38bc394 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -340,7 +340,9 @@ export default function( } const update = createUpdate(expirationTime); - update.payload = {children: element}; + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element}; callback = callback === undefined ? null : callback; if (callback !== null) { diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index e59324c3c4..05a8a1856d 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -67,7 +67,9 @@ export default function( const update = createUpdate(expirationTime); // Unmount the root by rendering null. update.tag = CaptureUpdate; - update.payload = {children: null}; + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element: null}; const error = errorInfo.value; update.callback = () => { onUncaughtError(error); From 25dda90c1ecb0c662ab06e2c80c1ee31e0ae9d36 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 2 May 2018 16:35:16 +0100 Subject: [PATCH 022/277] Mark context consumers with PerformedWork effect on render (#12729) * Mark new component types with PerformedWork effect * Don't do it for ForwardRef Since this has some overhead and ForwardRef is likely going to be used around context, let's skip it. We don't highlight ForwardRef alone in DevTools anyway. --- packages/react-reconciler/src/ReactFiberBeginWork.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index ce93bccfdd..5aa7ffc446 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1023,6 +1023,8 @@ export default function( } const newChildren = render(newValue); + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; reconcileChildren(current, workInProgress, newChildren); return workInProgress.child; } From 0bf24cc83eb8a899c73a96f00c2e63d2db680129 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 8 May 2018 09:31:33 +0900 Subject: [PATCH 023/277] setState returning null and undefined is no-op on the ShallowRenderer (#12756) --- .../src/ReactShallowRenderer.js | 5 ++++ .../__tests__/ReactShallowRenderer-test.js | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index 49be844e8b..9cafeeb2f1 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -301,6 +301,11 @@ class Updater { partialState = partialState(currentState, publicInstance.props); } + // Null and undefined are treated as no-ops. + if (partialState === null || partialState === undefined) { + return; + } + this._renderer._newState = { ...currentState, ...partialState, diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index 59ea0d9c69..b20f316d9e 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -1305,4 +1305,32 @@ describe('ReactShallowRenderer', () => { 'UNSAFE_componentWillUpdate', ]); }); + + it('should stop the upade when setState returns null or undefined', () => { + const log = []; + let instance; + class Component extends React.Component { + constructor(props) { + super(props); + this.state = { + count: 0, + }; + } + render() { + log.push('render'); + instance = this; + return null; + } + } + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + log.length = 0; + instance.setState(() => null); + instance.setState(() => undefined); + instance.setState(null); + instance.setState(undefined); + expect(log).toEqual([]); + instance.setState(state => ({count: state.count + 1})); + expect(log).toEqual(['render']); + }); }); From 3fb8be5c30d8f8ec01a4213eb621d5e7c1a6295c Mon Sep 17 00:00:00 2001 From: bee0060 Date: Tue, 8 May 2018 08:46:42 +0800 Subject: [PATCH 024/277] Minor fix params description for addPercent function (#12669) --- dangerfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dangerfile.js b/dangerfile.js index c3ac638d71..a892b10b05 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -53,7 +53,8 @@ function generateMDTable(headers, body) { /** * Generates a user-readable string from a percentage change - * @param {string[]} headers + * @param {number} change + * @param {boolean} includeEmoji */ function addPercent(change, includeEmoji) { if (!isFinite(change)) { From a9abd27e4f5aa1e68bd6035be901299327279ee2 Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Wed, 9 May 2018 15:28:13 -0700 Subject: [PATCH 025/277] [schedule] Support multiple callbacks in scheduler (#12746) * Support using id to cancel scheduled callback **what is the change?:** see title **why make this change?:** Once we support multiple callbacks you will need to use the id to specify which callback you mean. **test plan:** Added a test, ran all tests, lint, etc. * ran prettier * fix lint * Use object for storing callback info in scheduler * Wrap initial test in a describe block * Support multiple callbacks in `ReactScheduler` **what is the change?:** We keep a queue of callbacks instead of just one at a time, and call them in order first by their timeoutTime and then by the order which they were scheduled in. **why make this change?:** We plan on using this module to coordinate JS outside of React, so we will need to schedule more than one callback at a time. **test plan:** Added a boatload of shiny new tests. :) Plus ran all the old ones. NOTE: The tests do not yet cover the vital logic of callbacks timing out, and later commits will add the missing test coverage. * Heuristic to avoid looking for timed out callbacks when none timed out **what is the change?:** Tracks the current soonest timeOut time for all scheduled callbacks. **why make this change?:** We were checking every scheduled callback to see if it timed out on every tick. It's more efficient to skip that O(n) check if we know that none have timed out. **test plan:** Ran existing tests. Will write new tests to cover timeout behavior in more detail soon. * Put multiple callback support under a disabled feature flag **what is the change?:** See title **why make this change?:** We don't have error handling in place yet, so should maintain the old behavior until that is in place. But want to get this far to continue making incremental changes. **test plan:** Updated and ran tests. * Hide support for multiple callbacks under a feature flag **what is the change?:** see title **why make this change?:** We haven't added error handling yet, so should not expose this feature. **test plan:** Ran all tests, temporarily split out the tests for multiple callbacks into separate file. Will recombine once we remove the flag. * Fix nits from code review See comments on https://github.com/facebook/react/pull/12743 * update checklist in comments * Remove nested loop which calls additional timed out callbacks **what is the change?:** We used to re-run any callbacks which time out whilst other callbacks are running, but now we will only check once for timed out callbacks then then run them. **why make this change?:** To simplify the code and the behavior of this module. **test plan:** Ran all existing tests. * Remove feature flag **what is the change?:** see title **why make this change?:** Because only React is using this, and it sounds like async. rendering won't hit any different behavior due to these changes. **test plan:** Existing tests pass, and this allowed us to recombine all tests to run in both 'test' and 'test-build' modes. * remove outdated file * fix typo --- .../react-scheduler/src/ReactScheduler.js | 172 +++++++++++++----- .../src/__tests__/ReactScheduler-test.js | 171 ++++++++++++++++- .../shared/forks/ReactFeatureFlags.www.js | 4 +- 3 files changed, 293 insertions(+), 54 deletions(-) diff --git a/packages/react-scheduler/src/ReactScheduler.js b/packages/react-scheduler/src/ReactScheduler.js index b15e5f4428..2b0fa9f171 100644 --- a/packages/react-scheduler/src/ReactScheduler.js +++ b/packages/react-scheduler/src/ReactScheduler.js @@ -14,8 +14,8 @@ * control than requestAnimationFrame and requestIdleCallback. * Current TODO items: * X- Pull out the rIC polyfill built into React - * - Initial test coverage - * - Support for multiple callbacks + * X- Initial test coverage + * X- Support for multiple callbacks * - Support for two priorities; serial and deferred * - Better test coverage * - Better docblock @@ -31,6 +31,11 @@ // The frame rate is dynamically adjusted. import type {Deadline} from 'react-reconciler'; +type CallbackConfigType = {| + scheduledCallback: Deadline => void, + timeoutTime: number, + callbackId: number, // used for cancelling +|}; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import warning from 'fbjs/lib/warning'; @@ -85,12 +90,31 @@ if (!ExecutionEnvironment.canUseDOM) { clearTimeout(timeoutID); }; } else { - // Always polyfill requestIdleCallback and cancelIdleCallback + // We keep callbacks in a queue. + // Calling rIC will push in a new callback at the end of the queue. + // When we get idle time, callbacks are removed from the front of the queue + // and called. + const pendingCallbacks: Array = []; + + let callbackIdCounter = 0; + const getCallbackId = function(): number { + callbackIdCounter++; + return callbackIdCounter; + }; + + // When a callback is scheduled, we register it by adding it's id to this + // object. + // If the user calls 'cIC' with the id of that callback, it will be + // unregistered by removing the id from this object. + // Then we skip calling any callback which is not registered. + // This means cancelling is an O(1) time complexity instead of O(n). + const registeredCallbackIds: {[number]: boolean} = {}; + + // We track what the next soonest timeoutTime is, to be able to quickly tell + // if none of the scheduled callbacks have timed out. + let nextSoonestTimeoutTime = -1; - let scheduledRICCallback = null; let isIdleScheduled = false; - let timeoutTime = -1; - let isAnimationFrameScheduled = false; let frameDeadline = 0; @@ -100,7 +124,7 @@ if (!ExecutionEnvironment.canUseDOM) { let previousFrameTime = 33; let activeFrameTime = 33; - const frameDeadlineObject = { + const frameDeadlineObject: Deadline = { didTimeout: false, timeRemaining() { const remaining = frameDeadline - now(); @@ -108,6 +132,67 @@ if (!ExecutionEnvironment.canUseDOM) { }, }; + const safelyCallScheduledCallback = function(callback, callbackId) { + if (!registeredCallbackIds[callbackId]) { + // ignore cancelled callbacks + return; + } + try { + callback(frameDeadlineObject); + // Avoid using 'catch' to keep errors easy to debug + } finally { + // always clean up the callbackId, even if the callback throws + delete registeredCallbackIds[callbackId]; + } + }; + + /** + * Checks for timed out callbacks, runs them, and then checks again to see if + * any more have timed out. + * Keeps doing this until there are none which have currently timed out. + */ + const callTimedOutCallbacks = function() { + if (pendingCallbacks.length === 0) { + return; + } + + const currentTime = now(); + // TODO: this would be more efficient if deferred callbacks are stored in + // min heap. + // Or in a linked list with links for both timeoutTime order and insertion + // order. + // For now an easy compromise is the current approach: + // Keep a pointer to the soonest timeoutTime, and check that first. + // If it has not expired, we can skip traversing the whole list. + // If it has expired, then we step through all the callbacks. + if (nextSoonestTimeoutTime === -1 || nextSoonestTimeoutTime > currentTime) { + // We know that none of them have timed out yet. + return; + } + nextSoonestTimeoutTime = -1; // we will reset it below + + // keep checking until we don't find any more timed out callbacks + frameDeadlineObject.didTimeout = true; + for (let i = 0, len = pendingCallbacks.length; i < len; i++) { + const currentCallbackConfig = pendingCallbacks[i]; + const timeoutTime = currentCallbackConfig.timeoutTime; + if (timeoutTime !== -1 && timeoutTime <= currentTime) { + // it has timed out! + // call it + const callback = currentCallbackConfig.scheduledCallback; + safelyCallScheduledCallback(callback, timeoutTime); + } else { + if ( + timeoutTime !== -1 && + (nextSoonestTimeoutTime === -1 || + timeoutTime < nextSoonestTimeoutTime) + ) { + nextSoonestTimeoutTime = timeoutTime; + } + } + } + }; + // We use the postMessage trick to defer idle work until after the repaint. const messageKey = '__reactIdleCallback$' + @@ -119,36 +204,30 @@ if (!ExecutionEnvironment.canUseDOM) { return; } + if (pendingCallbacks.length === 0) { + return; + } isIdleScheduled = false; - const currentTime = now(); - if (frameDeadline - currentTime <= 0) { - // There's no time left in this idle period. Check if the callback has - // a timeout and whether it's been exceeded. - if (timeoutTime !== -1 && timeoutTime <= currentTime) { - // Exceeded the timeout. Invoke the callback even though there's no - // time left. - frameDeadlineObject.didTimeout = true; - } else { - // No timeout. - if (!isAnimationFrameScheduled) { - // Schedule another animation callback so we retry later. - isAnimationFrameScheduled = true; - requestAnimationFrame(animationTick); - } - // Exit without invoking the callback. - return; - } - } else { - // There's still time left in this idle period. - frameDeadlineObject.didTimeout = false; - } + // First call anything which has timed out, until we have caught up. + callTimedOutCallbacks(); - timeoutTime = -1; - const callback = scheduledRICCallback; - scheduledRICCallback = null; - if (callback !== null) { - callback(frameDeadlineObject); + let currentTime = now(); + // Next, as long as we have idle time, try calling more callbacks. + while (frameDeadline - currentTime > 0 && pendingCallbacks.length > 0) { + const latestCallbackConfig = pendingCallbacks.shift(); + frameDeadlineObject.didTimeout = false; + const latestCallback = latestCallbackConfig.scheduledCallback; + const newCallbackId = latestCallbackConfig.callbackId; + safelyCallScheduledCallback(latestCallback, newCallbackId); + currentTime = now(); + } + if (pendingCallbacks.length > 0) { + if (!isAnimationFrameScheduled) { + // Schedule another animation callback so we retry later. + isAnimationFrameScheduled = true; + requestAnimationFrame(animationTick); + } } }; // Assumes that we have addEventListener in this environment. Might need @@ -190,12 +269,23 @@ if (!ExecutionEnvironment.canUseDOM) { callback: (deadline: Deadline) => void, options?: {timeout: number}, ): number { - // This assumes that we only schedule one callback at a time because that's - // how Fiber uses it. - scheduledRICCallback = callback; + let timeoutTime = -1; if (options != null && typeof options.timeout === 'number') { timeoutTime = now() + options.timeout; } + if (timeoutTime > nextSoonestTimeoutTime) { + nextSoonestTimeoutTime = timeoutTime; + } + + const newCallbackId = getCallbackId(); + const scheduledCallbackConfig = { + scheduledCallback: callback, + callbackId: newCallbackId, + timeoutTime, + }; + pendingCallbacks.push(scheduledCallbackConfig); + + registeredCallbackIds[newCallbackId] = true; if (!isAnimationFrameScheduled) { // If rAF didn't already schedule one, we need to schedule a frame. // TODO: If this rAF doesn't materialize because the browser throttles, we @@ -204,13 +294,11 @@ if (!ExecutionEnvironment.canUseDOM) { isAnimationFrameScheduled = true; requestAnimationFrame(animationTick); } - return 0; + return newCallbackId; }; - cIC = function() { - scheduledRICCallback = null; - isIdleScheduled = false; - timeoutTime = -1; + cIC = function(callbackId: number) { + delete registeredCallbackIds[callbackId]; }; } diff --git a/packages/react-scheduler/src/__tests__/ReactScheduler-test.js b/packages/react-scheduler/src/__tests__/ReactScheduler-test.js index 1ed60b0fb3..41bbd4d496 100644 --- a/packages/react-scheduler/src/__tests__/ReactScheduler-test.js +++ b/packages/react-scheduler/src/__tests__/ReactScheduler-test.js @@ -41,16 +41,167 @@ describe('ReactScheduler', () => { ReactScheduler = require('react-scheduler'); }); - it('rIC calls the callback within the frame when not blocked', () => { - const {rIC} = ReactScheduler; - const cb = jest.fn(); - rIC(cb); - jest.runAllTimers(); - expect(cb.mock.calls.length).toBe(1); - // should have ... TODO details on what we expect - expect(cb.mock.calls[0][0].didTimeout).toBe(false); - expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number'); + describe('rIC', () => { + it('calls the callback within the frame when not blocked', () => { + const {rIC} = ReactScheduler; + const cb = jest.fn(); + rIC(cb); + jest.runAllTimers(); + expect(cb.mock.calls.length).toBe(1); + // should not have timed out and should include a timeRemaining method + expect(cb.mock.calls[0][0].didTimeout).toBe(false); + expect(typeof cb.mock.calls[0][0].timeRemaining()).toBe('number'); + }); + + describe('with multiple callbacks', () => { + it('accepts multiple callbacks and calls within frame when not blocked', () => { + const {rIC} = ReactScheduler; + const callbackLog = []; + const callbackA = jest.fn(() => callbackLog.push('A')); + const callbackB = jest.fn(() => callbackLog.push('B')); + rIC(callbackA); + // initially waits to call the callback + expect(callbackLog).toEqual([]); + // waits while second callback is passed + rIC(callbackB); + expect(callbackLog).toEqual([]); + // after a delay, calls as many callbacks as it has time for + jest.runAllTimers(); + expect(callbackLog).toEqual(['A', 'B']); + // callbackA should not have timed out and should include a timeRemaining method + expect(callbackA.mock.calls[0][0].didTimeout).toBe(false); + expect(typeof callbackA.mock.calls[0][0].timeRemaining()).toBe( + 'number', + ); + // callbackA should not have timed out and should include a timeRemaining method + expect(callbackB.mock.calls[0][0].didTimeout).toBe(false); + expect(typeof callbackB.mock.calls[0][0].timeRemaining()).toBe( + 'number', + ); + }); + + it( + 'schedules callbacks in correct order and' + + 'keeps calling them if there is time', + () => { + const {rIC} = ReactScheduler; + const callbackLog = []; + const callbackA = jest.fn(() => { + callbackLog.push('A'); + rIC(callbackC); + }); + const callbackB = jest.fn(() => { + callbackLog.push('B'); + }); + const callbackC = jest.fn(() => { + callbackLog.push('C'); + }); + + rIC(callbackA); + // initially waits to call the callback + expect(callbackLog).toEqual([]); + // continues waiting while B is scheduled + rIC(callbackB); + expect(callbackLog).toEqual([]); + // after a delay, calls the scheduled callbacks, + // and also calls new callbacks scheduled by current callbacks + jest.runAllTimers(); + expect(callbackLog).toEqual(['A', 'B', 'C']); + }, + ); + + it('schedules callbacks in correct order when callbacks have many nested rIC calls', () => { + const {rIC} = ReactScheduler; + const callbackLog = []; + const callbackA = jest.fn(() => { + callbackLog.push('A'); + rIC(callbackC); + rIC(callbackD); + }); + const callbackB = jest.fn(() => { + callbackLog.push('B'); + rIC(callbackE); + rIC(callbackF); + }); + const callbackC = jest.fn(() => { + callbackLog.push('C'); + }); + const callbackD = jest.fn(() => { + callbackLog.push('D'); + }); + const callbackE = jest.fn(() => { + callbackLog.push('E'); + }); + const callbackF = jest.fn(() => { + callbackLog.push('F'); + }); + + rIC(callbackA); + rIC(callbackB); + // initially waits to call the callback + expect(callbackLog).toEqual([]); + // while flushing callbacks, calls as many as it has time for + jest.runAllTimers(); + expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']); + }); + + it('schedules callbacks in correct order when they use rIC to schedule themselves', () => { + const {rIC} = ReactScheduler; + const callbackLog = []; + let callbackAIterations = 0; + const callbackA = jest.fn(() => { + if (callbackAIterations < 1) { + rIC(callbackA); + } + callbackLog.push('A' + callbackAIterations); + callbackAIterations++; + }); + const callbackB = jest.fn(() => callbackLog.push('B')); + + rIC(callbackA); + // initially waits to call the callback + expect(callbackLog).toEqual([]); + rIC(callbackB); + expect(callbackLog).toEqual([]); + // after a delay, calls the latest callback passed + jest.runAllTimers(); + expect(callbackLog).toEqual(['A0', 'B', 'A1']); + }); + }); }); - // TODO: test cIC and now + describe('cIC', () => { + it('cancels the scheduled callback', () => { + const {rIC, cIC} = ReactScheduler; + const cb = jest.fn(); + const callbackId = rIC(cb); + expect(cb.mock.calls.length).toBe(0); + cIC(callbackId); + jest.runAllTimers(); + expect(cb.mock.calls.length).toBe(0); + }); + + describe('with multiple callbacks', () => { + it('when one callback cancels the next one', () => { + const {rIC, cIC} = ReactScheduler; + const callbackLog = []; + let callbackBId; + const callbackA = jest.fn(() => { + callbackLog.push('A'); + cIC(callbackBId); + }); + const callbackB = jest.fn(() => callbackLog.push('B')); + rIC(callbackA); + callbackBId = rIC(callbackB); + // Initially doesn't call anything + expect(callbackLog).toEqual([]); + jest.runAllTimers(); + // B should not get called because A cancelled B + expect(callbackLog).toEqual(['A']); + expect(callbackB.mock.calls.length).toBe(0); + }); + }); + }); + + // TODO: test 'now' }); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 9153819ce5..df3560c6fb 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -12,11 +12,11 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www'; // Re-export dynamic flags from the www version. export const { - enableGetDerivedStateFromCatch, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, - warnAboutDeprecatedLifecycles, + enableGetDerivedStateFromCatch, replayFailedUnitOfWorkWithInvokeGuardedCallback, + warnAboutDeprecatedLifecycles, } = require('ReactFeatureFlags'); // The rest of the flags are static for better dead code elimination. From fc3777b1fe295fd2661f1974f5587d214791f04b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 10 May 2018 15:25:32 -0700 Subject: [PATCH 026/277] Add Profiler component for collecting new render timing info (#12745) Add a new component type, Profiler, that can be used to collect new render time metrics. Since this is a new, experimental API, it will be exported as React.unstable_Profiler initially. Most of the functionality for this component has been added behind a feature flag, enableProfileModeMetrics. When the feature flag is disabled, the component will just render its children with no additional behavior. When the flag is enabled, React will also collect timing information and pass it to the onRender function (as described below). --- .../src/server/ReactPartialRenderer.js | 2 + packages/react-is/src/ReactIs.js | 6 + .../react-is/src/__tests__/ReactIs-test.js | 14 + packages/react-reconciler/src/ReactFiber.js | 54 +- .../src/ReactFiberBeginWork.js | 50 + .../src/ReactFiberCommitWork.js | 26 +- .../src/ReactFiberCompleteWork.js | 11 + .../src/ReactFiberScheduler.js | 117 ++- .../src/ReactFiberUnwindWork.js | 26 +- .../src/ReactProfilerTimer.js | 144 +++ .../react-reconciler/src/ReactTypeOfMode.js | 7 +- .../ReactIncrementalPerf-test.internal.js | 18 +- ...ReactIncrementalPerf-test.internal.js.snap | 7 +- .../src/ReactTestRenderer.js | 21 +- .../__tests__/ReactShallowRenderer-test.js | 26 + .../ReactTestRendererTraversal-test.js | 4 +- packages/react/src/React.js | 6 +- .../__tests__/ReactProfiler-test.internal.js | 862 ++++++++++++++++++ .../ReactProfiler-test.internal.js.snap | 65 ++ packages/shared/ReactFeatureFlags.js | 3 + packages/shared/ReactSymbols.js | 3 + packages/shared/ReactTypeOfWork.js | 4 +- .../ReactFeatureFlags.native-fabric-fb.js | 1 + .../ReactFeatureFlags.native-fabric-oss.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + packages/shared/getComponentName.js | 15 +- packages/shared/isValidElementType.js | 8 +- 31 files changed, 1452 insertions(+), 54 deletions(-) create mode 100644 packages/react-reconciler/src/ReactProfilerTimer.js create mode 100644 packages/react/src/__tests__/ReactProfiler-test.internal.js create mode 100644 packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index ec26eba2c1..d0cef9c60a 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -34,6 +34,7 @@ import { REACT_CALL_TYPE, REACT_RETURN_TYPE, REACT_PORTAL_TYPE, + REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; @@ -811,6 +812,7 @@ class ReactDOMServerRenderer { switch (elementType) { case REACT_STRICT_MODE_TYPE: case REACT_ASYNC_MODE_TYPE: + case REACT_PROFILER_TYPE: case REACT_FRAGMENT_TYPE: { const nextChildren = toArray( ((nextChild: any): ReactElement).props.children, diff --git a/packages/react-is/src/ReactIs.js b/packages/react-is/src/ReactIs.js index 4418759a9b..e94599474d 100644 --- a/packages/react-is/src/ReactIs.js +++ b/packages/react-is/src/ReactIs.js @@ -16,6 +16,7 @@ import { REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, + REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, } from 'shared/ReactSymbols'; @@ -32,6 +33,7 @@ export function typeOf(object: any) { switch (type) { case REACT_ASYNC_MODE_TYPE: case REACT_FRAGMENT_TYPE: + case REACT_PROFILER_TYPE: case REACT_STRICT_MODE_TYPE: return type; default: @@ -60,6 +62,7 @@ export const ContextProvider = REACT_PROVIDER_TYPE; export const Element = REACT_ELEMENT_TYPE; export const ForwardRef = REACT_FORWARD_REF_TYPE; export const Fragment = REACT_FRAGMENT_TYPE; +export const Profiler = REACT_PROFILER_TYPE; export const Portal = REACT_PORTAL_TYPE; export const StrictMode = REACT_STRICT_MODE_TYPE; @@ -87,6 +90,9 @@ export function isForwardRef(object: any) { export function isFragment(object: any) { return typeOf(object) === REACT_FRAGMENT_TYPE; } +export function isProfiler(object: any) { + return typeOf(object) === REACT_PROFILER_TYPE; +} export function isPortal(object: any) { return typeOf(object) === REACT_PORTAL_TYPE; } diff --git a/packages/react-is/src/__tests__/ReactIs-test.js b/packages/react-is/src/__tests__/ReactIs-test.js index e4ce5074b1..04c7cec745 100644 --- a/packages/react-is/src/__tests__/ReactIs-test.js +++ b/packages/react-is/src/__tests__/ReactIs-test.js @@ -145,4 +145,18 @@ describe('ReactIs', () => { expect(ReactIs.isStrictMode()).toBe(false); expect(ReactIs.isStrictMode(
)).toBe(false); }); + + it('should identify profile root', () => { + expect( + ReactIs.typeOf(), + ).toBe(ReactIs.Profiler); + expect( + ReactIs.isProfiler( + , + ), + ).toBe(true); + expect(ReactIs.isProfiler({type: ReactIs.unstable_Profiler})).toBe(false); + expect(ReactIs.isProfiler()).toBe(false); + expect(ReactIs.isProfiler(
)).toBe(false); + }); }); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 393cecb527..51c4d52fe5 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -15,6 +15,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {UpdateQueue} from './ReactUpdateQueue'; import invariant from 'fbjs/lib/invariant'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import {NoEffect} from 'shared/ReactTypeOfSideEffect'; import { IndeterminateComponent, @@ -30,17 +31,19 @@ import { Mode, ContextProvider, ContextConsumer, + Profiler, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; import {NoWork} from './ReactFiberExpirationTime'; -import {NoContext, AsyncMode, StrictMode} from './ReactTypeOfMode'; +import {NoContext, AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode'; import { REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_RETURN_TYPE, REACT_CALL_TYPE, REACT_STRICT_MODE_TYPE, + REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, REACT_ASYNC_MODE_TYPE, @@ -150,6 +153,10 @@ export type Fiber = {| // memory if we need to. alternate: Fiber | null, + // Profiling metrics + selfBaseTime?: number, + treeBaseTime?: number, + // Conceptual aliases // workInProgress : Fiber -> alternate The alternate used for reuse happens // to be the same as work in progress. @@ -204,6 +211,11 @@ function FiberNode( this.alternate = null; + if (enableProfilerTimer) { + this.selfBaseTime = 0; + this.treeBaseTime = 0; + } + if (__DEV__) { this._debugID = debugCounter++; this._debugSource = null; @@ -298,6 +310,11 @@ export function createWorkInProgress( workInProgress.index = current.index; workInProgress.ref = current.ref; + if (enableProfilerTimer) { + workInProgress.selfBaseTime = current.selfBaseTime; + workInProgress.treeBaseTime = current.treeBaseTime; + } + return workInProgress; } @@ -343,6 +360,8 @@ export function createFiberFromElement( fiberTag = Mode; mode |= StrictMode; break; + case REACT_PROFILER_TYPE: + return createFiberFromProfiler(pendingProps, mode, expirationTime, key); case REACT_CALL_TYPE: fiberTag = CallComponent; break; @@ -440,6 +459,35 @@ export function createFiberFromFragment( return fiber; } +export function createFiberFromProfiler( + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +): Fiber { + if (__DEV__) { + if ( + typeof pendingProps.id !== 'string' || + typeof pendingProps.onRender !== 'function' + ) { + invariant( + false, + 'Profiler must specify an "id" string and "onRender" function as props', + ); + } + } + + const fiber = createFiber(Profiler, pendingProps, key, mode | ProfileMode); + fiber.type = REACT_PROFILER_TYPE; + fiber.expirationTime = expirationTime; + fiber.stateNode = { + duration: 0, + startTime: 0, + }; + + return fiber; +} + export function createFiberFromText( content: string, mode: TypeOfMode, @@ -509,6 +557,10 @@ export function assignFiberPropertiesInDEV( target.lastEffect = source.lastEffect; target.expirationTime = source.expirationTime; target.alternate = source.alternate; + if (enableProfilerTimer) { + target.selfBaseTime = source.selfBaseTime; + target.treeBaseTime = source.treeBaseTime; + } target._debugID = source._debugID; target._debugSource = source._debugSource; target._debugOwner = source._debugOwner; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 5aa7ffc446..2c80e26ffb 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -16,6 +16,7 @@ import type {NewContext} from './ReactFiberNewContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {ProfilerTimer} from './ReactProfilerTimer'; import checkPropTypes from 'prop-types/checkPropTypes'; import { @@ -34,6 +35,7 @@ import { Mode, ContextProvider, ContextConsumer, + Profiler, } from 'shared/ReactTypeOfWork'; import { NoEffect, @@ -42,12 +44,14 @@ import { ContentReset, Ref, DidCapture, + Update, } from 'shared/ReactTypeOfSideEffect'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import { enableGetDerivedStateFromCatch, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, + enableProfilerTimer, } from 'shared/ReactFeatureFlags'; import invariant from 'fbjs/lib/invariant'; import getComponentName from 'shared/getComponentName'; @@ -88,6 +92,7 @@ export default function( hydrationContext: HydrationContext, scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, + profilerTimer: ProfilerTimer, ) { const {shouldSetTextContent, shouldDeprioritizeSubtree} = config; @@ -95,6 +100,11 @@ export default function( const {pushProvider} = newContext; + const { + markActualRenderTimeStarted, + stopBaseRenderTimerIfRunning, + } = profilerTimer; + const { getMaskedContext, getUnmaskedContext, @@ -215,6 +225,25 @@ export default function( return workInProgress.child; } + function updateProfiler(current, workInProgress) { + const nextProps = workInProgress.pendingProps; + if (enableProfilerTimer) { + // Start render timer here and push start time onto queue + markActualRenderTimeStarted(workInProgress); + + // Let the "complete" phase know to stop the timer, + // And the scheduler to record the measured time. + workInProgress.effectTag |= Update; + } + if (workInProgress.memoizedProps === nextProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + const nextChildren = nextProps.children; + reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); + return workInProgress.child; + } + function markRef(current: Fiber | null, workInProgress: Fiber) { const ref = workInProgress.ref; if ( @@ -344,6 +373,10 @@ export default function( // the new API. // TODO: Warn in a future release. nextChildren = null; + + if (enableProfilerTimer) { + stopBaseRenderTimerIfRunning(); + } } else { if (__DEV__) { ReactDebugCurrentFiber.setCurrentPhase('render'); @@ -1054,6 +1087,11 @@ export default function( ): Fiber | null { cancelWorkTimer(workInProgress); + if (enableProfilerTimer) { + // Don't update "base" render times for bailouts. + stopBaseRenderTimerIfRunning(); + } + // TODO: We should ideally be able to bail out early if the children have no // more work to do. However, since we don't have a separation of this // Fiber's priority and its children yet - we don't know without doing lots @@ -1075,6 +1113,11 @@ export default function( function bailoutOnLowPriority(current, workInProgress) { cancelWorkTimer(workInProgress); + if (enableProfilerTimer) { + // Don't update "base" render times for bailouts. + stopBaseRenderTimerIfRunning(); + } + // TODO: Handle HostComponent tags here as well and call pushHostContext()? // See PR 8590 discussion for context switch (workInProgress.tag) { @@ -1093,6 +1136,11 @@ export default function( case ContextProvider: pushProvider(workInProgress); break; + case Profiler: + if (enableProfilerTimer) { + markActualRenderTimeStarted(workInProgress); + } + break; } // TODO: What if this is currently in progress? // How can that happen? How is this not being cloned? @@ -1173,6 +1221,8 @@ export default function( return updateFragment(current, workInProgress); case Mode: return updateMode(current, workInProgress); + case Profiler: + return updateProfiler(current, workInProgress); case ContextProvider: return updateContextProvider( current, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index a375490297..686a49219b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -17,6 +17,7 @@ import { enableMutatingReconciler, enableNoopReconciler, enablePersistentReconciler, + enableProfilerTimer, } from 'shared/ReactFeatureFlags'; import { ClassComponent, @@ -25,13 +26,14 @@ import { HostText, HostPortal, CallComponent, + Profiler, } from 'shared/ReactTypeOfWork'; import ReactErrorUtils from 'shared/ReactErrorUtils'; import { - Placement, - Update, ContentReset, + Placement, Snapshot, + Update, } from 'shared/ReactTypeOfSideEffect'; import {commitUpdateQueue} from './ReactUpdateQueue'; import invariant from 'fbjs/lib/invariant'; @@ -308,6 +310,10 @@ export default function( // We have no life-cycles associated with portals. return; } + case Profiler: { + // We have no life-cycles associated with Profiler. + return; + } default: { invariant( false, @@ -814,6 +820,22 @@ export default function( case HostRoot: { return; } + case Profiler: { + if (enableProfilerTimer) { + const onRender = finishedWork.memoizedProps.onRender; + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.stateNode.duration, + finishedWork.treeBaseTime, + ); + + // Reset actualTime after successful commit. + // By default, we append to this time to account for errors and pauses. + finishedWork.stateNode.duration = 0; + } + return; + } default: { invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index aec3bc17c6..e9752e38da 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -15,11 +15,13 @@ import type {LegacyContext} from './ReactFiberContext'; import type {NewContext} from './ReactFiberNewContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; +import type {ProfilerTimer} from './ReactProfilerTimer'; import { enableMutatingReconciler, enablePersistentReconciler, enableNoopReconciler, + enableProfilerTimer, } from 'shared/ReactFeatureFlags'; import { IndeterminateComponent, @@ -37,6 +39,7 @@ import { ForwardRef, Fragment, Mode, + Profiler, } from 'shared/ReactTypeOfWork'; import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; @@ -49,6 +52,7 @@ export default function( legacyContext: LegacyContext, newContext: NewContext, hydrationContext: HydrationContext, + profilerTimer: ProfilerTimer, ) { const { createInstance, @@ -67,6 +71,8 @@ export default function( popHostContainer, } = hostContext; + const {recordElapsedActualRenderTime} = profilerTimer; + const { popContextProvider: popLegacyContextProvider, popTopLevelContextObject: popTopLevelLegacyContextObject, @@ -591,6 +597,11 @@ export default function( return null; case Mode: return null; + case Profiler: + if (enableProfilerTimer) { + recordElapsedActualRenderTime(workInProgress); + } + return null; case HostPortal: popHostContainer(workInProgress); updateHostContainer(workInProgress); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 1a9846b0bd..fde57fd5a4 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -40,10 +40,12 @@ import { HostPortal, } from 'shared/ReactTypeOfWork'; import { + enableProfilerTimer, enableUserTimingAPI, - warnAboutDeprecatedLifecycles, replayFailedUnitOfWorkWithInvokeGuardedCallback, + warnAboutDeprecatedLifecycles, } from 'shared/ReactFeatureFlags'; +import {createProfilerTimer} from './ReactProfilerTimer'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; @@ -85,7 +87,7 @@ import { expirationTimeToMs, computeExpirationBucket, } from './ReactFiberExpirationTime'; -import {AsyncMode} from './ReactTypeOfMode'; +import {AsyncMode, ProfileMode} from './ReactTypeOfMode'; import ReactFiberLegacyContext from './ReactFiberContext'; import ReactFiberNewContext from './ReactFiberNewContext'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; @@ -158,10 +160,18 @@ if (__DEV__) { export default function( config: HostConfig, ) { + const { + now, + scheduleDeferredCallback, + cancelDeferredCallback, + prepareForCommit, + resetAfterCommit, + } = config; const stack = ReactFiberStack(); const hostContext = ReactFiberHostContext(config, stack); const legacyContext = ReactFiberLegacyContext(stack); const newContext = ReactFiberNewContext(stack); + const profilerTimer = createProfilerTimer(now); const {popHostContext, popHostContainer} = hostContext; const { popTopLevelContextObject: popTopLevelLegacyContextObject, @@ -179,6 +189,7 @@ export default function( hydrationContext, scheduleWork, computeExpirationForFiber, + profilerTimer, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -186,6 +197,7 @@ export default function( legacyContext, newContext, hydrationContext, + profilerTimer, ); const { throwException, @@ -194,6 +206,7 @@ export default function( createRootErrorUpdate, createClassErrorUpdate, } = ReactFiberUnwindWork( + config, hostContext, legacyContext, newContext, @@ -201,6 +214,7 @@ export default function( markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, onUncaughtError, + profilerTimer, ); const { commitBeforeMutationLifeCycles, @@ -219,13 +233,16 @@ export default function( markLegacyErrorBoundaryAsFailed, recalculateCurrentTime, ); + const { - now, - scheduleDeferredCallback, - cancelDeferredCallback, - prepareForCommit, - resetAfterCommit, - } = config; + checkActualRenderTimeStackEmpty, + pauseActualRenderTimerIfRunning, + recordElapsedBaseRenderTimeIfRunning, + resetActualRenderTimer, + resumeActualRenderTimerIfPaused, + startBaseRenderTimer, + stopBaseRenderTimerIfRunning, + } = profilerTimer; // Represents the current time in ms. const originalStartTimeMs = now(); @@ -305,6 +322,11 @@ export default function( originalReplayError = null; if (hasCaughtError()) { clearCaughtError(); + + if (enableProfilerTimer) { + // Stop "base" render timer again (after the re-thrown error). + stopBaseRenderTimerIfRunning(); + } } else { // If the begin phase did not fail the second time, set this pointer // back to the original value. @@ -645,6 +667,13 @@ export default function( } } + if (enableProfilerTimer) { + if (__DEV__) { + checkActualRenderTimeStackEmpty(); + } + resetActualRenderTimer(); + } + isCommitting = false; isWorking = false; stopCommitLifeCyclesTimer(); @@ -690,17 +719,36 @@ export default function( // TODO: Calls need to visit stateNode // Bubble up the earliest expiration time. - let child = workInProgress.child; - while (child !== null) { - if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) - ) { - newExpirationTime = child.expirationTime; + // (And "base" render timers if that feature flag is enabled) + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + let treeBaseTime = workInProgress.selfBaseTime; + let child = workInProgress.child; + while (child !== null) { + treeBaseTime += child.treeBaseTime; + if ( + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; + } + child = child.sibling; + } + workInProgress.treeBaseTime = treeBaseTime; + } else { + let child = workInProgress.child; + while (child !== null) { + if ( + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; + } + child = child.sibling; } - child = child.sibling; } + workInProgress.expirationTime = newExpirationTime; } @@ -875,7 +923,19 @@ export default function( workInProgress, ); } - let next = beginWork(current, workInProgress, nextRenderExpirationTime); + + let next; + if (enableProfilerTimer) { + startBaseRenderTimer(); + next = beginWork(current, workInProgress, nextRenderExpirationTime); + + // Update "base" time if the render wasn't bailed out on. + recordElapsedBaseRenderTimeIfRunning(workInProgress); + stopBaseRenderTimerIfRunning(); + } else { + next = beginWork(current, workInProgress, nextRenderExpirationTime); + } + if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); if (isReplayingFailedUnitOfWork) { @@ -911,6 +971,12 @@ export default function( while (nextUnitOfWork !== null && !shouldYield()) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } + + if (enableProfilerTimer) { + // If we didn't finish, pause the "actual" render timer. + // We'll restart it when we resume work. + pauseActualRenderTimerIfRunning(); + } } } @@ -953,6 +1019,11 @@ export default function( try { workLoop(isAsync); } catch (thrownValue) { + if (enableProfilerTimer) { + // Stop "base" render timer in the event of an error. + stopBaseRenderTimerIfRunning(); + } + if (nextUnitOfWork === null) { // This is a fatal error. didFatal = true; @@ -1516,6 +1587,10 @@ export default function( // the deadline. findHighestPriorityRoot(); + if (enableProfilerTimer) { + resumeActualRenderTimerIfPaused(); + } + if (enableUserTimingAPI && deadline !== null) { const didExpire = nextFlushedExpirationTime < recalculateCurrentTime(); const timeout = expirationTimeToMs(nextFlushedExpirationTime); @@ -1661,6 +1736,12 @@ export default function( // There's no time left. Mark this root as complete. We'll come // back and commit it later. root.finishedWork = finishedWork; + + if (enableProfilerTimer) { + // If we didn't finish, pause the "actual" render timer. + // We'll restart it when we resume work. + pauseActualRenderTimerIfRunning(); + } } } } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 05a8a1856d..3337f02fcd 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -7,12 +7,14 @@ * @flow */ +import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; import type {LegacyContext} from './ReactFiberContext'; import type {NewContext} from './ReactFiberNewContext'; import type {CapturedValue} from './ReactCapturedValue'; +import type {ProfilerTimer} from './ReactProfilerTimer'; import type {Update} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; @@ -29,17 +31,21 @@ import { HostComponent, HostPortal, ContextProvider, + Profiler, } from 'shared/ReactTypeOfWork'; import { - NoEffect, DidCapture, Incomplete, + NoEffect, ShouldCapture, } from 'shared/ReactTypeOfSideEffect'; +import { + enableGetDerivedStateFromCatch, + enableProfilerTimer, +} from 'shared/ReactFeatureFlags'; -import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags'; - -export default function( +export default function( + config: HostConfig, hostContext: HostContext, legacyContext: LegacyContext, newContext: NewContext, @@ -51,6 +57,7 @@ export default function( markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean, onUncaughtError: (error: mixed) => void, + profilerTimer: ProfilerTimer, ) { const {popHostContainer, popHostContext} = hostContext; const { @@ -58,6 +65,10 @@ export default function( popTopLevelContextObject: popTopLevelLegacyContextObject, } = legacyContext; const {popProvider} = newContext; + const { + resumeActualRenderTimerIfPaused, + recordElapsedActualRenderTime, + } = profilerTimer; function createRootErrorUpdate( fiber: Fiber, @@ -236,6 +247,13 @@ export default function( case ContextProvider: popProvider(interruptedWork); break; + case Profiler: + if (enableProfilerTimer) { + // Resume in case we're picking up on work that was paused. + resumeActualRenderTimerIfPaused(); + recordElapsedActualRenderTime(interruptedWork); + } + break; default: break; } diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js new file mode 100644 index 0000000000..d192bc1e91 --- /dev/null +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Fiber} from './ReactFiber'; + +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + +import warning from 'fbjs/lib/warning'; + +/** + * The "actual" render time is total time required to render the descendants of a Profiler component. + * This time is stored as a stack, since Profilers can be nested. + * This time is started during the "begin" phase and stopped during the "complete" phase. + * It is paused (and accumulated) in the event of an interruption or an aborted render. + */ + +export type ProfilerTimer = { + checkActualRenderTimeStackEmpty(): void, + markActualRenderTimeStarted(fiber: Fiber): void, + pauseActualRenderTimerIfRunning(): void, + recordElapsedActualRenderTime(fiber: Fiber): void, + resetActualRenderTimer(): void, + resumeActualRenderTimerIfPaused(): void, + recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void, + startBaseRenderTimer(): void, + stopBaseRenderTimerIfRunning(): void, +}; + +export function createProfilerTimer(now: () => number): ProfilerTimer { + let fiberStack: Array; + + if (__DEV__) { + fiberStack = []; + } + + let timerPausedAt: number = 0; + let totalElapsedPauseTime: number = 0; + + function checkActualRenderTimeStackEmpty(): void { + if (__DEV__) { + warning( + fiberStack.length === 0, + 'Expected an empty stack. Something was not reset properly.', + ); + } + } + + function markActualRenderTimeStarted(fiber: Fiber): void { + if (__DEV__) { + fiberStack.push(fiber); + } + fiber.stateNode.startTime = now() - totalElapsedPauseTime; + } + + function pauseActualRenderTimerIfRunning(): void { + if (timerPausedAt === 0) { + timerPausedAt = now(); + } + } + + function recordElapsedActualRenderTime(fiber: Fiber): void { + if (__DEV__) { + warning(fiber === fiberStack.pop(), 'Unexpected Fiber popped.'); + } + fiber.stateNode.duration += + now() - totalElapsedPauseTime - fiber.stateNode.startTime; + } + + function resetActualRenderTimer(): void { + totalElapsedPauseTime = 0; + } + + function resumeActualRenderTimerIfPaused(): void { + if (timerPausedAt > 0) { + totalElapsedPauseTime += now() - timerPausedAt; + timerPausedAt = 0; + } + } + + /** + * The "base" render time is the duration of the “begin” phase of work for a particular fiber. + * This time is measured and stored on each fiber. + * The time for all sibling fibers are accumulated and stored on their parent during the "complete" phase. + * If a fiber bails out (sCU false) then its "base" timer is cancelled and the fiber is not updated. + */ + + let baseStartTime: number = -1; + + function recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void { + if (baseStartTime !== -1) { + fiber.selfBaseTime = now() - baseStartTime; + } + } + + function startBaseRenderTimer(): void { + if (__DEV__) { + if (baseStartTime !== -1) { + warning( + false, + 'Cannot start base timer that is already running. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); + } + } + baseStartTime = now(); + } + + function stopBaseRenderTimerIfRunning(): void { + baseStartTime = -1; + } + + if (enableProfilerTimer) { + return { + checkActualRenderTimeStackEmpty, + markActualRenderTimeStarted, + pauseActualRenderTimerIfRunning, + recordElapsedActualRenderTime, + resetActualRenderTimer, + resumeActualRenderTimerIfPaused, + recordElapsedBaseRenderTimeIfRunning, + startBaseRenderTimer, + stopBaseRenderTimerIfRunning, + }; + } else { + return { + checkActualRenderTimeStackEmpty(): void {}, + markActualRenderTimeStarted(fiber: Fiber): void {}, + pauseActualRenderTimerIfRunning(): void {}, + recordElapsedActualRenderTime(fiber: Fiber): void {}, + resetActualRenderTimer(): void {}, + resumeActualRenderTimerIfPaused(): void {}, + recordElapsedBaseRenderTimeIfRunning(fiber: Fiber): void {}, + startBaseRenderTimer(): void {}, + stopBaseRenderTimerIfRunning(): void {}, + }; + } +} diff --git a/packages/react-reconciler/src/ReactTypeOfMode.js b/packages/react-reconciler/src/ReactTypeOfMode.js index e93b4d984d..4bf9da2ec8 100644 --- a/packages/react-reconciler/src/ReactTypeOfMode.js +++ b/packages/react-reconciler/src/ReactTypeOfMode.js @@ -9,6 +9,7 @@ export type TypeOfMode = number; -export const NoContext = 0b00; -export const AsyncMode = 0b01; -export const StrictMode = 0b10; +export const NoContext = 0b000; +export const AsyncMode = 0b001; +export const StrictMode = 0b010; +export const ProfileMode = 0b100; diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index d5dc2b6f6a..1e33804d12 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -187,15 +187,17 @@ describe('ReactDebugFiberPerf', () => { expect(getFlameChart()).toMatchSnapshot(); }); - it('does not include StrictMode or AsyncMode components in measurements', () => { + it('does not include AsyncMode, StrictMode, or Profiler components in measurements', () => { ReactNoop.render( - - - - - - - , + + + + + + + + + , ); addComment('Mount'); ReactNoop.flush(); diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index ffe5706d1c..119c55154e 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -91,13 +91,14 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc " `; -exports[`ReactDebugFiberPerf does not include StrictMode or AsyncMode components in measurements 1`] = ` +exports[`ReactDebugFiberPerf does not include AsyncMode, StrictMode, or Profiler components in measurements 1`] = ` "⚛ (Waiting for async callback... will force flush in 5230 ms) // Mount ⚛ (React Tree Reconciliation: Completed Root) - ⚛ Parent [mount] - ⚛ Child [mount] + ⚛ Profiler(test) [mount] + ⚛ Parent [mount] + ⚛ Child [mount] ⚛ (Committing Changes) ⚛ (Committing Snapshot Effects: 0 Total) diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index d3ef7aff4f..561aed6c58 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -27,6 +27,7 @@ import { ContextProvider, Mode, ForwardRef, + Profiler, } from 'shared/ReactTypeOfWork'; import invariant from 'fbjs/lib/invariant'; @@ -119,7 +120,7 @@ function removeChild( } // Current virtual time -let currentTime: number = 0; +let nowImplementation = () => 0; let scheduledCallback: ((deadline: Deadline) => mixed) | null = null; let yieldedValues: Array | null = null; @@ -221,9 +222,9 @@ const TestRenderer = ReactFiberReconciler({ getPublicInstance, - now(): number { - return currentTime; - }, + // This approach enables `now` to be mocked by tests, + // Even after the reconciler has initialized and read host config values. + now: () => nowImplementation(), mutation: { commitUpdate( @@ -383,6 +384,7 @@ function toTree(node: ?Fiber) { case ContextProvider: case ContextConsumer: case Mode: + case Profiler: case ForwardRef: return childrenToTree(node.child); default: @@ -486,6 +488,7 @@ class ReactTestInstance { case ContextProvider: case ContextConsumer: case Mode: + case Profiler: descend = true; break; default: @@ -737,7 +740,11 @@ const ReactTestRendererFiber = { } return TestRenderer.getPublicRootInstance(root); }, - unstable_flushSync: TestRenderer.flushSync, + unstable_flushSync(fn: Function) { + yieldedValues = []; + TestRenderer.flushSync(fn); + return yieldedValues; + }, }; Object.defineProperty( @@ -761,6 +768,10 @@ const ReactTestRendererFiber = { /* eslint-disable camelcase */ unstable_batchedUpdates: batchedUpdates, /* eslint-enable camelcase */ + + unstable_setNowImplementation(implementation: () => number): void { + nowImplementation = implementation; + }, }; export default ReactTestRendererFiber; diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index b20f316d9e..ce62bba280 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -230,6 +230,32 @@ describe('ReactShallowRenderer', () => { ]); }); + it('should handle Profiler', () => { + class SomeComponent extends React.Component { + render() { + return ( + +
+ + +
+
+ ); + } + } + + const shallowRenderer = createRenderer(); + const result = shallowRenderer.render(); + + expect(result.type).toBe(React.unstable_Profiler); + expect(result.props.children).toEqual( +
+ + +
, + ); + }); + it('should enable shouldComponentUpdate to prevent a re-render', () => { let renderCounter = 0; class SimpleComponent extends React.Component { diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js index e6f3909b0b..cefef5657c 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js @@ -37,7 +37,9 @@ describe('ReactTestRendererTraversal', () => { - + {}}> + + ); diff --git a/packages/react/src/React.js b/packages/react/src/React.js index d3a52cf61c..d474040611 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -8,9 +8,10 @@ import assign from 'object-assign'; import ReactVersion from 'shared/ReactVersion'; import { - REACT_FRAGMENT_TYPE, - REACT_STRICT_MODE_TYPE, REACT_ASYNC_MODE_TYPE, + REACT_FRAGMENT_TYPE, + REACT_PROFILER_TYPE, + REACT_STRICT_MODE_TYPE, } from 'shared/ReactSymbols'; import {Component, PureComponent} from './ReactBaseClasses'; @@ -51,6 +52,7 @@ const React = { Fragment: REACT_FRAGMENT_TYPE, StrictMode: REACT_STRICT_MODE_TYPE, unstable_AsyncMode: REACT_ASYNC_MODE_TYPE, + unstable_Profiler: REACT_PROFILER_TYPE, createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js new file mode 100644 index 0000000000..e97d8eeef9 --- /dev/null +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -0,0 +1,862 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * 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'; + +let React; +let ReactFeatureFlags; +let ReactTestRenderer; + +function loadModules({ + enableProfilerTimer = true, + replayFailedUnitOfWorkWithInvokeGuardedCallback = false, +} = {}) { + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffects = false; + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer; + ReactFeatureFlags.enableGetDerivedStateFromCatch = true; + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback; + React = require('react'); + ReactTestRenderer = require('react-test-renderer'); +} + +describe('Profiler', () => { + describe('works in profiling and non-profiling bundles', () => { + [true, false].forEach(flagEnabled => { + describe(`enableProfilerTimer ${ + flagEnabled ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules({enableProfilerTimer: flagEnabled}); + }); + + // This will throw in production too, + // But the test is only interested in verifying the DEV error message. + if (__DEV__) { + it('should warn if required params are missing', () => { + expect(() => { + ReactTestRenderer.create(); + }).toThrow( + 'Profiler must specify an "id" string and "onRender" function as props', + ); + }); + } + + it('should support an empty Profiler (with no children)', () => { + // As root + expect( + ReactTestRenderer.create( + , + ).toJSON(), + ).toMatchSnapshot(); + + // As non-root + expect( + ReactTestRenderer.create( +
+ +
, + ).toJSON(), + ).toMatchSnapshot(); + }); + + it('should render children', () => { + const FunctionalComponent = ({label}) => {label}; + const renderer = ReactTestRenderer.create( +
+ outside span + + inside span + + +
, + ); + expect(renderer.toJSON()).toMatchSnapshot(); + }); + + it('should support nested Profilers', () => { + const FunctionalComponent = ({label}) =>
{label}
; + class ClassComponent extends React.Component { + render() { + return {this.props.label}; + } + } + const renderer = ReactTestRenderer.create( + + + + + inner span + + , + ); + expect(renderer.toJSON()).toMatchSnapshot(); + }); + }); + }); + }); + + describe('onRender callback', () => { + let AdvanceTime; + let advanceTimeBy; + + const mockNowForTests = () => { + let currentTime = 0; + ReactTestRenderer.unstable_setNowImplementation(() => currentTime); + advanceTimeBy = amount => { + currentTime += amount; + }; + }; + + beforeEach(() => { + jest.resetModules(); + + loadModules(); + mockNowForTests(); + + AdvanceTime = class extends React.Component { + static defaultProps = { + byAmount: 10, + shouldComponentUpdate: true, + }; + shouldComponentUpdate(nextProps) { + return nextProps.shouldComponentUpdate; + } + render() { + // Simulate time passing when this component is rendered + advanceTimeBy(this.props.byAmount); + return this.props.children || null; + } + }; + }); + + it('is not invoked until the commit phase', () => { + const callback = jest.fn(); + + const Yield = ({value}) => { + renderer.unstable_yield(value); + return null; + }; + + const renderer = ReactTestRenderer.create( + + + + , + { + unstable_isAsync: true, + }, + ); + + // Times are logged until a render is committed. + renderer.unstable_flushThrough(['first']); + expect(callback).toHaveBeenCalledTimes(0); + expect(renderer.unstable_flushAll()).toEqual(['last']); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('logs render times for both mount and update', () => { + const callback = jest.fn(); + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + let [call] = callback.mock.calls; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('mount'); + expect(call[2]).toBe(10); // "actual" time + expect(call[3]).toBe(10); // "base" time + + callback.mockReset(); + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + [call] = callback.mock.calls; + + expect(call).toHaveLength(4); + expect(call[0]).toBe('test'); + expect(call[1]).toBe('update'); + expect(call[2]).toBe(10); // "actual" time + expect(call[3]).toBe(10); // "base" time + }); + + it('includes render times of nested Profilers in their parent times', () => { + const callback = jest.fn(); + + ReactTestRenderer.create( + + + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + // Callbacks bubble (reverse order). + const [childCall, parentCall] = callback.mock.calls; + expect(childCall[0]).toBe('child'); + expect(parentCall[0]).toBe('parent'); + + // Parent times should include child times + expect(childCall[2]).toBe(20); // "actual" time + expect(childCall[3]).toBe(20); // "base" time + expect(parentCall[2]).toBe(30); // "actual" time + expect(parentCall[3]).toBe(30); // "base" time + }); + + it('tracks sibling Profilers separately', () => { + const callback = jest.fn(); + + ReactTestRenderer.create( + + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [firstCall, secondCall] = callback.mock.calls; + expect(firstCall[0]).toBe('first'); + expect(secondCall[0]).toBe('second'); + + // Parent times should include child times + expect(firstCall[2]).toBe(20); // "actual" time + expect(firstCall[3]).toBe(20); // "base" time + expect(secondCall[2]).toBe(5); // "actual" time + expect(secondCall[3]).toBe(5); // "base" time + }); + + it('does not include time spent outside of profile root', () => { + const callback = jest.fn(); + + ReactTestRenderer.create( + + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + const [call] = callback.mock.calls; + expect(call[0]).toBe('test'); + expect(call[2]).toBe(5); // "actual" time + expect(call[3]).toBe(5); // "base" time + }); + + it('is not called when blocked by sCU false', () => { + const callback = jest.fn(); + + let instance; + class Updater extends React.Component { + state = {}; + render() { + instance = this; + return this.props.children; + } + } + + class Pure extends React.PureComponent { + render() { + return this.props.children; + } + } + + const renderer = ReactTestRenderer.create( + + + + + +
+ + + + + , + ); + + // All profile callbacks are called for initial render + expect(callback).toHaveBeenCalledTimes(3); + + callback.mockReset(); + + renderer.unstable_flushSync(() => { + instance.setState({ + count: 1, + }); + }); + + // Only call profile updates for paths that have re-rendered + // Since "inner" is beneath a pure compoent, it isn't called + expect(callback).toHaveBeenCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('middle'); + expect(callback.mock.calls[1][0]).toBe('outer'); + }); + + it('decreases "actual" time but not "base" time when sCU prevents an update', () => { + const callback = jest.fn(); + + const renderer = ReactTestRenderer.create( + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + renderer.update( + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(20); // "actual" time + expect(mountCall[3]).toBe(20); // "base" time + + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(10); // "actual" time + expect(updateCall[3]).toBe(20); // "base" time + }); + + it('includes time spent in render phase lifecycles', () => { + class WithLifecycles extends React.Component { + state = {}; + static getDerivedStateFromProps() { + advanceTimeBy(3); + return null; + } + shouldComponentUpdate() { + advanceTimeBy(7); + return true; + } + render() { + advanceTimeBy(5); + return null; + } + } + + const callback = jest.fn(); + + const renderer = ReactTestRenderer.create( + + + , + ); + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(8); // "actual" time + expect(mountCall[3]).toBe(8); // "base" time + + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(15); // "actual" time + expect(updateCall[3]).toBe(15); // "base" time + }); + + describe('with regard to interruptions', () => { + it('should accumulate "actual" time after a scheduling interruptions', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + renderer.unstable_yield('Yield:' + renderTime); + return null; + }; + + // Render partially, but run out of time before completing. + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isAsync: true}, + ); + expect(renderer.unstable_flushThrough(['Yield:2'])).toEqual([ + 'Yield:2', + ]); + expect(callback).toHaveBeenCalledTimes(0); + + // Resume render for remaining children. + expect(renderer.unstable_flushAll()).toEqual(['Yield:3']); + + // Verify that logged times include both durations above. + expect(callback).toHaveBeenCalledTimes(1); + expect(callback.mock.calls[0][2]).toBe(5); // "actual" time + expect(callback.mock.calls[0][3]).toBe(5); // "base" time + }); + + it('should not include time between frames', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + renderer.unstable_yield('Yield:' + renderTime); + return null; + }; + + // Render partially, but don't finish. + // This partial render should take 5ms of simulated time. + const renderer = ReactTestRenderer.create( + + + + + + + , + {unstable_isAsync: true}, + ); + expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([ + 'Yield:5', + ]); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + advanceTimeBy(50); + + // Flush the remaninig work, + // Which should take an additional 10ms of simulated time. + expect(renderer.unstable_flushAll()).toEqual(['Yield:10', 'Yield:17']); + expect(callback).toHaveBeenCalledTimes(2); + + const [innerCall, outerCall] = callback.mock.calls; + + // Verify that the "actual" time includes all work times, + // But not the time that elapsed between frames. + expect(innerCall[0]).toBe('inner'); + expect(innerCall[2]).toBe(17); // "actual" time + expect(innerCall[3]).toBe(17); // "base" time + expect(outerCall[0]).toBe('outer'); + expect(outerCall[2]).toBe(32); // "actual" time + expect(outerCall[3]).toBe(32); // "base" time + }); + + it('should report the expected times when a high-priority update replaces an in-progress initial render', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + renderer.unstable_yield('Yield:' + renderTime); + return null; + }; + + // Render a partially update, but don't finish. + // This partial render should take 10ms of simulated time. + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isAsync: true}, + ); + expect(renderer.unstable_flushThrough(['first'])).toEqual(['Yield:10']); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); + + // Interrupt with higher priority work. + // The interrupted work simulates an additional 5ms of time. + expect( + renderer.unstable_flushSync(() => { + renderer.update( + + + , + ); + }), + ).toEqual(['Yield:5']); + + // The initial work was thrown away in this case, + // So the "actual" and "base" times should only include the final rendered tree times. + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(5); // "actual" time + expect(call[3]).toBe(5); // "base" time + + callback.mockReset(); + + // Verify no more unexpected callbacks from low priority work + expect(renderer.unstable_flushAll()).toEqual([]); + expect(callback).toHaveBeenCalledTimes(0); + }); + + it('should report the expected times when a high-priority update replaces a low-priority update', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + renderer.unstable_yield('Yield:' + renderTime); + return null; + }; + + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isAsync: true}, + ); + + // Render everything initially. + // This should take 21 seconds of "actual" and "base" time. + expect(renderer.unstable_flushAll()).toEqual(['Yield:6', 'Yield:15']); + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(21); // "actual" time + expect(call[3]).toBe(21); // "base" time + + callback.mockReset(); + + // Render a partially update, but don't finish. + // This partial render should take 3ms of simulated time. + renderer.update( + + + + + , + ); + expect(renderer.unstable_flushThrough(['Yield:3'])).toEqual([ + 'Yield:3', + ]); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); + + // Render another 5ms of simulated time. + expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([ + 'Yield:5', + ]); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); + + // Interrupt with higher priority work. + // The interrupted work simulates an additional 11ms of time. + expect( + renderer.unstable_flushSync(() => { + renderer.update( + + + , + ); + }), + ).toEqual(['Yield:11']); + + // Verify that the "actual" time includes all three durations above. + // And the "base" time includes only the final rendered tree times. + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(19); // "actual" time + expect(call[3]).toBe(11); // "base" time + + // Verify no more unexpected callbacks from low priority work + expect(renderer.unstable_flushAll()).toEqual([]); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should report the expected times when a high-priority update interrupts a low-priority update', () => { + const callback = jest.fn(); + + const Yield = ({renderTime}) => { + advanceTimeBy(renderTime); + renderer.unstable_yield('Yield:' + renderTime); + return null; + }; + + let first; + class FirstComponent extends React.Component { + state = {renderTime: 1}; + render() { + first = this; + advanceTimeBy(this.state.renderTime); + renderer.unstable_yield('FirstComponent:' + this.state.renderTime); + return ; + } + } + let second; + class SecondComponent extends React.Component { + state = {renderTime: 2}; + render() { + second = this; + advanceTimeBy(this.state.renderTime); + renderer.unstable_yield('SecondComponent:' + this.state.renderTime); + return ; + } + } + + const renderer = ReactTestRenderer.create( + + + + , + {unstable_isAsync: true}, + ); + + // Render everything initially. + // This simulates a total of 14ms of "actual" render time. + // The "base" render time is also 14ms for the initial render. + expect(renderer.unstable_flushAll()).toEqual([ + 'FirstComponent:1', + 'Yield:4', + 'SecondComponent:2', + 'Yield:7', + ]); + expect(callback).toHaveBeenCalledTimes(1); + let call = callback.mock.calls[0]; + expect(call[2]).toBe(14); // "actual" time + expect(call[3]).toBe(14); // "base" time + + callback.mockClear(); + + // Render a partially update, but don't finish. + // This partial render will take 10ms of "actual" render time. + first.setState({renderTime: 10}); + expect(renderer.unstable_flushThrough(['FirstComponent:10'])).toEqual([ + 'FirstComponent:10', + ]); + expect(callback).toHaveBeenCalledTimes(0); + + // Simulate time moving forward while frame is paused. + advanceTimeBy(100); + + // Interrupt with higher priority work. + // This simulates a total of 37ms of "actual" render time. + expect( + renderer.unstable_flushSync(() => second.setState({renderTime: 30})), + ).toEqual(['SecondComponent:30', 'Yield:7']); + + // Verify that the "actual" time includes time spent in the both renders so far (10ms and 37ms). + // The "base" time should include the more recent times for the SecondComponent subtree, + // As well as the original times for the FirstComponent subtree. + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(47); // "actual" time + expect(call[3]).toBe(42); // "base" time + + callback.mockClear(); + + // Resume the original low priority update, with rebased state. + // This simulates a total of 14ms of "actual" render time, + // And does not include the original (interrupted) 10ms. + // The tree contains 42ms of "base" render time at this point, + // Reflecting the most recent (longer) render durations. + // TODO: This "actual" time should decrease by 10ms once the scheduler supports resuming. + expect(renderer.unstable_flushAll()).toEqual([ + 'FirstComponent:10', + 'Yield:4', + ]); + expect(callback).toHaveBeenCalledTimes(1); + call = callback.mock.calls[0]; + expect(call[2]).toBe(14); // "actual" time + expect(call[3]).toBe(51); // "base" time + }); + + [true, false].forEach(flagEnabled => { + describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${ + flagEnabled ? 'enabled' : 'disabled' + }`, () => { + beforeEach(() => { + jest.resetModules(); + + loadModules({ + replayFailedUnitOfWorkWithInvokeGuardedCallback: flagEnabled, + }); + mockNowForTests(); + }); + + it('should accumulate "actual" time after an error handled by componentDidCatch()', () => { + const callback = jest.fn(); + + const ThrowsError = () => { + advanceTimeBy(10); + throw Error('expected error'); + }; + + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + advanceTimeBy(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); + } + } + + ReactTestRenderer.create( + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + // Callbacks bubble (reverse order). + let [mountCall, updateCall] = callback.mock.calls; + + // The initial mount only includes the ErrorBoundary (which takes 2ms) + // But it spends time rendering all of the failed subtree also. + expect(mountCall[1]).toBe('mount'); + // "actual" time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError) + // If replayFailedUnitOfWorkWithInvokeGuardedCallback is enbaled, ThrowsError is replayed. + expect(mountCall[2]).toBe(flagEnabled && __DEV__ ? 27 : 17); + // "base" time includes: 2 (ErrorBoundary) + expect(mountCall[3]).toBe(2); + + // The update includes the ErrorBoundary and its fallback child + expect(updateCall[1]).toBe('update'); + // "actual" time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(updateCall[2]).toBe(22); + // "base" time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(updateCall[3]).toBe(22); + }); + + it('should accumulate "actual" time after an error handled by getDerivedStateFromCatch()', () => { + const callback = jest.fn(); + + const ThrowsError = () => { + advanceTimeBy(10); + throw Error('expected error'); + }; + + class ErrorBoundary extends React.Component { + state = {error: null}; + static getDerivedStateFromCatch(error) { + return {error}; + } + render() { + advanceTimeBy(2); + return this.state.error === null ? ( + this.props.children + ) : ( + + ); + } + } + + ReactTestRenderer.create( + + + + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + // Callbacks bubble (reverse order). + let [mountCall] = callback.mock.calls; + + // The initial mount includes the ErrorBoundary's error state, + // But i also spends "actual" time rendering UI that fails and isn't included. + expect(mountCall[1]).toBe('mount'); + // "actual" time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError) + // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime) + // If replayFailedUnitOfWorkWithInvokeGuardedCallback is enbaled, ThrowsError is replayed. + expect(mountCall[2]).toBe(flagEnabled && __DEV__ ? 49 : 39); + // "base" time includes: 2 (ErrorBoundary) + 20 (AdvanceTime) + expect(mountCall[3]).toBe(22); + }); + }); + }); + }); + + it('reflects the most recently rendered id value', () => { + const callback = jest.fn(); + + const renderer = ReactTestRenderer.create( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(1); + + renderer.update( + + + , + ); + + expect(callback).toHaveBeenCalledTimes(2); + + const [mountCall, updateCall] = callback.mock.calls; + + expect(mountCall[0]).toBe('one'); + expect(mountCall[1]).toBe('mount'); + expect(mountCall[2]).toBe(2); // "actual" time + expect(mountCall[3]).toBe(2); // "base" time + + expect(updateCall[0]).toBe('two'); + expect(updateCall[1]).toBe('update'); + expect(updateCall[2]).toBe(1); // "actual" time + expect(updateCall[3]).toBe(1); // "base" time + }); + }); +}); diff --git a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap new file mode 100644 index 0000000000..0022e5abd3 --- /dev/null +++ b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should render children 1`] = ` +
+ + outside span + + + inside span + + + functional component + +
+`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support an empty Profiler (with no children) 1`] = `null`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support an empty Profiler (with no children) 2`] = `
`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer disabled should support nested Profilers 1`] = ` +Array [ +
+ outer functional component +
, + + inner class component + , + + inner span + , +] +`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should render children 1`] = ` +
+ + outside span + + + inside span + + + functional component + +
+`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support an empty Profiler (with no children) 1`] = `null`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support an empty Profiler (with no children) 2`] = `
`; + +exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer enabled should support nested Profilers 1`] = ` +Array [ +
+ outer functional component +
, + + inner class component + , + + inner span + , +] +`; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 955668d13e..614a1b6cc7 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -37,6 +37,9 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; // Warn about deprecated, async-unsafe lifecycles; relates to RFC #6: export const warnAboutDeprecatedLifecycles = false; +// Gather advanced timing metrics for Profiler subtrees. +export const enableProfilerTimer = false; + // Only used in www builds. export function addUserTimingListener() { invariant(false, 'Not implemented.'); diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 12e0fdddad..672e51de8e 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -27,6 +27,9 @@ export const REACT_FRAGMENT_TYPE = hasSymbol export const REACT_STRICT_MODE_TYPE = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc; +export const REACT_PROFILER_TYPE = hasSymbol + ? Symbol.for('react.profile_root') + : 0xeacc; export const REACT_PROVIDER_TYPE = hasSymbol ? Symbol.for('react.provider') : 0xeacd; diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index 573b75aabc..1c672bb4b5 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -22,7 +22,8 @@ export type TypeOfWork = | 11 | 12 | 13 - | 14; + | 14 + | 15; export const IndeterminateComponent = 0; // Before we know whether it is functional or class export const FunctionalComponent = 1; @@ -39,3 +40,4 @@ export const Mode = 11; export const ContextConsumer = 12; export const ContextProvider = 13; export const ForwardRef = 14; +export const Profiler = 15; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js index e2fd437999..7eb809c952 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js @@ -18,6 +18,7 @@ export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; +export const enableProfilerTimer = __DEV__; // React Fabric uses persistent reconciler. export const enableMutatingReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js index c6a4862d85..c6b30c6532 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js @@ -18,6 +18,7 @@ export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; +export const enableProfilerTimer = false; // React Fabric uses persistent reconciler. export const enableMutatingReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 3026a69fc6..51e2133a67 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -19,6 +19,7 @@ export const { debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, replayFailedUnitOfWorkWithInvokeGuardedCallback, + enableProfilerTimer, } = require('ReactFeatureFlags'); // The rest of the flags are static for better dead code elimination. diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a238ca1688..5e03310dc0 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -21,6 +21,7 @@ export const enablePersistentReconciler = false; export const enableUserTimingAPI = __DEV__; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = false; +export const enableProfilerTimer = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 57d7c6bf53..780c62db4d 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -18,6 +18,7 @@ export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; +export const enableProfilerTimer = false; // react-reconciler/persistent entry point // uses a persistent reconciler. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 000f950a4f..486aecfa68 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -21,6 +21,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; export const enableMutatingReconciler = true; export const enableNoopReconciler = false; export const enablePersistentReconciler = false; +export const enableProfilerTimer = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index df3560c6fb..523e1604a6 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -17,6 +17,7 @@ export const { enableGetDerivedStateFromCatch, replayFailedUnitOfWorkWithInvokeGuardedCallback, warnAboutDeprecatedLifecycles, + enableProfilerTimer, } = require('ReactFeatureFlags'); // The rest of the flags are static for better dead code elimination. diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 23e10d1a73..ed448d8152 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -10,11 +10,14 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import { + REACT_ASYNC_MODE_TYPE, REACT_CALL_TYPE, + REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_RETURN_TYPE, REACT_PORTAL_TYPE, - REACT_FORWARD_REF_TYPE, + REACT_PROFILER_TYPE, + REACT_STRICT_MODE_TYPE, } from 'shared/ReactSymbols'; function getComponentName(fiber: Fiber): string | null { @@ -26,14 +29,20 @@ function getComponentName(fiber: Fiber): string | null { return type; } switch (type) { + case REACT_ASYNC_MODE_TYPE: + return 'AsyncMode'; + case REACT_CALL_TYPE: + return 'ReactCall'; case REACT_FRAGMENT_TYPE: return 'ReactFragment'; case REACT_PORTAL_TYPE: return 'ReactPortal'; - case REACT_CALL_TYPE: - return 'ReactCall'; + case REACT_PROFILER_TYPE: + return `Profiler(${fiber.pendingProps.id})`; case REACT_RETURN_TYPE: return 'ReactReturn'; + case REACT_STRICT_MODE_TYPE: + return 'StrictMode'; } if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index 7a333c8c11..c9a759d502 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -8,12 +8,13 @@ */ import { - REACT_FRAGMENT_TYPE, REACT_ASYNC_MODE_TYPE, - REACT_STRICT_MODE_TYPE, - REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, REACT_FORWARD_REF_TYPE, + REACT_FRAGMENT_TYPE, + REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, + REACT_STRICT_MODE_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -23,6 +24,7 @@ export default function isValidElementType(type: mixed) { // Note: its typeof might be other than 'symbol' or 'number' if it's a polyfill. type === REACT_FRAGMENT_TYPE || type === REACT_ASYNC_MODE_TYPE || + type === REACT_PROFILER_TYPE || type === REACT_STRICT_MODE_TYPE || (typeof type === 'object' && type !== null && From 42a126237507a2038e9dbd7222a94b0debb9a69a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 May 2018 18:08:11 -0700 Subject: [PATCH 027/277] Update sizes --- scripts/rollup/results.json | 300 ++++++++++++++++++------------------ 1 file changed, 150 insertions(+), 150 deletions(-) diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 67107645c5..f265beaf72 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -4,29 +4,29 @@ "filename": "react.development.js", "bundleType": "UMD_DEV", "packageName": "react", - "size": 56795, - "gzip": 15603 + "size": 57115, + "gzip": 15675 }, { "filename": "react.production.min.js", "bundleType": "UMD_PROD", "packageName": "react", - "size": 7164, - "gzip": 3039 + "size": 7194, + "gzip": 3050 }, { "filename": "react.development.js", "bundleType": "NODE_DEV", "packageName": "react", - "size": 47210, - "gzip": 13192 + "size": 47530, + "gzip": 13262 }, { "filename": "react.production.min.js", "bundleType": "NODE_PROD", "packageName": "react", - "size": 5668, - "gzip": 2468 + "size": 5698, + "gzip": 2476 }, { "filename": "React-dev.js", @@ -46,29 +46,29 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 626353, - "gzip": 144739 + "size": 639980, + "gzip": 147756 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 102821, - "gzip": 32649 + "size": 103714, + "gzip": 33074 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 610354, - "gzip": 140504 + "size": 623973, + "gzip": 143563 }, { "filename": "react-dom.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 101220, - "gzip": 31815 + "size": 102103, + "gzip": 32217 }, { "filename": "ReactDOM-dev.js", @@ -88,29 +88,29 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 41728, - "gzip": 11969 + "size": 41683, + "gzip": 11959 }, { "filename": "react-dom-test-utils.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 10898, - "gzip": 4053 + "size": 10852, + "gzip": 4037 }, { "filename": "react-dom-test-utils.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 36465, - "gzip": 10517 + "size": 36420, + "gzip": 10508 }, { "filename": "react-dom-test-utils.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 10150, - "gzip": 3820 + "size": 10105, + "gzip": 3815 }, { "filename": "ReactTestUtils-dev.js", @@ -123,29 +123,29 @@ "filename": "react-dom-unstable-native-dependencies.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 62648, - "gzip": 16409 + "size": 62645, + "gzip": 16407 }, { "filename": "react-dom-unstable-native-dependencies.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 11595, - "gzip": 4012 + "size": 11592, + "gzip": 4008 }, { "filename": "react-dom-unstable-native-dependencies.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 58210, - "gzip": 15123 + "size": 58207, + "gzip": 15121 }, { "filename": "react-dom-unstable-native-dependencies.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 10918, - "gzip": 3765 + "size": 10915, + "gzip": 3762 }, { "filename": "ReactDOMUnstableNativeDependencies-dev.js", @@ -165,29 +165,29 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 103762, - "gzip": 27108 + "size": 103942, + "gzip": 27140 }, { "filename": "react-dom-server.browser.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 15564, - "gzip": 5951 + "size": 15593, + "gzip": 5959 }, { "filename": "react-dom-server.browser.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 92806, - "gzip": 24789 + "size": 92986, + "gzip": 24826 }, { "filename": "react-dom-server.browser.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 14909, - "gzip": 5679 + "size": 14939, + "gzip": 5692 }, { "filename": "ReactDOMServer-dev.js", @@ -207,43 +207,43 @@ "filename": "react-dom-server.node.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 94774, - "gzip": 25349 + "size": 94954, + "gzip": 25386 }, { "filename": "react-dom-server.node.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 15733, - "gzip": 5983 + "size": 15763, + "gzip": 5997 }, { "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 424539, - "gzip": 92383 + "size": 438185, + "gzip": 95469 }, { "filename": "react-art.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-art", - "size": 92908, - "gzip": 28133 + "size": 93805, + "gzip": 28540 }, { "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 348598, - "gzip": 73299 + "size": 362236, + "gzip": 76390 }, { "filename": "react-art.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-art", - "size": 56579, - "gzip": 17175 + "size": 57462, + "gzip": 17574 }, { "filename": "ReactART-dev.js", @@ -291,29 +291,29 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 356387, - "gzip": 74965 + "size": 367787, + "gzip": 77332 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 56480, - "gzip": 17088 + "size": 57216, + "gzip": 17399 }, { "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 346996, - "gzip": 72227 + "size": 358388, + "gzip": 74592 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 55696, - "gzip": 16727 + "size": 56410, + "gzip": 16995 }, { "filename": "ReactTestRenderer-dev.js", @@ -326,29 +326,29 @@ "filename": "react-test-renderer-shallow.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 24526, - "gzip": 6546 + "size": 24939, + "gzip": 6654 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 7344, - "gzip": 2396 + "size": 7377, + "gzip": 2416 }, { "filename": "react-test-renderer-shallow.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 14042, - "gzip": 3514 + "size": 14595, + "gzip": 3660 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 7087, - "gzip": 2315 + "size": 7314, + "gzip": 2398 }, { "filename": "ReactShallowRenderer-dev.js", @@ -361,99 +361,99 @@ "filename": "react-noop-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 18663, - "gzip": 5148 + "size": 18684, + "gzip": 5165 }, { "filename": "react-noop-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 6521, - "gzip": 2566 + "size": 6479, + "gzip": 2549 }, { "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 326858, - "gzip": 67568 + "size": 337878, + "gzip": 69868 }, { "filename": "react-reconciler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 48375, - "gzip": 14567 + "size": 48996, + "gzip": 14768 }, { "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 326178, - "gzip": 67326 + "size": 337140, + "gzip": 69575 }, { "filename": "react-reconciler-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 47268, - "gzip": 14446 + "size": 47919, + "gzip": 14583 }, { "filename": "react-reconciler-reflection.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 11326, - "gzip": 3505 + "size": 11698, + "gzip": 3601 }, { "filename": "react-reconciler-reflection.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 2377, - "gzip": 1040 + "size": 2320, + "gzip": 1034 }, { "filename": "react-call-return.development.js", "bundleType": "NODE_DEV", "packageName": "react-call-return", - "size": 2683, - "gzip": 958 + "size": 2671, + "gzip": 955 }, { "filename": "react-call-return.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-call-return", - "size": 971, - "gzip": 525 + "size": 959, + "gzip": 522 }, { "filename": "react-is.development.js", "bundleType": "UMD_DEV", "packageName": "react-is", - "size": 4384, - "gzip": 1252 + "size": 4685, + "gzip": 1302 }, { "filename": "react-is.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-is", - "size": 1807, - "gzip": 748 + "size": 1892, + "gzip": 773 }, { "filename": "react-is.development.js", "bundleType": "NODE_DEV", "packageName": "react-is", - "size": 4195, - "gzip": 1201 + "size": 4496, + "gzip": 1245 }, { "filename": "react-is.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-is", - "size": 1743, - "gzip": 685 + "size": 1840, + "gzip": 707 }, { "filename": "ReactIs-dev.js", @@ -501,190 +501,190 @@ "filename": "React-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react", - "size": 47216, - "gzip": 12859 + "size": 47536, + "gzip": 12932 }, { "filename": "React-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react", - "size": 13749, - "gzip": 3815 + "size": 13822, + "gzip": 3825 }, { "filename": "ReactDOM-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 635369, - "gzip": 143277 + "size": 649280, + "gzip": 146389 }, { "filename": "ReactDOM-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 291114, - "gzip": 53216 + "size": 299975, + "gzip": 54678 }, { "filename": "ReactTestUtils-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 37779, - "gzip": 10710 + "size": 37734, + "gzip": 10701 }, { "filename": "ReactDOMUnstableNativeDependencies-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 58465, - "gzip": 14911 + "size": 58462, + "gzip": 14909 }, { "filename": "ReactDOMUnstableNativeDependencies-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 26974, - "gzip": 5507 + "size": 26971, + "gzip": 5503 }, { "filename": "ReactDOMServer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 96360, - "gzip": 24600 + "size": 96543, + "gzip": 24639 }, { "filename": "ReactDOMServer-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 32376, - "gzip": 7965 + "size": 32475, + "gzip": 7968 }, { "filename": "ReactART-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-art", - "size": 357056, - "gzip": 72841 + "size": 370976, + "gzip": 75980 }, { "filename": "ReactART-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-art", - "size": 171040, - "gzip": 28102 + "size": 179924, + "gzip": 29606 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 470028, - "gzip": 100556 + "size": 481278, + "gzip": 102913 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 222916, - "gzip": 37259 + "size": 230529, + "gzip": 38569 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 469772, - "gzip": 100493 + "size": 480999, + "gzip": 102846 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 222116, - "gzip": 37129 + "size": 224503, + "gzip": 37480 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 452022, - "gzip": 96052 + "size": 463244, + "gzip": 98382 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 207365, - "gzip": 34455 + "size": 209798, + "gzip": 34820 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 452057, - "gzip": 96067 + "size": 463280, + "gzip": 98398 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 207401, - "gzip": 34473 + "size": 209834, + "gzip": 34837 }, { "filename": "ReactTestRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 355733, - "gzip": 71836 + "size": 367404, + "gzip": 74293 }, { "filename": "ReactShallowRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 14759, - "gzip": 3631 + "size": 15318, + "gzip": 3781 }, { "filename": "ReactIs-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-is", - "size": 4263, - "gzip": 1220 + "size": 4564, + "gzip": 1270 }, { "filename": "ReactIs-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-is", - "size": 3414, - "gzip": 953 + "size": 3653, + "gzip": 977 }, { "filename": "react-scheduler.development.js", "bundleType": "UMD_DEV", "packageName": "react-scheduler", - "size": 10937, - "gzip": 3771 + "size": 13551, + "gzip": 4584 }, { "filename": "react-scheduler.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-scheduler", - "size": 1721, - "gzip": 873 + "size": 1979, + "gzip": 1032 }, { "filename": "react-scheduler.development.js", "bundleType": "NODE_DEV", "packageName": "react-scheduler", - "size": 10741, - "gzip": 3720 + "size": 13355, + "gzip": 4534 }, { "filename": "react-scheduler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-scheduler", - "size": 1792, - "gzip": 889 + "size": 2068, + "gzip": 1051 } ] } \ No newline at end of file From 6565795377d1d2c79a7708766f1af9e1a87517de Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 May 2018 18:09:10 -0700 Subject: [PATCH 028/277] Suspense (#12279) * Timeout component Adds Timeout component. If a promise is thrown from inside a Timeout component, React will suspend the in-progress render from committing. When the promise resolves, React will retry. If the render is suspended for longer than the maximum threshold, the Timeout switches to a placeholder state. The timeout threshold is defined as the minimum of: - The expiration time of the current render - The `ms` prop given to each Timeout component in the ancestor path of the thrown promise. * Add a test for nested fallbacks Co-authored-by: Andrew Clark * Resume on promise rejection React should resume rendering regardless of whether it resolves or rejects. * Wrap Suspense code in feature flag * Children of a Timeout must be strict mode compatible Async is not required for Suspense, but strict mode is. * Simplify list of pending work Some of this was added with "soft expiration" in mind, but now with our revised model for how soft expiration will work, this isn't necessary. It would be nice to remove more of this, but I think the list itself is inherent because we need a way to track the start times, for . * Only use the Timeout update queue to store promises, not for state It already worked this way in practice. * Wrap more Suspense-only paths in the feature flag * Attach promise listener immediately on suspend Instead of waiting for commit phase. * Infer approximate start time using expiration time * Remove list of pending priority levels We can replicate almost all the functionality by tracking just five separate levels: the highest/lowest priority pending levels, the highest/lowest priority suspended levels, and the lowest pinged level. We lose a bit of granularity, in that if there are multiple levels of pending updates, only the first and last ones are known. But in practice this likely isn't a big deal. These heuristics are almost entirely isolated to a single module and can be adjusted later, without API changes, if necessary. Non-IO-bound work is not affected at all. * ReactFiberPendingWork -> ReactFiberPendingPriority * Renaming method names from "pending work" to "pending priority" * Get rid of SuspenseThenable module Idk why I thought this was neccessary * Nits based on Sebastian's feedback * More naming nits + comments * Add test for hiding a suspended tree to unblock * Revert change to expiration time rounding This means you have to account for the start time approximation heuristic when writing Suspense tests, but that's going to be true regardless. When updating the tests, I also made a fix related to offscreen priority. We should never timeout inside a hidden tree. * palceholder -> placeholder --- packages/react-reconciler/src/ReactFiber.js | 8 + .../src/ReactFiberBeginWork.js | 52 +- .../src/ReactFiberClassComponent.js | 15 +- .../src/ReactFiberCommitWork.js | 8 + .../src/ReactFiberCompleteWork.js | 3 + .../src/ReactFiberExpirationTime.js | 9 +- .../src/ReactFiberPendingPriority.js | 196 ++++ .../src/ReactFiberReconciler.js | 15 +- .../react-reconciler/src/ReactFiberRoot.js | 23 + .../src/ReactFiberScheduler.js | 128 ++- .../src/ReactFiberUnwindWork.js | 155 ++- .../__tests__/ReactSuspense-test.internal.js | 946 ++++++++++++++++++ ...ReactIncrementalPerf-test.internal.js.snap | 54 +- packages/react/src/React.js | 7 + packages/shared/ReactFeatureFlags.js | 2 + packages/shared/ReactSymbols.js | 3 + packages/shared/ReactTypeOfWork.js | 4 +- .../ReactFeatureFlags.native-fabric-fb.js | 1 + .../ReactFeatureFlags.native-fabric-oss.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + packages/shared/isValidElementType.js | 2 + 25 files changed, 1561 insertions(+), 76 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberPendingPriority.js create mode 100644 packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 51c4d52fe5..720fb9ffe1 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -32,6 +32,7 @@ import { ContextProvider, ContextConsumer, Profiler, + TimeoutComponent, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -47,6 +48,7 @@ import { REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, REACT_ASYNC_MODE_TYPE, + REACT_TIMEOUT_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -368,6 +370,12 @@ export function createFiberFromElement( case REACT_RETURN_TYPE: fiberTag = ReturnComponent; break; + case REACT_TIMEOUT_TYPE: + fiberTag = TimeoutComponent; + // Suspense does not require async, but its children should be strict + // mode compatible. + mode |= StrictMode; + break; default: { if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 2c80e26ffb..489393bebb 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -36,19 +36,21 @@ import { ContextProvider, ContextConsumer, Profiler, + TimeoutComponent, } from 'shared/ReactTypeOfWork'; import { NoEffect, PerformedWork, Placement, ContentReset, - Ref, DidCapture, Update, + Ref, } from 'shared/ReactTypeOfSideEffect'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import { enableGetDerivedStateFromCatch, + enableSuspense, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, enableProfilerTimer, @@ -91,8 +93,12 @@ export default function( newContext: NewContext, hydrationContext: HydrationContext, scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, - computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, + computeExpirationForFiber: ( + startTime: ExpirationTime, + fiber: Fiber, + ) => ExpirationTime, profilerTimer: ProfilerTimer, + recalculateCurrentTime: () => ExpirationTime, ) { const {shouldSetTextContent, shouldDeprioritizeSubtree} = config; @@ -132,6 +138,7 @@ export default function( computeExpirationForFiber, memoizeProps, memoizeState, + recalculateCurrentTime, ); // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. @@ -758,6 +765,41 @@ export default function( return workInProgress.stateNode; } + function updateTimeoutComponent( + current, + workInProgress, + renderExpirationTime, + ) { + if (enableSuspense) { + const nextProps = workInProgress.pendingProps; + const prevProps = workInProgress.memoizedProps; + + const prevDidTimeout = workInProgress.memoizedState; + + // Check if we already attempted to render the normal state. If we did, + // and we timed out, render the placeholder state. + const alreadyCaptured = + (workInProgress.effectTag & DidCapture) === NoEffect; + const nextDidTimeout = !alreadyCaptured; + + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + + const render = nextProps.children; + const nextChildren = render(nextDidTimeout); + workInProgress.memoizedProps = nextProps; + workInProgress.memoizedState = nextDidTimeout; + reconcileChildren(current, workInProgress, nextChildren); + return workInProgress.child; + } else { + return null; + } + } + function updatePortalComponent( current, workInProgress, @@ -1209,6 +1251,12 @@ export default function( // A return component is just a placeholder, we can just run through the // next one immediately. return null; + case TimeoutComponent: + return updateTimeoutComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostPortal: return updatePortalComponent( current, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 444d1070e6..36bb58ecc5 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -152,9 +152,13 @@ export function applyDerivedStateFromProps( export default function( legacyContext: LegacyContext, scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, - computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, + computeExpirationForFiber: ( + currentTime: ExpirationTime, + fiber: Fiber, + ) => ExpirationTime, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, + recalculateCurrentTime: () => ExpirationTime, ) { const { cacheContext, @@ -168,7 +172,8 @@ export default function( isMounted, enqueueSetState(inst, payload, callback) { const fiber = ReactInstanceMap.get(inst); - const expirationTime = computeExpirationForFiber(fiber); + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); const update = createUpdate(expirationTime); update.payload = payload; @@ -184,7 +189,8 @@ export default function( }, enqueueReplaceState(inst, payload, callback) { const fiber = ReactInstanceMap.get(inst); - const expirationTime = computeExpirationForFiber(fiber); + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); const update = createUpdate(expirationTime); update.tag = ReplaceState; @@ -202,7 +208,8 @@ export default function( }, enqueueForceUpdate(inst, callback) { const fiber = ReactInstanceMap.get(inst); - const expirationTime = computeExpirationForFiber(fiber); + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); const update = createUpdate(expirationTime); update.tag = ForceUpdate; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 686a49219b..f3184ced83 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -27,6 +27,7 @@ import { HostPortal, CallComponent, Profiler, + TimeoutComponent, } from 'shared/ReactTypeOfWork'; import ReactErrorUtils from 'shared/ReactErrorUtils'; import { @@ -314,6 +315,10 @@ export default function( // We have no life-cycles associated with Profiler. return; } + case TimeoutComponent: { + // We have no life-cycles associated with Timeouts. + return; + } default: { invariant( false, @@ -836,6 +841,9 @@ export default function( } return; } + case TimeoutComponent: { + return; + } default: { invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e9752e38da..ab55061b12 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -40,6 +40,7 @@ import { Fragment, Mode, Profiler, + TimeoutComponent, } from 'shared/ReactTypeOfWork'; import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; @@ -593,6 +594,8 @@ export default function( return null; case ForwardRef: return null; + case TimeoutComponent: + return null; case Fragment: return null; case Mode: diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index dca42c4e3d..7143173c9c 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -38,8 +38,11 @@ export function computeExpirationBucket( expirationInMs: number, bucketSizeMs: number, ): ExpirationTime { - return ceiling( - currentTime + expirationInMs / UNIT_SIZE, - bucketSizeMs / UNIT_SIZE, + return ( + MAGIC_NUMBER_OFFSET + + ceiling( + currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE, + bucketSizeMs / UNIT_SIZE, + ) ); } diff --git a/packages/react-reconciler/src/ReactFiberPendingPriority.js b/packages/react-reconciler/src/ReactFiberPendingPriority.js new file mode 100644 index 0000000000..b34b266713 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberPendingPriority.js @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {FiberRoot} from './ReactFiberRoot'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +import {NoWork} from './ReactFiberExpirationTime'; + +import {enableSuspense} from 'shared/ReactFeatureFlags'; + +// TODO: Offscreen updates + +export function markPendingPriorityLevel( + root: FiberRoot, + expirationTime: ExpirationTime, +): void { + if (enableSuspense) { + // Update the latest and earliest pending times + const earliestPendingTime = root.earliestPendingTime; + if (earliestPendingTime === NoWork) { + // No other pending updates. + root.earliestPendingTime = root.latestPendingTime = expirationTime; + } else { + if (earliestPendingTime > expirationTime) { + // This is the earliest pending update. + root.earliestPendingTime = expirationTime; + } else { + const latestPendingTime = root.latestPendingTime; + if (latestPendingTime < expirationTime) { + // This is the latest pending update + root.latestPendingTime = expirationTime; + } + } + } + } +} + +export function markCommittedPriorityLevels( + root: FiberRoot, + currentTime: ExpirationTime, + earliestRemainingTime: ExpirationTime, +): void { + if (enableSuspense) { + if (earliestRemainingTime === NoWork) { + // Fast path. There's no remaining work. Clear everything. + root.earliestPendingTime = NoWork; + root.latestPendingTime = NoWork; + root.earliestSuspendedTime = NoWork; + root.latestSuspendedTime = NoWork; + root.latestPingedTime = NoWork; + return; + } + + // Let's see if the previous latest known pending level was just flushed. + const latestPendingTime = root.latestPendingTime; + if (latestPendingTime !== NoWork) { + if (latestPendingTime < earliestRemainingTime) { + // We've flushed all the known pending levels. + root.earliestPendingTime = root.latestPendingTime = NoWork; + } else { + const earliestPendingTime = root.earliestPendingTime; + if (earliestPendingTime < earliestRemainingTime) { + // We've flushed the earliest known pending level. Set this to the + // latest pending time. + root.earliestPendingTime = root.latestPendingTime; + } + } + } + + // Now let's handle the earliest remaining level in the whole tree. We need to + // decide whether to treat it as a pending level or as suspended. Check + // it falls within the range of known suspended levels. + + const earliestSuspendedTime = root.earliestSuspendedTime; + if (earliestSuspendedTime === NoWork) { + // There's no suspended work. Treat the earliest remaining level as a + // pending level. + markPendingPriorityLevel(root, earliestRemainingTime); + return; + } + + const latestSuspendedTime = root.latestSuspendedTime; + if (earliestRemainingTime > latestSuspendedTime) { + // The earliest remaining level is later than all the suspended work. That + // means we've flushed all the suspended work. + root.earliestSuspendedTime = NoWork; + root.latestSuspendedTime = NoWork; + root.latestPingedTime = NoWork; + + // There's no suspended work. Treat the earliest remaining level as a + // pending level. + markPendingPriorityLevel(root, earliestRemainingTime); + return; + } + + if (earliestRemainingTime < earliestSuspendedTime) { + // The earliest remaining time is earlier than all the suspended work. + // Treat it as a pending update. + markPendingPriorityLevel(root, earliestRemainingTime); + return; + } + + // The earliest remaining time falls within the range of known suspended + // levels. We should treat this as suspended work. + } +} + +export function markSuspendedPriorityLevel( + root: FiberRoot, + suspendedTime: ExpirationTime, +): void { + if (enableSuspense) { + // First, check the known pending levels and update them if needed. + const earliestPendingTime = root.earliestPendingTime; + const latestPendingTime = root.latestPendingTime; + if (earliestPendingTime === suspendedTime) { + if (latestPendingTime === suspendedTime) { + // Both known pending levels were suspended. Clear them. + root.earliestPendingTime = root.latestPendingTime = NoWork; + } else { + // The earliest pending level was suspended. Clear by setting it to the + // latest pending level. + root.earliestPendingTime = latestPendingTime; + } + } else if (latestPendingTime === suspendedTime) { + // The latest pending level was suspended. Clear by setting it to the + // latest pending level. + root.latestPendingTime = earliestPendingTime; + } + + // Next, if we're working on the lowest known suspended level, clear the ping. + // TODO: What if a promise suspends and pings before the root completes? + const latestSuspendedTime = root.latestSuspendedTime; + if (latestSuspendedTime === suspendedTime) { + root.latestPingedTime = NoWork; + } + + // Finally, update the known suspended levels. + const earliestSuspendedTime = root.earliestSuspendedTime; + if (earliestSuspendedTime === NoWork) { + // No other suspended levels. + root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime; + } else { + if (earliestSuspendedTime > suspendedTime) { + // This is the earliest suspended level. + root.earliestSuspendedTime = suspendedTime; + } else if (latestSuspendedTime < suspendedTime) { + // This is the latest suspended level + root.latestSuspendedTime = suspendedTime; + } + } + } +} + +export function markPingedPriorityLevel( + root: FiberRoot, + pingedTime: ExpirationTime, +): void { + if (enableSuspense) { + const latestSuspendedTime = root.latestSuspendedTime; + if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) { + const latestPingedTime = root.latestPingedTime; + if (latestPingedTime === NoWork || latestPingedTime < pingedTime) { + root.latestPingedTime = pingedTime; + } + } + } +} + +export function findNextPendingPriorityLevel(root: FiberRoot): ExpirationTime { + if (enableSuspense) { + const earliestSuspendedTime = root.earliestSuspendedTime; + const earliestPendingTime = root.earliestPendingTime; + if (earliestSuspendedTime === NoWork) { + // Fast path. There's no suspended work. + return earliestPendingTime; + } + + // First, check if there's known pending work. + if (earliestPendingTime !== NoWork) { + return earliestPendingTime; + } + + // Finally, if a suspended level was pinged, work on that. Otherwise there's + // nothing to work on. + return root.latestPingedTime; + } else { + return root.current.expirationTime; + } +} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index a3f38bc394..dd395af753 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -317,7 +317,6 @@ export default function( function scheduleRootUpdate( current: Fiber, element: ReactNodeList, - currentTime: ExpirationTime, expirationTime: ExpirationTime, callback: ?Function, ) { @@ -364,7 +363,6 @@ export default function( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, - currentTime: ExpirationTime, expirationTime: ExpirationTime, callback: ?Function, ) { @@ -390,13 +388,7 @@ export default function( container.pendingContext = context; } - return scheduleRootUpdate( - current, - element, - currentTime, - expirationTime, - callback, - ); + return scheduleRootUpdate(current, element, expirationTime, callback); } function findHostInstance(component: Object): PI | null { @@ -436,12 +428,11 @@ export default function( ): ExpirationTime { const current = container.current; const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(current); + const expirationTime = computeExpirationForFiber(currentTime, current); return updateContainerAtExpirationTime( element, container, parentComponent, - currentTime, expirationTime, callback, ); @@ -454,12 +445,10 @@ export default function( expirationTime, callback, ) { - const currentTime = recalculateCurrentTime(); return updateContainerAtExpirationTime( element, container, parentComponent, - currentTime, expirationTime, callback, ); diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 50bcafbd39..302675ad3c 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -28,6 +28,22 @@ export type FiberRoot = { pendingChildren: any, // The currently active root fiber. This is the mutable root of the tree. current: Fiber, + + // The following priority levels are used to distinguish between 1) + // uncommitted work, 2) uncommitted work that is suspended, and 3) uncommitted + // work that may be unsuspended. We choose not to track each individual + // pending level, trading granularity for performance. + // + // The earliest and latest priority levels that are suspended from committing. + earliestSuspendedTime: ExpirationTime, + latestSuspendedTime: ExpirationTime, + // The earliest and latest priority levels that are not known to be suspended. + earliestPendingTime: ExpirationTime, + latestPendingTime: ExpirationTime, + // The latest priority level that was pinged by a resolved promise and can + // be retried. + latestPingedTime: ExpirationTime, + pendingCommitExpirationTime: ExpirationTime, // A finished work-in-progress HostRoot that's ready to be committed. // TODO: The reason this is separate from isReadyForCommit is because the @@ -62,6 +78,13 @@ export function createFiberRoot( current: uninitializedFiber, containerInfo: containerInfo, pendingChildren: null, + + earliestPendingTime: NoWork, + latestPendingTime: NoWork, + earliestSuspendedTime: NoWork, + latestSuspendedTime: NoWork, + latestPingedTime: NoWork, + pendingCommitExpirationTime: NoWork, finishedWork: null, context: null, diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index fde57fd5a4..5c9657876c 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -58,6 +58,13 @@ import ReactFiberHostContext from './ReactFiberHostContext'; import ReactFiberHydrationContext from './ReactFiberHydrationContext'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; +import { + markPendingPriorityLevel, + markCommittedPriorityLevels, + findNextPendingPriorityLevel, + markSuspendedPriorityLevel, + markPingedPriorityLevel, +} from './ReactFiberPendingPriority'; import { recordEffect, recordScheduleUpdate, @@ -94,6 +101,10 @@ import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; +export type Thenable = { + then(resolve: () => mixed, reject?: () => mixed): mixed, +}; + const { invokeGuardedCallback, hasCaughtError, @@ -190,6 +201,7 @@ export default function( scheduleWork, computeExpirationForFiber, profilerTimer, + recalculateCurrentTime, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -211,10 +223,14 @@ export default function( legacyContext, newContext, scheduleWork, + computeExpirationForFiber, + recalculateCurrentTime, markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, onUncaughtError, profilerTimer, + suspendRoot, + retrySuspendedRoot, ); const { commitBeforeMutationLifeCycles, @@ -264,6 +280,8 @@ export default function( let nextRoot: FiberRoot | null = null; // The time at which we're currently rendering work. let nextRenderExpirationTime: ExpirationTime = NoWork; + let nextLatestTimeoutMs: number = -1; + let nextRenderIsExpired: boolean = false; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -288,9 +306,20 @@ export default function( originalReplayError = null; replayUnitOfWork = ( failedUnitOfWork: Fiber, - error: mixed, + thrownValue: mixed, isAsync: boolean, ) => { + if ( + thrownValue !== null && + typeof thrownValue === 'object' && + typeof thrownValue.then === 'function' + ) { + // Don't replay promises. Treat everything else like an error. + // TODO: Need to figure out a different strategy if/when we add + // support for catching other types. + return; + } + // Restore the original state of the work-in-progress assignFiberPropertiesInDEV( failedUnitOfWork, @@ -316,7 +345,7 @@ export default function( } // Replay the begin phase. isReplayingFailedUnitOfWork = true; - originalReplayError = error; + originalReplayError = thrownValue; invokeGuardedCallback(null, workLoop, null, isAsync); isReplayingFailedUnitOfWork = false; originalReplayError = null; @@ -354,6 +383,8 @@ export default function( nextRoot = null; nextRenderExpirationTime = NoWork; + nextLatestTimeoutMs = -1; + nextRenderIsExpired = false; nextUnitOfWork = null; isRootReadyForCommit = false; @@ -685,7 +716,8 @@ export default function( ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork); } - const remainingTime = root.current.expirationTime; + markCommittedPriorityLevels(root, currentTime, root.current.expirationTime); + const remainingTime = findNextPendingPriorityLevel(root); if (remainingTime === NoWork) { // If there's no remaining work, we can clear the set of already failed // error boundaries. @@ -849,7 +881,11 @@ export default function( // This fiber did not complete because something threw. Pop values off // the stack without entering the complete phase. If this is a boundary, // capture values if possible. - const next = unwindWork(workInProgress); + const next = unwindWork( + workInProgress, + nextRenderIsExpired, + nextRenderExpirationTime, + ); // Because this fiber did not complete, don't reset its expiration time. if (workInProgress.effectTag & DidCapture) { // Restarting an error boundary @@ -1003,6 +1039,7 @@ export default function( resetStack(); nextRoot = root; nextRenderExpirationTime = expirationTime; + nextLatestTimeoutMs = -1; nextUnitOfWork = createWorkInProgress( nextRoot.current, null, @@ -1013,6 +1050,9 @@ export default function( let didFatal = false; + nextRenderIsExpired = + !isAsync || nextRenderExpirationTime <= mostRecentCurrentTime; + startWorkLoopTimer(nextUnitOfWork); do { @@ -1056,10 +1096,13 @@ export default function( break; } throwException( + root, returnFiber, sourceFiber, thrownValue, + nextRenderIsExpired, nextRenderExpirationTime, + mostRecentCurrentTimeMs, ); nextUnitOfWork = completeUnitOfWork(sourceFiber); } @@ -1094,10 +1137,19 @@ export default function( stopWorkLoopTimer(interruptedBy, didCompleteRoot); interruptedBy = null; invariant( - false, + !nextRenderIsExpired, 'Expired work should have completed. This error is likely caused ' + 'by a bug in React. Please file an issue.', ); + markSuspendedPriorityLevel(root, expirationTime); + if (nextLatestTimeoutMs >= 0) { + setTimeout(() => { + retrySuspendedRoot(root, expirationTime); + }, nextLatestTimeoutMs); + } + const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root); + onBlock(firstUnblockedExpirationTime); + return null; } } else { stopWorkLoopTimer(interruptedBy, didCompleteRoot); @@ -1218,7 +1270,10 @@ export default function( return lastUniqueAsyncExpiration; } - function computeExpirationForFiber(fiber: Fiber) { + function computeExpirationForFiber( + currentTime: ExpirationTime, + fiber: Fiber, + ) { let expirationTime; if (expirationContext !== NoWork) { // An explicit expiration context was set; @@ -1239,11 +1294,9 @@ export default function( if (fiber.mode & AsyncMode) { if (isBatchingInteractiveUpdates) { // This is an interactive update - const currentTime = recalculateCurrentTime(); expirationTime = computeInteractiveExpiration(currentTime); } else { // This is an async update - const currentTime = recalculateCurrentTime(); expirationTime = computeAsyncExpiration(currentTime); } } else { @@ -1265,19 +1318,32 @@ export default function( return expirationTime; } - function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { - return scheduleWorkImpl(fiber, expirationTime, false); + // TODO: Rename this to scheduleTimeout or something + function suspendRoot( + root: FiberRoot, + thenable: Thenable, + timeoutMs: number, + suspendedTime: ExpirationTime, + ) { + // Schedule the timeout. + if (timeoutMs >= 0 && nextLatestTimeoutMs < timeoutMs) { + nextLatestTimeoutMs = timeoutMs; + } } - function scheduleWorkImpl( - fiber: Fiber, - expirationTime: ExpirationTime, - isErrorRecovery: boolean, - ) { + function retrySuspendedRoot(root, suspendedTime) { + markPingedPriorityLevel(root, suspendedTime); + const retryTime = findNextPendingPriorityLevel(root); + if (retryTime !== NoWork) { + requestRetry(root, retryTime); + } + } + + function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { recordScheduleUpdate(); if (__DEV__) { - if (!isErrorRecovery && fiber.tag === ClassComponent) { + if (fiber.tag === ClassComponent) { const instance = fiber.stateNode; warnAboutInvalidUpdates(instance); } @@ -1313,6 +1379,8 @@ export default function( interruptedBy = fiber; resetStack(); } + markPendingPriorityLevel(root, expirationTime); + const nextExpirationTimeToWorkOn = findNextPendingPriorityLevel(root); if ( // If we're in the render phase, we don't need to schedule this root // for an update, because we'll do it before we exit... @@ -1321,8 +1389,7 @@ export default function( // ...unless this is a different root than the one we're rendering. nextRoot !== root ) { - // Add this root to the root schedule. - requestWork(root, expirationTime); + requestWork(root, nextExpirationTimeToWorkOn); } if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { invariant( @@ -1335,7 +1402,7 @@ export default function( } } else { if (__DEV__) { - if (!isErrorRecovery && fiber.tag === ClassComponent) { + if (fiber.tag === ClassComponent) { warnAboutUpdateOnUnmounted(fiber); } } @@ -1434,6 +1501,18 @@ export default function( callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); } + function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) { + if ( + root.remainingExpirationTime === NoWork || + root.remainingExpirationTime < expirationTime + ) { + // For a retry, only update the remaining expiration time if it has a + // *lower priority* than the existing value. This is because, on a retry, + // we should attempt to coalesce as much as possible. + requestWork(root, expirationTime); + } + } + // requestWork is called by the scheduler whenever a root receives an update. // It's up to the renderer to call renderRoot at some point in the future. function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { @@ -1606,6 +1685,7 @@ export default function( (!deadlineDidExpire || recalculateCurrentTime() >= nextFlushedExpirationTime) ) { + recalculateCurrentTime(); performWorkOnRoot( nextFlushedRoot, nextFlushedExpirationTime, @@ -1807,6 +1887,16 @@ export default function( } } + function onBlock(remainingExpirationTime: ExpirationTime) { + invariant( + nextFlushedRoot !== null, + 'Should be working on a root. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); + // This root was blocked. Unschedule it until there's another update. + nextFlushedRoot.remainingExpirationTime = remainingExpirationTime; + } + // TODO: Batching should be implemented at the renderer level, not inside // the reconciler. function batchedUpdates(fn: (a: A) => R, a: A): R { diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 3337f02fcd..31bb8a258b 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -9,6 +9,7 @@ import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; +import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; import type {LegacyContext} from './ReactFiberContext'; @@ -16,11 +17,13 @@ import type {NewContext} from './ReactFiberNewContext'; import type {CapturedValue} from './ReactCapturedValue'; import type {ProfilerTimer} from './ReactProfilerTimer'; import type {Update} from './ReactUpdateQueue'; +import type {Thenable} from './ReactFiberScheduler'; import {createCapturedValue} from './ReactCapturedValue'; import { enqueueCapturedUpdate, createUpdate, + enqueueUpdate, CaptureUpdate, } from './ReactUpdateQueue'; import {logError} from './ReactFiberCommitWork'; @@ -32,6 +35,7 @@ import { HostPortal, ContextProvider, Profiler, + TimeoutComponent, } from 'shared/ReactTypeOfWork'; import { DidCapture, @@ -42,22 +46,33 @@ import { import { enableGetDerivedStateFromCatch, enableProfilerTimer, + enableSuspense, } from 'shared/ReactFeatureFlags'; +import {Never, Sync, expirationTimeToMs} from './ReactFiberExpirationTime'; + export default function( config: HostConfig, hostContext: HostContext, legacyContext: LegacyContext, newContext: NewContext, - scheduleWork: ( - fiber: Fiber, + scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, + computeExpirationForFiber: ( startTime: ExpirationTime, - expirationTime: ExpirationTime, - ) => void, + fiber: Fiber, + ) => ExpirationTime, + recalculateCurrentTime: () => ExpirationTime, markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean, onUncaughtError: (error: mixed) => void, profilerTimer: ProfilerTimer, + suspendRoot: ( + root: FiberRoot, + thenable: Thenable, + timeoutMs: number, + suspendedTime: ExpirationTime, + ) => void, + retrySuspendedRoot: (root: FiberRoot, suspendedTime: ExpirationTime) => void, ) { const {popHostContainer, popHostContext} = hostContext; const { @@ -132,19 +147,133 @@ export default function( return update; } + function schedulePing(finishedWork) { + // Once the promise resolves, we should try rendering the non- + // placeholder state again. + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, finishedWork); + const recoveryUpdate = createUpdate(expirationTime); + enqueueUpdate(finishedWork, recoveryUpdate, expirationTime); + scheduleWork(finishedWork, expirationTime); + } + function throwException( + root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, - rawValue: mixed, + value: mixed, + renderIsExpired: boolean, renderExpirationTime: ExpirationTime, + currentTimeMs: number, ) { // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; // Its effect list is no longer valid. sourceFiber.firstEffect = sourceFiber.lastEffect = null; - const value = createCapturedValue(rawValue, sourceFiber); + if ( + enableSuspense && + value !== null && + typeof value === 'object' && + typeof value.then === 'function' + ) { + // This is a thenable. + const thenable: Thenable = (value: any); + const expirationTimeMs = expirationTimeToMs(renderExpirationTime); + const startTimeMs = expirationTimeMs - 5000; + let elapsedMs = currentTimeMs - startTimeMs; + if (elapsedMs < 0) { + elapsedMs = 0; + } + const remainingTimeMs = expirationTimeMs - currentTimeMs; + + // Find the earliest timeout of all the timeouts in the ancestor path. + // TODO: Alternatively, we could store the earliest timeout on the context + // stack, rather than searching on every suspend. + let workInProgress = returnFiber; + let earliestTimeoutMs = -1; + searchForEarliestTimeout: do { + if (workInProgress.tag === TimeoutComponent) { + const current = workInProgress.alternate; + if (current !== null && current.memoizedState === true) { + // A parent Timeout already committed in a placeholder state. We + // need to handle this promise immediately. In other words, we + // should never suspend inside a tree that already expired. + earliestTimeoutMs = 0; + break searchForEarliestTimeout; + } + let timeoutPropMs = workInProgress.pendingProps.ms; + if (typeof timeoutPropMs === 'number') { + if (timeoutPropMs <= 0) { + earliestTimeoutMs = 0; + break searchForEarliestTimeout; + } else if ( + earliestTimeoutMs === -1 || + timeoutPropMs < earliestTimeoutMs + ) { + earliestTimeoutMs = timeoutPropMs; + } + } else if (earliestTimeoutMs === -1) { + earliestTimeoutMs = remainingTimeMs; + } + } + workInProgress = workInProgress.return; + } while (workInProgress !== null); + + // Compute the remaining time until the timeout. + const msUntilTimeout = earliestTimeoutMs - elapsedMs; + + if (renderExpirationTime === Never || msUntilTimeout > 0) { + // There's still time remaining. + suspendRoot(root, thenable, msUntilTimeout, renderExpirationTime); + const onResolveOrReject = () => { + retrySuspendedRoot(root, renderExpirationTime); + }; + thenable.then(onResolveOrReject, onResolveOrReject); + return; + } else { + // No time remaining. Need to fallback to placeholder. + // Find the nearest timeout that can be retried. + workInProgress = returnFiber; + do { + switch (workInProgress.tag) { + case HostRoot: { + // The root expired, but no fallback was provided. Throw a + // helpful error. + const message = + renderExpirationTime === Sync + ? 'A synchronous update was suspended, but no fallback UI ' + + 'was provided.' + : 'An update was suspended for longer than the timeout, ' + + 'but no fallback UI was provided.'; + value = new Error(message); + break; + } + case TimeoutComponent: { + if ((workInProgress.effectTag & DidCapture) === NoEffect) { + workInProgress.effectTag |= ShouldCapture; + const onResolveOrReject = schedulePing.bind( + null, + workInProgress, + ); + thenable.then(onResolveOrReject, onResolveOrReject); + return; + } + // Already captured during this render. Continue to the next + // Timeout ancestor. + break; + } + } + workInProgress = workInProgress.return; + } while (workInProgress !== null); + } + } + + // We didn't find a boundary that could handle this type of exception. Start + // over and traverse parent path again, this time treating the exception + // as an error. + value = createCapturedValue(value, sourceFiber); let workInProgress = returnFiber; do { switch (workInProgress.tag) { @@ -190,7 +319,11 @@ export default function( } while (workInProgress !== null); } - function unwindWork(workInProgress: Fiber) { + function unwindWork( + workInProgress: Fiber, + renderIsExpired: boolean, + renderExpirationTime: ExpirationTime, + ) { switch (workInProgress.tag) { case ClassComponent: { popLegacyContextProvider(workInProgress); @@ -215,6 +348,14 @@ export default function( popHostContext(workInProgress); return null; } + case TimeoutComponent: { + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; + } + return null; + } case HostPortal: popHostContainer(workInProgress); return null; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js new file mode 100644 index 0000000000..e834ca5eda --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -0,0 +1,946 @@ +let React; +let ReactFeatureFlags; +let Fragment; +let ReactNoop; +let SimpleCacheProvider; +let Timeout; + +let cache; +let TextResource; +let textResourceShouldFail; + +describe('ReactSuspense', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.enableSuspense = true; + React = require('react'); + Fragment = React.Fragment; + ReactNoop = require('react-noop-renderer'); + SimpleCacheProvider = require('simple-cache-provider'); + Timeout = React.Timeout; + + function invalidateCache() { + cache = SimpleCacheProvider.createCache(invalidateCache); + } + invalidateCache(); + TextResource = SimpleCacheProvider.createResource(([text, ms = 0]) => { + return new Promise((resolve, reject) => + setTimeout(() => { + if (textResourceShouldFail) { + ReactNoop.yield(`Promise rejected [${text}]`); + reject(new Error('Failed to load: ' + text)); + } else { + ReactNoop.yield(`Promise resolved [${text}]`); + resolve(text); + } + }, ms), + ); + }, ([text, ms]) => text); + textResourceShouldFail = false; + }); + + function div(...children) { + children = children.map(c => (typeof c === 'string' ? {text: c} : c)); + return {type: 'div', children, prop: undefined}; + } + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + function advanceTimers(ms) { + // Note: This advances Jest's virtual time but not React's. Use + // ReactNoop.expire for that. + if (typeof ms !== 'number') { + throw new Error('Must specify ms'); + } + jest.advanceTimersByTime(ms); + // Wait until the end of the current tick + return new Promise(resolve => { + setImmediate(resolve); + }); + } + + function Text(props) { + ReactNoop.yield(props.text); + return ; + } + + function AsyncText(props) { + const text = props.text; + try { + TextResource.read(cache, [props.text, props.ms]); + ReactNoop.yield(text); + return ; + } catch (promise) { + if (typeof promise.then === 'function') { + ReactNoop.yield(`Suspend! [${text}]`); + } else { + ReactNoop.yield(`Error! [${text}]`); + } + throw promise; + } + } + + function Fallback(props) { + return ( + + {didExpire => (didExpire ? props.placeholder : props.children)} + + ); + } + it('suspends rendering and continues later', async () => { + function Bar(props) { + ReactNoop.yield('Bar'); + return props.children; + } + + function Foo() { + ReactNoop.yield('Foo'); + return ( + + + + + + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'Foo', + 'Bar', + // A suspends + 'Suspend! [A]', + // But we keep rendering the siblings + 'B', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + + // Flush some of the time + await advanceTimers(50); + // Still nothing... + expect(ReactNoop.flush()).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([]); + + // Flush the promise completely + await advanceTimers(50); + // Renders successfully + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [A]', + 'Foo', + 'Bar', + 'A', + 'B', + ]); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); + + it('continues rendering siblings after suspending', async () => { + ReactNoop.render( + + + + + + , + ); + // B suspends. Continue rendering the remaining siblings. + expect(ReactNoop.flush()).toEqual(['A', 'Suspend! [B]', 'C', 'D']); + // Did not commit yet. + expect(ReactNoop.getChildren()).toEqual([]); + + // Wait for data to resolve + await advanceTimers(100); + // Renders successfully + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [B]', + 'A', + 'B', + 'C', + 'D', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('A'), + span('B'), + span('C'), + span('D'), + ]); + }); + + it('retries on error', async () => { + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + reset() { + this.setState({error: null}); + } + render() { + if (this.state.error !== null) { + return ; + } + return this.props.children; + } + } + + const errorBoundary = React.createRef(); + function App() { + return ( + + + + + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']); + expect(ReactNoop.getChildren()).toEqual([]); + + textResourceShouldFail = true; + ReactNoop.expire(1000); + await advanceTimers(1000); + textResourceShouldFail = false; + + expect(ReactNoop.flush()).toEqual([ + 'Promise rejected [Result]', + 'Error! [Result]', + 'Caught error: Failed to load: Result', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Caught error: Failed to load: Result'), + ]); + + // Reset the error boundary and cache, and try again. + errorBoundary.current.reset(); + cache.invalidate(); + + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']); + expect(ReactNoop.getChildren()).toEqual([span('Result')]); + }); + + it('retries on error after falling back to a placeholder', async () => { + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + reset() { + this.setState({error: null}); + } + render() { + if (this.state.error !== null) { + return ; + } + return this.props.children; + } + } + + const errorBoundary = React.createRef(); + function App() { + return ( + }> + + + + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']); + expect(ReactNoop.getChildren()).toEqual([]); + + ReactNoop.expire(2000); + await advanceTimers(2000); + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + textResourceShouldFail = true; + ReactNoop.expire(1000); + await advanceTimers(1000); + textResourceShouldFail = false; + + expect(ReactNoop.flush()).toEqual([ + 'Promise rejected [Result]', + 'Error! [Result]', + 'Caught error: Failed to load: Result', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Caught error: Failed to load: Result'), + ]); + + // Reset the error boundary and cache, and try again. + errorBoundary.current.reset(); + cache.invalidate(); + + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']); + ReactNoop.expire(3000); + await advanceTimers(3000); + expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']); + expect(ReactNoop.getChildren()).toEqual([span('Result')]); + }); + + it('can update at a higher priority while in a suspended state', async () => { + function App(props) { + return ( + + + + + ); + } + + // Initial mount + ReactNoop.render(); + ReactNoop.flush(); + await advanceTimers(0); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]); + + // Update the low-pri text + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'A', + // Suspends + 'Suspend! [2]', + ]); + + // While we're still waiting for the low-pri update to complete, update the + // high-pri text at high priority. + ReactNoop.flushSync(() => { + ReactNoop.render(); + }); + expect(ReactNoop.flush()).toEqual(['B', '1']); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]); + + // Unblock the low-pri text and finish + await advanceTimers(0); + expect(ReactNoop.flush()).toEqual(['Promise resolved [2]']); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]); + }); + + it('keeps working on lower priority work after being pinged', async () => { + function App(props) { + return ( + + + {props.showB && } + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Suspend! [A]']); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance React's virtual time by enough to fall into a new async bucket. + ReactNoop.expire(1200); + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'B']); + expect(ReactNoop.getChildren()).toEqual([]); + + await advanceTimers(0); + expect(ReactNoop.flush()).toEqual(['Promise resolved [A]', 'A', 'B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); + + it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => { + function App(props) { + if (props.hide) { + return ; + } + return ( + + + + ); + } + + // Schedule a high pri update and a low pri update, without rendering in + // between. + ReactNoop.interactiveUpdates(() => { + // High pri + ReactNoop.render(); + }); + // Low pri + ReactNoop.render(); + + expect(ReactNoop.flush()).toEqual([ + // The first update suspends + 'Suspend! [Async]', + // but we have another pending update that we can work on + '(empty)', + ]); + expect(ReactNoop.getChildren()).toEqual([span('(empty)')]); + }); + + it('coalesces all async updates when in a suspended state', async () => { + ReactNoop.render( + + + , + ); + ReactNoop.flush(); + await advanceTimers(0); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('A')]); + + ReactNoop.render( + + + , + ); + expect(ReactNoop.flush()).toEqual(['Suspend! [B]']); + expect(ReactNoop.getChildren()).toEqual([span('A')]); + + // Advance React's virtual time so that C falls into a new expiration bucket + ReactNoop.expire(1000); + ReactNoop.render( + + + , + ); + expect(ReactNoop.flush()).toEqual([ + // Tries C first, since it has a later expiration time + 'Suspend! [C]', + // Does not retry B, because its promise has not resolved yet. + ]); + + expect(ReactNoop.getChildren()).toEqual([span('A')]); + + // Unblock B + await advanceTimers(90); + // Even though B's promise resolved, the view is still suspended because it + // coalesced with C. + expect(ReactNoop.flush()).toEqual(['Promise resolved [B]']); + expect(ReactNoop.getChildren()).toEqual([span('A')]); + + // Unblock C + await advanceTimers(50); + expect(ReactNoop.flush()).toEqual(['Promise resolved [C]', 'C']); + expect(ReactNoop.getChildren()).toEqual([span('C')]); + }); + + it('forces an expiration after an update times out', async () => { + ReactNoop.render( + + }> + + + + , + ); + + expect(ReactNoop.flush()).toEqual([ + // The async child suspends + 'Suspend! [Async]', + // Continue on the sibling + 'Sync', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance both React's virtual time and Jest's timers by enough to expire + // the update, but not by enough to flush the suspending promise. + ReactNoop.expire(10000); + await advanceTimers(10000); + expect(ReactNoop.flushExpired()).toEqual([ + // Still suspended. + 'Suspend! [Async]', + // Now that the update has expired, we render the fallback UI + 'Loading...', + 'Sync', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]); + + // Once the promise resolves, we render the suspended view + await advanceTimers(10000); + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); + }); + + it('switches to an inner fallback even if it expires later', async () => { + ReactNoop.render( + + + }> + + }> + + + + , + ); + + expect(ReactNoop.flush()).toEqual([ + 'Sync', + // The async content suspends + 'Suspend! [Outer content]', + 'Suspend! [Inner content]', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + + // Expire the outer timeout, but don't expire the inner one. + // We should see the outer loading placeholder. + ReactNoop.expire(1500); + await advanceTimers(1500); + expect(ReactNoop.flush()).toEqual([ + 'Sync', + // Still suspended. + 'Suspend! [Outer content]', + 'Suspend! [Inner content]', + // We attempt to fallback to the inner placeholder + 'Loading inner...', + // But the outer content is still suspended, so we need to fallback to + // the outer placeholder. + 'Loading outer...', + ]); + + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Loading outer...'), + ]); + + // Resolve the outer content's promise + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [Outer content]', + 'Outer content', + // Inner content still hasn't loaded + 'Suspend! [Inner content]', + 'Loading inner...', + ]); + // We should now see the inner fallback UI. + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Outer content'), + span('Loading inner...'), + ]); + + // Finally, flush the inner promise. We should see the complete screen. + ReactNoop.expire(3000); + await advanceTimers(3000); + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [Inner content]', + 'Inner content', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Outer content'), + span('Inner content'), + ]); + }); + + it('renders an expiration boundary synchronously', async () => { + // Synchronously render a tree that suspends + ReactNoop.flushSync(() => + ReactNoop.render( + + }> + + + + , + ), + ); + expect(ReactNoop.clearYields()).toEqual([ + // The async child suspends + 'Suspend! [Async]', + // We immediately render the fallback UI + 'Loading...', + // Continue on the sibling + 'Sync', + ]); + // The tree commits synchronously + expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]); + + // Once the promise resolves, we render the suspended view + await advanceTimers(0); + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); + }); + + it('suspending inside an expired expiration boundary will bubble to the next one', async () => { + ReactNoop.flushSync(() => + ReactNoop.render( + + }> + }> + + + + + , + ), + ); + expect(ReactNoop.clearYields()).toEqual([ + 'Suspend! [Async]', + 'Suspend! [Loading (inner)...]', + 'Sync', + 'Loading (outer)...', + ]); + // The tree commits synchronously + expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]); + }); + + it('expires early with a `timeout` option', async () => { + ReactNoop.render( + + }> + + + + , + ); + + expect(ReactNoop.flush()).toEqual([ + // The async child suspends + 'Suspend! [Async]', + // Continue on the sibling + 'Sync', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance both React's virtual time and Jest's timers by enough to trigger + // the timeout, but not by enough to flush the promise or reach the true + // expiration time. + ReactNoop.expire(2000); + await advanceTimers(2000); + expect(ReactNoop.flush()).toEqual([ + // Still suspended. + 'Suspend! [Async]', + // Now that the expiration view has timed out, we render the fallback UI + 'Loading...', + 'Sync', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]); + + // Once the promise resolves, we render the suspended view + await advanceTimers(1000); + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); + }); + + it('throws a helpful error when a synchronous update is suspended', () => { + expect(() => { + ReactNoop.flushSync(() => + ReactNoop.render({() => }), + ); + }).toThrow( + 'A synchronous update was suspended, but no fallback UI was provided.', + ); + }); + + it('throws a helpful error when an expired update is suspended', async () => { + ReactNoop.render( + {() => }, + ); + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']); + await advanceTimers(10000); + ReactNoop.expire(10000); + expect(() => { + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']); + }).toThrow( + 'An update was suspended for longer than the timeout, but no fallback ' + + 'UI was provided.', + ); + }); + + it('a Timeout component correctly handles more than one suspended child', async () => { + ReactNoop.render( + + + + , + ); + ReactNoop.expire(10000); + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Suspend! [B]']); + expect(ReactNoop.getChildren()).toEqual([]); + + await advanceTimers(100); + + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [A]', + 'Promise resolved [B]', + 'A', + 'B', + ]); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); + + it('can resume rendering earlier than a timeout', async () => { + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance time by an amount slightly smaller than what's necessary to + // resolve the promise + await advanceTimers(99); + + // Nothing has rendered yet + expect(ReactNoop.flush()).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([]); + + // Resolve the promise + await advanceTimers(1); + // We can now resume rendering + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async')]); + }); + + it('starts working on an update even if its priority falls between two suspended levels', async () => { + function App(props) { + return ( + + {props.text === 'C' ? ( + + ) : ( + + )} + + ); + } + + // Schedule an update + ReactNoop.render(); + // The update should suspend. + expect(ReactNoop.flush()).toEqual(['Suspend! [A]']); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance time until right before it expires. This number may need to + // change if the default expiration for low priority updates is adjusted. + await advanceTimers(4999); + ReactNoop.expire(4999); + expect(ReactNoop.flush()).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([]); + + // Schedule another low priority update. + ReactNoop.render(); + // This update should also suspend. + expect(ReactNoop.flush()).toEqual(['Suspend! [B]']); + expect(ReactNoop.getChildren()).toEqual([]); + + // Schedule a high priority update. Its expiration time will fall between + // the expiration times of the previous two updates. + ReactNoop.interactiveUpdates(() => { + ReactNoop.render(); + }); + expect(ReactNoop.flush()).toEqual(['C']); + expect(ReactNoop.getChildren()).toEqual([span('C')]); + + await advanceTimers(10000); + // Flush the remaining work. + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [A]', + 'Promise resolved [B]', + ]); + expect(ReactNoop.getChildren()).toEqual([span('C')]); + }); + + it('can hide a tree to unblock its surroundings', async () => { + function App() { + return ( + + {didTimeout => ( + + + {didTimeout ? : null} + + )} + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']); + expect(ReactNoop.getChildren()).toEqual([]); + + ReactNoop.expire(2000); + await advanceTimers(2000); + expect(ReactNoop.flush()).toEqual([ + 'Suspend! [Async]', + 'Loading...', + 'Suspend! [Async]', + ]); + expect(ReactNoop.getChildren()).toEqual([div(), span('Loading...')]); + + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); + expect(ReactNoop.getChildren()).toEqual([div(span('Async'))]); + }); + + describe('splitting a high-pri update into high and low', () => { + React = require('react'); + + class AsyncValue extends React.Component { + state = {asyncValue: this.props.defaultValue}; + componentDidMount() { + ReactNoop.deferredUpdates(() => { + this.setState((state, props) => ({asyncValue: props.value})); + }); + } + componentDidUpdate() { + if (this.props.value !== this.state.asyncValue) { + ReactNoop.deferredUpdates(() => { + this.setState((state, props) => ({asyncValue: props.value})); + }); + } + } + render() { + return this.props.children(this.state.asyncValue); + } + } + + it('coalesces async values when in a suspended state', async () => { + function App(props) { + const highPriText = props.text; + return ( + + + {lowPriText => ( + + + {lowPriText && ( + + )} + + )} + + + ); + } + + function renderAppSync(props) { + ReactNoop.flushSync(() => ReactNoop.render()); + } + + // Initial mount + renderAppSync({text: 'A'}); + expect(ReactNoop.flush()).toEqual([ + // First we render at high priority + 'High-pri: A', + // Then we come back later to render a low priority + 'High-pri: A', + // The low-pri view suspends + 'Suspend! [Low-pri: A]', + ]); + expect(ReactNoop.getChildren()).toEqual([span('High-pri: A')]); + + // Partially flush the promise for 'A', not by enough to resolve it. + await advanceTimers(99); + + // Advance React's virtual time so that the next update falls into a new + // expiration bucket + ReactNoop.expire(2000); + // Update to B. At this point, the low-pri view still hasn't updated + // to 'A'. + renderAppSync({text: 'B'}); + expect(ReactNoop.flush()).toEqual([ + // First we render at high priority + 'High-pri: B', + // Then we come back later to render a low priority + 'High-pri: B', + // The low-pri view suspends + 'Suspend! [Low-pri: B]', + ]); + expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]); + + // Flush the rest of the promise for 'A', without flushing the one + // for 'B'. + await advanceTimers(1); + expect(ReactNoop.flush()).toEqual([ + // A is unblocked + 'Promise resolved [Low-pri: A]', + // But we don't try to render it, because there's a lower priority + // update that is also suspended. + ]); + expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]); + + // Flush the remaining work. + await advanceTimers(99); + expect(ReactNoop.flush()).toEqual([ + // B is unblocked + 'Promise resolved [Low-pri: B]', + // Now we can continue rendering the async view + 'High-pri: B', + 'Low-pri: B', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('High-pri: B'), + span('Low-pri: B'), + ]); + }); + }); + + describe('a Delay component', () => { + function Never() { + // Throws a promise that resolves after some arbitrarily large + // number of seconds. The idea is that this component will never + // resolve. It's always wrapped by a Timeout. + throw new Promise(resolve => setTimeout(() => resolve(), 10000)); + } + + function Delay({ms}) { + return ( + + {didTimeout => { + if (didTimeout) { + // Once ms has elapsed, render null. This allows the rest of the + // tree to resume rendering. + return null; + } + return ; + }} + + ); + } + + function DebouncedText({text, ms}) { + return ( + + + + + ); + } + + it('works', async () => { + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([]); + + await advanceTimers(800); + ReactNoop.expire(800); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([]); + + await advanceTimers(1000); + ReactNoop.expire(1000); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('A')]); + }); + }); +}); diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index 119c55154e..4ef5ab8201 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount ⚛ (React Tree Reconciliation: Completed Root) @@ -15,7 +15,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` ⚛ (Calling Lifecycle Methods: 1 Total) ⚛ AllLifecycles.componentDidMount -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Update ⚛ (React Tree Reconciliation: Completed Root) @@ -31,7 +31,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` ⚛ (Calling Lifecycle Methods: 2 Total) ⚛ AllLifecycles.componentDidUpdate -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Unmount ⚛ (React Tree Reconciliation: Completed Root) @@ -45,7 +45,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` `; exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // The commit phase should mention A and B just once ⚛ (React Tree Reconciliation: Completed Root) @@ -62,7 +62,7 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc ⚛ A.componentDidUpdate ⚛ B.componentDidUpdate -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Because of deduplication, we don't know B was cascading, // but we should still see the warning for the commit phase. @@ -92,7 +92,7 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc `; exports[`ReactDebugFiberPerf does not include AsyncMode, StrictMode, or Profiler components in measurements 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount ⚛ (React Tree Reconciliation: Completed Root) @@ -108,7 +108,7 @@ exports[`ReactDebugFiberPerf does not include AsyncMode, StrictMode, or Profiler `; exports[`ReactDebugFiberPerf does not include context provider or consumer in measurements 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount ⚛ (React Tree Reconciliation: Completed Root) @@ -143,7 +143,7 @@ exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is `; exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Should not print a warning ⚛ (React Tree Reconciliation: Completed Root) @@ -156,7 +156,7 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi ⚛ (Committing Host Effects: 1 Total) ⚛ (Calling Lifecycle Methods: 0 Total) -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Should not print a warning ⚛ (React Tree Reconciliation: Completed Root) @@ -172,7 +172,7 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi `; exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount ⚛ (React Tree Reconciliation: Completed Root) @@ -184,7 +184,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` ⚛ (Committing Host Effects: 1 Total) ⚛ (Calling Lifecycle Methods: 0 Total) -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Update ⚛ (React Tree Reconciliation: Completed Root) @@ -196,7 +196,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` ⚛ (Committing Host Effects: 2 Total) ⚛ (Calling Lifecycle Methods: 2 Total) -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Unmount ⚛ (React Tree Reconciliation: Completed Root) @@ -209,7 +209,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` `; exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Start mounting Parent and A ⚛ (React Tree Reconciliation: Yielded) @@ -217,14 +217,14 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = ` ⚛ A [mount] ⚛ Child [mount] -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount B just a little (but not enough to memoize) ⚛ (React Tree Reconciliation: Yielded) ⚛ Parent [mount] ⚛ B [mount] -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Complete B and Parent ⚛ (React Tree Reconciliation: Completed Root) @@ -263,7 +263,7 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = ` `; exports[`ReactDebugFiberPerf properly displays the forwardRef component in measurements 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Mount ⚛ (React Tree Reconciliation: Completed Root) @@ -283,7 +283,7 @@ exports[`ReactDebugFiberPerf properly displays the forwardRef component in measu `; exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Stop on Baddie and restart from Boundary ⚛ (React Tree Reconciliation: Yielded) @@ -313,7 +313,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` `; exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Will fatal ⚛ (React Tree Reconciliation: Yielded) @@ -327,7 +327,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` ⚛ (Committing Host Effects: 1 Total) ⚛ (Calling Lifecycle Methods: 1 Total) -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) // Will reconcile from a clean state ⚛ (React Tree Reconciliation: Completed Root) @@ -342,7 +342,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` `; exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Should include just A and B, no Parents ⚛ (React Tree Reconciliation: Completed Root) @@ -357,7 +357,7 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` `; exports[`ReactDebugFiberPerf supports portals 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Completed Root) ⚛ Parent [mount] @@ -371,7 +371,7 @@ exports[`ReactDebugFiberPerf supports portals 1`] = ` `; exports[`ReactDebugFiberPerf supports returns 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Completed Root) ⚛ App [mount] @@ -390,12 +390,12 @@ exports[`ReactDebugFiberPerf supports returns 1`] = ` `; exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) ⚛ (React Tree Reconciliation: Yielded) ⚛ Foo [mount] -⚛ (Waiting for async callback... will force flush in 5230 ms) +⚛ (Waiting for async callback... will force flush in 5250 ms) ⛔ (React Tree Reconciliation: Completed Root) Warning: A top-level update interrupted the previous render ⚛ Foo [mount] ⚛ (Committing Changes) @@ -413,7 +413,7 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = `; exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = ` -"⛔ (Waiting for async callback... will force flush in 5230 ms) Warning: React was blocked by main thread +"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread ⚛ (React Tree Reconciliation: Completed Root) ⚛ Foo [mount] @@ -426,7 +426,7 @@ exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = ` `; exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Should print a warning ⚛ (React Tree Reconciliation: Completed Root) @@ -450,7 +450,7 @@ exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` `; exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5230 ms) +"⚛ (Waiting for async callback... will force flush in 5250 ms) // Rendering the first root ⚛ (React Tree Reconciliation: Completed Root) diff --git a/packages/react/src/React.js b/packages/react/src/React.js index d474040611..2f6221a23e 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -12,7 +12,9 @@ import { REACT_FRAGMENT_TYPE, REACT_PROFILER_TYPE, REACT_STRICT_MODE_TYPE, + REACT_TIMEOUT_TYPE, } from 'shared/ReactSymbols'; +import {enableSuspense} from 'shared/ReactFeatureFlags'; import {Component, PureComponent} from './ReactBaseClasses'; import {createRef} from './ReactCreateRef'; @@ -53,6 +55,7 @@ const React = { StrictMode: REACT_STRICT_MODE_TYPE, unstable_AsyncMode: REACT_ASYNC_MODE_TYPE, unstable_Profiler: REACT_PROFILER_TYPE, + Timeout: REACT_TIMEOUT_TYPE, createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, @@ -68,6 +71,10 @@ const React = { }, }; +if (enableSuspense) { + React.Timeout = REACT_TIMEOUT_TYPE; +} + if (__DEV__) { Object.assign(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, { // These should not be included in production. diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 614a1b6cc7..16a44b21c9 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -21,6 +21,8 @@ export const enablePersistentReconciler = false; // Experimental error-boundary API that can recover from errors within a single // render phase export const enableGetDerivedStateFromCatch = false; +// Suspense +export const enableSuspense = false; // Helps identify side effects in begin-phase lifecycle hooks and setState reducers: export const debugRenderPhaseSideEffects = false; diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 672e51de8e..9dc1d3627a 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -42,6 +42,9 @@ export const REACT_ASYNC_MODE_TYPE = hasSymbol export const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0; +export const REACT_TIMEOUT_TYPE = hasSymbol + ? Symbol.for('react.timeout') + : 0xead1; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index 1c672bb4b5..e6c1ace1d9 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -23,7 +23,8 @@ export type TypeOfWork = | 12 | 13 | 14 - | 15; + | 15 + | 16; export const IndeterminateComponent = 0; // Before we know whether it is functional or class export const FunctionalComponent = 1; @@ -41,3 +42,4 @@ export const ContextConsumer = 12; export const ContextProvider = 13; export const ForwardRef = 14; export const Profiler = 15; +export const TimeoutComponent = 16; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js index 7eb809c952..3ad3795f2a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js @@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; +export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = __DEV__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js index c6b30c6532..501818e8f2 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js @@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; +export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 51e2133a67..034e8bbeed 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -15,6 +15,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-fb'; // Re-export dynamic flags from the fbsource version. export const { enableGetDerivedStateFromCatch, + enableSuspense, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 5e03310dc0..92503166ee 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -15,6 +15,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-oss'; export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableGetDerivedStateFromCatch = false; +export const enableSuspense = false; export const enableMutatingReconciler = true; export const enableNoopReconciler = false; export const enablePersistentReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 780c62db4d..06c3b7dec0 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; +export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 486aecfa68..2ac0e75d1b 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false; export const debugRenderPhaseSideEffectsForStrictMode = false; export const enableUserTimingAPI = __DEV__; export const enableGetDerivedStateFromCatch = false; +export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; export const enableMutatingReconciler = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 523e1604a6..aaa1ecc17c 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -12,6 +12,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www'; // Re-export dynamic flags from the www version. export const { + enableSuspense, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, enableGetDerivedStateFromCatch, diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index c9a759d502..3e99627225 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -15,6 +15,7 @@ import { REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, + REACT_TIMEOUT_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -26,6 +27,7 @@ export default function isValidElementType(type: mixed) { type === REACT_ASYNC_MODE_TYPE || type === REACT_PROFILER_TYPE || type === REACT_STRICT_MODE_TYPE || + type === REACT_TIMEOUT_TYPE || (typeof type === 'object' && type !== null && (type.$$typeof === REACT_PROVIDER_TYPE || From b0726e99476ea67c7558cbf268685998a38ade7c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 May 2018 18:34:01 -0700 Subject: [PATCH 029/277] Support sharing context objects between concurrent renderers (#12779) * Support concurrent primary and secondary renderers. As a workaround to support multiple concurrent renderers, we categorize some renderers as primary and others as secondary. We only expect there to be two concurrent renderers at most: React Native (primary) and Fabric (secondary); React DOM (primary) and React ART (secondary). Secondary renderers store their context values on separate fields. * Add back concurrent renderer warning Only warn for two concurrent primary or two concurrent secondary renderers. * Change "_secondary" suffix to "2" #EveryBitCounts --- packages/react-art/src/ReactART.js | 3 + .../react-art/src/__tests__/ReactART-test.js | 54 ++++++++++++++ packages/react-dom/src/client/ReactDOM.js | 2 + .../src/ReactFabricRenderer.js | 3 + .../src/ReactNativeFiberRenderer.js | 2 + packages/react-noop-renderer/src/ReactNoop.js | 2 + .../src/ReactFiberBeginWork.js | 10 ++- .../src/ReactFiberNewContext.js | 70 ++++++++++++++----- .../src/ReactFiberReconciler.js | 5 ++ .../src/ReactFiberScheduler.js | 2 +- .../src/ReactTestRenderer.js | 2 + packages/react/src/ReactContext.js | 8 +++ packages/shared/ReactTypes.js | 3 + 13 files changed, 144 insertions(+), 22 deletions(-) diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index 04478434e8..a7f9a3064c 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -478,6 +478,9 @@ const ARTRenderer = ReactFiberReconciler({ now: ReactScheduler.now, + // The ART renderer is secondary to the React DOM renderer. + isPrimaryRenderer: false, + mutation: { appendChild(parentInstance, child) { if (child.parentNode === parentInstance) { diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js index fbce04a9a2..ace737e526 100644 --- a/packages/react-art/src/__tests__/ReactART-test.js +++ b/packages/react-art/src/__tests__/ReactART-test.js @@ -339,6 +339,60 @@ describe('ReactART', () => { doClick(instance); expect(onClick2).toBeCalled(); }); + + it('can concurrently render with a "primary" renderer while sharing context', () => { + const CurrentRendererContext = React.createContext(null); + + function Yield(props) { + testRenderer.unstable_yield(props.value); + return null; + } + + let ops = []; + function LogCurrentRenderer() { + return ( + + {currentRenderer => { + ops.push(currentRenderer); + return null; + }} + + ); + } + + // Using test renderer instead of the DOM renderer here because async + // testing APIs for the DOM renderer don't exist. + const testRenderer = renderer.create( + + + + + + , + { + unstable_isAsync: true, + }, + ); + + testRenderer.unstable_flushThrough(['A']); + + ReactDOM.render( + + + + + + , + container, + ); + + expect(ops).toEqual([null, 'ART']); + + ops = []; + expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']); + + expect(ops).toEqual(['Test']); + }); }); describe('ReactARTComponents', () => { diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 82032f1d1d..81683c4774 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -690,6 +690,8 @@ const DOMRenderer = ReactFiberReconciler({ now: ReactScheduler.now, + isPrimaryRenderer: true, + mutation: { commitMount( domElement: Instance, diff --git a/packages/react-native-renderer/src/ReactFabricRenderer.js b/packages/react-native-renderer/src/ReactFabricRenderer.js index 0ec040bddf..fa2fabef64 100644 --- a/packages/react-native-renderer/src/ReactFabricRenderer.js +++ b/packages/react-native-renderer/src/ReactFabricRenderer.js @@ -217,6 +217,9 @@ const ReactFabricRenderer = ReactFiberReconciler({ now: ReactNativeFrameScheduling.now, + // The Fabric renderer is secondary to the existing React Native renderer. + isPrimaryRenderer: false, + prepareForCommit(): void { // Noop }, diff --git a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js b/packages/react-native-renderer/src/ReactNativeFiberRenderer.js index 7de768b3b2..2854f6488f 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeFiberRenderer.js @@ -169,6 +169,8 @@ const NativeRenderer = ReactFiberReconciler({ now: ReactNativeFrameScheduling.now, + isPrimaryRenderer: true, + prepareForCommit(): void { // Noop }, diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 5cd6df0fbd..e9c82cbe9c 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -185,6 +185,8 @@ let SharedHostConfig = { now(): number { return elapsedTimeInMs; }, + + isPrimaryRenderer: true, }; const NoopRenderer = ReactFiberReconciler({ diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 489393bebb..14927b2db9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -104,7 +104,11 @@ export default function( const {pushHostContext, pushHostContainer} = hostContext; - const {pushProvider} = newContext; + const { + pushProvider, + getContextCurrentValue, + getContextChangedBits, + } = newContext; const { markActualRenderTimeStarted, @@ -1048,8 +1052,8 @@ export default function( const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - const newValue = context._currentValue; - const changedBits = context._changedBits; + const newValue = getContextCurrentValue(context); + const changedBits = getContextChangedBits(context); if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index ab9c27e88a..3aaf553ec5 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -11,14 +11,16 @@ import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; import type {StackCursor, Stack} from './ReactFiberStack'; -import warning from 'fbjs/lib/warning'; - export type NewContext = { pushProvider(providerFiber: Fiber): void, popProvider(providerFiber: Fiber): void, + getContextCurrentValue(context: ReactContext): any, + getContextChangedBits(context: ReactContext): number, }; -export default function(stack: Stack) { +import warning from 'fbjs/lib/warning'; + +export default function(stack: Stack, isPrimaryRenderer: boolean) { const {createCursor, push, pop} = stack; const providerCursor: StackCursor = createCursor(null); @@ -34,21 +36,38 @@ export default function(stack: Stack) { function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type._context; - push(changedBitsCursor, context._changedBits, providerFiber); - push(valueCursor, context._currentValue, providerFiber); - push(providerCursor, providerFiber, providerFiber); + if (isPrimaryRenderer) { + push(changedBitsCursor, context._changedBits, providerFiber); + push(valueCursor, context._currentValue, providerFiber); + push(providerCursor, providerFiber, providerFiber); - context._currentValue = providerFiber.pendingProps.value; - context._changedBits = providerFiber.stateNode; + context._currentValue = providerFiber.pendingProps.value; + context._changedBits = providerFiber.stateNode; + if (__DEV__) { + warning( + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer = rendererSigil; + } + } else { + push(changedBitsCursor, context._changedBits2, providerFiber); + push(valueCursor, context._currentValue2, providerFiber); + push(providerCursor, providerFiber, providerFiber); - if (__DEV__) { - warning( - context._currentRenderer === null || - context._currentRenderer === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer = rendererSigil; + context._currentValue2 = providerFiber.pendingProps.value; + context._changedBits2 = providerFiber.stateNode; + if (__DEV__) { + warning( + context._currentRenderer2 === null || + context._currentRenderer2 === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer2 = rendererSigil; + } } } @@ -61,12 +80,27 @@ export default function(stack: Stack) { pop(changedBitsCursor, providerFiber); const context: ReactContext = providerFiber.type._context; - context._currentValue = currentValue; - context._changedBits = changedBits; + if (isPrimaryRenderer) { + context._currentValue = currentValue; + context._changedBits = changedBits; + } else { + context._currentValue2 = currentValue; + context._changedBits2 = changedBits; + } + } + + function getContextCurrentValue(context: ReactContext): any { + return isPrimaryRenderer ? context._currentValue : context._currentValue2; + } + + function getContextChangedBits(context: ReactContext): number { + return isPrimaryRenderer ? context._changedBits : context._changedBits2; } return { pushProvider, popProvider, + getContextCurrentValue, + getContextChangedBits, }; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index dd395af753..52a42646ba 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -95,6 +95,11 @@ export type HostConfig = { now(): number, + // Temporary workaround for scenario where multiple renderers concurrently + // render using the same context objects. E.g. React DOM and React ART on the + // same page. DOM is the primary renderer; ART is the secondary renderer. + isPrimaryRenderer: boolean, + +hydration?: HydrationHostConfig, +mutation?: MutableUpdatesHostConfig, diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 5c9657876c..562039393d 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -181,7 +181,7 @@ export default function( const stack = ReactFiberStack(); const hostContext = ReactFiberHostContext(config, stack); const legacyContext = ReactFiberLegacyContext(stack); - const newContext = ReactFiberNewContext(stack); + const newContext = ReactFiberNewContext(stack, config.isPrimaryRenderer); const profilerTimer = createProfilerTimer(now); const {popHostContext, popHostContainer} = hostContext; const { diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 561aed6c58..7fa54534d3 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -226,6 +226,8 @@ const TestRenderer = ReactFiberReconciler({ // Even after the reconciler has initialized and read host config values. now: () => nowImplementation(), + isPrimaryRenderer: true, + mutation: { commitUpdate( instance: Instance, diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 10190accc4..d15163e813 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -36,7 +36,14 @@ export function createContext( _calculateChangedBits: calculateChangedBits, _defaultValue: defaultValue, _currentValue: defaultValue, + // As a workaround to support multiple concurrent renderers, we categorize + // some renderers as primary and others as secondary. We only expect + // there to be two concurrent renderers at most: React Native (primary) and + // Fabric (secondary); React DOM (primary) and React ART (secondary). + // Secondary renderers store their context values on separate fields. + _currentValue2: defaultValue, _changedBits: 0, + _changedBits2: 0, // These are circular Provider: (null: any), Consumer: (null: any), @@ -50,6 +57,7 @@ export function createContext( if (__DEV__) { context._currentRenderer = null; + context._currentRenderer2 = null; } return context; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 689ed18bf9..c47a380a92 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -85,10 +85,13 @@ export type ReactContext = { _defaultValue: T, _currentValue: T, + _currentValue2: T, _changedBits: number, + _changedBits2: number, // DEV only _currentRenderer?: Object | null, + _currentRenderer2?: Object | null, }; export type ReactPortal = { From 4f459bb144cad7c12e49d8afa2449ff6d9ed9e1f Mon Sep 17 00:00:00 2001 From: Filipp Riabchun Date: Fri, 11 May 2018 20:03:08 +0300 Subject: [PATCH 030/277] Shallow renderer: pass component instance to setState updater as `this` (#12784) * Shallow renderer: pass component instance to setState updater as `this` * Run prettier --- .../src/ReactShallowRenderer.js | 6 +++++- .../__tests__/ReactShallowRenderer-test.js | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index 9cafeeb2f1..3134f991ee 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -298,7 +298,11 @@ class Updater { const currentState = this._renderer._newState || publicInstance.state; if (typeof partialState === 'function') { - partialState = partialState(currentState, publicInstance.props); + partialState = partialState.call( + publicInstance, + currentState, + publicInstance.props, + ); } // Null and undefined are treated as no-ops. diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index ce62bba280..c4cdcd6b65 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -945,6 +945,27 @@ describe('ReactShallowRenderer', () => { expect(result.props.children).toEqual(2); }); + it('can access component instance from setState updater function', done => { + let instance; + + class SimpleComponent extends React.Component { + state = {}; + + render() { + instance = this; + return null; + } + } + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + instance.setState(function updater(state, props) { + expect(this).toBe(instance); + done(); + }); + }); + it('can setState with a callback', () => { let instance; From 4b2e65d32e3adec7d4ef5ddd5fcb35e617e0cde6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 May 2018 18:45:00 -0700 Subject: [PATCH 031/277] Put recent change to getDerivedStateFromProps behind a feature flag (#12788) This will allow us to safely ship it at Facebook and get a better idea for if/how it breaks existing product code. --- .../src/ReactFiberClassComponent.js | 15 ++++--- .../ReactIncremental-test.internal.js | 45 +++++++++++++++++++ packages/shared/ReactFeatureFlags.js | 3 ++ .../ReactFeatureFlags.native-fabric-fb.js | 1 + .../ReactFeatureFlags.native-fabric-oss.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 10 files changed, 64 insertions(+), 6 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 36bb58ecc5..0d63543971 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -16,6 +16,7 @@ import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, + fireGetDerivedStateFromPropsOnStateUpdates, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings'; import {isMounted} from 'react-reconciler/reflection'; @@ -937,12 +938,14 @@ export default function( } if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps( - workInProgress, - getDerivedStateFromProps, - newProps, - ); - newState = workInProgress.memoizedState; + if (fireGetDerivedStateFromPropsOnStateUpdates || oldProps !== newProps) { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } } if ( diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index a8b66373d8..20df85d404 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1464,6 +1464,51 @@ describe('ReactIncremental', () => { expect(instance.state).toEqual({foo: 'foo'}); }); + it('does not call getDerivedStateFromProps for state-only updates if feature flag is disabled', () => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.fireGetDerivedStateFromPropsOnStateUpdates = false; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + + let ops = []; + let instance; + + class LifeCycle extends React.Component { + state = {}; + static getDerivedStateFromProps(props, prevState) { + ops.push('getDerivedStateFromProps'); + return {foo: 'foo'}; + } + changeState() { + this.setState({foo: 'bar'}); + } + componentDidUpdate() { + ops.push('componentDidUpdate'); + } + render() { + ops.push('render'); + instance = this; + return null; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual(['getDerivedStateFromProps', 'render']); + expect(instance.state).toEqual({foo: 'foo'}); + + ops = []; + + instance.changeState(); + ReactNoop.flush(); + + expect(ops).toEqual(['render', 'componentDidUpdate']); + expect(instance.state).toEqual({foo: 'bar'}); + }); + xit('does not call componentWillReceiveProps for state-only updates', () => { let ops = []; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 16a44b21c9..beefc20c0e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -42,6 +42,9 @@ export const warnAboutDeprecatedLifecycles = false; // Gather advanced timing metrics for Profiler subtrees. export const enableProfilerTimer = false; +// Fires getDerivedStateFromProps for state *or* props changes +export const fireGetDerivedStateFromPropsOnStateUpdates = true; + // Only used in www builds. export function addUserTimingListener() { invariant(false, 'Not implemented.'); diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js index 3ad3795f2a..cd5c6f115e 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js @@ -25,6 +25,7 @@ export const enableProfilerTimer = __DEV__; export const enableMutatingReconciler = false; export const enableNoopReconciler = false; export const enablePersistentReconciler = true; +export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js index 501818e8f2..aa9dc6dd4c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js @@ -25,6 +25,7 @@ export const enableProfilerTimer = false; export const enableMutatingReconciler = false; export const enableNoopReconciler = false; export const enablePersistentReconciler = true; +export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 034e8bbeed..a98a2fe144 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -21,6 +21,7 @@ export const { warnAboutDeprecatedLifecycles, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, + fireGetDerivedStateFromPropsOnStateUpdates, } = require('ReactFeatureFlags'); // The rest of the flags are static for better dead code elimination. diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 92503166ee..0b93bcbd3a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -23,6 +23,7 @@ export const enableUserTimingAPI = __DEV__; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = false; export const enableProfilerTimer = false; +export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 06c3b7dec0..ecb19ad1bc 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -20,6 +20,7 @@ export const enableSuspense = false; export const warnAboutDeprecatedLifecycles = false; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const enableProfilerTimer = false; +export const fireGetDerivedStateFromPropsOnStateUpdates = true; // react-reconciler/persistent entry point // uses a persistent reconciler. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 2ac0e75d1b..7aad93e150 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -23,6 +23,7 @@ export const enableMutatingReconciler = true; export const enableNoopReconciler = false; export const enablePersistentReconciler = false; export const enableProfilerTimer = false; +export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index aaa1ecc17c..725ff1c5e5 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -19,6 +19,7 @@ export const { replayFailedUnitOfWorkWithInvokeGuardedCallback, warnAboutDeprecatedLifecycles, enableProfilerTimer, + fireGetDerivedStateFromPropsOnStateUpdates, } = require('ReactFeatureFlags'); // The rest of the flags are static for better dead code elimination. From 7b19f93ab9159164ad35b09abcd0c7a936ff3e3c Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 13 May 2018 21:12:25 +0100 Subject: [PATCH 032/277] Record sizes --- scripts/error-codes/codes.json | 3 +- scripts/rollup/results.json | 208 ++++++++++++++++----------------- 2 files changed, 106 insertions(+), 105 deletions(-) diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 6627e703d1..4d190fe68f 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -267,5 +267,6 @@ "265": "This unit of work tag cannot capture errors. This error is likely caused by a bug in React. Please file an issue.", "266": "A subscription must return an unsubscribe function.", "267": "React.cloneElement(...): The argument must be a React element, but you passed %s.", - "268": "Argument appears to not be a ReactComponent. Keys: %s" + "268": "Argument appears to not be a ReactComponent. Keys: %s", + "269": "Profiler must specify an \"id\" string and \"onRender\" function as props" } diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index f265beaf72..01863fae3d 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -4,29 +4,29 @@ "filename": "react.development.js", "bundleType": "UMD_DEV", "packageName": "react", - "size": 57115, - "gzip": 15675 + "size": 58796, + "gzip": 16346 }, { "filename": "react.production.min.js", "bundleType": "UMD_PROD", "packageName": "react", - "size": 7194, - "gzip": 3050 + "size": 7279, + "gzip": 3082 }, { "filename": "react.development.js", "bundleType": "NODE_DEV", "packageName": "react", - "size": 47530, - "gzip": 13262 + "size": 49211, + "gzip": 13949 }, { "filename": "react.production.min.js", "bundleType": "NODE_PROD", "packageName": "react", - "size": 5698, - "gzip": 2476 + "size": 5781, + "gzip": 2502 }, { "filename": "React-dev.js", @@ -46,29 +46,29 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 639980, - "gzip": 147756 + "size": 657360, + "gzip": 151075 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 103714, - "gzip": 33074 + "size": 104902, + "gzip": 33416 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 623973, - "gzip": 143563 + "size": 641349, + "gzip": 146842 }, { "filename": "react-dom.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 102103, - "gzip": 32217 + "size": 103269, + "gzip": 32503 }, { "filename": "ReactDOM-dev.js", @@ -165,8 +165,8 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 103942, - "gzip": 27140 + "size": 104020, + "gzip": 27181 }, { "filename": "react-dom-server.browser.production.min.js", @@ -179,8 +179,8 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 92986, - "gzip": 24826 + "size": 93064, + "gzip": 24867 }, { "filename": "react-dom-server.browser.production.min.js", @@ -207,8 +207,8 @@ "filename": "react-dom-server.node.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 94954, - "gzip": 25386 + "size": 95032, + "gzip": 25427 }, { "filename": "react-dom-server.node.production.min.js", @@ -221,29 +221,29 @@ "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 438185, - "gzip": 95469 + "size": 455628, + "gzip": 98884 }, { "filename": "react-art.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-art", - "size": 93805, - "gzip": 28540 + "size": 94976, + "gzip": 28852 }, { "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 362236, - "gzip": 76390 + "size": 379675, + "gzip": 79763 }, { "filename": "react-art.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-art", - "size": 57462, - "gzip": 17574 + "size": 58630, + "gzip": 17902 }, { "filename": "ReactART-dev.js", @@ -291,29 +291,29 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 367787, - "gzip": 77332 + "size": 385091, + "gzip": 80661 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 57216, - "gzip": 17399 + "size": 58378, + "gzip": 17695 }, { "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 358388, - "gzip": 74592 + "size": 375688, + "gzip": 77920 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 56410, - "gzip": 16995 + "size": 57578, + "gzip": 17367 }, { "filename": "ReactTestRenderer-dev.js", @@ -326,29 +326,29 @@ "filename": "react-test-renderer-shallow.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 24939, - "gzip": 6654 + "size": 24960, + "gzip": 6657 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 7377, - "gzip": 2416 + "size": 7384, + "gzip": 2418 }, { "filename": "react-test-renderer-shallow.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 14595, - "gzip": 3660 + "size": 14616, + "gzip": 3663 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 7314, - "gzip": 2398 + "size": 7321, + "gzip": 2401 }, { "filename": "ReactShallowRenderer-dev.js", @@ -361,43 +361,43 @@ "filename": "react-noop-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 18684, - "gzip": 5165 + "size": 18791, + "gzip": 5222 }, { "filename": "react-noop-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 6479, - "gzip": 2549 + "size": 6500, + "gzip": 2562 }, { "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 337878, - "gzip": 69868 + "size": 355226, + "gzip": 73207 }, { "filename": "react-reconciler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 48996, - "gzip": 14768 + "size": 50143, + "gzip": 15094 }, { "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 337140, - "gzip": 69575 + "size": 354412, + "gzip": 72892 }, { "filename": "react-reconciler-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 47919, - "gzip": 14583 + "size": 49057, + "gzip": 14893 }, { "filename": "react-reconciler-reflection.development.js", @@ -431,29 +431,29 @@ "filename": "react-is.development.js", "bundleType": "UMD_DEV", "packageName": "react-is", - "size": 4685, - "gzip": 1302 + "size": 4791, + "gzip": 1326 }, { "filename": "react-is.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-is", - "size": 1892, - "gzip": 773 + "size": 1937, + "gzip": 786 }, { "filename": "react-is.development.js", "bundleType": "NODE_DEV", "packageName": "react-is", - "size": 4496, - "gzip": 1245 + "size": 4602, + "gzip": 1268 }, { "filename": "react-is.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-is", - "size": 1840, - "gzip": 707 + "size": 1886, + "gzip": 721 }, { "filename": "ReactIs-dev.js", @@ -501,29 +501,29 @@ "filename": "React-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react", - "size": 47536, - "gzip": 12932 + "size": 49318, + "gzip": 13556 }, { "filename": "React-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react", - "size": 13822, - "gzip": 3825 + "size": 13793, + "gzip": 3864 }, { "filename": "ReactDOM-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 649280, - "gzip": 146389 + "size": 667163, + "gzip": 149794 }, { "filename": "ReactDOM-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 299975, - "gzip": 54678 + "size": 311309, + "gzip": 56436 }, { "filename": "ReactTestUtils-dev.js", @@ -550,8 +550,8 @@ "filename": "ReactDOMServer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 96543, - "gzip": 24639 + "size": 96693, + "gzip": 24675 }, { "filename": "ReactDOMServer-prod.js", @@ -564,99 +564,99 @@ "filename": "ReactART-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-art", - "size": 370976, - "gzip": 75980 + "size": 388922, + "gzip": 79451 }, { "filename": "ReactART-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-art", - "size": 179924, - "gzip": 29606 + "size": 191156, + "gzip": 31368 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 481278, - "gzip": 102913 + "size": 499080, + "gzip": 106402 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 230529, - "gzip": 38569 + "size": 241853, + "gzip": 40274 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 480999, - "gzip": 102846 + "size": 498734, + "gzip": 106325 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 224503, - "gzip": 37480 + "size": 227735, + "gzip": 38044 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 463244, - "gzip": 98382 + "size": 481057, + "gzip": 101898 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 209798, - "gzip": 34820 + "size": 212992, + "gzip": 35357 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 463280, - "gzip": 98398 + "size": 481093, + "gzip": 101917 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 209834, - "gzip": 34837 + "size": 213028, + "gzip": 35376 }, { "filename": "ReactTestRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 367404, - "gzip": 74293 + "size": 385220, + "gzip": 77735 }, { "filename": "ReactShallowRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 15318, - "gzip": 3781 + "size": 15371, + "gzip": 3790 }, { "filename": "ReactIs-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-is", - "size": 4564, - "gzip": 1270 + "size": 4674, + "gzip": 1294 }, { "filename": "ReactIs-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-is", - "size": 3653, - "gzip": 977 + "size": 3760, + "gzip": 998 }, { "filename": "react-scheduler.development.js", From 8506062975b36cad1b92a39e66bfa59f637ee672 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Mon, 14 May 2018 12:18:31 +0200 Subject: [PATCH 033/277] remove unused ES3-specific packages - refs #12716 (#12797) --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 9b625ea397..ed1a5cad14 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,6 @@ "babel-plugin-transform-es2015-shorthand-properties": "^6.5.0", "babel-plugin-transform-es2015-spread": "^6.5.2", "babel-plugin-transform-es2015-template-literals": "^6.5.2", - "babel-plugin-transform-es3-member-expression-literals": "^6.5.0", - "babel-plugin-transform-es3-property-literals": "^6.5.0", "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-plugin-transform-react-jsx-source": "^6.8.0", "babel-plugin-transform-regenerator": "^6.26.0", From d430e1358227f316ef41650c8e1b9674de11ab84 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Mon, 14 May 2018 20:35:20 +0900 Subject: [PATCH 034/277] Fix a typo (#12798) --- .../src/__tests__/ReactShallowRenderer-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index c4cdcd6b65..5ff7eae701 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -1353,7 +1353,7 @@ describe('ReactShallowRenderer', () => { ]); }); - it('should stop the upade when setState returns null or undefined', () => { + it('should stop the update when setState returns null or undefined', () => { const log = []; let instance; class Component extends React.Component { From 0470854f5522dde521d46049bcac894b2e86d280 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 14 May 2018 13:57:33 +0100 Subject: [PATCH 035/277] Split ReactNoop into normal and persistent exports (#12793) * Copy-paste ReactNoop into ReactNoopPersistent * Split ReactNoop into normal and persistent exports * ReactNoopShared -> createReactNoop --- .../react-noop-renderer/npm/persistent.js | 7 + packages/react-noop-renderer/package.json | 1 + packages/react-noop-renderer/persistent.js | 18 + packages/react-noop-renderer/src/ReactNoop.js | 579 +---------------- .../src/ReactNoopPersistent.js | 23 + .../src/createReactNoop.js | 582 ++++++++++++++++++ .../ReactPersistent-test.internal.js | 28 +- scripts/rollup/bundles.js | 23 + 8 files changed, 671 insertions(+), 590 deletions(-) create mode 100644 packages/react-noop-renderer/npm/persistent.js create mode 100644 packages/react-noop-renderer/persistent.js create mode 100644 packages/react-noop-renderer/src/ReactNoopPersistent.js create mode 100644 packages/react-noop-renderer/src/createReactNoop.js diff --git a/packages/react-noop-renderer/npm/persistent.js b/packages/react-noop-renderer/npm/persistent.js new file mode 100644 index 0000000000..14991d5371 --- /dev/null +++ b/packages/react-noop-renderer/npm/persistent.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-noop-renderer-persistent.production.min.js'); +} else { + module.exports = require('./cjs/react-noop-renderer-persistent.development.js'); +} diff --git a/packages/react-noop-renderer/package.json b/packages/react-noop-renderer/package.json index afd4aaef71..25b3a429e5 100644 --- a/packages/react-noop-renderer/package.json +++ b/packages/react-noop-renderer/package.json @@ -20,6 +20,7 @@ "LICENSE", "README.md", "index.js", + "persistent.js", "cjs/" ] } diff --git a/packages/react-noop-renderer/persistent.js b/packages/react-noop-renderer/persistent.js new file mode 100644 index 0000000000..f235fad17b --- /dev/null +++ b/packages/react-noop-renderer/persistent.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +const ReactNoopPersistent = require('./src/ReactNoopPersistent'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactNoopPersistent.default + ? ReactNoopPersistent.default + : ReactNoopPersistent; diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index e9c82cbe9c..91ae6ca749 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -14,581 +14,10 @@ * environment. */ -import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; -import type {ReactNodeList} from 'shared/ReactTypes'; -import ReactFiberReconciler from 'react-reconciler'; -import {enablePersistentReconciler} from 'shared/ReactFeatureFlags'; -import * as ReactPortal from 'shared/ReactPortal'; -import emptyObject from 'fbjs/lib/emptyObject'; -import expect from 'expect'; +import createReactNoop from './createReactNoop'; -const UPDATE_SIGNAL = {}; - -let scheduledCallback = null; - -type Container = {rootID: string, children: Array}; -type Props = {prop: any, hidden?: boolean}; -type Instance = {| - type: string, - id: number, - children: Array, - prop: any, -|}; -type TextInstance = {|text: string, id: number|}; - -let instanceCounter = 0; -let failInBeginPhase = false; - -function appendChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); -} - -function insertBefore( - parentInstance: Instance | Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - const beforeIndex = parentInstance.children.indexOf(beforeChild); - if (beforeIndex === -1) { - throw new Error('This child does not exist.'); - } - parentInstance.children.splice(beforeIndex, 0, child); -} - -function removeChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index === -1) { - throw new Error('This child does not exist.'); - } - parentInstance.children.splice(index, 1); -} - -let elapsedTimeInMs = 0; - -let SharedHostConfig = { - getRootHostContext() { - if (failInBeginPhase) { - throw new Error('Error in host config.'); - } - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - getPublicInstance(instance) { - return instance; - }, - - createInstance(type: string, props: Props): Instance { - const inst = { - id: instanceCounter++, - type: type, - children: [], - prop: props.prop, - }; - // Hide from unit tests - Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); - return inst; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.children.push(child); - }, - - finalizeInitialChildren( - domElement: Instance, - type: string, - props: Props, - ): boolean { - return false; - }, - - prepareUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - ): null | {} { - if (oldProps === null) { - throw new Error('Should have old props'); - } - if (newProps === null) { - throw new Error('Should have new props'); - } - return UPDATE_SIGNAL; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return ( - typeof props.children === 'string' || typeof props.children === 'number' - ); - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return !!props.hidden; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): TextInstance { - const inst = {text: text, id: instanceCounter++}; - // Hide from unit tests - Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); - return inst; - }, - - scheduleDeferredCallback(callback) { - if (scheduledCallback) { - throw new Error( - 'Scheduling a callback twice is excessive. Instead, keep track of ' + - 'whether the callback has already been scheduled.', - ); - } - scheduledCallback = callback; - return 0; - }, - - cancelDeferredCallback() { - if (scheduledCallback === null) { - throw new Error('No callback is scheduled.'); - } - scheduledCallback = null; - }, - - prepareForCommit(): void {}, - - resetAfterCommit(): void {}, - - now(): number { - return elapsedTimeInMs; - }, - - isPrimaryRenderer: true, -}; - -const NoopRenderer = ReactFiberReconciler({ - ...SharedHostConfig, - mutation: { - commitMount(instance: Instance, type: string, newProps: Props): void { - // Noop - }, - - commitUpdate( - instance: Instance, - updatePayload: Object, - type: string, - oldProps: Props, - newProps: Props, - ): void { - if (oldProps === null) { - throw new Error('Should have old props'); - } - instance.prop = newProps.prop; - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - - resetTextContent(instance: Instance): void {}, - }, -}); - -const PersistentNoopRenderer = enablePersistentReconciler - ? ReactFiberReconciler({ - ...SharedHostConfig, - persistence: { - cloneInstance( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - recyclableInstance: null | Instance, - ): Instance { - const clone = { - id: instance.id, - type: type, - children: keepChildren ? instance.children : [], - prop: newProps.prop, - }; - Object.defineProperty(clone, 'id', { - value: clone.id, - enumerable: false, - }); - return clone; - }, - - createContainerChildSet( - container: Container, - ): Array { - return []; - }, - - appendChildToContainerChildSet( - childSet: Array, - child: Instance | TextInstance, - ): void { - childSet.push(child); - }, - - finalizeContainerChildren( - container: Container, - newChildren: Array, - ): void {}, - - replaceContainerChildren( - container: Container, - newChildren: Array, - ): void { - container.children = newChildren; - }, - }, - }) - : null; - -const rootContainers = new Map(); -const roots = new Map(); -const persistentRoots = new Map(); -const DEFAULT_ROOT_ID = ''; - -let yieldedValues = null; - -let unitsRemaining; - -function* flushUnitsOfWork(n: number): Generator, void, void> { - let didStop = false; - while (!didStop && scheduledCallback !== null) { - let cb = scheduledCallback; - scheduledCallback = null; - unitsRemaining = n; - cb({ - timeRemaining() { - if (yieldedValues !== null) { - return 0; - } - if (unitsRemaining-- > 0) { - return 999; - } - didStop = true; - return 0; - }, - // React's scheduler has its own way of keeping track of expired - // work and doesn't read this, so don't bother setting it to the - // correct value. - didTimeout: false, - }); - - if (yieldedValues !== null) { - const values = yieldedValues; - yieldedValues = null; - yield values; - } - } -} - -const ReactNoop = { - getChildren(rootID: string = DEFAULT_ROOT_ID) { - const container = rootContainers.get(rootID); - if (container) { - return container.children; - } else { - return null; - } - }, - - createPortal( - children: ReactNodeList, - container: Container, - key: ?string = null, - ) { - return ReactPortal.createPortal(children, container, null, key); - }, - - // Shortcut for testing a single root - render(element: React$Element, callback: ?Function) { - ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback); - }, - - renderToRootWithID( - element: React$Element, - rootID: string, - callback: ?Function, - ) { - let root = roots.get(rootID); - if (!root) { - const container = {rootID: rootID, children: []}; - rootContainers.set(rootID, container); - root = NoopRenderer.createContainer(container, true, false); - roots.set(rootID, root); - } - NoopRenderer.updateContainer(element, root, null, callback); - }, - - renderToPersistentRootWithID( - element: React$Element, - rootID: string, - callback: ?Function, - ) { - if (PersistentNoopRenderer === null) { - throw new Error( - 'Enable ReactFeatureFlags.enablePersistentReconciler to use it in tests.', - ); - } - let root = persistentRoots.get(rootID); - if (!root) { - const container = {rootID: rootID, children: []}; - rootContainers.set(rootID, container); - root = PersistentNoopRenderer.createContainer(container, true, false); - persistentRoots.set(rootID, root); - } - PersistentNoopRenderer.updateContainer(element, root, null, callback); - }, - - unmountRootWithID(rootID: string) { - const root = roots.get(rootID); - if (root) { - NoopRenderer.updateContainer(null, root, null, () => { - roots.delete(rootID); - rootContainers.delete(rootID); - }); - } - }, - - findInstance( - componentOrElement: Element | ?React$Component, - ): null | Instance | TextInstance { - if (componentOrElement == null) { - return null; - } - // Unsound duck typing. - const component = (componentOrElement: any); - if (typeof component.id === 'number') { - return component; - } - return NoopRenderer.findHostInstance(component); - }, - - flushDeferredPri(timeout: number = Infinity): Array { - // The legacy version of this function decremented the timeout before - // returning the new time. - // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead. - const n = timeout / 5 - 1; - - let values = []; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const value of flushUnitsOfWork(n)) { - values.push(...value); - } - return values; - }, - - flush(): Array { - return ReactNoop.flushUnitsOfWork(Infinity); - }, - - flushAndYield( - unitsOfWork: number = Infinity, - ): Generator, void, void> { - return flushUnitsOfWork(unitsOfWork); - }, - - flushUnitsOfWork(n: number): Array { - let values = yieldedValues || []; - yieldedValues = null; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const value of flushUnitsOfWork(n)) { - values.push(...value); - } - return values; - }, - - flushThrough(expected: Array): void { - let actual = []; - if (expected.length !== 0) { - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const value of flushUnitsOfWork(Infinity)) { - actual.push(...value); - if (actual.length >= expected.length) { - break; - } - } - } - expect(actual).toEqual(expected); - }, - - expire(ms: number): void { - elapsedTimeInMs += ms; - }, - - flushExpired(): Array { - return ReactNoop.flushUnitsOfWork(0); - }, - - yield(value: mixed) { - if (yieldedValues === null) { - yieldedValues = [value]; - } else { - yieldedValues.push(value); - } - }, - - clearYields() { - const values = yieldedValues; - yieldedValues = null; - return values; - }, - - hasScheduledCallback() { - return !!scheduledCallback; - }, - - batchedUpdates: NoopRenderer.batchedUpdates, - - deferredUpdates: NoopRenderer.deferredUpdates, - - unbatchedUpdates: NoopRenderer.unbatchedUpdates, - - interactiveUpdates: NoopRenderer.interactiveUpdates, - - flushSync(fn: () => mixed) { - yieldedValues = []; - NoopRenderer.flushSync(fn); - return yieldedValues; - }, - - // Logs the current state of the tree. - dumpTree(rootID: string = DEFAULT_ROOT_ID) { - const root = roots.get(rootID); - const rootContainer = rootContainers.get(rootID); - if (!root || !rootContainer) { - console.log('Nothing rendered yet.'); - return; - } - - let bufferedLog = []; - function log(...args) { - bufferedLog.push(...args, '\n'); - } - - function logHostInstances(children: Array, depth) { - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const indent = ' '.repeat(depth); - if (typeof child.text === 'string') { - log(indent + '- ' + child.text); - } else { - // $FlowFixMe - The child should've been refined now. - log(indent + '- ' + child.type + '#' + child.id); - // $FlowFixMe - The child should've been refined now. - logHostInstances(child.children, depth + 1); - } - } - } - function logContainer(container: Container, depth) { - log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); - logHostInstances(container.children, depth + 1); - } - - function logUpdateQueue(updateQueue: UpdateQueue, depth) { - log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); - const firstUpdate = updateQueue.firstUpdate; - if (!firstUpdate) { - return; - } - - log(' '.repeat(depth + 1) + '~', '[' + firstUpdate.expirationTime + ']'); - while (firstUpdate.next) { - log( - ' '.repeat(depth + 1) + '~', - '[' + firstUpdate.expirationTime + ']', - ); - } - } - - function logFiber(fiber: Fiber, depth) { - log( - ' '.repeat(depth) + - '- ' + - // need to explicitly coerce Symbol to a string - (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), - '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', - ); - if (fiber.updateQueue) { - logUpdateQueue(fiber.updateQueue, depth); - } - // const childInProgress = fiber.progressedChild; - // if (childInProgress && childInProgress !== fiber.child) { - // log( - // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority, - // ); - // logFiber(childInProgress, depth + 1); - // if (fiber.child) { - // log(' '.repeat(depth + 1) + 'CURRENT'); - // } - // } else if (fiber.child && fiber.updateQueue) { - // log(' '.repeat(depth + 1) + 'CHILDREN'); - // } - if (fiber.child) { - logFiber(fiber.child, depth + 1); - } - if (fiber.sibling) { - logFiber(fiber.sibling, depth); - } - } - - log('HOST INSTANCES:'); - logContainer(rootContainer, 0); - log('FIBERS:'); - logFiber(root.current, 0); - - console.log(...bufferedLog); - }, - - simulateErrorInHostConfig(fn: () => void) { - failInBeginPhase = true; - try { - fn(); - } finally { - failInBeginPhase = false; - } - }, -}; +const ReactNoop = createReactNoop( + true, // useMutation +); export default ReactNoop; diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js new file mode 100644 index 0000000000..32c6dcebcb --- /dev/null +++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +/** + * This is a renderer of React that doesn't have a render target output. + * It is useful to demonstrate the internals of the reconciler in isolation + * and for testing semantics of reconciliation separate from the host + * environment. + */ + +import createReactNoop from './createReactNoop'; + +const ReactNoopPersistent = createReactNoop( + false, // useMutation +); + +export default ReactNoopPersistent; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js new file mode 100644 index 0000000000..2667122928 --- /dev/null +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -0,0 +1,582 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +/** + * This is a renderer of React that doesn't have a render target output. + * It is useful to demonstrate the internals of the reconciler in isolation + * and for testing semantics of reconciliation separate from the host + * environment. + */ + +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; +import type {ReactNodeList} from 'shared/ReactTypes'; + +import ReactFiberReconciler from 'react-reconciler'; +import * as ReactPortal from 'shared/ReactPortal'; +import emptyObject from 'fbjs/lib/emptyObject'; +import expect from 'expect'; + +type Container = {rootID: string, children: Array}; +type Props = {prop: any, hidden?: boolean}; +type Instance = {| + type: string, + id: number, + children: Array, + prop: any, +|}; +type TextInstance = {|text: string, id: number|}; + +function createReactNoop(useMutation: boolean) { + const UPDATE_SIGNAL = {}; + let scheduledCallback = null; + + let instanceCounter = 0; + let failInBeginPhase = false; + + function appendChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); + } + + function insertBefore( + parentInstance: Instance | Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + const beforeIndex = parentInstance.children.indexOf(beforeChild); + if (beforeIndex === -1) { + throw new Error('This child does not exist.'); + } + parentInstance.children.splice(beforeIndex, 0, child); + } + + function removeChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index === -1) { + throw new Error('This child does not exist.'); + } + parentInstance.children.splice(index, 1); + } + + let elapsedTimeInMs = 0; + + const sharedHostConfig = { + getRootHostContext() { + if (failInBeginPhase) { + throw new Error('Error in host config.'); + } + return emptyObject; + }, + + getChildHostContext() { + return emptyObject; + }, + + getPublicInstance(instance) { + return instance; + }, + + createInstance(type: string, props: Props): Instance { + const inst = { + id: instanceCounter++, + type: type, + children: [], + prop: props.prop, + }; + // Hide from unit tests + Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); + return inst; + }, + + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.children.push(child); + }, + + finalizeInitialChildren( + domElement: Instance, + type: string, + props: Props, + ): boolean { + return false; + }, + + prepareUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + ): null | {} { + if (oldProps === null) { + throw new Error('Should have old props'); + } + if (newProps === null) { + throw new Error('Should have new props'); + } + return UPDATE_SIGNAL; + }, + + shouldSetTextContent(type: string, props: Props): boolean { + return ( + typeof props.children === 'string' || typeof props.children === 'number' + ); + }, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return !!props.hidden; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, + ): TextInstance { + const inst = {text: text, id: instanceCounter++}; + // Hide from unit tests + Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false}); + return inst; + }, + + scheduleDeferredCallback(callback) { + if (scheduledCallback) { + throw new Error( + 'Scheduling a callback twice is excessive. Instead, keep track of ' + + 'whether the callback has already been scheduled.', + ); + } + scheduledCallback = callback; + return 0; + }, + + cancelDeferredCallback() { + if (scheduledCallback === null) { + throw new Error('No callback is scheduled.'); + } + scheduledCallback = null; + }, + + prepareForCommit(): void {}, + + resetAfterCommit(): void {}, + + now(): number { + return elapsedTimeInMs; + }, + + isPrimaryRenderer: true, + }; + + const hostConfig = useMutation + ? { + ...sharedHostConfig, + mutation: { + commitMount(instance: Instance, type: string, newProps: Props): void { + // Noop + }, + + commitUpdate( + instance: Instance, + updatePayload: Object, + type: string, + oldProps: Props, + newProps: Props, + ): void { + if (oldProps === null) { + throw new Error('Should have old props'); + } + instance.prop = newProps.prop; + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.text = newText; + }, + + appendChild: appendChild, + appendChildToContainer: appendChild, + insertBefore: insertBefore, + insertInContainerBefore: insertBefore, + removeChild: removeChild, + removeChildFromContainer: removeChild, + + resetTextContent(instance: Instance): void {}, + }, + } + : { + ...sharedHostConfig, + persistence: { + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const clone = { + id: instance.id, + type: type, + children: keepChildren ? instance.children : [], + prop: newProps.prop, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + return clone; + }, + + createContainerChildSet( + container: Container, + ): Array { + return []; + }, + + appendChildToContainerChildSet( + childSet: Array, + child: Instance | TextInstance, + ): void { + childSet.push(child); + }, + + finalizeContainerChildren( + container: Container, + newChildren: Array, + ): void {}, + + replaceContainerChildren( + container: Container, + newChildren: Array, + ): void { + container.children = newChildren; + }, + }, + }; + + const NoopRenderer = ReactFiberReconciler(hostConfig); + + const rootContainers = new Map(); + const roots = new Map(); + const DEFAULT_ROOT_ID = ''; + + let yieldedValues = null; + + let unitsRemaining; + + function* flushUnitsOfWork(n: number): Generator, void, void> { + let didStop = false; + while (!didStop && scheduledCallback !== null) { + let cb = scheduledCallback; + scheduledCallback = null; + unitsRemaining = n; + cb({ + timeRemaining() { + if (yieldedValues !== null) { + return 0; + } + if (unitsRemaining-- > 0) { + return 999; + } + didStop = true; + return 0; + }, + // React's scheduler has its own way of keeping track of expired + // work and doesn't read this, so don't bother setting it to the + // correct value. + didTimeout: false, + }); + + if (yieldedValues !== null) { + const values = yieldedValues; + yieldedValues = null; + yield values; + } + } + } + + const ReactNoop = { + getChildren(rootID: string = DEFAULT_ROOT_ID) { + const container = rootContainers.get(rootID); + if (container) { + return container.children; + } else { + return null; + } + }, + + createPortal( + children: ReactNodeList, + container: Container, + key: ?string = null, + ) { + return ReactPortal.createPortal(children, container, null, key); + }, + + // Shortcut for testing a single root + render(element: React$Element, callback: ?Function) { + ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback); + }, + + renderToRootWithID( + element: React$Element, + rootID: string, + callback: ?Function, + ) { + let root = roots.get(rootID); + if (!root) { + const container = {rootID: rootID, children: []}; + rootContainers.set(rootID, container); + root = NoopRenderer.createContainer(container, true, false); + roots.set(rootID, root); + } + NoopRenderer.updateContainer(element, root, null, callback); + }, + + unmountRootWithID(rootID: string) { + const root = roots.get(rootID); + if (root) { + NoopRenderer.updateContainer(null, root, null, () => { + roots.delete(rootID); + rootContainers.delete(rootID); + }); + } + }, + + findInstance( + componentOrElement: Element | ?React$Component, + ): null | Instance | TextInstance { + if (componentOrElement == null) { + return null; + } + // Unsound duck typing. + const component = (componentOrElement: any); + if (typeof component.id === 'number') { + return component; + } + return NoopRenderer.findHostInstance(component); + }, + + flushDeferredPri(timeout: number = Infinity): Array { + // The legacy version of this function decremented the timeout before + // returning the new time. + // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead. + const n = timeout / 5 - 1; + + let values = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const value of flushUnitsOfWork(n)) { + values.push(...value); + } + return values; + }, + + flush(): Array { + return ReactNoop.flushUnitsOfWork(Infinity); + }, + + flushAndYield( + unitsOfWork: number = Infinity, + ): Generator, void, void> { + return flushUnitsOfWork(unitsOfWork); + }, + + flushUnitsOfWork(n: number): Array { + let values = yieldedValues || []; + yieldedValues = null; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const value of flushUnitsOfWork(n)) { + values.push(...value); + } + return values; + }, + + flushThrough(expected: Array): void { + let actual = []; + if (expected.length !== 0) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const value of flushUnitsOfWork(Infinity)) { + actual.push(...value); + if (actual.length >= expected.length) { + break; + } + } + } + expect(actual).toEqual(expected); + }, + + expire(ms: number): void { + elapsedTimeInMs += ms; + }, + + flushExpired(): Array { + return ReactNoop.flushUnitsOfWork(0); + }, + + yield(value: mixed) { + if (yieldedValues === null) { + yieldedValues = [value]; + } else { + yieldedValues.push(value); + } + }, + + clearYields() { + const values = yieldedValues; + yieldedValues = null; + return values; + }, + + hasScheduledCallback() { + return !!scheduledCallback; + }, + + batchedUpdates: NoopRenderer.batchedUpdates, + + deferredUpdates: NoopRenderer.deferredUpdates, + + unbatchedUpdates: NoopRenderer.unbatchedUpdates, + + interactiveUpdates: NoopRenderer.interactiveUpdates, + + flushSync(fn: () => mixed) { + yieldedValues = []; + NoopRenderer.flushSync(fn); + return yieldedValues; + }, + + // Logs the current state of the tree. + dumpTree(rootID: string = DEFAULT_ROOT_ID) { + const root = roots.get(rootID); + const rootContainer = rootContainers.get(rootID); + if (!root || !rootContainer) { + console.log('Nothing rendered yet.'); + return; + } + + let bufferedLog = []; + function log(...args) { + bufferedLog.push(...args, '\n'); + } + + function logHostInstances( + children: Array, + depth, + ) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const indent = ' '.repeat(depth); + if (typeof child.text === 'string') { + log(indent + '- ' + child.text); + } else { + // $FlowFixMe - The child should've been refined now. + log(indent + '- ' + child.type + '#' + child.id); + // $FlowFixMe - The child should've been refined now. + logHostInstances(child.children, depth + 1); + } + } + } + function logContainer(container: Container, depth) { + log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); + logHostInstances(container.children, depth + 1); + } + + function logUpdateQueue(updateQueue: UpdateQueue, depth) { + log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); + const firstUpdate = updateQueue.firstUpdate; + if (!firstUpdate) { + return; + } + + log( + ' '.repeat(depth + 1) + '~', + '[' + firstUpdate.expirationTime + ']', + ); + while (firstUpdate.next) { + log( + ' '.repeat(depth + 1) + '~', + '[' + firstUpdate.expirationTime + ']', + ); + } + } + + function logFiber(fiber: Fiber, depth) { + log( + ' '.repeat(depth) + + '- ' + + // need to explicitly coerce Symbol to a string + (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), + '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', + ); + if (fiber.updateQueue) { + logUpdateQueue(fiber.updateQueue, depth); + } + // const childInProgress = fiber.progressedChild; + // if (childInProgress && childInProgress !== fiber.child) { + // log( + // ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority, + // ); + // logFiber(childInProgress, depth + 1); + // if (fiber.child) { + // log(' '.repeat(depth + 1) + 'CURRENT'); + // } + // } else if (fiber.child && fiber.updateQueue) { + // log(' '.repeat(depth + 1) + 'CHILDREN'); + // } + if (fiber.child) { + logFiber(fiber.child, depth + 1); + } + if (fiber.sibling) { + logFiber(fiber.sibling, depth); + } + } + + log('HOST INSTANCES:'); + logContainer(rootContainer, 0); + log('FIBERS:'); + logFiber(root.current, 0); + + console.log(...bufferedLog); + }, + + simulateErrorInHostConfig(fn: () => void) { + failInBeginPhase = true; + try { + fn(); + } finally { + failInBeginPhase = false; + } + }, + }; + + return ReactNoop; +} + +export default createReactNoop; diff --git a/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js index 1445ddb798..1757e95a78 100644 --- a/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js @@ -11,7 +11,7 @@ 'use strict'; let React; -let ReactNoop; +let ReactNoopPersistent; let ReactPortal; describe('ReactPersistent', () => { @@ -24,14 +24,12 @@ describe('ReactPersistent', () => { ReactFeatureFlags.enableNoopReconciler = false; React = require('react'); - ReactNoop = require('react-noop-renderer'); + ReactNoopPersistent = require('react-noop-renderer/persistent'); ReactPortal = require('shared/ReactPortal'); }); - const DEFAULT_ROOT_ID = 'persistent-test'; - function render(element) { - ReactNoop.renderToPersistentRootWithID(element, DEFAULT_ROOT_ID); + ReactNoopPersistent.render(element); } function div(...children) { @@ -44,7 +42,7 @@ describe('ReactPersistent', () => { } function getChildren() { - return ReactNoop.getChildren(DEFAULT_ROOT_ID); + return ReactNoopPersistent.getChildren(); } it('can update child nodes of a host instance', () => { @@ -62,12 +60,12 @@ describe('ReactPersistent', () => { } render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const originalChildren = getChildren(); expect(originalChildren).toEqual([div(span())]); render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div(span(), span())]); @@ -96,12 +94,12 @@ describe('ReactPersistent', () => { } render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const originalChildren = getChildren(); expect(originalChildren).toEqual([div(span('Hello'))]); render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div(span('Hello'), span('World'))]); @@ -122,12 +120,12 @@ describe('ReactPersistent', () => { } render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const originalChildren = getChildren(); expect(originalChildren).toEqual([div('Hello', span())]); render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div('World', span())]); @@ -167,7 +165,7 @@ describe('ReactPersistent', () => { {ReactPortal.createPortal(, portalContainer, null)} , ); - ReactNoop.flush(); + ReactNoopPersistent.flush(); expect(emptyPortalChildSet).toEqual([]); @@ -185,7 +183,7 @@ describe('ReactPersistent', () => { )} , ); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const newChildren = getChildren(); expect(newChildren).toEqual([div()]); @@ -202,7 +200,7 @@ describe('ReactPersistent', () => { // Deleting the Portal, should clear its children render(); - ReactNoop.flush(); + ReactNoopPersistent.flush(); const clearedPortalChildren = portalContainer.children; expect(clearedPortalChildren).toEqual([]); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index f6681c6467..e9f54a11de 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -275,6 +275,29 @@ const bundles = [ }), }, + /******* React Noop Persistent Renderer (used for tests) *******/ + { + label: 'noop-persistent', + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-noop-renderer/persistent', + global: 'ReactNoopRendererPersistent', + externals: ['react', 'expect'], + // React Noop uses generators. However GCC currently + // breaks when we attempt to use them in the output. + // So we precompile them with regenerator, and include + // it as a runtime dependency of React Noop. In practice + // this isn't an issue because React Noop is only used + // in our tests. We wouldn't want to do this for any + // public package though. + babel: opts => + Object.assign({}, opts, { + plugins: opts.plugins.concat([ + require.resolve('babel-plugin-transform-regenerator'), + ]), + }), + }, + /******* React Reconciler *******/ { label: 'react-reconciler', From 37d12e29169f4ef4ab06eb0626cde4f0fee729cc Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 14 May 2018 16:20:33 +0100 Subject: [PATCH 036/277] Update lockfile --- yarn.lock | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0c9feb48b6..caca3e89e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -784,18 +784,6 @@ babel-plugin-transform-es2015-template-literals@^6.5.2: dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-es3-member-expression-literals@^6.5.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz#733d3444f3ecc41bef8ed1a6a4e09657b8969ebb" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es3-property-literals@^6.5.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz#b2078d5842e22abf40f73e8cde9cd3711abd5758" - dependencies: - babel-runtime "^6.22.0" - babel-plugin-transform-flow-strip-types@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" From 72542030cffee83ac359374a5e7cec9742183b11 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 14 May 2018 17:49:41 +0100 Subject: [PATCH 037/277] Use Java version of Google Closure Compiler (#12800) * makes closure compiler threaded * Dans PR with a closure compiler java version * Remove unused dep * Pin GCC * Prettier * Nit rename * Fix error handling * Name plugins consistently * Fix lint * Maybe this works? * or this * AppVeyor * Fix lint --- .circleci/config.yml | 2 +- appveyor.yml | 4 +- package.json | 2 +- scripts/rollup/build.js | 28 +++++++------- scripts/rollup/plugins/closure-plugin.js | 35 ++++++++++++++++++ scripts/rollup/plugins/sizes-plugin.js | 1 + scripts/rollup/plugins/use-forks-plugin.js | 1 + yarn.lock | 43 ++++++++-------------- 8 files changed, 71 insertions(+), 45 deletions(-) create mode 100644 scripts/rollup/plugins/closure-plugin.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 86fd8c7412..73a6ca773c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: docker: - - image: circleci/node:8 + - image: circleci/openjdk:8-jdk-node-browsers environment: TZ: /usr/share/zoneinfo/America/Los_Angeles diff --git a/appveyor.yml b/appveyor.yml index 0c97cd4d10..6437f33d3d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,8 @@ init: - git config --global core.autocrlf input environment: - matrix: - - nodejs_version: 8 + nodejs_version: 8 + JAVA_HOME: C:\Program Files\Java\jdk1.8.0 # Finish on first failed build matrix: diff --git a/package.json b/package.json index ed1a5cad14..3b1e2f6168 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "git-branch": "^0.3.0", "glob": "^6.0.4", "glob-stream": "^6.1.0", + "google-closure-compiler": "20180506.0.0", "gzip-js": "~0.3.2", "gzip-size": "^3.0.0", "jasmine-check": "^1.0.0-rc.0", @@ -84,7 +85,6 @@ "rimraf": "^2.6.1", "rollup": "^0.52.1", "rollup-plugin-babel": "^3.0.1", - "rollup-plugin-closure-compiler-js": "^1.0.6", "rollup-plugin-commonjs": "^8.2.6", "rollup-plugin-node-resolve": "^2.1.1", "rollup-plugin-prettier": "^0.3.0", diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 1a9e075814..3f736b87c6 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -2,7 +2,7 @@ const {rollup} = require('rollup'); const babel = require('rollup-plugin-babel'); -const closure = require('rollup-plugin-closure-compiler-js'); +const closure = require('./plugins/closure-plugin'); const commonjs = require('rollup-plugin-commonjs'); const prettier = require('rollup-plugin-prettier'); const replace = require('rollup-plugin-replace'); @@ -62,15 +62,15 @@ const errorCodeOpts = { }; const closureOptions = { - compilationLevel: 'SIMPLE', - languageIn: 'ECMASCRIPT5_STRICT', - languageOut: 'ECMASCRIPT5_STRICT', + compilation_level: 'SIMPLE', + language_in: 'ECMASCRIPT5_STRICT', + language_out: 'ECMASCRIPT5_STRICT', env: 'CUSTOM', - warningLevel: 'QUIET', - applyInputSourceMaps: false, - useTypesForOptimization: false, - processCommonJsModules: false, - rewritePolyfills: false, + warning_level: 'QUIET', + apply_input_source_maps: false, + use_types_for_optimization: false, + process_common_js_modules: false, + rewrite_polyfills: false, }; function getBabelConfig(updateBabelOptions, bundleType, filename) { @@ -264,7 +264,7 @@ function getPlugins( Object.assign({}, closureOptions, { // Don't let it create global variables in the browser. // https://github.com/facebook/react/issues/10909 - assumeFunctionWrapper: !isInGlobalScope, + assume_function_wrapper: !isInGlobalScope, // Works because `google-closure-compiler-js` is forked in Yarn lockfile. // We can remove this if GCC merges my PR: // https://github.com/google/closure-compiler/pull/2707 @@ -460,9 +460,9 @@ function handleRollupError(error) { console.error( `\x1b[31m-- ${error.code}${error.plugin ? ` (${error.plugin})` : ''} --` ); - console.error(error.message); - const {file, line, column} = error.loc; - if (file) { + console.error(error.stack); + if (error.loc && error.loc.file) { + const {file, line, column} = error.loc; // This looks like an error from Rollup, e.g. missing export. // We'll use the accurate line numbers provided by Rollup but // use Babel code frame because it looks nicer. @@ -473,7 +473,7 @@ function handleRollupError(error) { highlightCode: true, }); console.error(frame); - } else { + } else if (error.codeFrame) { // This looks like an error from a plugin (e.g. Babel). // In this case we'll resort to displaying the provided code frame // because we can't be sure the reported location is accurate. diff --git a/scripts/rollup/plugins/closure-plugin.js b/scripts/rollup/plugins/closure-plugin.js new file mode 100644 index 0000000000..0c95ee99e2 --- /dev/null +++ b/scripts/rollup/plugins/closure-plugin.js @@ -0,0 +1,35 @@ +'use strict'; + +const ClosureCompiler = require('google-closure-compiler').compiler; +const {promisify} = require('util'); +const fs = require('fs'); +const tmp = require('tmp'); +const writeFileAsync = promisify(fs.writeFile); + +function compile(flags) { + return new Promise((resolve, reject) => { + const closureCompiler = new ClosureCompiler(flags); + closureCompiler.run(function(exitCode, stdOut, stdErr) { + if (!stdErr) { + resolve(stdOut); + } else { + reject(new Error(stdErr)); + } + }); + }); +} + +module.exports = function closure(flags = {}) { + return { + name: 'scripts/rollup/plugins/closure-plugin', + async transformBundle(code) { + const inputFile = tmp.fileSync(); + const tempPath = inputFile.name; + flags = Object.assign({}, flags, {js: tempPath}); + await writeFileAsync(tempPath, code, 'utf8'); + const compiledCode = await compile(flags); + inputFile.removeCallback(); + return {code: compiledCode}; + }, + }; +}; diff --git a/scripts/rollup/plugins/sizes-plugin.js b/scripts/rollup/plugins/sizes-plugin.js index 514795464d..d581d6e0e4 100644 --- a/scripts/rollup/plugins/sizes-plugin.js +++ b/scripts/rollup/plugins/sizes-plugin.js @@ -10,6 +10,7 @@ const gzip = require('gzip-size'); module.exports = function sizes(options) { return { + name: 'scripts/rollup/plugins/sizes-plugin', ongenerate(bundle, obj) { const size = Buffer.byteLength(obj.code); const gzipSize = gzip.sync(obj.code); diff --git a/scripts/rollup/plugins/use-forks-plugin.js b/scripts/rollup/plugins/use-forks-plugin.js index a1c06b1849..b0f7e4e1b9 100644 --- a/scripts/rollup/plugins/use-forks-plugin.js +++ b/scripts/rollup/plugins/use-forks-plugin.js @@ -37,6 +37,7 @@ function useForks(forks) { ); }); return { + name: 'scripts/rollup/plugins/use-forks-plugin', resolveId(importee, importer) { if (!importer || !importee) { return null; diff --git a/yarn.lock b/yarn.lock index caca3e89e2..08eb3a49a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2548,13 +2548,13 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -google-closure-compiler-js@>20170000: - version "20180402.0.0" - resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20180402.0.0.tgz#b90ee11c597030b90ed1c6a054dd728aba81ab2d" +google-closure-compiler@20180506.0.0: + version "20180506.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20180506.0.0.tgz#f59cc34dbf8c9a4f48fba3ebb2cf098d25e345ab" dependencies: - minimist "^1.2.0" + chalk "^1.0.0" vinyl "^2.0.1" - webpack-core "^0.6.8" + vinyl-sourcemaps-apply "^0.2.0" graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "4.1.11" @@ -4814,12 +4814,6 @@ rollup-plugin-babel@^3.0.1: dependencies: rollup-pluginutils "^1.5.0" -rollup-plugin-closure-compiler-js@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/rollup-plugin-closure-compiler-js/-/rollup-plugin-closure-compiler-js-1.0.6.tgz#58e3e31297ad1a532d9114108bc06f2756d72c3d" - dependencies: - google-closure-compiler-js ">20170000" - rollup-plugin-commonjs@^8.2.6: version "8.2.6" resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.2.6.tgz#27e5b9069ff94005bb01e01bb46a1e4873784677" @@ -5007,10 +5001,6 @@ sntp@2.x.x: dependencies: hoek "4.x.x" -source-list-map@~0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" - source-map-support@^0.2.10: version "0.2.10" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.2.10.tgz#ea5a3900a1c1cb25096a0ae8cc5c2b4b10ded3dc" @@ -5035,7 +5025,7 @@ source-map@0.1.32: dependencies: amdefine ">=0.0.4" -source-map@^0.4.4, source-map@~0.4.0, source-map@~0.4.1, source-map@~0.4.2: +source-map@^0.4.4, source-map@~0.4.0, source-map@~0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: @@ -5045,14 +5035,14 @@ source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, sour version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" +source-map@^0.5.1, source-map@~0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + source-map@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -source-map@~0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" @@ -5546,6 +5536,12 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" @@ -5591,13 +5587,6 @@ webidl-conversions@^4.0.1, webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" -webpack-core@^0.6.8: - version "0.6.9" - resolved "https://registry.yarnpkg.com/webpack-core/-/webpack-core-0.6.9.tgz#fc571588c8558da77be9efb6debdc5a3b172bdc2" - dependencies: - source-list-map "~0.1.7" - source-map "~0.4.1" - whatwg-encoding@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" From 2a4d2ca7fc2857bf3ca5d3628211747d7885ed6b Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Mon, 14 May 2018 10:07:31 -0700 Subject: [PATCH 038/277] Set owner correctly inside forwardRef and context consumer (#12777) Previously, _owner would be null if you create an element inside forwardRef or inside a context consumer. This is used by ReactNativeFiberInspector when traversing the hierarchy and also to give more info in some warning texts. This also means you'll now correctly get a warning if you call setState inside one of these. Test Plan: Tim tried it in the RN inspector. --- .../src/ReactFiberBeginWork.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 14927b2db9..d08ebd1330 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -201,7 +201,17 @@ export default function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } } - const nextChildren = render(nextProps, ref); + + let nextChildren; + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + ReactDebugCurrentFiber.setCurrentPhase('render'); + nextChildren = render(nextProps, ref); + ReactDebugCurrentFiber.setCurrentPhase(null); + } else { + nextChildren = render(nextProps, ref); + } + reconcileChildren(current, workInProgress, nextChildren); memoizeProps(workInProgress, nextProps); return workInProgress.child; @@ -1101,7 +1111,16 @@ export default function( ); } - const newChildren = render(newValue); + let newChildren; + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + ReactDebugCurrentFiber.setCurrentPhase('render'); + newChildren = render(newValue); + ReactDebugCurrentFiber.setCurrentPhase(null); + } else { + newChildren = render(newValue); + } + // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; reconcileChildren(current, workInProgress, newChildren); From c4abfa401503b483944d044c2d6c12c5562a1a8b Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Mon, 14 May 2018 10:10:36 -0700 Subject: [PATCH 039/277] Add context provider/consumer to getComponentName (#12778) RN Inspector uses these. --- packages/shared/getComponentName.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index ed448d8152..a5f61c5efc 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -12,11 +12,13 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import { REACT_ASYNC_MODE_TYPE, REACT_CALL_TYPE, + REACT_CONTEXT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, - REACT_RETURN_TYPE, REACT_PORTAL_TYPE, REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, + REACT_RETURN_TYPE, REACT_STRICT_MODE_TYPE, } from 'shared/ReactSymbols'; @@ -33,12 +35,16 @@ function getComponentName(fiber: Fiber): string | null { return 'AsyncMode'; case REACT_CALL_TYPE: return 'ReactCall'; + case REACT_CONTEXT_TYPE: + return 'Context.Consumer'; case REACT_FRAGMENT_TYPE: return 'ReactFragment'; case REACT_PORTAL_TYPE: return 'ReactPortal'; case REACT_PROFILER_TYPE: return `Profiler(${fiber.pendingProps.id})`; + case REACT_PROVIDER_TYPE: + return 'Context.Provider'; case REACT_RETURN_TYPE: return 'ReactReturn'; case REACT_STRICT_MODE_TYPE: From 0ba63aa141e56887127de975704569c9b27c2014 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 14 May 2018 10:39:30 -0700 Subject: [PATCH 040/277] Mark React Native and Fabric renderers as @generated (#12801) Mark React Native and Fabric renderers as @generated --- scripts/rollup/wrappers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index ecec9d82df..26c2dfdd50 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -129,6 +129,7 @@ ${license} * @noflow * @providesModule ${globalName}-dev * @preventMunge + * ${'@gen' + 'erated'} */ 'use strict'; @@ -148,6 +149,7 @@ ${license} * @noflow * @providesModule ${globalName}-prod * @preventMunge + * ${'@gen' + 'erated'} */ ${source}`; @@ -160,6 +162,7 @@ ${license} * * @noflow * @preventMunge + * ${'@gen' + 'erated'} */ 'use strict'; @@ -178,6 +181,7 @@ ${license} * * @noflow * @preventMunge + * ${'@gen' + 'erated'} */ ${source}`; From c5d3104fc0958027820972f3416ec572ded89f28 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 14 May 2018 14:56:48 -0700 Subject: [PATCH 041/277] Do not fire getDerivedStateFromProps unless props or state have changed (#12802) Fixes an oversight from #12600. getDerivedStateFromProps should fire if either props *or* state have changed, but not if *neither* have changed. This prevents a parent from re-rendering if a deep child receives an update. --- .../src/ReactFiberClassComponent.js | 41 +++++++++---------- .../ReactIncremental-test.internal.js | 33 +++++++++++++++ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 0d63543971..52a6346ffb 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -802,16 +802,6 @@ export default function( ); newState = workInProgress.memoizedState; } - - if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps( - workInProgress, - getDerivedStateFromProps, - newProps, - ); - newState = workInProgress.memoizedState; - } - if ( oldProps === newProps && oldState === newState && @@ -829,6 +819,15 @@ export default function( return false; } + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + const shouldUpdate = checkShouldComponentUpdate( workInProgress, oldProps, @@ -937,17 +936,6 @@ export default function( newState = workInProgress.memoizedState; } - if (typeof getDerivedStateFromProps === 'function') { - if (fireGetDerivedStateFromPropsOnStateUpdates || oldProps !== newProps) { - applyDerivedStateFromProps( - workInProgress, - getDerivedStateFromProps, - newProps, - ); - newState = workInProgress.memoizedState; - } - } - if ( oldProps === newProps && oldState === newState && @@ -978,6 +966,17 @@ export default function( return false; } + if (typeof getDerivedStateFromProps === 'function') { + if (fireGetDerivedStateFromPropsOnStateUpdates || oldProps !== newProps) { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + } + const shouldUpdate = checkShouldComponentUpdate( workInProgress, oldProps, diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index 20df85d404..a8289b113d 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1464,6 +1464,39 @@ describe('ReactIncremental', () => { expect(instance.state).toEqual({foo: 'foo'}); }); + it('does not call getDerivedStateFromProps if neither state nor props have changed', () => { + class Parent extends React.Component { + state = {parentRenders: 0}; + static getDerivedStateFromProps(props, prevState) { + ReactNoop.yield('getDerivedStateFromProps'); + return prevState.parentRenders + 1; + } + render() { + ReactNoop.yield('Parent'); + return ; + } + } + + class Child extends React.Component { + render() { + ReactNoop.yield('Child'); + return this.props.parentRenders; + } + } + + const child = React.createRef(null); + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'getDerivedStateFromProps', + 'Parent', + 'Child', + ]); + + // Schedule an update on the child. The parent should not re-render. + child.current.setState({}); + expect(ReactNoop.flush()).toEqual(['Child']); + }); + it('does not call getDerivedStateFromProps for state-only updates if feature flag is disabled', () => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); From c802d29bd16753aef15863db8fb3f338c0067c75 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 14 May 2018 15:34:01 -0700 Subject: [PATCH 042/277] Use HostContext to warn about invalid View/Text nesting (#12766) --- .../src/ReactFabricRenderer.js | 45 +++++-- .../src/ReactNativeFiberRenderer.js | 44 ++++++- .../__tests__/ReactFabric-test.internal.js | 120 ++++++++++++++---- .../ReactFabricAndNative-test.internal.js | 4 +- .../ReactNativeEvents-test.internal.js | 2 +- .../ReactNativeMount-test.internal.js | 102 +++++++++++++-- .../ReactFabric-test.internal.js.snap | 92 +++++++------- .../ReactNativeMount-test.internal.js.snap | 84 ++++++------ 8 files changed, 352 insertions(+), 141 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricRenderer.js b/packages/react-native-renderer/src/ReactFabricRenderer.js index fa2fabef64..9c597c2e53 100644 --- a/packages/react-native-renderer/src/ReactFabricRenderer.js +++ b/packages/react-native-renderer/src/ReactFabricRenderer.js @@ -22,7 +22,7 @@ import * as ReactNativeViewConfigRegistry from 'ReactNativeViewConfigRegistry'; import ReactFiberReconciler from 'react-reconciler'; import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev'; -import emptyObject from 'fbjs/lib/emptyObject'; +import invariant from 'fbjs/lib/invariant'; // Modules provided by RN: import TextInputState from 'TextInputState'; @@ -35,6 +35,10 @@ import UIManager from 'UIManager'; // This means that they never overlap. let nextReactTag = 2; +type HostContext = $ReadOnly<{| + isInAParentText: boolean, +|}>; + /** * This is used for refs on host components. */ @@ -135,7 +139,7 @@ const ReactFabricRenderer = ReactFiberReconciler({ type: string, props: Props, rootContainerInstance: Container, - hostContext: {}, + hostContext: HostContext, internalInstanceHandle: Object, ): Instance { const tag = nextReactTag; @@ -151,6 +155,11 @@ const ReactFabricRenderer = ReactFiberReconciler({ } } + invariant( + type !== 'RCTView' || !hostContext.isInAParentText, + 'Nesting of within is not currently supported.', + ); + const updatePayload = ReactNativeAttributePayload.create( props, viewConfig.validAttributes, @@ -175,9 +184,14 @@ const ReactFabricRenderer = ReactFiberReconciler({ createTextInstance( text: string, rootContainerInstance: Container, - hostContext: {}, + hostContext: HostContext, internalInstanceHandle: Object, ): TextInstance { + invariant( + hostContext.isInAParentText, + 'Text strings must be rendered within a component.', + ); + const tag = nextReactTag; nextReactTag += 2; @@ -203,12 +217,27 @@ const ReactFabricRenderer = ReactFiberReconciler({ return false; }, - getRootHostContext(): {} { - return emptyObject; + getRootHostContext(rootContainerInstance: Container): HostContext { + return {isInAParentText: false}; }, - getChildHostContext(): {} { - return emptyObject; + getChildHostContext( + parentHostContext: HostContext, + type: string, + ): HostContext { + const prevIsInAParentText = parentHostContext.isInAParentText; + const isInAParentText = + type === 'AndroidTextInput' || // Android + type === 'RCTMultilineTextInputView' || // iOS + type === 'RCTSinglelineTextInputView' || // iOS + type === 'RCTText' || + type === 'RCTVirtualText'; + + if (prevIsInAParentText !== isInAParentText) { + return {isInAParentText}; + } else { + return parentHostContext; + } }, getPublicInstance(instance) { @@ -230,7 +259,7 @@ const ReactFabricRenderer = ReactFiberReconciler({ oldProps: Props, newProps: Props, rootContainerInstance: Container, - hostContext: {}, + hostContext: HostContext, ): null | Object { const viewConfig = instance.canonical.viewConfig; const updatePayload = ReactNativeAttributePayload.diff( diff --git a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js b/packages/react-native-renderer/src/ReactNativeFiberRenderer.js index 2854f6488f..01fe4816b4 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeFiberRenderer.js @@ -12,6 +12,7 @@ import type {ReactNativeBaseComponentViewConfig} from './ReactNativeTypes'; import ReactFiberReconciler from 'react-reconciler'; import emptyObject from 'fbjs/lib/emptyObject'; import invariant from 'fbjs/lib/invariant'; + // Modules provided by RN: import UIManager from 'UIManager'; import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev'; @@ -35,6 +36,10 @@ export type Instance = { type Props = Object; type TextInstance = number; +type HostContext = $ReadOnly<{| + isInAParentText: boolean, +|}>; + // Counter for uniquely identifying views. // % 10 === 1 means it is a rootTag. // % 2 === 0 means it is a Fabric tag. @@ -71,7 +76,7 @@ const NativeRenderer = ReactFiberReconciler({ type: string, props: Props, rootContainerInstance: Container, - hostContext: {}, + hostContext: HostContext, internalInstanceHandle: Object, ): Instance { const tag = allocateTag(); @@ -85,6 +90,11 @@ const NativeRenderer = ReactFiberReconciler({ } } + invariant( + type !== 'RCTView' || !hostContext.isInAParentText, + 'Nesting of within is not currently supported.', + ); + const updatePayload = ReactNativeAttributePayload.create( props, viewConfig.validAttributes, @@ -110,9 +120,14 @@ const NativeRenderer = ReactFiberReconciler({ createTextInstance( text: string, rootContainerInstance: Container, - hostContext: {}, + hostContext: HostContext, internalInstanceHandle: Object, ): TextInstance { + invariant( + hostContext.isInAParentText, + 'Text strings must be rendered within a component.', + ); + const tag = allocateTag(); UIManager.createView( @@ -155,12 +170,27 @@ const NativeRenderer = ReactFiberReconciler({ return false; }, - getRootHostContext(): {} { - return emptyObject; + getRootHostContext(rootContainerInstance: Container): HostContext { + return {isInAParentText: false}; }, - getChildHostContext(): {} { - return emptyObject; + getChildHostContext( + parentHostContext: HostContext, + type: string, + ): HostContext { + const prevIsInAParentText = parentHostContext.isInAParentText; + const isInAParentText = + type === 'AndroidTextInput' || // Android + type === 'RCTMultilineTextInputView' || // iOS + type === 'RCTSinglelineTextInputView' || // iOS + type === 'RCTText' || + type === 'RCTVirtualText'; + + if (prevIsInAParentText !== isInAParentText) { + return {isInAParentText}; + } else { + return parentHostContext; + } }, getPublicInstance(instance) { @@ -181,7 +211,7 @@ const NativeRenderer = ReactFiberReconciler({ oldProps: Props, newProps: Props, rootContainerInstance: Container, - hostContext: {}, + hostContext: HostContext, ): null | Object { return emptyObject; }, diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index a1741f7dc4..cc697f4ddb 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -33,9 +33,9 @@ describe('ReactFabric', () => { }); it('should be able to create and render a native component', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); ReactFabric.render(, 1); @@ -45,9 +45,9 @@ describe('ReactFabric', () => { }); it('should be able to create and update a native component', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); const firstNode = {}; @@ -67,9 +67,9 @@ describe('ReactFabric', () => { }); it('should not call FabricUIManager.cloneNode after render for properties that have not changed', () => { - const Text = createReactNativeComponentClass('Text', () => ({ + const Text = createReactNativeComponentClass('RCTText', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'Text', + uiViewClassName: 'RCTText', })); ReactFabric.render(1, 11); @@ -110,15 +110,15 @@ describe('ReactFabric', () => { }); it('should only pass props diffs to FabricUIManager.cloneNode', () => { - const View = createReactNativeComponentClass('View', () => ({ + const Text = createReactNativeComponentClass('RCTText', () => ({ validAttributes: {foo: true, bar: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTText', })); ReactFabric.render( - + 1 - , + , 11, ); expect(FabricUIManager.cloneNode).not.toBeCalled(); @@ -127,9 +127,9 @@ describe('ReactFabric', () => { expect(FabricUIManager.cloneNodeWithNewChildrenAndProps).not.toBeCalled(); ReactFabric.render( - + 1 - , + , 11, ); expect(FabricUIManager.cloneNodeWithNewProps.mock.calls[0][1]).toEqual({ @@ -138,9 +138,9 @@ describe('ReactFabric', () => { expect(FabricUIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); ReactFabric.render( - + 2 - , + , 11, ); expect( @@ -152,9 +152,9 @@ describe('ReactFabric', () => { }); it('should not call UIManager.updateView from setNativeProps for properties that have not changed', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); class Subclass extends ReactFabric.NativeComponent { @@ -187,9 +187,9 @@ describe('ReactFabric', () => { }); it('returns the correct instance and calls it in the callback', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); let a; @@ -208,9 +208,9 @@ describe('ReactFabric', () => { }); it('renders and reorders children', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {title: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); class Component extends React.Component { @@ -249,9 +249,9 @@ describe('ReactFabric', () => { }); it('should call complete after inserting children', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); const snapshots = []; @@ -272,4 +272,80 @@ describe('ReactFabric', () => { ); expect(snapshots).toMatchSnapshot(); }); + + it('should throw when is used inside of a ancestor', () => { + const Image = createReactNativeComponentClass('RCTImage', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTImage', + })); + const Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTText', + })); + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTView', + })); + + expect(() => + ReactFabric.render( + + + , + 11, + ), + ).toThrow('Nesting of within is not currently supported.'); + + // Non-View things (e.g. Image) are fine + ReactFabric.render( + + + , + 11, + ); + }); + + it('should throw for text not inside of a ancestor', () => { + const ScrollView = createReactNativeComponentClass('RCTScrollView', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTScrollView', + })); + const Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTText', + })); + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTView', + })); + + expect(() => ReactFabric.render(this should warn, 11)).toThrow( + 'Text strings must be rendered within a component.', + ); + + expect(() => + ReactFabric.render( + + hi hello hi + , + 11, + ), + ).toThrow('Text strings must be rendered within a component.'); + }); + + it('should not throw for text inside of an indirect ancestor', () => { + const Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTText', + })); + + const Indirection = () => 'Hi'; + + ReactFabric.render( + + + , + 11, + ); + }); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js index a78dd5d228..faaa82f3ef 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -31,9 +31,9 @@ describe('ReactFabric', () => { }); it('find Fabric nodes with the RN renderer', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {title: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); let ref = React.createRef(); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js index 07fbe1692a..13fce4d77d 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js @@ -153,7 +153,7 @@ it('handles events', () => { it('handles events on text nodes', () => { expect(RCTEventEmitter.register.mock.calls.length).toBe(1); const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; - const Text = fakeRequireNativeComponent('Text', {}); + const Text = fakeRequireNativeComponent('RCTText', {}); class ContextHack extends React.Component { static childContextTypes = {isInAParentText: PropTypes.bool}; diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index f93a369d87..60b5d79f5d 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -27,9 +27,9 @@ describe('ReactNative', () => { }); it('should be able to create and render a native component', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); ReactNative.render(, 1); @@ -40,9 +40,9 @@ describe('ReactNative', () => { }); it('should be able to create and update a native component', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); ReactNative.render(, 11); @@ -57,13 +57,13 @@ describe('ReactNative', () => { expect(UIManager.createView.mock.calls.length).toBe(1); expect(UIManager.setChildren.mock.calls.length).toBe(1); expect(UIManager.manageChildren).not.toBeCalled(); - expect(UIManager.updateView).toBeCalledWith(3, 'View', {foo: 'bar'}); + expect(UIManager.updateView).toBeCalledWith(3, 'RCTView', {foo: 'bar'}); }); it('should not call UIManager.updateView after render for properties that have not changed', () => { - const Text = createReactNativeComponentClass('Text', () => ({ + const Text = createReactNativeComponentClass('RCTText', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'Text', + uiViewClassName: 'RCTText', })); ReactNative.render(1, 11); @@ -87,9 +87,9 @@ describe('ReactNative', () => { }); it('should not call UIManager.updateView from setNativeProps for properties that have not changed', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); class Subclass extends ReactNative.NativeComponent { @@ -122,9 +122,9 @@ describe('ReactNative', () => { }); it('returns the correct instance and calls it in the callback', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); let a; @@ -143,9 +143,9 @@ describe('ReactNative', () => { }); it('renders and reorders children', () => { - const View = createReactNativeComponentClass('View', () => ({ + const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {title: true}, - uiViewClassName: 'View', + uiViewClassName: 'RCTView', })); class Component extends React.Component { @@ -182,4 +182,80 @@ describe('ReactNative', () => { ReactNative.render(, 11); expect(mockArgs.length).toEqual(0); }); + + it('should throw when is used inside of a ancestor', () => { + const Image = createReactNativeComponentClass('RCTImage', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTImage', + })); + const Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTText', + })); + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTView', + })); + + expect(() => + ReactNative.render( + + + , + 11, + ), + ).toThrow('Nesting of within is not currently supported.'); + + // Non-View things (e.g. Image) are fine + ReactNative.render( + + + , + 11, + ); + }); + + it('should throw for text not inside of a ancestor', () => { + const ScrollView = createReactNativeComponentClass('RCTScrollView', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTScrollView', + })); + const Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTText', + })); + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTView', + })); + + expect(() => ReactNative.render(this should warn, 11)).toThrow( + 'Text strings must be rendered within a component.', + ); + + expect(() => + ReactNative.render( + + hi hello hi + , + 11, + ), + ).toThrow('Text strings must be rendered within a component.'); + }); + + it('should not throw for text inside of an indirect ancestor', () => { + const Text = createReactNativeComponentClass('RCTText', () => ({ + validAttributes: {}, + uiViewClassName: 'RCTText', + })); + + const Indirection = () => 'Hi'; + + ReactNative.render( + + + , + 11, + ); + }); }); diff --git a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap index 6e190e635d..4a0f4d8cf5 100644 --- a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap +++ b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap @@ -2,69 +2,69 @@ exports[`ReactFabric renders and reorders children 1`] = ` "11 - View null - View {\\"title\\":\\"a\\"} - View {\\"title\\":\\"b\\"} - View {\\"title\\":\\"c\\"} - View {\\"title\\":\\"d\\"} - View {\\"title\\":\\"e\\"} - View {\\"title\\":\\"f\\"} - View {\\"title\\":\\"g\\"} - View {\\"title\\":\\"h\\"} - View {\\"title\\":\\"i\\"} - View {\\"title\\":\\"j\\"} - View {\\"title\\":\\"k\\"} - View {\\"title\\":\\"l\\"} - View {\\"title\\":\\"m\\"} - View {\\"title\\":\\"n\\"} - View {\\"title\\":\\"o\\"} - View {\\"title\\":\\"p\\"} - View {\\"title\\":\\"q\\"} - View {\\"title\\":\\"r\\"} - View {\\"title\\":\\"s\\"} - View {\\"title\\":\\"t\\"}" + RCTView null + RCTView {\\"title\\":\\"a\\"} + RCTView {\\"title\\":\\"b\\"} + RCTView {\\"title\\":\\"c\\"} + RCTView {\\"title\\":\\"d\\"} + RCTView {\\"title\\":\\"e\\"} + RCTView {\\"title\\":\\"f\\"} + RCTView {\\"title\\":\\"g\\"} + RCTView {\\"title\\":\\"h\\"} + RCTView {\\"title\\":\\"i\\"} + RCTView {\\"title\\":\\"j\\"} + RCTView {\\"title\\":\\"k\\"} + RCTView {\\"title\\":\\"l\\"} + RCTView {\\"title\\":\\"m\\"} + RCTView {\\"title\\":\\"n\\"} + RCTView {\\"title\\":\\"o\\"} + RCTView {\\"title\\":\\"p\\"} + RCTView {\\"title\\":\\"q\\"} + RCTView {\\"title\\":\\"r\\"} + RCTView {\\"title\\":\\"s\\"} + RCTView {\\"title\\":\\"t\\"}" `; exports[`ReactFabric renders and reorders children 2`] = ` "11 - View null - View {\\"title\\":\\"m\\"} - View {\\"title\\":\\"x\\"} - View {\\"title\\":\\"h\\"} - View {\\"title\\":\\"p\\"} - View {\\"title\\":\\"g\\"} - View {\\"title\\":\\"w\\"} - View {\\"title\\":\\"f\\"} - View {\\"title\\":\\"r\\"} - View {\\"title\\":\\"a\\"} - View {\\"title\\":\\"l\\"} - View {\\"title\\":\\"k\\"} - View {\\"title\\":\\"e\\"} - View {\\"title\\":\\"o\\"} - View {\\"title\\":\\"i\\"} - View {\\"title\\":\\"v\\"} - View {\\"title\\":\\"c\\"} - View {\\"title\\":\\"s\\"} - View {\\"title\\":\\"t\\"} - View {\\"title\\":\\"z\\"} - View {\\"title\\":\\"y\\"}" + RCTView null + RCTView {\\"title\\":\\"m\\"} + RCTView {\\"title\\":\\"x\\"} + RCTView {\\"title\\":\\"h\\"} + RCTView {\\"title\\":\\"p\\"} + RCTView {\\"title\\":\\"g\\"} + RCTView {\\"title\\":\\"w\\"} + RCTView {\\"title\\":\\"f\\"} + RCTView {\\"title\\":\\"r\\"} + RCTView {\\"title\\":\\"a\\"} + RCTView {\\"title\\":\\"l\\"} + RCTView {\\"title\\":\\"k\\"} + RCTView {\\"title\\":\\"e\\"} + RCTView {\\"title\\":\\"o\\"} + RCTView {\\"title\\":\\"i\\"} + RCTView {\\"title\\":\\"v\\"} + RCTView {\\"title\\":\\"c\\"} + RCTView {\\"title\\":\\"s\\"} + RCTView {\\"title\\":\\"t\\"} + RCTView {\\"title\\":\\"z\\"} + RCTView {\\"title\\":\\"y\\"}" `; exports[`ReactFabric should call complete after inserting children 1`] = ` Array [ - "View {\\"foo\\":\\"a\\"} - View {\\"foo\\":\\"b\\"}", + "RCTView {\\"foo\\":\\"a\\"} + RCTView {\\"foo\\":\\"b\\"}", ] `; exports[`ReactFabric should only pass props diffs to FabricUIManager.cloneNode 1`] = ` "11 - View {\\"foo\\":\\"a\\",\\"bar\\":\\"b\\"} + RCTText {\\"foo\\":\\"a\\",\\"bar\\":\\"b\\"} RCTRawText {\\"text\\":\\"1\\"}" `; exports[`ReactFabric should only pass props diffs to FabricUIManager.cloneNode 2`] = ` "11 - View {\\"foo\\":\\"b\\",\\"bar\\":\\"b\\"} + RCTText {\\"foo\\":\\"b\\",\\"bar\\":\\"b\\"} RCTRawText {\\"text\\":\\"2\\"}" `; diff --git a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap index 5225479968..9cf7df9217 100644 --- a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap +++ b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap @@ -2,50 +2,50 @@ exports[`ReactNative renders and reorders children 1`] = ` " {} - View null - View {\\"title\\":\\"a\\"} - View {\\"title\\":\\"b\\"} - View {\\"title\\":\\"c\\"} - View {\\"title\\":\\"d\\"} - View {\\"title\\":\\"e\\"} - View {\\"title\\":\\"f\\"} - View {\\"title\\":\\"g\\"} - View {\\"title\\":\\"h\\"} - View {\\"title\\":\\"i\\"} - View {\\"title\\":\\"j\\"} - View {\\"title\\":\\"k\\"} - View {\\"title\\":\\"l\\"} - View {\\"title\\":\\"m\\"} - View {\\"title\\":\\"n\\"} - View {\\"title\\":\\"o\\"} - View {\\"title\\":\\"p\\"} - View {\\"title\\":\\"q\\"} - View {\\"title\\":\\"r\\"} - View {\\"title\\":\\"s\\"} - View {\\"title\\":\\"t\\"}" + RCTView null + RCTView {\\"title\\":\\"a\\"} + RCTView {\\"title\\":\\"b\\"} + RCTView {\\"title\\":\\"c\\"} + RCTView {\\"title\\":\\"d\\"} + RCTView {\\"title\\":\\"e\\"} + RCTView {\\"title\\":\\"f\\"} + RCTView {\\"title\\":\\"g\\"} + RCTView {\\"title\\":\\"h\\"} + RCTView {\\"title\\":\\"i\\"} + RCTView {\\"title\\":\\"j\\"} + RCTView {\\"title\\":\\"k\\"} + RCTView {\\"title\\":\\"l\\"} + RCTView {\\"title\\":\\"m\\"} + RCTView {\\"title\\":\\"n\\"} + RCTView {\\"title\\":\\"o\\"} + RCTView {\\"title\\":\\"p\\"} + RCTView {\\"title\\":\\"q\\"} + RCTView {\\"title\\":\\"r\\"} + RCTView {\\"title\\":\\"s\\"} + RCTView {\\"title\\":\\"t\\"}" `; exports[`ReactNative renders and reorders children 2`] = ` " {} - View null - View {\\"title\\":\\"m\\"} - View {\\"title\\":\\"x\\"} - View {\\"title\\":\\"h\\"} - View {\\"title\\":\\"p\\"} - View {\\"title\\":\\"g\\"} - View {\\"title\\":\\"w\\"} - View {\\"title\\":\\"f\\"} - View {\\"title\\":\\"r\\"} - View {\\"title\\":\\"a\\"} - View {\\"title\\":\\"l\\"} - View {\\"title\\":\\"k\\"} - View {\\"title\\":\\"e\\"} - View {\\"title\\":\\"o\\"} - View {\\"title\\":\\"i\\"} - View {\\"title\\":\\"v\\"} - View {\\"title\\":\\"c\\"} - View {\\"title\\":\\"s\\"} - View {\\"title\\":\\"t\\"} - View {\\"title\\":\\"z\\"} - View {\\"title\\":\\"y\\"}" + RCTView null + RCTView {\\"title\\":\\"m\\"} + RCTView {\\"title\\":\\"x\\"} + RCTView {\\"title\\":\\"h\\"} + RCTView {\\"title\\":\\"p\\"} + RCTView {\\"title\\":\\"g\\"} + RCTView {\\"title\\":\\"w\\"} + RCTView {\\"title\\":\\"f\\"} + RCTView {\\"title\\":\\"r\\"} + RCTView {\\"title\\":\\"a\\"} + RCTView {\\"title\\":\\"l\\"} + RCTView {\\"title\\":\\"k\\"} + RCTView {\\"title\\":\\"e\\"} + RCTView {\\"title\\":\\"o\\"} + RCTView {\\"title\\":\\"i\\"} + RCTView {\\"title\\":\\"v\\"} + RCTView {\\"title\\":\\"c\\"} + RCTView {\\"title\\":\\"s\\"} + RCTView {\\"title\\":\\"t\\"} + RCTView {\\"title\\":\\"z\\"} + RCTView {\\"title\\":\\"y\\"}" `; From b2d16047ae8a6405983e8244bf3b482bc57cd5a4 Mon Sep 17 00:00:00 2001 From: Timothy Yung Date: Mon, 14 May 2018 16:36:50 -0700 Subject: [PATCH 043/277] Fix Type for ReactNative.NativeComponent (#12805) --- .../src/ReactNativeTypes.js | 24 ++++++++++++++++--- packages/shared/ReactTypes.js | 1 - scripts/circleci/check_modules.sh | 4 +--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index bf94119739..c38b0b44eb 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -4,10 +4,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @format * @flow - * @providesModule ReactNativeTypes */ +import React from 'react'; + export type MeasureOnSuccessCallback = ( x: number, y: number, @@ -52,6 +54,22 @@ export type ReactNativeBaseComponentViewConfig = { export type ViewConfigGetter = () => ReactNativeBaseComponentViewConfig; +/** + * Class only exists for its Flow type. + */ +class ReactNativeComponent extends React.Component { + blur(): void {} + focus(): void {} + measure(callback: MeasureOnSuccessCallback): void {} + measureInWindow(callback: MeasureInWindowOnSuccessCallback): void {} + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail?: () => void, + ): void {} + setNativeProps(nativeProps: Object): void {} +} + /** * This type keeps ReactNativeFiberHostComponent and NativeMethodsMixin in sync. * It can also provide types for ReactNative applications that use NMM or refs. @@ -87,7 +105,7 @@ type SecretInternalsFabricType = { * Provide minimal Flow typing for the high-level RN API and call it a day. */ export type ReactNativeType = { - NativeComponent: any, + NativeComponent: typeof ReactNativeComponent, findNodeHandle(componentOrHandle: any): ?number, render( element: React$Element, @@ -102,7 +120,7 @@ export type ReactNativeType = { }; export type ReactFabricType = { - NativeComponent: any, + NativeComponent: typeof ReactNativeComponent, findNodeHandle(componentOrHandle: any): ?number, render( element: React$Element, diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index c47a380a92..ac4eed362d 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. * * @flow - * @providesModule ReactTypes */ export type ReactNode = diff --git a/scripts/circleci/check_modules.sh b/scripts/circleci/check_modules.sh index 176f4a8ba4..b9ee63b6ee 100755 --- a/scripts/circleci/check_modules.sh +++ b/scripts/circleci/check_modules.sh @@ -3,9 +3,7 @@ set -e # Make sure we don't introduce accidental @providesModule annotations. -EXPECTED='packages/react-native-renderer/src/ReactNativeTypes.js -packages/shared/ReactTypes.js -scripts/rollup/wrappers.js' +EXPECTED='scripts/rollup/wrappers.js' ACTUAL=$(git grep -l @providesModule -- './*.js' ':!scripts/rollup/shims/*.js') # Colors From 45b90d48668dae48aad611819b891d029aa2fb27 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 01:12:28 +0100 Subject: [PATCH 044/277] Move renderer host configs into separate modules (#12791) * Separate test renderer host config * Separate ART renderer host config * Separate ReactDOM host config * Extract RN Fabric host config * Extract RN host config --- packages/react-art/src/ReactART.js | 410 +------------ packages/react-art/src/ReactARTHostConfig.js | 390 ++++++++++++ packages/react-art/src/ReactARTInternals.js | 34 ++ packages/react-dom/src/client/ReactDOM.js | 547 +---------------- .../src/client/ReactDOMHostConfig.js | 568 ++++++++++++++++++ .../src/ReactFabricHostConfig.js | 355 +++++++++++ .../src/ReactFabricRenderer.js | 345 +---------- .../src/ReactNativeFiberHostComponent.js | 2 +- ...erRenderer.js => ReactNativeHostConfig.js} | 9 +- .../src/ReactNativeRenderer.js | 5 +- .../src/ReactTestHostConfig.js | 218 +++++++ .../src/ReactTestRenderer.js | 299 +-------- .../src/ReactTestRendererScheduling.js | 100 +++ 13 files changed, 1697 insertions(+), 1585 deletions(-) create mode 100644 packages/react-art/src/ReactARTHostConfig.js create mode 100644 packages/react-art/src/ReactARTInternals.js create mode 100644 packages/react-dom/src/client/ReactDOMHostConfig.js create mode 100644 packages/react-native-renderer/src/ReactFabricHostConfig.js rename packages/react-native-renderer/src/{ReactNativeFiberRenderer.js => ReactNativeHostConfig.js} (98%) create mode 100644 packages/react-test-renderer/src/ReactTestHostConfig.js create mode 100644 packages/react-test-renderer/src/ReactTestRendererScheduling.js diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index a7f9a3064c..d24395c462 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -7,267 +7,18 @@ import React from 'react'; import ReactFiberReconciler from 'react-reconciler'; -import * as ReactScheduler from 'react-scheduler'; +import Transform from 'art/core/transform'; import Mode from 'art/modes/current'; import FastNoSideEffects from 'art/modes/fast-noSideEffects'; -import Transform from 'art/core/transform'; -import invariant from 'fbjs/lib/invariant'; -import emptyObject from 'fbjs/lib/emptyObject'; + +import ReactARTHostConfig from './ReactARTHostConfig'; +import {TYPES, childrenAsString} from './ReactARTInternals'; Mode.setCurrent( // Change to 'art/modes/dom' for easier debugging via SVG FastNoSideEffects, ); -const pooledTransform = new Transform(); - -const EVENT_TYPES = { - onClick: 'click', - onMouseMove: 'mousemove', - onMouseOver: 'mouseover', - onMouseOut: 'mouseout', - onMouseUp: 'mouseup', - onMouseDown: 'mousedown', -}; - -const TYPES = { - CLIPPING_RECTANGLE: 'ClippingRectangle', - GROUP: 'Group', - SHAPE: 'Shape', - TEXT: 'Text', -}; - -const UPDATE_SIGNAL = {}; - -/** Helper Methods */ - -function addEventListeners(instance, type, listener) { - // We need to explicitly unregister before unmount. - // For this reason we need to track subscriptions. - if (!instance._listeners) { - instance._listeners = {}; - instance._subscriptions = {}; - } - - instance._listeners[type] = listener; - - if (listener) { - if (!instance._subscriptions[type]) { - instance._subscriptions[type] = instance.subscribe( - type, - createEventHandler(instance), - instance, - ); - } - } else { - if (instance._subscriptions[type]) { - instance._subscriptions[type](); - delete instance._subscriptions[type]; - } - } -} - -function childrenAsString(children) { - if (!children) { - return ''; - } else if (typeof children === 'string') { - return children; - } else if (children.length) { - return children.join(''); - } else { - return ''; - } -} - -function createEventHandler(instance) { - return function handleEvent(event) { - const listener = instance._listeners[event.type]; - - if (!listener) { - // Noop - } else if (typeof listener === 'function') { - listener.call(instance, event); - } else if (listener.handleEvent) { - listener.handleEvent(event); - } - }; -} - -function destroyEventListeners(instance) { - if (instance._subscriptions) { - for (let type in instance._subscriptions) { - instance._subscriptions[type](); - } - } - - instance._subscriptions = null; - instance._listeners = null; -} - -function getScaleX(props) { - if (props.scaleX != null) { - return props.scaleX; - } else if (props.scale != null) { - return props.scale; - } else { - return 1; - } -} - -function getScaleY(props) { - if (props.scaleY != null) { - return props.scaleY; - } else if (props.scale != null) { - return props.scale; - } else { - return 1; - } -} - -function isSameFont(oldFont, newFont) { - if (oldFont === newFont) { - return true; - } else if (typeof newFont === 'string' || typeof oldFont === 'string') { - return false; - } else { - return ( - newFont.fontSize === oldFont.fontSize && - newFont.fontStyle === oldFont.fontStyle && - newFont.fontVariant === oldFont.fontVariant && - newFont.fontWeight === oldFont.fontWeight && - newFont.fontFamily === oldFont.fontFamily - ); - } -} - -/** Render Methods */ - -function applyClippingRectangleProps(instance, props, prevProps = {}) { - applyNodeProps(instance, props, prevProps); - - instance.width = props.width; - instance.height = props.height; -} - -function applyGroupProps(instance, props, prevProps = {}) { - applyNodeProps(instance, props, prevProps); - - instance.width = props.width; - instance.height = props.height; -} - -function applyNodeProps(instance, props, prevProps = {}) { - const scaleX = getScaleX(props); - const scaleY = getScaleY(props); - - pooledTransform - .transformTo(1, 0, 0, 1, 0, 0) - .move(props.x || 0, props.y || 0) - .rotate(props.rotation || 0, props.originX, props.originY) - .scale(scaleX, scaleY, props.originX, props.originY); - - if (props.transform != null) { - pooledTransform.transform(props.transform); - } - - if ( - instance.xx !== pooledTransform.xx || - instance.yx !== pooledTransform.yx || - instance.xy !== pooledTransform.xy || - instance.yy !== pooledTransform.yy || - instance.x !== pooledTransform.x || - instance.y !== pooledTransform.y - ) { - instance.transformTo(pooledTransform); - } - - if (props.cursor !== prevProps.cursor || props.title !== prevProps.title) { - instance.indicate(props.cursor, props.title); - } - - if (instance.blend && props.opacity !== prevProps.opacity) { - instance.blend(props.opacity == null ? 1 : props.opacity); - } - - if (props.visible !== prevProps.visible) { - if (props.visible == null || props.visible) { - instance.show(); - } else { - instance.hide(); - } - } - - for (let type in EVENT_TYPES) { - addEventListeners(instance, EVENT_TYPES[type], props[type]); - } -} - -function applyRenderableNodeProps(instance, props, prevProps = {}) { - applyNodeProps(instance, props, prevProps); - - if (prevProps.fill !== props.fill) { - if (props.fill && props.fill.applyFill) { - props.fill.applyFill(instance); - } else { - instance.fill(props.fill); - } - } - if ( - prevProps.stroke !== props.stroke || - prevProps.strokeWidth !== props.strokeWidth || - prevProps.strokeCap !== props.strokeCap || - prevProps.strokeJoin !== props.strokeJoin || - // TODO: Consider deep check of stokeDash; may benefit VML in IE. - prevProps.strokeDash !== props.strokeDash - ) { - instance.stroke( - props.stroke, - props.strokeWidth, - props.strokeCap, - props.strokeJoin, - props.strokeDash, - ); - } -} - -function applyShapeProps(instance, props, prevProps = {}) { - applyRenderableNodeProps(instance, props, prevProps); - - const path = props.d || childrenAsString(props.children); - - const prevDelta = instance._prevDelta; - const prevPath = instance._prevPath; - - if ( - path !== prevPath || - path.delta !== prevDelta || - prevProps.height !== props.height || - prevProps.width !== props.width - ) { - instance.draw(path, props.width, props.height); - - instance._prevDelta = path.delta; - instance._prevPath = path; - } -} - -function applyTextProps(instance, props, prevProps = {}) { - applyRenderableNodeProps(instance, props, prevProps); - - const string = props.children; - - if ( - instance._currentString !== string || - !isSameFont(props.font, prevProps.font) || - props.alignment !== prevProps.alignment || - props.path !== prevProps.path - ) { - instance.draw(string, props.font, props.alignment, props.path); - - instance._currentString = string; - } -} - /** Declarative fill-type objects; API design not finalized */ const slice = Array.prototype.slice; @@ -383,158 +134,7 @@ class Text extends React.Component { /** ART Renderer */ -const ARTRenderer = ReactFiberReconciler({ - appendInitialChild(parentInstance, child) { - if (typeof child === 'string') { - // Noop for string children of Text (eg {'foo'}{'bar'}) - invariant(false, 'Text children should already be flattened.'); - return; - } - - child.inject(parentInstance); - }, - - createInstance(type, props, internalInstanceHandle) { - let instance; - - switch (type) { - case TYPES.CLIPPING_RECTANGLE: - instance = Mode.ClippingRectangle(); - instance._applyProps = applyClippingRectangleProps; - break; - case TYPES.GROUP: - instance = Mode.Group(); - instance._applyProps = applyGroupProps; - break; - case TYPES.SHAPE: - instance = Mode.Shape(); - instance._applyProps = applyShapeProps; - break; - case TYPES.TEXT: - instance = Mode.Text( - props.children, - props.font, - props.alignment, - props.path, - ); - instance._applyProps = applyTextProps; - break; - } - - invariant(instance, 'ReactART does not support the type "%s"', type); - - instance._applyProps(instance, props); - - return instance; - }, - - createTextInstance(text, rootContainerInstance, internalInstanceHandle) { - return text; - }, - - finalizeInitialChildren(domElement, type, props) { - return false; - }, - - getPublicInstance(instance) { - return instance; - }, - - prepareForCommit() { - // Noop - }, - - prepareUpdate(domElement, type, oldProps, newProps) { - return UPDATE_SIGNAL; - }, - - resetAfterCommit() { - // Noop - }, - - resetTextContent(domElement) { - // Noop - }, - - shouldDeprioritizeSubtree(type, props) { - return false; - }, - - getRootHostContext() { - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - scheduleDeferredCallback: ReactScheduler.rIC, - - shouldSetTextContent(type, props) { - return ( - typeof props.children === 'string' || typeof props.children === 'number' - ); - }, - - now: ReactScheduler.now, - - // The ART renderer is secondary to the React DOM renderer. - isPrimaryRenderer: false, - - mutation: { - appendChild(parentInstance, child) { - if (child.parentNode === parentInstance) { - child.eject(); - } - child.inject(parentInstance); - }, - - appendChildToContainer(parentInstance, child) { - if (child.parentNode === parentInstance) { - child.eject(); - } - child.inject(parentInstance); - }, - - insertBefore(parentInstance, child, beforeChild) { - invariant( - child !== beforeChild, - 'ReactART: Can not insert node before itself', - ); - child.injectBefore(beforeChild); - }, - - insertInContainerBefore(parentInstance, child, beforeChild) { - invariant( - child !== beforeChild, - 'ReactART: Can not insert node before itself', - ); - child.injectBefore(beforeChild); - }, - - removeChild(parentInstance, child) { - destroyEventListeners(child); - child.eject(); - }, - - removeChildFromContainer(parentInstance, child) { - destroyEventListeners(child); - child.eject(); - }, - - commitTextUpdate(textInstance, oldText, newText) { - // Noop - }, - - commitMount(instance, type, newProps) { - // Noop - }, - - commitUpdate(instance, updatePayload, type, oldProps, newProps) { - instance._applyProps(instance, newProps, oldProps); - }, - }, -}); +const ARTRenderer = ReactFiberReconciler(ReactARTHostConfig); /** API */ diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js new file mode 100644 index 0000000000..cd059537cc --- /dev/null +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -0,0 +1,390 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as ReactScheduler from 'react-scheduler'; +import Transform from 'art/core/transform'; +import Mode from 'art/modes/current'; +import invariant from 'fbjs/lib/invariant'; +import emptyObject from 'fbjs/lib/emptyObject'; + +import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals'; + +const pooledTransform = new Transform(); + +const UPDATE_SIGNAL = {}; + +/** Helper Methods */ + +function addEventListeners(instance, type, listener) { + // We need to explicitly unregister before unmount. + // For this reason we need to track subscriptions. + if (!instance._listeners) { + instance._listeners = {}; + instance._subscriptions = {}; + } + + instance._listeners[type] = listener; + + if (listener) { + if (!instance._subscriptions[type]) { + instance._subscriptions[type] = instance.subscribe( + type, + createEventHandler(instance), + instance, + ); + } + } else { + if (instance._subscriptions[type]) { + instance._subscriptions[type](); + delete instance._subscriptions[type]; + } + } +} + +function createEventHandler(instance) { + return function handleEvent(event) { + const listener = instance._listeners[event.type]; + + if (!listener) { + // Noop + } else if (typeof listener === 'function') { + listener.call(instance, event); + } else if (listener.handleEvent) { + listener.handleEvent(event); + } + }; +} + +function destroyEventListeners(instance) { + if (instance._subscriptions) { + for (let type in instance._subscriptions) { + instance._subscriptions[type](); + } + } + + instance._subscriptions = null; + instance._listeners = null; +} + +function getScaleX(props) { + if (props.scaleX != null) { + return props.scaleX; + } else if (props.scale != null) { + return props.scale; + } else { + return 1; + } +} + +function getScaleY(props) { + if (props.scaleY != null) { + return props.scaleY; + } else if (props.scale != null) { + return props.scale; + } else { + return 1; + } +} + +function isSameFont(oldFont, newFont) { + if (oldFont === newFont) { + return true; + } else if (typeof newFont === 'string' || typeof oldFont === 'string') { + return false; + } else { + return ( + newFont.fontSize === oldFont.fontSize && + newFont.fontStyle === oldFont.fontStyle && + newFont.fontVariant === oldFont.fontVariant && + newFont.fontWeight === oldFont.fontWeight && + newFont.fontFamily === oldFont.fontFamily + ); + } +} + +/** Render Methods */ + +function applyClippingRectangleProps(instance, props, prevProps = {}) { + applyNodeProps(instance, props, prevProps); + + instance.width = props.width; + instance.height = props.height; +} + +function applyGroupProps(instance, props, prevProps = {}) { + applyNodeProps(instance, props, prevProps); + + instance.width = props.width; + instance.height = props.height; +} + +function applyNodeProps(instance, props, prevProps = {}) { + const scaleX = getScaleX(props); + const scaleY = getScaleY(props); + + pooledTransform + .transformTo(1, 0, 0, 1, 0, 0) + .move(props.x || 0, props.y || 0) + .rotate(props.rotation || 0, props.originX, props.originY) + .scale(scaleX, scaleY, props.originX, props.originY); + + if (props.transform != null) { + pooledTransform.transform(props.transform); + } + + if ( + instance.xx !== pooledTransform.xx || + instance.yx !== pooledTransform.yx || + instance.xy !== pooledTransform.xy || + instance.yy !== pooledTransform.yy || + instance.x !== pooledTransform.x || + instance.y !== pooledTransform.y + ) { + instance.transformTo(pooledTransform); + } + + if (props.cursor !== prevProps.cursor || props.title !== prevProps.title) { + instance.indicate(props.cursor, props.title); + } + + if (instance.blend && props.opacity !== prevProps.opacity) { + instance.blend(props.opacity == null ? 1 : props.opacity); + } + + if (props.visible !== prevProps.visible) { + if (props.visible == null || props.visible) { + instance.show(); + } else { + instance.hide(); + } + } + + for (let type in EVENT_TYPES) { + addEventListeners(instance, EVENT_TYPES[type], props[type]); + } +} + +function applyRenderableNodeProps(instance, props, prevProps = {}) { + applyNodeProps(instance, props, prevProps); + + if (prevProps.fill !== props.fill) { + if (props.fill && props.fill.applyFill) { + props.fill.applyFill(instance); + } else { + instance.fill(props.fill); + } + } + if ( + prevProps.stroke !== props.stroke || + prevProps.strokeWidth !== props.strokeWidth || + prevProps.strokeCap !== props.strokeCap || + prevProps.strokeJoin !== props.strokeJoin || + // TODO: Consider deep check of stokeDash; may benefit VML in IE. + prevProps.strokeDash !== props.strokeDash + ) { + instance.stroke( + props.stroke, + props.strokeWidth, + props.strokeCap, + props.strokeJoin, + props.strokeDash, + ); + } +} + +function applyShapeProps(instance, props, prevProps = {}) { + applyRenderableNodeProps(instance, props, prevProps); + + const path = props.d || childrenAsString(props.children); + + const prevDelta = instance._prevDelta; + const prevPath = instance._prevPath; + + if ( + path !== prevPath || + path.delta !== prevDelta || + prevProps.height !== props.height || + prevProps.width !== props.width + ) { + instance.draw(path, props.width, props.height); + + instance._prevDelta = path.delta; + instance._prevPath = path; + } +} + +function applyTextProps(instance, props, prevProps = {}) { + applyRenderableNodeProps(instance, props, prevProps); + + const string = props.children; + + if ( + instance._currentString !== string || + !isSameFont(props.font, prevProps.font) || + props.alignment !== prevProps.alignment || + props.path !== prevProps.path + ) { + instance.draw(string, props.font, props.alignment, props.path); + + instance._currentString = string; + } +} + +const ReactARTHostConfig = { + appendInitialChild(parentInstance, child) { + if (typeof child === 'string') { + // Noop for string children of Text (eg {'foo'}{'bar'}) + invariant(false, 'Text children should already be flattened.'); + return; + } + + child.inject(parentInstance); + }, + + createInstance(type, props, internalInstanceHandle) { + let instance; + + switch (type) { + case TYPES.CLIPPING_RECTANGLE: + instance = Mode.ClippingRectangle(); + instance._applyProps = applyClippingRectangleProps; + break; + case TYPES.GROUP: + instance = Mode.Group(); + instance._applyProps = applyGroupProps; + break; + case TYPES.SHAPE: + instance = Mode.Shape(); + instance._applyProps = applyShapeProps; + break; + case TYPES.TEXT: + instance = Mode.Text( + props.children, + props.font, + props.alignment, + props.path, + ); + instance._applyProps = applyTextProps; + break; + } + + invariant(instance, 'ReactART does not support the type "%s"', type); + + instance._applyProps(instance, props); + + return instance; + }, + + createTextInstance(text, rootContainerInstance, internalInstanceHandle) { + return text; + }, + + finalizeInitialChildren(domElement, type, props) { + return false; + }, + + getPublicInstance(instance) { + return instance; + }, + + prepareForCommit() { + // Noop + }, + + prepareUpdate(domElement, type, oldProps, newProps) { + return UPDATE_SIGNAL; + }, + + resetAfterCommit() { + // Noop + }, + + resetTextContent(domElement) { + // Noop + }, + + shouldDeprioritizeSubtree(type, props) { + return false; + }, + + getRootHostContext() { + return emptyObject; + }, + + getChildHostContext() { + return emptyObject; + }, + + scheduleDeferredCallback: ReactScheduler.rIC, + + shouldSetTextContent(type, props) { + return ( + typeof props.children === 'string' || typeof props.children === 'number' + ); + }, + + now: ReactScheduler.now, + + // The ART renderer is secondary to the React DOM renderer. + isPrimaryRenderer: false, + + mutation: { + appendChild(parentInstance, child) { + if (child.parentNode === parentInstance) { + child.eject(); + } + child.inject(parentInstance); + }, + + appendChildToContainer(parentInstance, child) { + if (child.parentNode === parentInstance) { + child.eject(); + } + child.inject(parentInstance); + }, + + insertBefore(parentInstance, child, beforeChild) { + invariant( + child !== beforeChild, + 'ReactART: Can not insert node before itself', + ); + child.injectBefore(beforeChild); + }, + + insertInContainerBefore(parentInstance, child, beforeChild) { + invariant( + child !== beforeChild, + 'ReactART: Can not insert node before itself', + ); + child.injectBefore(beforeChild); + }, + + removeChild(parentInstance, child) { + destroyEventListeners(child); + child.eject(); + }, + + removeChildFromContainer(parentInstance, child) { + destroyEventListeners(child); + child.eject(); + }, + + commitTextUpdate(textInstance, oldText, newText) { + // Noop + }, + + commitMount(instance, type, newProps) { + // Noop + }, + + commitUpdate(instance, updatePayload, type, oldProps, newProps) { + instance._applyProps(instance, newProps, oldProps); + }, + }, +}; + +export default ReactARTHostConfig; diff --git a/packages/react-art/src/ReactARTInternals.js b/packages/react-art/src/ReactARTInternals.js new file mode 100644 index 0000000000..b31c9c8964 --- /dev/null +++ b/packages/react-art/src/ReactARTInternals.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const TYPES = { + CLIPPING_RECTANGLE: 'ClippingRectangle', + GROUP: 'Group', + SHAPE: 'Shape', + TEXT: 'Text', +}; + +export const EVENT_TYPES = { + onClick: 'click', + onMouseMove: 'mousemove', + onMouseOver: 'mouseover', + onMouseOut: 'mouseout', + onMouseUp: 'mouseup', + onMouseDown: 'mousedown', +}; + +export function childrenAsString(children) { + if (!children) { + return ''; + } else if (typeof children === 'string') { + return children; + } else if (children.length) { + return children.join(''); + } else { + return ''; + } +} diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 81683c4774..40450e5f5e 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -14,12 +14,12 @@ import type { FiberRoot, Batch as FiberRootBatch, } from 'react-reconciler/src/ReactFiberRoot'; +import type {Container} from './ReactDOMHostConfig'; import '../shared/checkReact'; import './ReactDOMClientInjection'; import ReactFiberReconciler from 'react-reconciler'; -// TODO: direct imports like some-package/src/* are bad. Fix me. import * as ReactPortal from 'shared/ReactPortal'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; @@ -29,53 +29,29 @@ import * as EventPluginRegistry from 'events/EventPluginRegistry'; import * as EventPropagators from 'events/EventPropagators'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; import ReactVersion from 'shared/ReactVersion'; -import * as ReactScheduler from 'react-scheduler'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; import lowPriorityWarning from 'shared/lowPriorityWarning'; import warning from 'fbjs/lib/warning'; +import ReactDOMHostConfig from './ReactDOMHostConfig'; import * as ReactDOMComponentTree from './ReactDOMComponentTree'; import * as ReactDOMFiberComponent from './ReactDOMFiberComponent'; -import * as ReactInputSelection from './ReactInputSelection'; -import setTextContent from './setTextContent'; -import validateDOMNesting from './validateDOMNesting'; -import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter'; import * as ReactDOMEventListener from '../events/ReactDOMEventListener'; -import {getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, - TEXT_NODE, COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, } from '../shared/HTMLNodeType'; import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty'; -const { - createElement, - createTextNode, - setInitialProperties, - diffProperties, - updateProperties, - diffHydratedProperties, - diffHydratedText, - warnForUnmatchedText, - warnForDeletedHydratableElement, - warnForDeletedHydratableText, - warnForInsertedHydratedElement, - warnForInsertedHydratedText, -} = ReactDOMFiberComponent; -const {updatedAncestorInfo} = validateDOMNesting; -const {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree; -let SUPPRESS_HYDRATION_WARNING; let topLevelUpdateWarnings; let warnOnInvalidCallback; let didWarnAboutUnstableCreatePortal = false; if (__DEV__) { - SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; if ( typeof Map !== 'function' || Map.prototype == null || @@ -157,26 +133,6 @@ type DOMContainer = _reactRootContainer: ?Root, }); -type Container = Element | Document; -type Props = { - autoFocus?: boolean, - children?: mixed, - hidden?: boolean, - suppressHydrationWarning?: boolean, -}; -type Instance = Element; -type TextInstance = Text; - -type HostContextDev = { - namespace: string, - ancestorInfo: mixed, -}; -type HostContextProd = string; -type HostContext = HostContextDev | HostContextProd; - -let eventsEnabled: ?boolean = null; -let selectionInformation: ?mixed = null; - type Batch = FiberRootBatch & { render(children: ReactNodeList): Work, then(onComplete: () => mixed): void, @@ -491,504 +447,7 @@ function shouldHydrateDueToLegacyHeuristic(container) { ); } -function shouldAutoFocusHostComponent(type: string, props: Props): boolean { - switch (type) { - case 'button': - case 'input': - case 'select': - case 'textarea': - return !!props.autoFocus; - } - return false; -} - -const DOMRenderer = ReactFiberReconciler({ - getRootHostContext(rootContainerInstance: Container): HostContext { - let type; - let namespace; - const nodeType = rootContainerInstance.nodeType; - switch (nodeType) { - case DOCUMENT_NODE: - case DOCUMENT_FRAGMENT_NODE: { - type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; - let root = (rootContainerInstance: any).documentElement; - namespace = root ? root.namespaceURI : getChildNamespace(null, ''); - break; - } - default: { - const container: any = - nodeType === COMMENT_NODE - ? rootContainerInstance.parentNode - : rootContainerInstance; - const ownNamespace = container.namespaceURI || null; - type = container.tagName; - namespace = getChildNamespace(ownNamespace, type); - break; - } - } - if (__DEV__) { - const validatedTag = type.toLowerCase(); - const ancestorInfo = updatedAncestorInfo(null, validatedTag, null); - return {namespace, ancestorInfo}; - } - return namespace; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - if (__DEV__) { - const parentHostContextDev = ((parentHostContext: any): HostContextDev); - const namespace = getChildNamespace(parentHostContextDev.namespace, type); - const ancestorInfo = updatedAncestorInfo( - parentHostContextDev.ancestorInfo, - type, - null, - ); - return {namespace, ancestorInfo}; - } - const parentNamespace = ((parentHostContext: any): HostContextProd); - return getChildNamespace(parentNamespace, type); - }, - - getPublicInstance(instance) { - return instance; - }, - - prepareForCommit(): void { - eventsEnabled = ReactBrowserEventEmitter.isEnabled(); - selectionInformation = ReactInputSelection.getSelectionInformation(); - ReactBrowserEventEmitter.setEnabled(false); - }, - - resetAfterCommit(): void { - ReactInputSelection.restoreSelection(selectionInformation); - selectionInformation = null; - ReactBrowserEventEmitter.setEnabled(eventsEnabled); - eventsEnabled = null; - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - let parentNamespace: string; - if (__DEV__) { - // TODO: take namespace into account when validating. - const hostContextDev = ((hostContext: any): HostContextDev); - validateDOMNesting(type, null, hostContextDev.ancestorInfo); - if ( - typeof props.children === 'string' || - typeof props.children === 'number' - ) { - const string = '' + props.children; - const ownAncestorInfo = updatedAncestorInfo( - hostContextDev.ancestorInfo, - type, - null, - ); - validateDOMNesting(null, string, ownAncestorInfo); - } - parentNamespace = hostContextDev.namespace; - } else { - parentNamespace = ((hostContext: any): HostContextProd); - } - const domElement: Instance = createElement( - type, - props, - rootContainerInstance, - parentNamespace, - ); - precacheFiberNode(internalInstanceHandle, domElement); - updateFiberProps(domElement, props); - return domElement; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.appendChild(child); - }, - - finalizeInitialChildren( - domElement: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - setInitialProperties(domElement, type, props, rootContainerInstance); - return shouldAutoFocusHostComponent(type, props); - }, - - prepareUpdate( - domElement: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Array { - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - if ( - typeof newProps.children !== typeof oldProps.children && - (typeof newProps.children === 'string' || - typeof newProps.children === 'number') - ) { - const string = '' + newProps.children; - const ownAncestorInfo = updatedAncestorInfo( - hostContextDev.ancestorInfo, - type, - null, - ); - validateDOMNesting(null, string, ownAncestorInfo); - } - } - return diffProperties( - domElement, - type, - oldProps, - newProps, - rootContainerInstance, - ); - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return ( - type === 'textarea' || - typeof props.children === 'string' || - typeof props.children === 'number' || - (typeof props.dangerouslySetInnerHTML === 'object' && - props.dangerouslySetInnerHTML !== null && - typeof props.dangerouslySetInnerHTML.__html === 'string') - ); - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return !!props.hidden; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - validateDOMNesting(null, text, hostContextDev.ancestorInfo); - } - const textNode: TextInstance = createTextNode(text, rootContainerInstance); - precacheFiberNode(internalInstanceHandle, textNode); - return textNode; - }, - - now: ReactScheduler.now, - - isPrimaryRenderer: true, - - mutation: { - commitMount( - domElement: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Despite the naming that might imply otherwise, this method only - // fires if there is an `Update` effect scheduled during mounting. - // This happens if `finalizeInitialChildren` returns `true` (which it - // does to implement the `autoFocus` attribute on the client). But - // there are also other cases when this might happen (such as patching - // up text content during hydration mismatch). So we'll check this again. - if (shouldAutoFocusHostComponent(type, newProps)) { - ((domElement: any): - | HTMLButtonElement - | HTMLInputElement - | HTMLSelectElement - | HTMLTextAreaElement).focus(); - } - }, - - commitUpdate( - domElement: Instance, - updatePayload: Array, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Update the props handle so that we know which props are the ones with - // with current event handlers. - updateFiberProps(domElement, newProps); - // Apply the diff to the DOM node. - updateProperties(domElement, updatePayload, type, oldProps, newProps); - }, - - resetTextContent(domElement: Instance): void { - setTextContent(domElement, ''); - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.nodeValue = newText; - }, - - appendChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.appendChild(child); - }, - - appendChildToContainer( - container: Container, - child: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, container); - } else { - container.appendChild(child); - } - }, - - insertBefore( - parentInstance: Instance, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - parentInstance.insertBefore(child, beforeChild); - }, - - insertInContainerBefore( - container: Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, beforeChild); - } else { - container.insertBefore(child, beforeChild); - } - }, - - removeChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.removeChild(child); - }, - - removeChildFromContainer( - container: Container, - child: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).removeChild(child); - } else { - container.removeChild(child); - } - }, - }, - - hydration: { - canHydrateInstance( - instance: Instance | TextInstance, - type: string, - props: Props, - ): null | Instance { - if ( - instance.nodeType !== ELEMENT_NODE || - type.toLowerCase() !== instance.nodeName.toLowerCase() - ) { - return null; - } - // This has now been refined to an element node. - return ((instance: any): Instance); - }, - - canHydrateTextInstance( - instance: Instance | TextInstance, - text: string, - ): null | TextInstance { - if (text === '' || instance.nodeType !== TEXT_NODE) { - // Empty strings are not parsed by HTML so there won't be a correct match here. - return null; - } - // This has now been refined to a text node. - return ((instance: any): TextInstance); - }, - - getNextHydratableSibling( - instance: Instance | TextInstance, - ): null | Instance | TextInstance { - let node = instance.nextSibling; - // Skip non-hydratable nodes. - while ( - node && - node.nodeType !== ELEMENT_NODE && - node.nodeType !== TEXT_NODE - ) { - node = node.nextSibling; - } - return (node: any); - }, - - getFirstHydratableChild( - parentInstance: Container | Instance, - ): null | Instance | TextInstance { - let next = parentInstance.firstChild; - // Skip non-hydratable nodes. - while ( - next && - next.nodeType !== ELEMENT_NODE && - next.nodeType !== TEXT_NODE - ) { - next = next.nextSibling; - } - return (next: any); - }, - - hydrateInstance( - instance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): null | Array { - precacheFiberNode(internalInstanceHandle, instance); - // TODO: Possibly defer this until the commit phase where all the events - // get attached. - updateFiberProps(instance, props); - let parentNamespace: string; - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - parentNamespace = hostContextDev.namespace; - } else { - parentNamespace = ((hostContext: any): HostContextProd); - } - return diffHydratedProperties( - instance, - type, - props, - parentNamespace, - rootContainerInstance, - ); - }, - - hydrateTextInstance( - textInstance: TextInstance, - text: string, - internalInstanceHandle: Object, - ): boolean { - precacheFiberNode(internalInstanceHandle, textInstance); - return diffHydratedText(textInstance, text); - }, - - didNotMatchHydratedContainerTextInstance( - parentContainer: Container, - textInstance: TextInstance, - text: string, - ) { - if (__DEV__) { - warnForUnmatchedText(textInstance, text); - } - }, - - didNotMatchHydratedTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - textInstance: TextInstance, - text: string, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForUnmatchedText(textInstance, text); - } - }, - - didNotHydrateContainerInstance( - parentContainer: Container, - instance: Instance | TextInstance, - ) { - if (__DEV__) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentContainer, (instance: any)); - } else { - warnForDeletedHydratableText(parentContainer, (instance: any)); - } - } - }, - - didNotHydrateInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - instance: Instance | TextInstance, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentInstance, (instance: any)); - } else { - warnForDeletedHydratableText(parentInstance, (instance: any)); - } - } - }, - - didNotFindHydratableContainerInstance( - parentContainer: Container, - type: string, - props: Props, - ) { - if (__DEV__) { - warnForInsertedHydratedElement(parentContainer, type, props); - } - }, - - didNotFindHydratableContainerTextInstance( - parentContainer: Container, - text: string, - ) { - if (__DEV__) { - warnForInsertedHydratedText(parentContainer, text); - } - }, - - didNotFindHydratableInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - type: string, - props: Props, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedElement(parentInstance, type, props); - } - }, - - didNotFindHydratableTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - text: string, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedText(parentInstance, text); - } - }, - }, - - scheduleDeferredCallback: ReactScheduler.rIC, - cancelDeferredCallback: ReactScheduler.cIC, -}); +const DOMRenderer = ReactFiberReconciler(ReactDOMHostConfig); ReactGenericBatching.injection.injectRenderer(DOMRenderer); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js new file mode 100644 index 0000000000..95471f2279 --- /dev/null +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -0,0 +1,568 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as ReactScheduler from 'react-scheduler'; + +import * as ReactDOMComponentTree from './ReactDOMComponentTree'; +import * as ReactDOMFiberComponent from './ReactDOMFiberComponent'; +import * as ReactInputSelection from './ReactInputSelection'; +import setTextContent from './setTextContent'; +import validateDOMNesting from './validateDOMNesting'; +import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter'; +import {getChildNamespace} from '../shared/DOMNamespaces'; +import { + ELEMENT_NODE, + TEXT_NODE, + COMMENT_NODE, + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, +} from '../shared/HTMLNodeType'; + +export type Container = Element | Document; +type Props = { + autoFocus?: boolean, + children?: mixed, + hidden?: boolean, + suppressHydrationWarning?: boolean, +}; +type Instance = Element; +type TextInstance = Text; + +type HostContextDev = { + namespace: string, + ancestorInfo: mixed, +}; +type HostContextProd = string; +type HostContext = HostContextDev | HostContextProd; + +const { + createElement, + createTextNode, + setInitialProperties, + diffProperties, + updateProperties, + diffHydratedProperties, + diffHydratedText, + warnForUnmatchedText, + warnForDeletedHydratableElement, + warnForDeletedHydratableText, + warnForInsertedHydratedElement, + warnForInsertedHydratedText, +} = ReactDOMFiberComponent; +const {updatedAncestorInfo} = validateDOMNesting; +const {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree; + +let SUPPRESS_HYDRATION_WARNING; +if (__DEV__) { + SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; +} + +let eventsEnabled: ?boolean = null; +let selectionInformation: ?mixed = null; + +function shouldAutoFocusHostComponent(type: string, props: Props): boolean { + switch (type) { + case 'button': + case 'input': + case 'select': + case 'textarea': + return !!props.autoFocus; + } + return false; +} + +const ReactDOMHostConfig = { + getRootHostContext(rootContainerInstance: Container): HostContext { + let type; + let namespace; + const nodeType = rootContainerInstance.nodeType; + switch (nodeType) { + case DOCUMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: { + type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; + let root = (rootContainerInstance: any).documentElement; + namespace = root ? root.namespaceURI : getChildNamespace(null, ''); + break; + } + default: { + const container: any = + nodeType === COMMENT_NODE + ? rootContainerInstance.parentNode + : rootContainerInstance; + const ownNamespace = container.namespaceURI || null; + type = container.tagName; + namespace = getChildNamespace(ownNamespace, type); + break; + } + } + if (__DEV__) { + const validatedTag = type.toLowerCase(); + const ancestorInfo = updatedAncestorInfo(null, validatedTag, null); + return {namespace, ancestorInfo}; + } + return namespace; + }, + + getChildHostContext( + parentHostContext: HostContext, + type: string, + ): HostContext { + if (__DEV__) { + const parentHostContextDev = ((parentHostContext: any): HostContextDev); + const namespace = getChildNamespace(parentHostContextDev.namespace, type); + const ancestorInfo = updatedAncestorInfo( + parentHostContextDev.ancestorInfo, + type, + null, + ); + return {namespace, ancestorInfo}; + } + const parentNamespace = ((parentHostContext: any): HostContextProd); + return getChildNamespace(parentNamespace, type); + }, + + getPublicInstance(instance: Instance | TextInstance): * { + return instance; + }, + + prepareForCommit(): void { + eventsEnabled = ReactBrowserEventEmitter.isEnabled(); + selectionInformation = ReactInputSelection.getSelectionInformation(); + ReactBrowserEventEmitter.setEnabled(false); + }, + + resetAfterCommit(): void { + ReactInputSelection.restoreSelection(selectionInformation); + selectionInformation = null; + ReactBrowserEventEmitter.setEnabled(eventsEnabled); + eventsEnabled = null; + }, + + createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): Instance { + let parentNamespace: string; + if (__DEV__) { + // TODO: take namespace into account when validating. + const hostContextDev = ((hostContext: any): HostContextDev); + validateDOMNesting(type, null, hostContextDev.ancestorInfo); + if ( + typeof props.children === 'string' || + typeof props.children === 'number' + ) { + const string = '' + props.children; + const ownAncestorInfo = updatedAncestorInfo( + hostContextDev.ancestorInfo, + type, + null, + ); + validateDOMNesting(null, string, ownAncestorInfo); + } + parentNamespace = hostContextDev.namespace; + } else { + parentNamespace = ((hostContext: any): HostContextProd); + } + const domElement: Instance = createElement( + type, + props, + rootContainerInstance, + parentNamespace, + ); + precacheFiberNode(internalInstanceHandle, domElement); + updateFiberProps(domElement, props); + return domElement; + }, + + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.appendChild(child); + }, + + finalizeInitialChildren( + domElement: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + ): boolean { + setInitialProperties(domElement, type, props, rootContainerInstance); + return shouldAutoFocusHostComponent(type, props); + }, + + prepareUpdate( + domElement: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, + ): null | Array { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + if ( + typeof newProps.children !== typeof oldProps.children && + (typeof newProps.children === 'string' || + typeof newProps.children === 'number') + ) { + const string = '' + newProps.children; + const ownAncestorInfo = updatedAncestorInfo( + hostContextDev.ancestorInfo, + type, + null, + ); + validateDOMNesting(null, string, ownAncestorInfo); + } + } + return diffProperties( + domElement, + type, + oldProps, + newProps, + rootContainerInstance, + ); + }, + + shouldSetTextContent(type: string, props: Props): boolean { + return ( + type === 'textarea' || + typeof props.children === 'string' || + typeof props.children === 'number' || + (typeof props.dangerouslySetInnerHTML === 'object' && + props.dangerouslySetInnerHTML !== null && + typeof props.dangerouslySetInnerHTML.__html === 'string') + ); + }, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return !!props.hidden; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): TextInstance { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + validateDOMNesting(null, text, hostContextDev.ancestorInfo); + } + const textNode: TextInstance = createTextNode(text, rootContainerInstance); + precacheFiberNode(internalInstanceHandle, textNode); + return textNode; + }, + + now: ReactScheduler.now, + + isPrimaryRenderer: true, + + mutation: { + commitMount( + domElement: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, + ): void { + // Despite the naming that might imply otherwise, this method only + // fires if there is an `Update` effect scheduled during mounting. + // This happens if `finalizeInitialChildren` returns `true` (which it + // does to implement the `autoFocus` attribute on the client). But + // there are also other cases when this might happen (such as patching + // up text content during hydration mismatch). So we'll check this again. + if (shouldAutoFocusHostComponent(type, newProps)) { + ((domElement: any): + | HTMLButtonElement + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement).focus(); + } + }, + + commitUpdate( + domElement: Instance, + updatePayload: Array, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + ): void { + // Update the props handle so that we know which props are the ones with + // with current event handlers. + updateFiberProps(domElement, newProps); + // Apply the diff to the DOM node. + updateProperties(domElement, updatePayload, type, oldProps, newProps); + }, + + resetTextContent(domElement: Instance): void { + setTextContent(domElement, ''); + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.nodeValue = newText; + }, + + appendChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.appendChild(child); + }, + + appendChildToContainer( + container: Container, + child: Instance | TextInstance, + ): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, container); + } else { + container.appendChild(child); + } + }, + + insertBefore( + parentInstance: Instance, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, + ): void { + parentInstance.insertBefore(child, beforeChild); + }, + + insertInContainerBefore( + container: Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, + ): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, beforeChild); + } else { + container.insertBefore(child, beforeChild); + } + }, + + removeChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.removeChild(child); + }, + + removeChildFromContainer( + container: Container, + child: Instance | TextInstance, + ): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).removeChild(child); + } else { + container.removeChild(child); + } + }, + }, + + hydration: { + canHydrateInstance( + instance: Instance | TextInstance, + type: string, + props: Props, + ): null | Instance { + if ( + instance.nodeType !== ELEMENT_NODE || + type.toLowerCase() !== instance.nodeName.toLowerCase() + ) { + return null; + } + // This has now been refined to an element node. + return ((instance: any): Instance); + }, + + canHydrateTextInstance( + instance: Instance | TextInstance, + text: string, + ): null | TextInstance { + if (text === '' || instance.nodeType !== TEXT_NODE) { + // Empty strings are not parsed by HTML so there won't be a correct match here. + return null; + } + // This has now been refined to a text node. + return ((instance: any): TextInstance); + }, + + getNextHydratableSibling( + instance: Instance | TextInstance, + ): null | Instance | TextInstance { + let node = instance.nextSibling; + // Skip non-hydratable nodes. + while ( + node && + node.nodeType !== ELEMENT_NODE && + node.nodeType !== TEXT_NODE + ) { + node = node.nextSibling; + } + return (node: any); + }, + + getFirstHydratableChild( + parentInstance: Container | Instance, + ): null | Instance | TextInstance { + let next = parentInstance.firstChild; + // Skip non-hydratable nodes. + while ( + next && + next.nodeType !== ELEMENT_NODE && + next.nodeType !== TEXT_NODE + ) { + next = next.nextSibling; + } + return (next: any); + }, + + hydrateInstance( + instance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): null | Array { + precacheFiberNode(internalInstanceHandle, instance); + // TODO: Possibly defer this until the commit phase where all the events + // get attached. + updateFiberProps(instance, props); + let parentNamespace: string; + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + parentNamespace = hostContextDev.namespace; + } else { + parentNamespace = ((hostContext: any): HostContextProd); + } + return diffHydratedProperties( + instance, + type, + props, + parentNamespace, + rootContainerInstance, + ); + }, + + hydrateTextInstance( + textInstance: TextInstance, + text: string, + internalInstanceHandle: Object, + ): boolean { + precacheFiberNode(internalInstanceHandle, textInstance); + return diffHydratedText(textInstance, text); + }, + + didNotMatchHydratedContainerTextInstance( + parentContainer: Container, + textInstance: TextInstance, + text: string, + ) { + if (__DEV__) { + warnForUnmatchedText(textInstance, text); + } + }, + + didNotMatchHydratedTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + textInstance: TextInstance, + text: string, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForUnmatchedText(textInstance, text); + } + }, + + didNotHydrateContainerInstance( + parentContainer: Container, + instance: Instance | TextInstance, + ) { + if (__DEV__) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentContainer, (instance: any)); + } else { + warnForDeletedHydratableText(parentContainer, (instance: any)); + } + } + }, + + didNotHydrateInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + instance: Instance | TextInstance, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentInstance, (instance: any)); + } else { + warnForDeletedHydratableText(parentInstance, (instance: any)); + } + } + }, + + didNotFindHydratableContainerInstance( + parentContainer: Container, + type: string, + props: Props, + ) { + if (__DEV__) { + warnForInsertedHydratedElement(parentContainer, type, props); + } + }, + + didNotFindHydratableContainerTextInstance( + parentContainer: Container, + text: string, + ) { + if (__DEV__) { + warnForInsertedHydratedText(parentContainer, text); + } + }, + + didNotFindHydratableInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + type: string, + props: Props, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedElement(parentInstance, type, props); + } + }, + + didNotFindHydratableTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + text: string, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedText(parentInstance, text); + } + }, + }, + + scheduleDeferredCallback: ReactScheduler.rIC, + cancelDeferredCallback: ReactScheduler.cIC, +}; + +export default ReactDOMHostConfig; diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js new file mode 100644 index 0000000000..921067e516 --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, + NativeMethodsMixinType, + ReactNativeBaseComponentViewConfig, +} from './ReactNativeTypes'; + +import {mountSafeCallback, warnForStyleProps} from './NativeMethodsMixinUtils'; +import * as ReactNativeAttributePayload from './ReactNativeAttributePayload'; +import * as ReactNativeFrameScheduling from './ReactNativeFrameScheduling'; +import * as ReactNativeViewConfigRegistry from 'ReactNativeViewConfigRegistry'; + +import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev'; +import invariant from 'fbjs/lib/invariant'; + +// Modules provided by RN: +import TextInputState from 'TextInputState'; +import FabricUIManager from 'FabricUIManager'; +import UIManager from 'UIManager'; + +// Counter for uniquely identifying views. +// % 10 === 1 means it is a rootTag. +// % 2 === 0 means it is a Fabric tag. +// This means that they never overlap. +let nextReactTag = 2; + +type HostContext = $ReadOnly<{| + isInAParentText: boolean, +|}>; + +/** + * This is used for refs on host components. + */ +class ReactFabricHostComponent { + _nativeTag: number; + viewConfig: ReactNativeBaseComponentViewConfig; + currentProps: Props; + + constructor( + tag: number, + viewConfig: ReactNativeBaseComponentViewConfig, + props: Props, + ) { + this._nativeTag = tag; + this.viewConfig = viewConfig; + this.currentProps = props; + } + + blur() { + TextInputState.blurTextInput(this._nativeTag); + } + + focus() { + TextInputState.focusTextInput(this._nativeTag); + } + + measure(callback: MeasureOnSuccessCallback) { + UIManager.measure(this._nativeTag, mountSafeCallback(this, callback)); + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + UIManager.measureInWindow( + this._nativeTag, + mountSafeCallback(this, callback), + ); + } + + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail: () => void /* currently unused */, + ) { + UIManager.measureLayout( + this._nativeTag, + relativeToNativeNode, + mountSafeCallback(this, onFail), + mountSafeCallback(this, onSuccess), + ); + } + + setNativeProps(nativeProps: Object) { + if (__DEV__) { + warnForStyleProps(nativeProps, this.viewConfig.validAttributes); + } + + const updatePayload = ReactNativeAttributePayload.create( + nativeProps, + this.viewConfig.validAttributes, + ); + + // Avoid the overhead of bridge calls if there's no update. + // This is an expensive no-op for Android, and causes an unnecessary + // view invalidation for certain components (eg RCTTextInput) on iOS. + if (updatePayload != null) { + UIManager.updateView( + this._nativeTag, + this.viewConfig.uiViewClassName, + updatePayload, + ); + } + } +} + +// eslint-disable-next-line no-unused-expressions +(ReactFabricHostComponent.prototype: NativeMethodsMixinType); + +type Node = Object; +type ChildSet = Object; +type Container = number; +type Instance = { + node: Node, + canonical: ReactFabricHostComponent, +}; +type Props = Object; +type TextInstance = { + node: Node, +}; + +const ReacFabricHostConfig = { + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + FabricUIManager.appendChild(parentInstance.node, child.node); + }, + + createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): Instance { + const tag = nextReactTag; + nextReactTag += 2; + + const viewConfig = ReactNativeViewConfigRegistry.get(type); + + if (__DEV__) { + for (const key in viewConfig.validAttributes) { + if (props.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutationInDev(props[key]); + } + } + } + + invariant( + type !== 'RCTView' || !hostContext.isInAParentText, + 'Nesting of within is not currently supported.', + ); + + const updatePayload = ReactNativeAttributePayload.create( + props, + viewConfig.validAttributes, + ); + + const node = FabricUIManager.createNode( + tag, // reactTag + viewConfig.uiViewClassName, // viewName + rootContainerInstance, // rootTag + updatePayload, // props + internalInstanceHandle, // internalInstanceHandle + ); + + const component = new ReactFabricHostComponent(tag, viewConfig, props); + + return { + node: node, + canonical: component, + }; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): TextInstance { + invariant( + hostContext.isInAParentText, + 'Text strings must be rendered within a component.', + ); + + const tag = nextReactTag; + nextReactTag += 2; + + const node = FabricUIManager.createNode( + tag, // reactTag + 'RCTRawText', // viewName + rootContainerInstance, // rootTag + {text: text}, // props + internalInstanceHandle, // instance handle + ); + + return { + node: node, + }; + }, + + finalizeInitialChildren( + parentInstance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + ): boolean { + return false; + }, + + getRootHostContext(rootContainerInstance: Container): HostContext { + return {isInAParentText: false}; + }, + + getChildHostContext( + parentHostContext: HostContext, + type: string, + ): HostContext { + const prevIsInAParentText = parentHostContext.isInAParentText; + const isInAParentText = + type === 'AndroidTextInput' || // Android + type === 'RCTMultilineTextInputView' || // iOS + type === 'RCTSinglelineTextInputView' || // iOS + type === 'RCTText' || + type === 'RCTVirtualText'; + + if (prevIsInAParentText !== isInAParentText) { + return {isInAParentText}; + } else { + return parentHostContext; + } + }, + + getPublicInstance(instance: Instance): * { + return instance.canonical; + }, + + now: ReactNativeFrameScheduling.now, + + // The Fabric renderer is secondary to the existing React Native renderer. + isPrimaryRenderer: false, + + prepareForCommit(): void { + // Noop + }, + + prepareUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, + ): null | Object { + const viewConfig = instance.canonical.viewConfig; + const updatePayload = ReactNativeAttributePayload.diff( + oldProps, + newProps, + viewConfig.validAttributes, + ); + // TODO: If the event handlers have changed, we need to update the current props + // in the commit phase but there is no host config hook to do it yet. + return updatePayload; + }, + + resetAfterCommit(): void { + // Noop + }, + + scheduleDeferredCallback: ReactNativeFrameScheduling.scheduleDeferredCallback, + cancelDeferredCallback: ReactNativeFrameScheduling.cancelDeferredCallback, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; + }, + + shouldSetTextContent(type: string, props: Props): boolean { + // TODO (bvaughn) Revisit this decision. + // Always returning false simplifies the createInstance() implementation, + // But creates an additional child Fiber for raw text children. + // No additional native views are created though. + // It's not clear to me which is better so I'm deferring for now. + // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 + return false; + }, + + persistence: { + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const node = instance.node; + let clone; + if (keepChildren) { + if (updatePayload !== null) { + clone = FabricUIManager.cloneNodeWithNewProps(node, updatePayload); + } else { + clone = FabricUIManager.cloneNode(node); + } + } else { + if (updatePayload !== null) { + clone = FabricUIManager.cloneNodeWithNewChildrenAndProps( + node, + updatePayload, + ); + } else { + clone = FabricUIManager.cloneNodeWithNewChildren(node); + } + } + return { + node: clone, + canonical: instance.canonical, + }; + }, + + createContainerChildSet(container: Container): ChildSet { + return FabricUIManager.createChildSet(container); + }, + + appendChildToContainerChildSet( + childSet: ChildSet, + child: Instance | TextInstance, + ): void { + FabricUIManager.appendChildToSet(childSet, child.node); + }, + + finalizeContainerChildren( + container: Container, + newChildren: ChildSet, + ): void { + FabricUIManager.completeRoot(container, newChildren); + }, + + replaceContainerChildren( + container: Container, + newChildren: ChildSet, + ): void {}, + }, +}; + +export default ReacFabricHostConfig; diff --git a/packages/react-native-renderer/src/ReactFabricRenderer.js b/packages/react-native-renderer/src/ReactFabricRenderer.js index 9c597c2e53..8f77d283c9 100644 --- a/packages/react-native-renderer/src/ReactFabricRenderer.js +++ b/packages/react-native-renderer/src/ReactFabricRenderer.js @@ -7,350 +7,9 @@ * @flow */ -import type { - MeasureInWindowOnSuccessCallback, - MeasureLayoutOnSuccessCallback, - MeasureOnSuccessCallback, - NativeMethodsMixinType, - ReactNativeBaseComponentViewConfig, -} from './ReactNativeTypes'; - -import {mountSafeCallback, warnForStyleProps} from './NativeMethodsMixinUtils'; -import * as ReactNativeAttributePayload from './ReactNativeAttributePayload'; -import * as ReactNativeFrameScheduling from './ReactNativeFrameScheduling'; -import * as ReactNativeViewConfigRegistry from 'ReactNativeViewConfigRegistry'; import ReactFiberReconciler from 'react-reconciler'; +import ReactFabricHostConfig from './ReactFabricHostConfig'; -import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev'; -import invariant from 'fbjs/lib/invariant'; - -// Modules provided by RN: -import TextInputState from 'TextInputState'; -import FabricUIManager from 'FabricUIManager'; -import UIManager from 'UIManager'; - -// Counter for uniquely identifying views. -// % 10 === 1 means it is a rootTag. -// % 2 === 0 means it is a Fabric tag. -// This means that they never overlap. -let nextReactTag = 2; - -type HostContext = $ReadOnly<{| - isInAParentText: boolean, -|}>; - -/** - * This is used for refs on host components. - */ -class ReactFabricHostComponent { - _nativeTag: number; - viewConfig: ReactNativeBaseComponentViewConfig; - currentProps: Props; - - constructor( - tag: number, - viewConfig: ReactNativeBaseComponentViewConfig, - props: Props, - ) { - this._nativeTag = tag; - this.viewConfig = viewConfig; - this.currentProps = props; - } - - blur() { - TextInputState.blurTextInput(this._nativeTag); - } - - focus() { - TextInputState.focusTextInput(this._nativeTag); - } - - measure(callback: MeasureOnSuccessCallback) { - UIManager.measure(this._nativeTag, mountSafeCallback(this, callback)); - } - - measureInWindow(callback: MeasureInWindowOnSuccessCallback) { - UIManager.measureInWindow( - this._nativeTag, - mountSafeCallback(this, callback), - ); - } - - measureLayout( - relativeToNativeNode: number, - onSuccess: MeasureLayoutOnSuccessCallback, - onFail: () => void /* currently unused */, - ) { - UIManager.measureLayout( - this._nativeTag, - relativeToNativeNode, - mountSafeCallback(this, onFail), - mountSafeCallback(this, onSuccess), - ); - } - - setNativeProps(nativeProps: Object) { - if (__DEV__) { - warnForStyleProps(nativeProps, this.viewConfig.validAttributes); - } - - const updatePayload = ReactNativeAttributePayload.create( - nativeProps, - this.viewConfig.validAttributes, - ); - - // Avoid the overhead of bridge calls if there's no update. - // This is an expensive no-op for Android, and causes an unnecessary - // view invalidation for certain components (eg RCTTextInput) on iOS. - if (updatePayload != null) { - UIManager.updateView( - this._nativeTag, - this.viewConfig.uiViewClassName, - updatePayload, - ); - } - } -} - -// eslint-disable-next-line no-unused-expressions -(ReactFabricHostComponent.prototype: NativeMethodsMixinType); - -type Node = Object; -type ChildSet = Object; -type Container = number; -type Instance = { - node: Node, - canonical: ReactFabricHostComponent, -}; -type Props = Object; -type TextInstance = { - node: Node, -}; - -const ReactFabricRenderer = ReactFiberReconciler({ - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - FabricUIManager.appendChild(parentInstance.node, child.node); - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - const tag = nextReactTag; - nextReactTag += 2; - - const viewConfig = ReactNativeViewConfigRegistry.get(type); - - if (__DEV__) { - for (const key in viewConfig.validAttributes) { - if (props.hasOwnProperty(key)) { - deepFreezeAndThrowOnMutationInDev(props[key]); - } - } - } - - invariant( - type !== 'RCTView' || !hostContext.isInAParentText, - 'Nesting of within is not currently supported.', - ); - - const updatePayload = ReactNativeAttributePayload.create( - props, - viewConfig.validAttributes, - ); - - const node = FabricUIManager.createNode( - tag, // reactTag - viewConfig.uiViewClassName, // viewName - rootContainerInstance, // rootTag - updatePayload, // props - internalInstanceHandle, // internalInstanceHandle - ); - - const component = new ReactFabricHostComponent(tag, viewConfig, props); - - return { - node: node, - canonical: component, - }; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - invariant( - hostContext.isInAParentText, - 'Text strings must be rendered within a component.', - ); - - const tag = nextReactTag; - nextReactTag += 2; - - const node = FabricUIManager.createNode( - tag, // reactTag - 'RCTRawText', // viewName - rootContainerInstance, // rootTag - {text: text}, // props - internalInstanceHandle, // instance handle - ); - - return { - node: node, - }; - }, - - finalizeInitialChildren( - parentInstance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - return false; - }, - - getRootHostContext(rootContainerInstance: Container): HostContext { - return {isInAParentText: false}; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - const prevIsInAParentText = parentHostContext.isInAParentText; - const isInAParentText = - type === 'AndroidTextInput' || // Android - type === 'RCTMultilineTextInputView' || // iOS - type === 'RCTSinglelineTextInputView' || // iOS - type === 'RCTText' || - type === 'RCTVirtualText'; - - if (prevIsInAParentText !== isInAParentText) { - return {isInAParentText}; - } else { - return parentHostContext; - } - }, - - getPublicInstance(instance) { - return instance.canonical; - }, - - now: ReactNativeFrameScheduling.now, - - // The Fabric renderer is secondary to the existing React Native renderer. - isPrimaryRenderer: false, - - prepareForCommit(): void { - // Noop - }, - - prepareUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Object { - const viewConfig = instance.canonical.viewConfig; - const updatePayload = ReactNativeAttributePayload.diff( - oldProps, - newProps, - viewConfig.validAttributes, - ); - // TODO: If the event handlers have changed, we need to update the current props - // in the commit phase but there is no host config hook to do it yet. - return updatePayload; - }, - - resetAfterCommit(): void { - // Noop - }, - - scheduleDeferredCallback: ReactNativeFrameScheduling.scheduleDeferredCallback, - cancelDeferredCallback: ReactNativeFrameScheduling.cancelDeferredCallback, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - // TODO (bvaughn) Revisit this decision. - // Always returning false simplifies the createInstance() implementation, - // But creates an additional child Fiber for raw text children. - // No additional native views are created though. - // It's not clear to me which is better so I'm deferring for now. - // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 - return false; - }, - - persistence: { - cloneInstance( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - recyclableInstance: null | Instance, - ): Instance { - const node = instance.node; - let clone; - if (keepChildren) { - if (updatePayload !== null) { - clone = FabricUIManager.cloneNodeWithNewProps(node, updatePayload); - } else { - clone = FabricUIManager.cloneNode(node); - } - } else { - if (updatePayload !== null) { - clone = FabricUIManager.cloneNodeWithNewChildrenAndProps( - node, - updatePayload, - ); - } else { - clone = FabricUIManager.cloneNodeWithNewChildren(node); - } - } - return { - node: clone, - canonical: instance.canonical, - }; - }, - - createContainerChildSet(container: Container): ChildSet { - return FabricUIManager.createChildSet(container); - }, - - appendChildToContainerChildSet( - childSet: ChildSet, - child: Instance | TextInstance, - ): void { - FabricUIManager.appendChildToSet(childSet, child.node); - }, - - finalizeContainerChildren( - container: Container, - newChildren: ChildSet, - ): void { - FabricUIManager.completeRoot(container, newChildren); - }, - - replaceContainerChildren( - container: Container, - newChildren: ChildSet, - ): void {}, - }, -}); +const ReactFabricRenderer = ReactFiberReconciler(ReactFabricHostConfig); export default ReactFabricRenderer; diff --git a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js index 86c0d3ad70..3d5bf5bbc9 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js +++ b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js @@ -14,7 +14,7 @@ import type { NativeMethodsMixinType, ReactNativeBaseComponentViewConfig, } from './ReactNativeTypes'; -import type {Instance} from './ReactNativeFiberRenderer'; +import type {Instance} from './ReactNativeHostConfig'; // Modules provided by RN: import TextInputState from 'TextInputState'; diff --git a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js similarity index 98% rename from packages/react-native-renderer/src/ReactNativeFiberRenderer.js rename to packages/react-native-renderer/src/ReactNativeHostConfig.js index 01fe4816b4..b766d1e655 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -9,7 +9,6 @@ import type {ReactNativeBaseComponentViewConfig} from './ReactNativeTypes'; -import ReactFiberReconciler from 'react-reconciler'; import emptyObject from 'fbjs/lib/emptyObject'; import invariant from 'fbjs/lib/invariant'; @@ -64,7 +63,7 @@ function recursivelyUncacheFiberNode(node: Instance | TextInstance) { } } -const NativeRenderer = ReactFiberReconciler({ +const ReactNativeHostConfig = { appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, @@ -193,7 +192,7 @@ const NativeRenderer = ReactFiberReconciler({ } }, - getPublicInstance(instance) { + getPublicInstance(instance: Instance): * { return instance; }, @@ -427,6 +426,6 @@ const NativeRenderer = ReactFiberReconciler({ // Noop }, }, -}); +}; -export default NativeRenderer; +export default ReactNativeHostConfig; diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index f8e984d0e6..ce55def9e6 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -12,6 +12,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import './ReactNativeInjection'; +import ReactFiberReconciler from 'react-reconciler'; import * as ReactPortal from 'shared/ReactPortal'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; import ReactVersion from 'shared/ReactVersion'; @@ -20,16 +21,18 @@ import UIManager from 'UIManager'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; +import ReactNativeHostConfig from './ReactNativeHostConfig'; import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeComponent from './ReactNativeComponent'; import * as ReactNativeComponentTree from './ReactNativeComponentTree'; -import ReactNativeFiberRenderer from './ReactNativeFiberRenderer'; import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import getComponentName from 'shared/getComponentName'; import warning from 'fbjs/lib/warning'; +const ReactNativeFiberRenderer = ReactFiberReconciler(ReactNativeHostConfig); + const findHostInstance = ReactNativeFiberRenderer.findHostInstance; function findNodeHandle(componentOrHandle: any): ?number { diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js new file mode 100644 index 0000000000..ef7c6c927c --- /dev/null +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import emptyObject from 'fbjs/lib/emptyObject'; + +import * as TestRendererScheduling from './ReactTestRendererScheduling'; + +export type Instance = {| + type: string, + props: Object, + children: Array, + rootContainerInstance: Container, + tag: 'INSTANCE', +|}; + +export type TextInstance = {| + text: string, + tag: 'TEXT', +|}; + +type Container = {| + children: Array, + createNodeMock: Function, + tag: 'CONTAINER', +|}; + +type Props = Object; + +const UPDATE_SIGNAL = {}; + +function getPublicInstance(inst: Instance | TextInstance): * { + switch (inst.tag) { + case 'INSTANCE': + const createNodeMock = inst.rootContainerInstance.createNodeMock; + return createNodeMock({ + type: inst.type, + props: inst.props, + }); + default: + return inst; + } +} + +function appendChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); +} + +function insertBefore( + parentInstance: Instance | Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + const beforeIndex = parentInstance.children.indexOf(beforeChild); + parentInstance.children.splice(beforeIndex, 0, child); +} + +function removeChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + parentInstance.children.splice(index, 1); +} + +const ReactTestHostConfig = { + getRootHostContext() { + return emptyObject; + }, + + getChildHostContext() { + return emptyObject; + }, + + prepareForCommit(): void { + // noop + }, + + resetAfterCommit(): void { + // noop + }, + + createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, + ): Instance { + return { + type, + props, + children: [], + rootContainerInstance, + tag: 'INSTANCE', + }; + }, + + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); + }, + + finalizeInitialChildren( + testElement: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + ): boolean { + return false; + }, + + prepareUpdate( + testElement: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: Object, + ): null | {} { + return UPDATE_SIGNAL; + }, + + shouldSetTextContent(type: string, props: Props): boolean { + return false; + }, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, + ): TextInstance { + return { + text, + tag: 'TEXT', + }; + }, + + getPublicInstance, + + scheduleDeferredCallback: TestRendererScheduling.scheduleDeferredCallback, + cancelDeferredCallback: TestRendererScheduling.cancelDeferredCallback, + // This approach enables `now` to be mocked by tests, + // Even after the reconciler has initialized and read host config values. + now: () => TestRendererScheduling.nowImplementation(), + + isPrimaryRenderer: true, + + mutation: { + commitUpdate( + instance: Instance, + updatePayload: {}, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + ): void { + instance.type = type; + instance.props = newProps; + }, + + commitMount( + instance: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, + ): void { + // noop + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.text = newText; + }, + resetTextContent(testElement: Instance): void { + // noop + }, + + appendChild: appendChild, + appendChildToContainer: appendChild, + insertBefore: insertBefore, + insertInContainerBefore: insertBefore, + removeChild: removeChild, + removeChildFromContainer: removeChild, + }, +}; + +export default ReactTestHostConfig; diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 7fa54534d3..5fab9af606 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -9,12 +9,11 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; -import type {Deadline} from 'react-reconciler/src/ReactFiberReconciler'; +import type {Instance, TextInstance} from './ReactTestHostConfig'; import ReactFiberReconciler from 'react-reconciler'; import {batchedUpdates} from 'events/ReactGenericBatching'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; -import emptyObject from 'fbjs/lib/emptyObject'; import { Fragment, FunctionalComponent, @@ -31,6 +30,9 @@ import { } from 'shared/ReactTypeOfWork'; import invariant from 'fbjs/lib/invariant'; +import ReactTestHostConfig from './ReactTestHostConfig'; +import * as TestRendererScheduling from './ReactTestRendererScheduling'; + type TestRendererOptions = { createNodeMock: (element: React$Element) => any, unstable_isAsync: boolean, @@ -44,26 +46,6 @@ type ReactTestRendererJSON = {| |}; type ReactTestRendererNode = ReactTestRendererJSON | string; -type Container = {| - children: Array, - createNodeMock: Function, - tag: 'CONTAINER', -|}; - -type Props = Object; -type Instance = {| - type: string, - props: Object, - children: Array, - rootContainerInstance: Container, - tag: 'INSTANCE', -|}; - -type TextInstance = {| - text: string, - tag: 'TEXT', -|}; - type FindOptions = $Shape<{ // performs a "greedy" search: if a matching node is found, will continue // to search within the matching node's children. (default: true) @@ -72,203 +54,7 @@ type FindOptions = $Shape<{ export type Predicate = (node: ReactTestInstance) => ?boolean; -const UPDATE_SIGNAL = {}; - -function getPublicInstance(inst: Instance | TextInstance): * { - switch (inst.tag) { - case 'INSTANCE': - const createNodeMock = inst.rootContainerInstance.createNodeMock; - return createNodeMock({ - type: inst.type, - props: inst.props, - }); - default: - return inst; - } -} - -function appendChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); -} - -function insertBefore( - parentInstance: Instance | Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - const beforeIndex = parentInstance.children.indexOf(beforeChild); - parentInstance.children.splice(beforeIndex, 0, child); -} - -function removeChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - parentInstance.children.splice(index, 1); -} - -// Current virtual time -let nowImplementation = () => 0; -let scheduledCallback: ((deadline: Deadline) => mixed) | null = null; -let yieldedValues: Array | null = null; - -const TestRenderer = ReactFiberReconciler({ - getRootHostContext() { - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - prepareForCommit(): void { - // noop - }, - - resetAfterCommit(): void { - // noop - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): Instance { - return { - type, - props, - children: [], - rootContainerInstance, - tag: 'INSTANCE', - }; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); - }, - - finalizeInitialChildren( - testElement: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - return false; - }, - - prepareUpdate( - testElement: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: Object, - ): null | {} { - return UPDATE_SIGNAL; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return false; - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): TextInstance { - return { - text, - tag: 'TEXT', - }; - }, - - scheduleDeferredCallback( - callback: (deadline: Deadline) => mixed, - options?: {timeout: number}, - ): number { - scheduledCallback = callback; - return 0; - }, - - cancelDeferredCallback(timeoutID: number): void { - scheduledCallback = null; - }, - - getPublicInstance, - - // This approach enables `now` to be mocked by tests, - // Even after the reconciler has initialized and read host config values. - now: () => nowImplementation(), - - isPrimaryRenderer: true, - - mutation: { - commitUpdate( - instance: Instance, - updatePayload: {}, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - instance.type = type; - instance.props = newProps; - }, - - commitMount( - instance: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // noop - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - resetTextContent(testElement: Instance): void { - // noop - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - }, -}); +const TestRenderer = ReactFiberReconciler(ReactTestHostConfig); const defaultTestOptions = { createNodeMock: function() { @@ -444,7 +230,7 @@ class ReactTestInstance { get instance() { if (this._fiber.tag === HostComponent) { - return getPublicInstance(this._fiber.stateNode); + return ReactTestHostConfig.getPublicInstance(this._fiber.stateNode); } else { return this._fiber.stateNode; } @@ -676,77 +462,20 @@ const ReactTestRendererFiber = { container = null; root = null; }, - unstable_flushAll(): Array { - yieldedValues = null; - while (scheduledCallback !== null) { - const cb = scheduledCallback; - scheduledCallback = null; - cb({ - timeRemaining() { - // Keep rendering until there's no more work - return 999; - }, - // React's scheduler has its own way of keeping track of expired - // work and doesn't read this, so don't bother setting it to the - // correct value. - didTimeout: false, - }); - } - if (yieldedValues === null) { - // Always return an array. - return []; - } - return yieldedValues; - }, - unstable_flushThrough(expectedValues: Array): Array { - let didStop = false; - yieldedValues = null; - while (scheduledCallback !== null && !didStop) { - const cb = scheduledCallback; - scheduledCallback = null; - cb({ - timeRemaining() { - if ( - yieldedValues !== null && - yieldedValues.length >= expectedValues.length - ) { - // We at least as many values as expected. Stop rendering. - didStop = true; - return 0; - } - // Keep rendering. - return 999; - }, - // React's scheduler has its own way of keeping track of expired - // work and doesn't read this, so don't bother setting it to the - // correct value. - didTimeout: false, - }); - } - if (yieldedValues === null) { - // Always return an array. - return []; - } - return yieldedValues; - }, - unstable_yield(value: mixed): void { - if (yieldedValues === null) { - yieldedValues = [value]; - } else { - yieldedValues.push(value); - } - }, getInstance() { if (root == null || root.current == null) { return null; } return TestRenderer.getPublicRootInstance(root); }, + unstable_flushAll: TestRendererScheduling.flushAll, unstable_flushSync(fn: Function) { - yieldedValues = []; - TestRenderer.flushSync(fn); - return yieldedValues; + return TestRendererScheduling.withCleanYields(() => { + TestRenderer.flushSync(fn); + }); }, + unstable_flushThrough: TestRendererScheduling.flushThrough, + unstable_yield: TestRendererScheduling.yieldValue, }; Object.defineProperty( @@ -771,9 +500,7 @@ const ReactTestRendererFiber = { unstable_batchedUpdates: batchedUpdates, /* eslint-enable camelcase */ - unstable_setNowImplementation(implementation: () => number): void { - nowImplementation = implementation; - }, + unstable_setNowImplementation: TestRendererScheduling.setNowImplementation, }; export default ReactTestRendererFiber; diff --git a/packages/react-test-renderer/src/ReactTestRendererScheduling.js b/packages/react-test-renderer/src/ReactTestRendererScheduling.js new file mode 100644 index 0000000000..dfa7616a3b --- /dev/null +++ b/packages/react-test-renderer/src/ReactTestRendererScheduling.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Deadline} from 'react-reconciler/src/ReactFiberReconciler'; + +// Current virtual time +export let nowImplementation = () => 0; +export let scheduledCallback: ((deadline: Deadline) => mixed) | null = null; +export let yieldedValues: Array | null = null; + +export function scheduleDeferredCallback( + callback: (deadline: Deadline) => mixed, + options?: {timeout: number}, +): number { + scheduledCallback = callback; + return 0; +} + +export function cancelDeferredCallback(timeoutID: number): void { + scheduledCallback = null; +} + +export function setNowImplementation(implementation: () => number): void { + nowImplementation = implementation; +} + +export function flushAll(): Array { + yieldedValues = null; + while (scheduledCallback !== null) { + const cb = scheduledCallback; + scheduledCallback = null; + cb({ + timeRemaining() { + // Keep rendering until there's no more work + return 999; + }, + // React's scheduler has its own way of keeping track of expired + // work and doesn't read this, so don't bother setting it to the + // correct value. + didTimeout: false, + }); + } + if (yieldedValues === null) { + // Always return an array. + return []; + } + return yieldedValues; +} + +export function flushThrough(expectedValues: Array): Array { + let didStop = false; + yieldedValues = null; + while (scheduledCallback !== null && !didStop) { + const cb = scheduledCallback; + scheduledCallback = null; + cb({ + timeRemaining() { + if ( + yieldedValues !== null && + yieldedValues.length >= expectedValues.length + ) { + // We at least as many values as expected. Stop rendering. + didStop = true; + return 0; + } + // Keep rendering. + return 999; + }, + // React's scheduler has its own way of keeping track of expired + // work and doesn't read this, so don't bother setting it to the + // correct value. + didTimeout: false, + }); + } + if (yieldedValues === null) { + // Always return an array. + return []; + } + return yieldedValues; +} + +export function yieldValue(value: mixed): void { + if (yieldedValues === null) { + yieldedValues = [value]; + } else { + yieldedValues.push(value); + } +} + +export function withCleanYields(fn: Function) { + yieldedValues = []; + fn(); + return yieldedValues; +} From 369dd4fb1742917cb14486e33bb0e59bda62b23d Mon Sep 17 00:00:00 2001 From: Timothy Yung Date: Mon, 14 May 2018 17:47:47 -0700 Subject: [PATCH 045/277] Update headers for React Native shims (#12806) --- packages/react-native-renderer/src/ReactNativeTypes.js | 2 ++ scripts/rollup/shims/react-native-fb/ReactFeatureFlags.js | 3 ++- scripts/rollup/shims/react-native/NativeMethodsMixin.js | 2 +- scripts/rollup/shims/react-native/ReactDebugTool.js | 3 ++- scripts/rollup/shims/react-native/ReactFabric.js | 3 ++- scripts/rollup/shims/react-native/ReactNative.js | 3 ++- scripts/rollup/shims/react-native/ReactNativeComponentTree.js | 2 +- .../rollup/shims/react-native/ReactNativeViewConfigRegistry.js | 3 ++- scripts/rollup/shims/react-native/ReactPerf.js | 3 ++- .../shims/react-native/createReactNativeComponentClass.js | 2 +- 10 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index c38b0b44eb..70a3203658 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -89,7 +89,9 @@ export type NativeMethodsMixinType = { type SecretInternalsType = { NativeMethodsMixin: NativeMethodsMixinType, + ReactDebugTool?: any, ReactNativeComponentTree: any, + ReactPerf?: any, computeComponentStackForErrorReporting(tag: number): string, // TODO (bvaughn) Decide which additional types to expose here? // And how much information to fill in for the above types. diff --git a/scripts/rollup/shims/react-native-fb/ReactFeatureFlags.js b/scripts/rollup/shims/react-native-fb/ReactFeatureFlags.js index 44add71fc5..ea5c5ba0f1 100644 --- a/scripts/rollup/shims/react-native-fb/ReactFeatureFlags.js +++ b/scripts/rollup/shims/react-native-fb/ReactFeatureFlags.js @@ -4,7 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactFeatureFlags + * @format + * @flow */ 'use strict'; diff --git a/scripts/rollup/shims/react-native/NativeMethodsMixin.js b/scripts/rollup/shims/react-native/NativeMethodsMixin.js index cd0e673b3b..70e1d9dda5 100644 --- a/scripts/rollup/shims/react-native/NativeMethodsMixin.js +++ b/scripts/rollup/shims/react-native/NativeMethodsMixin.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule NativeMethodsMixin + * @format * @flow */ diff --git a/scripts/rollup/shims/react-native/ReactDebugTool.js b/scripts/rollup/shims/react-native/ReactDebugTool.js index e45af4fb09..edde3b629e 100644 --- a/scripts/rollup/shims/react-native/ReactDebugTool.js +++ b/scripts/rollup/shims/react-native/ReactDebugTool.js @@ -4,7 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactDebugTool + * @format + * @flow */ 'use strict'; diff --git a/scripts/rollup/shims/react-native/ReactFabric.js b/scripts/rollup/shims/react-native/ReactFabric.js index 4162ca62fe..a5e317ba4a 100644 --- a/scripts/rollup/shims/react-native/ReactFabric.js +++ b/scripts/rollup/shims/react-native/ReactFabric.js @@ -4,9 +4,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactFabric + * @format * @flow */ + 'use strict'; const BatchedBridge = require('BatchedBridge'); diff --git a/scripts/rollup/shims/react-native/ReactNative.js b/scripts/rollup/shims/react-native/ReactNative.js index a648cafbc3..fd7d3e3dbe 100644 --- a/scripts/rollup/shims/react-native/ReactNative.js +++ b/scripts/rollup/shims/react-native/ReactNative.js @@ -4,9 +4,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactNative + * @format * @flow */ + 'use strict'; import type {ReactNativeType} from 'ReactNativeTypes'; diff --git a/scripts/rollup/shims/react-native/ReactNativeComponentTree.js b/scripts/rollup/shims/react-native/ReactNativeComponentTree.js index 30ec5257f4..702b621a5e 100644 --- a/scripts/rollup/shims/react-native/ReactNativeComponentTree.js +++ b/scripts/rollup/shims/react-native/ReactNativeComponentTree.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactNativeComponentTree + * @format * @flow */ diff --git a/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js b/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js index ffb0fa213f..44a7f1827d 100644 --- a/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js +++ b/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js @@ -4,9 +4,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactNativeViewConfigRegistry + * @format * @flow */ + 'use strict'; import type { diff --git a/scripts/rollup/shims/react-native/ReactPerf.js b/scripts/rollup/shims/react-native/ReactPerf.js index bb6777fff0..637036afaf 100644 --- a/scripts/rollup/shims/react-native/ReactPerf.js +++ b/scripts/rollup/shims/react-native/ReactPerf.js @@ -4,7 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule ReactPerf + * @format + * @flow */ 'use strict'; diff --git a/scripts/rollup/shims/react-native/createReactNativeComponentClass.js b/scripts/rollup/shims/react-native/createReactNativeComponentClass.js index 1a050e8b3c..4ea7d28183 100644 --- a/scripts/rollup/shims/react-native/createReactNativeComponentClass.js +++ b/scripts/rollup/shims/react-native/createReactNativeComponentClass.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @providesModule createReactNativeComponentClass + * @format * @flow */ From 8c747d01cb395ce06d93c2db6bb3f7b95b57d09d Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Mon, 14 May 2018 18:47:40 -0700 Subject: [PATCH 046/277] Use ReactFiberErrorDialog fork for Fabric renderer (#12807) --- scripts/rollup/forks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 97f73879ea..0a223c59ae 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -121,6 +121,7 @@ const forks = Object.freeze({ case RN_FB_PROD: switch (entry) { case 'react-native-renderer': + case 'react-native-renderer/fabric': // Use the RN fork which plays well with redbox. return 'react-reconciler/src/forks/ReactFiberErrorDialog.native.js'; default: From 73f59e6f3152f0579625ecd50caa936920c8e5eb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 14 May 2018 19:18:47 -0700 Subject: [PATCH 047/277] Use global state for `hasForceUpdate` instead of persisting to queue (#12808) * Use global state for `hasForceUpdate` instead of persisting to queue Fixes a bug where `hasForceUpdate` was not reset on commit. Ideally we'd use a tuple and return `hasForceUpdate` from `processUpdateQueue`. * Remove underscore and add comment * Remove temporary variables --- .../src/ReactFiberClassComponent.js | 60 +++++++++---------- .../react-reconciler/src/ReactUpdateQueue.js | 23 ++++--- .../ReactIncremental-test.internal.js | 23 +++++++ 3 files changed, 67 insertions(+), 39 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 52a6346ffb..0a0cebddae 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -32,6 +32,8 @@ import {StrictMode} from './ReactTypeOfMode'; import { enqueueUpdate, processUpdateQueue, + checkHasForceUpdateAfterProcessing, + resetHasForceUpdateBeforeProcessing, createUpdate, ReplaceState, ForceUpdate, @@ -235,14 +237,6 @@ export default function( newState, newContext, ) { - if ( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) { - // If forceUpdate was called, disregard sCU. - return true; - } - const instance = workInProgress.stateNode; const ctor = workInProgress.type; if (typeof instance.shouldComponentUpdate === 'function') { @@ -789,6 +783,8 @@ export default function( } } + resetHasForceUpdateBeforeProcessing(); + const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; @@ -806,10 +802,7 @@ export default function( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. @@ -828,14 +821,16 @@ export default function( newState = workInProgress.memoizedState; } - const shouldUpdate = checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ); + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, + ); if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, @@ -922,6 +917,8 @@ export default function( } } + resetHasForceUpdateBeforeProcessing(); + const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; @@ -940,10 +937,7 @@ export default function( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. @@ -977,14 +971,16 @@ export default function( } } - const shouldUpdate = checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ); + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, + ); if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 574a3c0740..dc3791c4e1 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -131,9 +131,6 @@ export type UpdateQueue = { firstCapturedEffect: Update | null, lastCapturedEffect: Update | null, - - // TODO: Workaround for lack of tuples. Could use global state instead. - hasForceUpdate: boolean, }; export const UpdateState = 0; @@ -141,6 +138,11 @@ export const ReplaceState = 1; export const ForceUpdate = 2; export const CaptureUpdate = 3; +// Global state that is reset at the beginning of calling `processUpdateQueue`. +// It should only be read right after calling `processUpdateQueue`, via +// `checkHasForceUpdateAfterProcessing`. +let hasForceUpdate = false; + let didWarnUpdateInsideUpdate; let currentlyProcessingQueue; export let resetCurrentlyProcessingQueue; @@ -164,7 +166,6 @@ export function createUpdateQueue(baseState: State): UpdateQueue { lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, - hasForceUpdate: false, }; return queue; } @@ -183,8 +184,6 @@ function cloneUpdateQueue( firstCapturedUpdate: null, lastCapturedUpdate: null, - hasForceUpdate: false, - firstEffect: null, lastEffect: null, @@ -423,7 +422,7 @@ function getStateFromUpdate( return Object.assign({}, prevState, partialState); } case ForceUpdate: { - queue.hasForceUpdate = true; + hasForceUpdate = true; return prevState; } } @@ -437,6 +436,8 @@ export function processUpdateQueue( instance: any, renderExpirationTime: ExpirationTime, ): void { + hasForceUpdate = false; + if ( queue.expirationTime === NoWork || queue.expirationTime > renderExpirationTime @@ -595,6 +596,14 @@ function callCallback(callback, context) { callback.call(context); } +export function resetHasForceUpdateBeforeProcessing() { + hasForceUpdate = false; +} + +export function checkHasForceUpdateAfterProcessing(): boolean { + return hasForceUpdate; +} + export function commitUpdateQueue( finishedWork: Fiber, finishedQueue: UpdateQueue, diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index a8289b113d..f58228d952 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1115,6 +1115,29 @@ describe('ReactIncremental', () => { expect(ops).toEqual(['Foo', 'Bar', 'Baz', 'Bar', 'Baz']); }); + it('should clear forceUpdate after update is flushed', () => { + let a = 0; + + class Foo extends React.PureComponent { + render() { + const msg = `A: ${a}, B: ${this.props.b}`; + ReactNoop.yield(msg); + return msg; + } + } + + const foo = React.createRef(null); + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['A: 0, B: 0']); + + a = 1; + foo.current.forceUpdate(); + expect(ReactNoop.flush()).toEqual(['A: 1, B: 0']); + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([]); + }); + xit('can call sCU while resuming a partly mounted component', () => { let ops = []; From bde4b1659fd4aea796482f24d40c2e834fca635f Mon Sep 17 00:00:00 2001 From: Timothy Yung Date: Mon, 14 May 2018 20:28:55 -0700 Subject: [PATCH 048/277] Delete ReactPerf and ReactDebugTool Stubs (#12809) --- .../react-native-renderer/src/ReactFabric.js | 22 ------------------- .../src/ReactNativeRenderer.js | 22 ------------------- .../src/ReactNativeTypes.js | 2 -- .../shims/react-native/ReactDebugTool.js | 18 --------------- .../rollup/shims/react-native/ReactPerf.js | 17 -------------- 5 files changed, 81 deletions(-) delete mode 100644 scripts/rollup/shims/react-native/ReactDebugTool.js delete mode 100644 scripts/rollup/shims/react-native/ReactPerf.js diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 9e2c82548d..130b199580 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -118,28 +118,6 @@ const ReactFabric: ReactFabricType = { }, }; -if (__DEV__) { - // $FlowFixMe - Object.assign( - ReactFabric.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, - { - // TODO: none of these work since Fiber. Remove these dependencies. - // Used by RCTRenderingPerf, Systrace: - ReactDebugTool: { - addHook() {}, - removeHook() {}, - }, - // Used by ReactPerfStallHandler, RCTRenderingPerf: - ReactPerf: { - start() {}, - stop() {}, - printInclusive() {}, - printWasted() {}, - }, - }, - ); -} - ReactFabricRenderer.injectIntoDevTools({ findFiberByHostInstance: ReactNativeComponentTree.getClosestInstanceFromNode, getInspectorDataForViewTag: getInspectorDataForViewTag, diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index ce55def9e6..7b5f3c7805 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -147,28 +147,6 @@ const ReactNativeRenderer: ReactNativeType = { }, }; -if (__DEV__) { - // $FlowFixMe - Object.assign( - ReactNativeRenderer.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, - { - // TODO: none of these work since Fiber. Remove these dependencies. - // Used by RCTRenderingPerf, Systrace: - ReactDebugTool: { - addHook() {}, - removeHook() {}, - }, - // Used by ReactPerfStallHandler, RCTRenderingPerf: - ReactPerf: { - start() {}, - stop() {}, - printInclusive() {}, - printWasted() {}, - }, - }, - ); -} - ReactNativeFiberRenderer.injectIntoDevTools({ findFiberByHostInstance: ReactNativeComponentTree.getClosestInstanceFromNode, getInspectorDataForViewTag: getInspectorDataForViewTag, diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 70a3203658..c38b0b44eb 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -89,9 +89,7 @@ export type NativeMethodsMixinType = { type SecretInternalsType = { NativeMethodsMixin: NativeMethodsMixinType, - ReactDebugTool?: any, ReactNativeComponentTree: any, - ReactPerf?: any, computeComponentStackForErrorReporting(tag: number): string, // TODO (bvaughn) Decide which additional types to expose here? // And how much information to fill in for the above types. diff --git a/scripts/rollup/shims/react-native/ReactDebugTool.js b/scripts/rollup/shims/react-native/ReactDebugTool.js deleted file mode 100644 index edde3b629e..0000000000 --- a/scripts/rollup/shims/react-native/ReactDebugTool.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ - -'use strict'; - -const { - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, -} = require('ReactNative'); - -module.exports = - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactDebugTool; diff --git a/scripts/rollup/shims/react-native/ReactPerf.js b/scripts/rollup/shims/react-native/ReactPerf.js deleted file mode 100644 index 637036afaf..0000000000 --- a/scripts/rollup/shims/react-native/ReactPerf.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ - -'use strict'; - -const { - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, -} = require('ReactNative'); - -module.exports = __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactPerf; From 1047980dca0830cd55e1622f3fbefc38aeaadb91 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 15 May 2018 12:18:35 +0300 Subject: [PATCH 049/277] Remove unused context param from `countChildren` (#12787) --- packages/react/src/ReactChildren.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index b44b833912..635db9c8a0 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -361,7 +361,7 @@ function mapChildren(children, func, context) { * @param {?*} children Children tree container. * @return {number} The number of children. */ -function countChildren(children, context) { +function countChildren(children) { return traverseAllChildren(children, emptyFunction.thatReturnsNull, null); } From e96dc140599363029bd05565d58bcd4a432db370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Spie=C3=9F?= Date: Tue, 15 May 2018 11:38:50 +0200 Subject: [PATCH 050/277] Use browser event names for top-level event types in React DOM (#12629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add TopLevelEventTypes * Fix `ReactBrowserEventEmitter` * Fix EventPluginUtils * Fix TapEventPlugin * Fix ResponderEventPlugin * Update ReactDOMFiberComponent * Fix BeforeInputEventPlugin * Fix ChangeEventPlugin * Fix EnterLeaveEventPlugin * Add missing non top event type used in ChangeEventPlugin * Fix SelectEventPlugin * Fix SimpleEventPlugin * Fix outstanding Flow issues and move TopLevelEventTypes * Inline a list of all events in `ReactTestUtils` * Fix tests * Make it pretty * Fix completly unrelated typo * Don’t use map constructor because of IE11 * Update typings, revert changes to native code * Make topLevelTypes in ResponderEventPlugin injectable and create DOM and ReactNative variant * Set proper dependencies for DOMResponderEventPlugin * Prettify * Make some react dom tests no longer depend on internal API * Use factories to create top level speific generic event modules * Remove unused dependency * Revert exposed module renaming, hide store creation, and inline dependency decleration * Add Flow types to createResponderEventPlugin and its consumers * Remove unused dependency * Use opaque flow type for TopLevelType * Add missing semis * Use raw event names as top level identifer * Upgrade baylon This is required for parsing opaque flow types in our CI tests. * Clean up flow types * Revert Map changes of ReactBrowserEventEmitter * Upgrade babel-* packages Apparently local unit tests also have issues with parsing JavaScript modules that contain opaque types (not sure why I didn't notice earlier!?). * Revert Map changes of SimpleEventPlugin * Clean up ReactTestUtils * Add missing semi * Fix Flow issue * Make TopLevelType clearer * Favor for loops * Explain the new DOMTopLevelEventTypes concept * Use static injection for Responder plugin types * Remove null check and rely on flow checks * Add missing ResponderEventPlugin dependencies --- .../fixtures/password-inputs/index.js | 2 +- package.json | 2 +- packages/events/EventPluginHub.js | 5 +- packages/events/EventPluginUtils.js | 15 - packages/events/PluginModuleType.js | 3 +- packages/events/ReactSyntheticEventType.js | 3 +- packages/events/ResponderEventPlugin.js | 90 +- .../events/ResponderTopLevelEventTypes.js | 31 + packages/events/ResponderTouchHistoryStore.js | 2 +- packages/events/TopLevelEventTypes.js | 23 + .../forks/ResponderTopLevelEventTypes.dom.js | 41 + .../react-dom/src/__tests__/ReactDOM-test.js | 11 +- .../src/__tests__/ReactDOMFiber-test.js | 20 +- .../src/__tests__/ReactDOMInput-test.js | 7 +- .../src/client/ReactDOMFiberComponent.js | 62 +- .../src/events/BeforeInputEventPlugin.js | 98 +- .../src/events/BrowserEventConstants.js | 97 -- .../react-dom/src/events/ChangeEventPlugin.js | 44 +- .../src/events/DOMTopLevelEventTypes.js | 205 +++ .../src/events/EnterLeaveEventPlugin.js | 11 +- .../react-dom/src/events/EventListener.js | 4 +- .../src/events/ReactBrowserEventEmitter.js | 55 +- .../src/events/ReactDOMEventListener.js | 44 +- .../react-dom/src/events/SelectEventPlugin.js | 42 +- .../react-dom/src/events/SimpleEventPlugin.js | 303 ++-- .../react-dom/src/events/TapEventPlugin.js | 35 +- .../__tests__/TapEventPlugin-test.internal.js | 0 .../src/test-utils/ReactTestUtils.js | 155 +- .../src/ReactNativeBridgeEventPlugin.js | 3 +- .../src/ReactNativeEventEmitter.js | 7 +- scripts/rollup/forks.js | 8 + yarn.lock | 1541 ++++++++++++----- 32 files changed, 1969 insertions(+), 1000 deletions(-) create mode 100644 packages/events/ResponderTopLevelEventTypes.js create mode 100644 packages/events/TopLevelEventTypes.js create mode 100644 packages/events/forks/ResponderTopLevelEventTypes.dom.js delete mode 100644 packages/react-dom/src/events/BrowserEventConstants.js create mode 100644 packages/react-dom/src/events/DOMTopLevelEventTypes.js rename packages/react-dom/src/{ => events}/__tests__/TapEventPlugin-test.internal.js (100%) diff --git a/fixtures/dom/src/components/fixtures/password-inputs/index.js b/fixtures/dom/src/components/fixtures/password-inputs/index.js index f94dce97dc..1c37f09d58 100644 --- a/fixtures/dom/src/components/fixtures/password-inputs/index.js +++ b/fixtures/dom/src/components/fixtures/password-inputs/index.js @@ -15,7 +15,7 @@ function NumberInputs() { `} affectedBrowsers="IE Edge, IE 11"> -
  • Type any string (not an actual password
  • +
  • Type any string (not an actual password)
  • diff --git a/package.json b/package.json index 3b1e2f6168..b8eb946ea7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "babel-plugin-transform-regenerator": "^6.26.0", "babel-preset-react": "^6.5.0", "babel-traverse": "^6.9.0", - "babylon": "6.15.0", + "babylon": "6.18.0", "bundle-collapser": "^1.1.1", "chalk": "^1.1.3", "cli-table": "^0.3.1", diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index d59ba35ff6..27840402ef 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -25,6 +25,7 @@ import type {PluginModule} from './PluginModuleType'; import type {ReactSyntheticEvent} from './ReactSyntheticEventType'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {AnyNativeEvent} from './PluginModuleType'; +import type {TopLevelType} from './TopLevelEventTypes'; /** * Internal queue of events that have accumulated their dispatches and are @@ -165,7 +166,7 @@ export function getListener(inst: Fiber, registrationName: string) { * @internal */ function extractEvents( - topLevelType: string, + topLevelType: TopLevelType, targetInst: Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, @@ -227,7 +228,7 @@ export function runEventsInBatch( } export function runExtractedEventsInBatch( - topLevelType: string, + topLevelType: TopLevelType, targetInst: Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, diff --git a/packages/events/EventPluginUtils.js b/packages/events/EventPluginUtils.js index d5e5a5e847..19a89e30de 100644 --- a/packages/events/EventPluginUtils.js +++ b/packages/events/EventPluginUtils.js @@ -30,21 +30,6 @@ export const injection = { }, }; -export function isEndish(topLevelType) { - return ( - topLevelType === 'topMouseUp' || - topLevelType === 'topTouchEnd' || - topLevelType === 'topTouchCancel' - ); -} - -export function isMoveish(topLevelType) { - return topLevelType === 'topMouseMove' || topLevelType === 'topTouchMove'; -} -export function isStartish(topLevelType) { - return topLevelType === 'topMouseDown' || topLevelType === 'topTouchStart'; -} - let validateEventDispatches; if (__DEV__) { validateEventDispatches = function(event) { diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index 5657ac3514..22d53b1ccb 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -12,6 +12,7 @@ import type { DispatchConfig, ReactSyntheticEvent, } from './ReactSyntheticEventType'; +import type {TopLevelType} from './TopLevelEventTypes'; export type EventTypes = {[key: string]: DispatchConfig}; @@ -22,7 +23,7 @@ export type PluginName = string; export type PluginModule = { eventTypes: EventTypes, extractEvents: ( - topLevelType: string, + topLevelType: TopLevelType, targetInst: Fiber, nativeTarget: NativeEvent, nativeEventTarget: EventTarget, diff --git a/packages/events/ReactSyntheticEventType.js b/packages/events/ReactSyntheticEventType.js index b2b0b688fc..2f248f7029 100644 --- a/packages/events/ReactSyntheticEventType.js +++ b/packages/events/ReactSyntheticEventType.js @@ -9,9 +9,10 @@ */ import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {TopLevelType} from './TopLevelEventTypes'; export type DispatchConfig = { - dependencies: Array, + dependencies: Array, phasedRegistrationNames?: { bubbled: string, captured: string, diff --git a/packages/events/ResponderEventPlugin.js b/packages/events/ResponderEventPlugin.js index 202f8931f9..dc14523fff 100644 --- a/packages/events/ResponderEventPlugin.js +++ b/packages/events/ResponderEventPlugin.js @@ -8,9 +8,6 @@ import {getLowestCommonAncestor, isAncestor} from 'shared/ReactTreeTraversal'; import { - isStartish, - isMoveish, - isEndish, executeDirectDispatch, hasDispatches, executeDispatchesInOrderStopAtTrue, @@ -24,6 +21,17 @@ import { import ResponderSyntheticEvent from './ResponderSyntheticEvent'; import ResponderTouchHistoryStore from './ResponderTouchHistoryStore'; import accumulate from './accumulate'; +import { + TOP_SCROLL, + TOP_SELECTION_CHANGE, + TOP_TOUCH_CANCEL, + isStartish, + isMoveish, + isEndish, + startDependencies, + moveDependencies, + endDependencies, +} from './ResponderTopLevelEventTypes'; /** * Instance of element that should respond to touch/move types of interactions, @@ -37,11 +45,6 @@ let responderInst = null; */ let trackedTouchCount = 0; -/** - * Last reported number of active touches. - */ -let previousActiveTouches = 0; - const changeResponder = function(nextResponderInst, blockHostResponder) { const oldResponderInst = responderInst; responderInst = nextResponderInst; @@ -64,6 +67,7 @@ const eventTypes = { bubbled: 'onStartShouldSetResponder', captured: 'onStartShouldSetResponderCapture', }, + dependencies: startDependencies, }, /** @@ -80,6 +84,7 @@ const eventTypes = { bubbled: 'onScrollShouldSetResponder', captured: 'onScrollShouldSetResponderCapture', }, + dependencies: [TOP_SCROLL], }, /** @@ -94,6 +99,7 @@ const eventTypes = { bubbled: 'onSelectionChangeShouldSetResponder', captured: 'onSelectionChangeShouldSetResponderCapture', }, + dependencies: [TOP_SELECTION_CHANGE], }, /** @@ -105,21 +111,44 @@ const eventTypes = { bubbled: 'onMoveShouldSetResponder', captured: 'onMoveShouldSetResponderCapture', }, + dependencies: moveDependencies, }, /** * Direct responder events dispatched directly to responder. Do not bubble. */ - responderStart: {registrationName: 'onResponderStart'}, - responderMove: {registrationName: 'onResponderMove'}, - responderEnd: {registrationName: 'onResponderEnd'}, - responderRelease: {registrationName: 'onResponderRelease'}, + responderStart: { + registrationName: 'onResponderStart', + dependencies: startDependencies, + }, + responderMove: { + registrationName: 'onResponderMove', + dependencies: moveDependencies, + }, + responderEnd: { + registrationName: 'onResponderEnd', + dependencies: endDependencies, + }, + responderRelease: { + registrationName: 'onResponderRelease', + dependencies: endDependencies, + }, responderTerminationRequest: { registrationName: 'onResponderTerminationRequest', + dependencies: [], + }, + responderGrant: { + registrationName: 'onResponderGrant', + dependencies: [], + }, + responderReject: { + registrationName: 'onResponderReject', + dependencies: [], + }, + responderTerminate: { + registrationName: 'onResponderTerminate', + dependencies: [], }, - responderGrant: {registrationName: 'onResponderGrant'}, - responderReject: {registrationName: 'onResponderReject'}, - responderTerminate: {registrationName: 'onResponderTerminate'}, }; /** @@ -322,7 +351,7 @@ function setResponderAndExtractTransfer( ? eventTypes.startShouldSetResponder : isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder - : topLevelType === 'topSelectionChange' + : topLevelType === TOP_SELECTION_CHANGE ? eventTypes.selectionChangeShouldSetResponder : eventTypes.scrollShouldSetResponder; @@ -427,8 +456,8 @@ function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) { // responderIgnoreScroll: We are trying to migrate away from specifically // tracking native scroll events here and responderIgnoreScroll indicates we // will send topTouchCancel to handle canceling touch events instead - ((topLevelType === 'topScroll' && !nativeEvent.responderIgnoreScroll) || - (trackedTouchCount > 0 && topLevelType === 'topSelectionChange') || + ((topLevelType === TOP_SCROLL && !nativeEvent.responderIgnoreScroll) || + (trackedTouchCount > 0 && topLevelType === TOP_SELECTION_CHANGE) || isStartish(topLevelType) || isMoveish(topLevelType)) ); @@ -534,7 +563,7 @@ const ResponderEventPlugin = { } const isResponderTerminate = - responderInst && topLevelType === 'topTouchCancel'; + responderInst && topLevelType === TOP_TOUCH_CANCEL; const isResponderRelease = responderInst && !isResponderTerminate && @@ -556,23 +585,10 @@ const ResponderEventPlugin = { changeResponder(null); } - const numberActiveTouches = - ResponderTouchHistoryStore.touchHistory.numberActiveTouches; - if ( - ResponderEventPlugin.GlobalInteractionHandler && - numberActiveTouches !== previousActiveTouches - ) { - ResponderEventPlugin.GlobalInteractionHandler.onChange( - numberActiveTouches, - ); - } - previousActiveTouches = numberActiveTouches; - return extracted; }, GlobalResponderHandler: null, - GlobalInteractionHandler: null, injection: { /** @@ -580,17 +596,9 @@ const ResponderEventPlugin = { * Object that handles any change in responder. Use this to inject * integration with an existing touch handling system etc. */ - injectGlobalResponderHandler: function(GlobalResponderHandler) { + injectGlobalResponderHandler(GlobalResponderHandler) { ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler; }, - - /** - * @param {{onChange: (numberActiveTouches) => void} GlobalInteractionHandler - * Object that handles any change in the number of active touches. - */ - injectGlobalInteractionHandler: function(GlobalInteractionHandler) { - ResponderEventPlugin.GlobalInteractionHandler = GlobalInteractionHandler; - }, }, }; diff --git a/packages/events/ResponderTopLevelEventTypes.js b/packages/events/ResponderTopLevelEventTypes.js new file mode 100644 index 0000000000..54ad053a66 --- /dev/null +++ b/packages/events/ResponderTopLevelEventTypes.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export const TOP_TOUCH_START = 'topTouchStart'; +export const TOP_TOUCH_MOVE = 'topTouchMove'; +export const TOP_TOUCH_END = 'topTouchEnd'; +export const TOP_TOUCH_CANCEL = 'topTouchCancel'; +export const TOP_SCROLL = 'topScroll'; +export const TOP_SELECTION_CHANGE = 'topSelectionChange'; + +export function isStartish(topLevelType: mixed): boolean { + return topLevelType === TOP_TOUCH_START; +} + +export function isMoveish(topLevelType: mixed): boolean { + return topLevelType === TOP_TOUCH_MOVE; +} + +export function isEndish(topLevelType: mixed): boolean { + return topLevelType === TOP_TOUCH_END || topLevelType === TOP_TOUCH_CANCEL; +} + +export const startDependencies = [TOP_TOUCH_START]; +export const moveDependencies = [TOP_TOUCH_MOVE]; +export const endDependencies = [TOP_TOUCH_CANCEL, TOP_TOUCH_END]; diff --git a/packages/events/ResponderTouchHistoryStore.js b/packages/events/ResponderTouchHistoryStore.js index 0c703a5970..807a5a08d9 100644 --- a/packages/events/ResponderTouchHistoryStore.js +++ b/packages/events/ResponderTouchHistoryStore.js @@ -10,7 +10,7 @@ import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; -import {isEndish, isMoveish, isStartish} from './EventPluginUtils'; +import {isStartish, isMoveish, isEndish} from './ResponderTopLevelEventTypes'; /** * Tracks the position and time of each active touch by `touch.identifier`. We diff --git a/packages/events/TopLevelEventTypes.js b/packages/events/TopLevelEventTypes.js new file mode 100644 index 0000000000..f9d4722abb --- /dev/null +++ b/packages/events/TopLevelEventTypes.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {DOMTopLevelEventType} from 'react-dom/src/events/DOMTopLevelEventTypes'; + +type RNTopLevelEventType = + | 'topMouseDown' + | 'topMouseMove' + | 'topMouseUp' + | 'topScroll' + | 'topSelectionChange' + | 'topTouchCancel' + | 'topTouchEnd' + | 'topTouchMove' + | 'topTouchStart'; + +export type TopLevelType = DOMTopLevelEventType | RNTopLevelEventType; diff --git a/packages/events/forks/ResponderTopLevelEventTypes.dom.js b/packages/events/forks/ResponderTopLevelEventTypes.dom.js new file mode 100644 index 0000000000..5e009dc56c --- /dev/null +++ b/packages/events/forks/ResponderTopLevelEventTypes.dom.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Note: ideally these would be imported from DOMTopLevelEventTypes, +// but our build system currently doesn't let us do that from a fork. + +export const TOP_TOUCH_START = 'touchstart'; +export const TOP_TOUCH_MOVE = 'touchmove'; +export const TOP_TOUCH_END = 'touchend'; +export const TOP_TOUCH_CANCEL = 'touchcancel'; +export const TOP_SCROLL = 'scroll'; +export const TOP_SELECTION_CHANGE = 'selectionchange'; +export const TOP_MOUSE_DOWN = 'mousedown'; +export const TOP_MOUSE_MOVE = 'mousemove'; +export const TOP_MOUSE_UP = 'mouseup'; + +export function isStartish(topLevelType: mixed): boolean { + return topLevelType === TOP_TOUCH_START || topLevelType === TOP_MOUSE_DOWN; +} + +export function isMoveish(topLevelType: mixed): boolean { + return topLevelType === TOP_TOUCH_MOVE || topLevelType === TOP_MOUSE_MOVE; +} + +export function isEndish(topLevelType: mixed): boolean { + return ( + topLevelType === TOP_TOUCH_END || + topLevelType === TOP_TOUCH_CANCEL || + topLevelType === TOP_MOUSE_UP + ); +} + +export const startDependencies = [TOP_TOUCH_START, TOP_MOUSE_DOWN]; +export const moveDependencies = [TOP_TOUCH_MOVE, TOP_MOUSE_MOVE]; +export const endDependencies = [TOP_TOUCH_CANCEL, TOP_TOUCH_END, TOP_MOUSE_UP]; diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index 5e7d2772e3..cca40165fd 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -309,14 +309,9 @@ describe('ReactDOM', () => { const actual = []; function click(node) { - const fakeNativeEvent = function() {}; - fakeNativeEvent.target = node; - fakeNativeEvent.path = [node, container]; - ReactTestUtils.simulateNativeEventOnNode( - 'topClick', - node, - fakeNativeEvent, - ); + ReactTestUtils.Simulate.click(node, { + path: [node, container], + }); } class Wrapper extends React.Component { diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index deffd4680d..a93dda6ce1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -838,12 +838,7 @@ describe('ReactDOMFiber', () => { expect(portal.tagName).toBe('DIV'); - const fakeNativeEvent = {}; - ReactTestUtils.simulateNativeEventOnNode( - 'topClick', - portal, - fakeNativeEvent, - ); + ReactTestUtils.Simulate.click(portal); expect(ops).toEqual(['portal clicked', 'parent clicked']); }); @@ -858,14 +853,12 @@ describe('ReactDOMFiber', () => { function simulateMouseMove(from, to) { if (from) { - ReactTestUtils.simulateNativeEventOnNode('topMouseOut', from, { - target: from, + ReactTestUtils.SimulateNative.mouseOut(from, { relatedTarget: to, }); } if (to) { - ReactTestUtils.simulateNativeEventOnNode('topMouseOver', to, { - target: to, + ReactTestUtils.SimulateNative.mouseOver(to, { relatedTarget: from, }); } @@ -983,12 +976,7 @@ describe('ReactDOMFiber', () => { expect(node.tagName).toEqual('DIV'); function click(target) { - const fakeNativeEvent = {}; - ReactTestUtils.simulateNativeEventOnNode( - 'topClick', - target, - fakeNativeEvent, - ); + ReactTestUtils.Simulate.click(target); } click(node); diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 03eed7bc1e..f5513557ec 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -694,10 +694,9 @@ describe('ReactDOMInput', () => { setUntrackedValue.call(node, 'giraffe'); - const fakeNativeEvent = function() {}; - fakeNativeEvent.target = node; - fakeNativeEvent.path = [node, container]; - ReactTestUtils.simulateNativeEventOnNode('topInput', node, fakeNativeEvent); + ReactTestUtils.SimulateNative.input(node, { + path: [node, container], + }); expect(handled).toBe(true); }); diff --git a/packages/react-dom/src/client/ReactDOMFiberComponent.js b/packages/react-dom/src/client/ReactDOMFiberComponent.js index ceb20873c2..4c3c7a9c1d 100644 --- a/packages/react-dom/src/client/ReactDOMFiberComponent.js +++ b/packages/react-dom/src/client/ReactDOMFiberComponent.js @@ -21,8 +21,16 @@ import * as ReactDOMFiberTextarea from './ReactDOMFiberTextarea'; import * as inputValueTracking from './inputValueTracking'; import setInnerHTML from './setInnerHTML'; import setTextContent from './setTextContent'; +import { + TOP_ERROR, + TOP_INVALID, + TOP_LOAD, + TOP_RESET, + TOP_SUBMIT, + TOP_TOGGLE, +} from '../events/DOMTopLevelEventTypes'; import {listenTo, trapBubbledEvent} from '../events/ReactBrowserEventEmitter'; -import {mediaEventTypes} from '../events/BrowserEventConstants'; +import {mediaEventTypes} from '../events/DOMTopLevelEventTypes'; import * as CSSPropertyOperations from '../shared/CSSPropertyOperations'; import {Namespaces, getIntrinsicNamespace} from '../shared/DOMNamespaces'; import { @@ -440,43 +448,41 @@ export function setInitialProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent('topLoad', 'load', domElement); + trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'video': case 'audio': // Create listener for each media event - for (const event in mediaEventTypes) { - if (mediaEventTypes.hasOwnProperty(event)) { - trapBubbledEvent(event, mediaEventTypes[event], domElement); - } + for (let i = 0; i < mediaEventTypes.length; i++) { + trapBubbledEvent(mediaEventTypes[i], domElement); } props = rawProps; break; case 'source': - trapBubbledEvent('topError', 'error', domElement); + trapBubbledEvent(TOP_ERROR, domElement); props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent('topError', 'error', domElement); - trapBubbledEvent('topLoad', 'load', domElement); + trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'form': - trapBubbledEvent('topReset', 'reset', domElement); - trapBubbledEvent('topSubmit', 'submit', domElement); + trapBubbledEvent(TOP_RESET, domElement); + trapBubbledEvent(TOP_SUBMIT, domElement); props = rawProps; break; case 'details': - trapBubbledEvent('topToggle', 'toggle', domElement); + trapBubbledEvent(TOP_TOGGLE, domElement); props = rawProps; break; case 'input': ReactDOMFiberInput.initWrapperState(domElement, rawProps); props = ReactDOMFiberInput.getHostProps(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -488,7 +494,7 @@ export function setInitialProperties( case 'select': ReactDOMFiberSelect.initWrapperState(domElement, rawProps); props = ReactDOMFiberSelect.getHostProps(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -496,7 +502,7 @@ export function setInitialProperties( case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -829,36 +835,34 @@ export function diffHydratedProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent('topLoad', 'load', domElement); + trapBubbledEvent(TOP_LOAD, domElement); break; case 'video': case 'audio': // Create listener for each media event - for (const event in mediaEventTypes) { - if (mediaEventTypes.hasOwnProperty(event)) { - trapBubbledEvent(event, mediaEventTypes[event], domElement); - } + for (let i = 0; i < mediaEventTypes.length; i++) { + trapBubbledEvent(mediaEventTypes[i], domElement); } break; case 'source': - trapBubbledEvent('topError', 'error', domElement); + trapBubbledEvent(TOP_ERROR, domElement); break; case 'img': case 'image': case 'link': - trapBubbledEvent('topError', 'error', domElement); - trapBubbledEvent('topLoad', 'load', domElement); + trapBubbledEvent(TOP_ERROR, domElement); + trapBubbledEvent(TOP_LOAD, domElement); break; case 'form': - trapBubbledEvent('topReset', 'reset', domElement); - trapBubbledEvent('topSubmit', 'submit', domElement); + trapBubbledEvent(TOP_RESET, domElement); + trapBubbledEvent(TOP_SUBMIT, domElement); break; case 'details': - trapBubbledEvent('topToggle', 'toggle', domElement); + trapBubbledEvent(TOP_TOGGLE, domElement); break; case 'input': ReactDOMFiberInput.initWrapperState(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -868,14 +872,14 @@ export function diffHydratedProperties( break; case 'select': ReactDOMFiberSelect.initWrapperState(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); - trapBubbledEvent('topInvalid', 'invalid', domElement); + trapBubbledEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); diff --git a/packages/react-dom/src/events/BeforeInputEventPlugin.js b/packages/react-dom/src/events/BeforeInputEventPlugin.js index 3307a70bde..9bdb8dab7d 100644 --- a/packages/react-dom/src/events/BeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/BeforeInputEventPlugin.js @@ -5,11 +5,23 @@ * LICENSE file in the root directory of this source tree. */ -import type {TopLevelTypes} from './BrowserEventConstants'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; import {accumulateTwoPhaseDispatches} from 'events/EventPropagators'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; +import { + TOP_BLUR, + TOP_COMPOSITION_START, + TOP_COMPOSITION_END, + TOP_COMPOSITION_UPDATE, + TOP_KEY_DOWN, + TOP_KEY_PRESS, + TOP_KEY_UP, + TOP_MOUSE_DOWN, + TOP_TEXT_INPUT, + TOP_PASTE, +} from './DOMTopLevelEventTypes'; import * as FallbackCompositionState from './FallbackCompositionState'; import SyntheticCompositionEvent from './SyntheticCompositionEvent'; import SyntheticInputEvent from './SyntheticInputEvent'; @@ -50,10 +62,10 @@ const eventTypes = { captured: 'onBeforeInputCapture', }, dependencies: [ - 'topCompositionEnd', - 'topKeyPress', - 'topTextInput', - 'topPaste', + TOP_COMPOSITION_END, + TOP_KEY_PRESS, + TOP_TEXT_INPUT, + TOP_PASTE, ], }, compositionEnd: { @@ -62,12 +74,12 @@ const eventTypes = { captured: 'onCompositionEndCapture', }, dependencies: [ - 'topBlur', - 'topCompositionEnd', - 'topKeyDown', - 'topKeyPress', - 'topKeyUp', - 'topMouseDown', + TOP_BLUR, + TOP_COMPOSITION_END, + TOP_KEY_DOWN, + TOP_KEY_PRESS, + TOP_KEY_UP, + TOP_MOUSE_DOWN, ], }, compositionStart: { @@ -76,12 +88,12 @@ const eventTypes = { captured: 'onCompositionStartCapture', }, dependencies: [ - 'topBlur', - 'topCompositionStart', - 'topKeyDown', - 'topKeyPress', - 'topKeyUp', - 'topMouseDown', + TOP_BLUR, + TOP_COMPOSITION_START, + TOP_KEY_DOWN, + TOP_KEY_PRESS, + TOP_KEY_UP, + TOP_MOUSE_DOWN, ], }, compositionUpdate: { @@ -90,12 +102,12 @@ const eventTypes = { captured: 'onCompositionUpdateCapture', }, dependencies: [ - 'topBlur', - 'topCompositionUpdate', - 'topKeyDown', - 'topKeyPress', - 'topKeyUp', - 'topMouseDown', + TOP_BLUR, + TOP_COMPOSITION_UPDATE, + TOP_KEY_DOWN, + TOP_KEY_PRESS, + TOP_KEY_UP, + TOP_MOUSE_DOWN, ], }, }; @@ -124,11 +136,11 @@ function isKeypressCommand(nativeEvent) { */ function getCompositionEventType(topLevelType) { switch (topLevelType) { - case 'topCompositionStart': + case TOP_COMPOSITION_START: return eventTypes.compositionStart; - case 'topCompositionEnd': + case TOP_COMPOSITION_END: return eventTypes.compositionEnd; - case 'topCompositionUpdate': + case TOP_COMPOSITION_UPDATE: return eventTypes.compositionUpdate; } } @@ -142,7 +154,7 @@ function getCompositionEventType(topLevelType) { * @return {boolean} */ function isFallbackCompositionStart(topLevelType, nativeEvent) { - return topLevelType === 'topKeyDown' && nativeEvent.keyCode === START_KEYCODE; + return topLevelType === TOP_KEY_DOWN && nativeEvent.keyCode === START_KEYCODE; } /** @@ -154,16 +166,16 @@ function isFallbackCompositionStart(topLevelType, nativeEvent) { */ function isFallbackCompositionEnd(topLevelType, nativeEvent) { switch (topLevelType) { - case 'topKeyUp': + case TOP_KEY_UP: // Command keys insert or clear IME input. return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1; - case 'topKeyDown': + case TOP_KEY_DOWN: // Expect IME keyCode on each keydown. If we get any other // code we must have exited earlier. return nativeEvent.keyCode !== START_KEYCODE; - case 'topKeyPress': - case 'topMouseDown': - case 'topBlur': + case TOP_KEY_PRESS: + case TOP_MOUSE_DOWN: + case TOP_BLUR: // Events are not possible without cancelling IME. return true; default: @@ -252,15 +264,15 @@ function extractCompositionEvent( } /** - * @param {TopLevelTypes} topLevelType Record from `BrowserEventConstants`. + * @param {TopLevelType} topLevelType Number from `TopLevelType`. * @param {object} nativeEvent Native browser event. * @return {?string} The string corresponding to this `beforeInput` event. */ -function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { +function getNativeBeforeInputChars(topLevelType: TopLevelType, nativeEvent) { switch (topLevelType) { - case 'topCompositionEnd': + case TOP_COMPOSITION_END: return getDataFromCustomEvent(nativeEvent); - case 'topKeyPress': + case TOP_KEY_PRESS: /** * If native `textInput` events are available, our goal is to make * use of them. However, there is a special case: the spacebar key. @@ -283,7 +295,7 @@ function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { hasSpaceKeypress = true; return SPACEBAR_CHAR; - case 'topTextInput': + case TOP_TEXT_INPUT: // Record the characters to be added to the DOM. const chars = nativeEvent.data; @@ -306,18 +318,18 @@ function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { * For browsers that do not provide the `textInput` event, extract the * appropriate string to use for SyntheticInputEvent. * - * @param {string} topLevelType Record from `BrowserEventConstants`. + * @param {number} topLevelType Number from `TopLevelEventTypes`. * @param {object} nativeEvent Native browser event. * @return {?string} The fallback string for this `beforeInput` event. */ -function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { +function getFallbackBeforeInputChars(topLevelType: TopLevelType, nativeEvent) { // If we are currently composing (IME) and using a fallback to do so, // try to extract the composed characters from the fallback object. // If composition event is available, we extract a string only at // compositionevent, otherwise extract it at fallback events. if (isComposing) { if ( - topLevelType === 'topCompositionEnd' || + topLevelType === TOP_COMPOSITION_END || (!canUseCompositionEvent && isFallbackCompositionEnd(topLevelType, nativeEvent)) ) { @@ -330,11 +342,11 @@ function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { } switch (topLevelType) { - case 'topPaste': + case TOP_PASTE: // If a paste event occurs after a keypress, throw out the input // chars. Paste events should not lead to BeforeInput events. return null; - case 'topKeyPress': + case TOP_KEY_PRESS: /** * As of v27, Firefox may fire keypress events even when no character * will be inserted. A few possibilities: @@ -365,7 +377,7 @@ function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { } } return null; - case 'topCompositionEnd': + case TOP_COMPOSITION_END: return useFallbackCompositionData ? null : nativeEvent.data; default: return null; diff --git a/packages/react-dom/src/events/BrowserEventConstants.js b/packages/react-dom/src/events/BrowserEventConstants.js deleted file mode 100644 index 203752892f..0000000000 --- a/packages/react-dom/src/events/BrowserEventConstants.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import getVendorPrefixedEventName from './getVendorPrefixedEventName'; - -/** - * Types of raw signals from the browser caught at the top level. - * - * For events like 'submit' or audio/video events which don't consistently - * bubble (which we trap at a lower node than `document`), binding - * at `document` would cause duplicate events so we don't include them here. - */ -export const topLevelTypes = { - topAnimationEnd: getVendorPrefixedEventName('animationend'), - topAnimationIteration: getVendorPrefixedEventName('animationiteration'), - topAnimationStart: getVendorPrefixedEventName('animationstart'), - topBlur: 'blur', - topCancel: 'cancel', - topChange: 'change', - topClick: 'click', - topClose: 'close', - topCompositionEnd: 'compositionend', - topCompositionStart: 'compositionstart', - topCompositionUpdate: 'compositionupdate', - topContextMenu: 'contextmenu', - topCopy: 'copy', - topCut: 'cut', - topDoubleClick: 'dblclick', - topDrag: 'drag', - topDragEnd: 'dragend', - topDragEnter: 'dragenter', - topDragExit: 'dragexit', - topDragLeave: 'dragleave', - topDragOver: 'dragover', - topDragStart: 'dragstart', - topDrop: 'drop', - topFocus: 'focus', - topInput: 'input', - topKeyDown: 'keydown', - topKeyPress: 'keypress', - topKeyUp: 'keyup', - topLoad: 'load', - topLoadStart: 'loadstart', - topMouseDown: 'mousedown', - topMouseMove: 'mousemove', - topMouseOut: 'mouseout', - topMouseOver: 'mouseover', - topMouseUp: 'mouseup', - topPaste: 'paste', - topScroll: 'scroll', - topSelectionChange: 'selectionchange', - topTextInput: 'textInput', - topToggle: 'toggle', - topTouchCancel: 'touchcancel', - topTouchEnd: 'touchend', - topTouchMove: 'touchmove', - topTouchStart: 'touchstart', - topTransitionEnd: getVendorPrefixedEventName('transitionend'), - topWheel: 'wheel', -}; - -// There are so many media events, it makes sense to just -// maintain a list of them. Note these aren't technically -// "top-level" since they don't bubble. We should come up -// with a better naming convention if we come to refactoring -// the event system. -export const mediaEventTypes = { - topAbort: 'abort', - topCanPlay: 'canplay', - topCanPlayThrough: 'canplaythrough', - topDurationChange: 'durationchange', - topEmptied: 'emptied', - topEncrypted: 'encrypted', - topEnded: 'ended', - topError: 'error', - topLoadedData: 'loadeddata', - topLoadedMetadata: 'loadedmetadata', - topLoadStart: 'loadstart', - topPause: 'pause', - topPlay: 'play', - topPlaying: 'playing', - topProgress: 'progress', - topRateChange: 'ratechange', - topSeeked: 'seeked', - topSeeking: 'seeking', - topStalled: 'stalled', - topSuspend: 'suspend', - topTimeUpdate: 'timeupdate', - topVolumeChange: 'volumechange', - topWaiting: 'waiting', -}; - -export type TopLevelTypes = $Enum; diff --git a/packages/react-dom/src/events/ChangeEventPlugin.js b/packages/react-dom/src/events/ChangeEventPlugin.js index 5e0dcbb3be..ce0d328d87 100644 --- a/packages/react-dom/src/events/ChangeEventPlugin.js +++ b/packages/react-dom/src/events/ChangeEventPlugin.js @@ -13,6 +13,16 @@ import SyntheticEvent from 'events/SyntheticEvent'; import isTextInputElement from 'shared/isTextInputElement'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; +import { + TOP_BLUR, + TOP_CHANGE, + TOP_CLICK, + TOP_FOCUS, + TOP_INPUT, + TOP_KEY_DOWN, + TOP_KEY_UP, + TOP_SELECTION_CHANGE, +} from './DOMTopLevelEventTypes'; import getEventTarget from './getEventTarget'; import isEventSupported from './isEventSupported'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; @@ -26,14 +36,14 @@ const eventTypes = { captured: 'onChangeCapture', }, dependencies: [ - 'topBlur', - 'topChange', - 'topClick', - 'topFocus', - 'topInput', - 'topKeyDown', - 'topKeyUp', - 'topSelectionChange', + TOP_BLUR, + TOP_CHANGE, + TOP_CLICK, + TOP_FOCUS, + TOP_INPUT, + TOP_KEY_DOWN, + TOP_KEY_UP, + TOP_SELECTION_CHANGE, ], }, }; @@ -100,7 +110,7 @@ function getInstIfValueChanged(targetInst) { } function getTargetInstForChangeEvent(topLevelType, targetInst) { - if (topLevelType === 'topChange') { + if (topLevelType === TOP_CHANGE) { return targetInst; } } @@ -155,7 +165,7 @@ function handlePropertyChange(nativeEvent) { } function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) { - if (topLevelType === 'topFocus') { + if (topLevelType === TOP_FOCUS) { // In IE9, propertychange fires for most input events but is buggy and // doesn't fire when text is deleted, but conveniently, selectionchange // appears to fire in all of the remaining cases so we catch those and @@ -168,7 +178,7 @@ function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) { // missed a blur event somehow. stopWatchingForValueChange(); startWatchingForValueChange(target, targetInst); - } else if (topLevelType === 'topBlur') { + } else if (topLevelType === TOP_BLUR) { stopWatchingForValueChange(); } } @@ -176,9 +186,9 @@ function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) { // For IE8 and IE9. function getTargetInstForInputEventPolyfill(topLevelType, targetInst) { if ( - topLevelType === 'topSelectionChange' || - topLevelType === 'topKeyUp' || - topLevelType === 'topKeyDown' + topLevelType === TOP_SELECTION_CHANGE || + topLevelType === TOP_KEY_UP || + topLevelType === TOP_KEY_DOWN ) { // On the selectionchange event, the target is just document which isn't // helpful for us so just check activeElement instead. @@ -210,13 +220,13 @@ function shouldUseClickEvent(elem) { } function getTargetInstForClickEvent(topLevelType, targetInst) { - if (topLevelType === 'topClick') { + if (topLevelType === TOP_CLICK) { return getInstIfValueChanged(targetInst); } } function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) { - if (topLevelType === 'topInput' || topLevelType === 'topChange') { + if (topLevelType === TOP_INPUT || topLevelType === TOP_CHANGE) { return getInstIfValueChanged(targetInst); } } @@ -292,7 +302,7 @@ const ChangeEventPlugin = { } // When blurring, set the value attribute for number inputs - if (topLevelType === 'topBlur') { + if (topLevelType === TOP_BLUR) { handleControlledInputBlur(targetInst, targetNode); } }, diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js new file mode 100644 index 0000000000..79f13057f9 --- /dev/null +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {TopLevelType} from 'events/TopLevelEventTypes'; +import getVendorPrefixedEventName from './getVendorPrefixedEventName'; + +/** + * To identify top level events in react-dom, we use constants defined by this + * module. Those are completely opaque to every other module but we rely on them + * being the raw DOM event names inside this module. This allows us to build a + * very efficient mapping from top level identifiers to the raw event type. + * + * The use of an `opaque` flow type makes sure that we can only access the value + * of a constant in this module. + */ + +// eslint-disable-next-line no-undef +export opaque type DOMTopLevelEventType = + | 'abort' + | 'animationend' + | 'animationiteration' + | 'animationstart' + | 'blur' + | 'canplay' + | 'canplaythrough' + | 'cancel' + | 'change' + | 'click' + | 'close' + | 'compositionend' + | 'compositionstart' + | 'compositionupdate' + | 'contextmenu' + | 'copy' + | 'cut' + | 'dblclick' + | 'drag' + | 'dragend' + | 'dragenter' + | 'dragexit' + | 'dragleave' + | 'dragover' + | 'dragstart' + | 'drop' + | 'durationchange' + | 'emptied' + | 'encrypted' + | 'ended' + | 'error' + | 'focus' + | 'input' + | 'invalid' + | 'keydown' + | 'keypress' + | 'keyup' + | 'load' + | 'loadstart' + | 'loadeddata' + | 'loadedmetadata' + | 'mousedown' + | 'mousemove' + | 'mouseout' + | 'mouseover' + | 'mouseup' + | 'paste' + | 'pause' + | 'play' + | 'playing' + | 'progress' + | 'ratechange' + | 'reset' + | 'scroll' + | 'seeked' + | 'seeking' + | 'selectionchange' + | 'stalled' + | 'submit' + | 'suspend' + | 'textInput' + | 'timeupdate' + | 'toggle' + | 'touchcancel' + | 'touchend' + | 'touchmove' + | 'touchstart' + | 'transitionend' + | 'volumechange' + | 'waiting' + | 'wheel'; + +export const TOP_ABORT: TopLevelType = 'abort'; +export const TOP_ANIMATION_END: TopLevelType = getVendorPrefixedEventName( + 'animationend', +); +export const TOP_ANIMATION_ITERATION: TopLevelType = getVendorPrefixedEventName( + 'animationiteration', +); +export const TOP_ANIMATION_START: TopLevelType = getVendorPrefixedEventName( + 'animationstart', +); +export const TOP_BLUR: TopLevelType = 'blur'; +export const TOP_CAN_PLAY: TopLevelType = 'canplay'; +export const TOP_CAN_PLAY_THROUGH: TopLevelType = 'canplaythrough'; +export const TOP_CANCEL: TopLevelType = 'cancel'; +export const TOP_CHANGE: TopLevelType = 'change'; +export const TOP_CLICK: TopLevelType = 'click'; +export const TOP_CLOSE: TopLevelType = 'close'; +export const TOP_COMPOSITION_END: TopLevelType = 'compositionend'; +export const TOP_COMPOSITION_START: TopLevelType = 'compositionstart'; +export const TOP_COMPOSITION_UPDATE: TopLevelType = 'compositionupdate'; +export const TOP_CONTEXT_MENU: TopLevelType = 'contextmenu'; +export const TOP_COPY: TopLevelType = 'copy'; +export const TOP_CUT: TopLevelType = 'cut'; +export const TOP_DOUBLE_CLICK: TopLevelType = 'dblclick'; +export const TOP_DRAG: TopLevelType = 'drag'; +export const TOP_DRAG_END: TopLevelType = 'dragend'; +export const TOP_DRAG_ENTER: TopLevelType = 'dragenter'; +export const TOP_DRAG_EXIT: TopLevelType = 'dragexit'; +export const TOP_DRAG_LEAVE: TopLevelType = 'dragleave'; +export const TOP_DRAG_OVER: TopLevelType = 'dragover'; +export const TOP_DRAG_START: TopLevelType = 'dragstart'; +export const TOP_DROP: TopLevelType = 'drop'; +export const TOP_DURATION_CHANGE: TopLevelType = 'durationchange'; +export const TOP_EMPTIED: TopLevelType = 'emptied'; +export const TOP_ENCRYPTED: TopLevelType = 'encrypted'; +export const TOP_ENDED: TopLevelType = 'ended'; +export const TOP_ERROR: TopLevelType = 'error'; +export const TOP_FOCUS: TopLevelType = 'focus'; +export const TOP_INPUT: TopLevelType = 'input'; +export const TOP_INVALID: TopLevelType = 'invalid'; +export const TOP_KEY_DOWN: TopLevelType = 'keydown'; +export const TOP_KEY_PRESS: TopLevelType = 'keypress'; +export const TOP_KEY_UP: TopLevelType = 'keyup'; +export const TOP_LOAD: TopLevelType = 'load'; +export const TOP_LOAD_START: TopLevelType = 'loadstart'; +export const TOP_LOADED_DATA: TopLevelType = 'loadeddata'; +export const TOP_LOADED_METADATA: TopLevelType = 'loadedmetadata'; +export const TOP_MOUSE_DOWN: TopLevelType = 'mousedown'; +export const TOP_MOUSE_MOVE: TopLevelType = 'mousemove'; +export const TOP_MOUSE_OUT: TopLevelType = 'mouseout'; +export const TOP_MOUSE_OVER: TopLevelType = 'mouseover'; +export const TOP_MOUSE_UP: TopLevelType = 'mouseup'; +export const TOP_PASTE: TopLevelType = 'paste'; +export const TOP_PAUSE: TopLevelType = 'pause'; +export const TOP_PLAY: TopLevelType = 'play'; +export const TOP_PLAYING: TopLevelType = 'playing'; +export const TOP_PROGRESS: TopLevelType = 'progress'; +export const TOP_RATE_CHANGE: TopLevelType = 'ratechange'; +export const TOP_RESET: TopLevelType = 'reset'; +export const TOP_SCROLL: TopLevelType = 'scroll'; +export const TOP_SEEKED: TopLevelType = 'seeked'; +export const TOP_SEEKING: TopLevelType = 'seeking'; +export const TOP_SELECTION_CHANGE: TopLevelType = 'selectionchange'; +export const TOP_STALLED: TopLevelType = 'stalled'; +export const TOP_SUBMIT: TopLevelType = 'submit'; +export const TOP_SUSPEND: TopLevelType = 'suspend'; +export const TOP_TEXT_INPUT: TopLevelType = 'textInput'; +export const TOP_TIME_UPDATE: TopLevelType = 'timeupdate'; +export const TOP_TOGGLE: TopLevelType = 'toggle'; +export const TOP_TOUCH_CANCEL: TopLevelType = 'touchcancel'; +export const TOP_TOUCH_END: TopLevelType = 'touchend'; +export const TOP_TOUCH_MOVE: TopLevelType = 'touchmove'; +export const TOP_TOUCH_START: TopLevelType = 'touchstart'; +export const TOP_TRANSITION_END: TopLevelType = getVendorPrefixedEventName( + 'transitionend', +); +export const TOP_VOLUME_CHANGE: TopLevelType = 'volumechange'; +export const TOP_WAITING: TopLevelType = 'waiting'; +export const TOP_WHEEL: TopLevelType = 'wheel'; + +export const mediaEventTypes: Array = [ + TOP_ABORT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_LOAD_START, + TOP_PAUSE, + TOP_PLAY, + TOP_PLAYING, + TOP_PROGRESS, + TOP_RATE_CHANGE, + TOP_SEEKED, + TOP_SEEKING, + TOP_STALLED, + TOP_SUSPEND, + TOP_TIME_UPDATE, + TOP_VOLUME_CHANGE, + TOP_WAITING, +]; + +export function getRawEventName(topLevelType: TopLevelType): string { + return topLevelType; +} diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js index 9a2be568c1..3e15dd8a90 100644 --- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js @@ -7,6 +7,7 @@ import {accumulateEnterLeaveDispatches} from 'events/EventPropagators'; +import {TOP_MOUSE_OUT, TOP_MOUSE_OVER} from './DOMTopLevelEventTypes'; import SyntheticMouseEvent from './SyntheticMouseEvent'; import { getClosestInstanceFromNode, @@ -16,11 +17,11 @@ import { const eventTypes = { mouseEnter: { registrationName: 'onMouseEnter', - dependencies: ['topMouseOut', 'topMouseOver'], + dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER], }, mouseLeave: { registrationName: 'onMouseLeave', - dependencies: ['topMouseOut', 'topMouseOver'], + dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER], }, }; @@ -41,12 +42,12 @@ const EnterLeaveEventPlugin = { nativeEventTarget, ) { if ( - topLevelType === 'topMouseOver' && + topLevelType === TOP_MOUSE_OVER && (nativeEvent.relatedTarget || nativeEvent.fromElement) ) { return null; } - if (topLevelType !== 'topMouseOut' && topLevelType !== 'topMouseOver') { + if (topLevelType !== TOP_MOUSE_OUT && topLevelType !== TOP_MOUSE_OVER) { // Must not be a mouse in or mouse out - ignoring. return null; } @@ -67,7 +68,7 @@ const EnterLeaveEventPlugin = { let from; let to; - if (topLevelType === 'topMouseOut') { + if (topLevelType === TOP_MOUSE_OUT) { from = targetInst; const related = nativeEvent.relatedTarget || nativeEvent.toElement; to = related ? getClosestInstanceFromNode(related) : null; diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index e87c3886eb..8bec245b13 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -8,7 +8,7 @@ */ export function addEventBubbleListener( - element: Element, + element: Document | Element, eventType: string, listener: Function, ): void { @@ -16,7 +16,7 @@ export function addEventBubbleListener( } export function addEventCaptureListener( - element: Element, + element: Document | Element, eventType: string, listener: Function, ): void { diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 8991734a75..0a2742a654 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -3,9 +3,18 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @flow */ import {registrationNameDependencies} from 'events/EventPluginRegistry'; +import { + TOP_BLUR, + TOP_CANCEL, + TOP_CLOSE, + TOP_FOCUS, + TOP_SCROLL, +} from './DOMTopLevelEventTypes'; import { setEnabled, isEnabled, @@ -13,7 +22,6 @@ import { trapCapturedEvent, } from './ReactDOMEventListener'; import isEventSupported from './isEventSupported'; -import {topLevelTypes} from './BrowserEventConstants'; /** * Summary of `ReactBrowserEventEmitter` event handling: @@ -79,7 +87,7 @@ let reactTopListenersCounter = 0; */ const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2); -function getListeningForDocument(mountAt) { +function getListeningForDocument(mountAt: any) { // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty` // directly. if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) { @@ -108,37 +116,39 @@ function getListeningForDocument(mountAt) { * they bubble to document. * * @param {string} registrationName Name of listener (e.g. `onClick`). - * @param {object} contentDocumentHandle Document which owns the container + * @param {object} mountAt Container where to mount the listener */ -export function listenTo(registrationName, contentDocumentHandle) { - const mountAt = contentDocumentHandle; +export function listenTo( + registrationName: string, + mountAt: Document | Element, +) { const isListening = getListeningForDocument(mountAt); const dependencies = registrationNameDependencies[registrationName]; for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { - if (dependency === 'topScroll') { - trapCapturedEvent('topScroll', 'scroll', mountAt); - } else if (dependency === 'topFocus' || dependency === 'topBlur') { - trapCapturedEvent('topFocus', 'focus', mountAt); - trapCapturedEvent('topBlur', 'blur', mountAt); + if (dependency === TOP_SCROLL) { + trapCapturedEvent(TOP_SCROLL, mountAt); + } else if (dependency === TOP_FOCUS || dependency === TOP_BLUR) { + trapCapturedEvent(TOP_FOCUS, mountAt); + trapCapturedEvent(TOP_BLUR, mountAt); // to make sure blur and focus event listeners are only attached once - isListening.topBlur = true; - isListening.topFocus = true; - } else if (dependency === 'topCancel') { + isListening[TOP_BLUR] = true; + isListening[TOP_FOCUS] = true; + } else if (dependency === TOP_CANCEL) { if (isEventSupported('cancel', true)) { - trapCapturedEvent('topCancel', 'cancel', mountAt); + trapCapturedEvent(TOP_CANCEL, mountAt); } - isListening.topCancel = true; - } else if (dependency === 'topClose') { + isListening[TOP_CANCEL] = true; + } else if (dependency === TOP_CLOSE) { if (isEventSupported('close', true)) { - trapCapturedEvent('topClose', 'close', mountAt); + trapCapturedEvent(TOP_CLOSE, mountAt); } - isListening.topClose = true; - } else if (topLevelTypes.hasOwnProperty(dependency)) { - trapBubbledEvent(dependency, topLevelTypes[dependency], mountAt); + isListening[TOP_CLOSE] = true; + } else { + trapBubbledEvent(dependency, mountAt); } isListening[dependency] = true; @@ -146,7 +156,10 @@ export function listenTo(registrationName, contentDocumentHandle) { } } -export function isListeningToAllDependencies(registrationName, mountAt) { +export function isListeningToAllDependencies( + registrationName: string, + mountAt: Document | Element, +) { const isListening = getListeningForDocument(mountAt); const dependencies = registrationNameDependencies[registrationName]; for (let i = 0; i < dependencies.length; i++) { diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index df831eb848..0125a8437b 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -3,17 +3,23 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @flow */ import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; import {isFiberMounted} from 'react-reconciler/reflection'; import {HostRoot} from 'shared/ReactTypeOfWork'; +import type {AnyNativeEvent} from 'events/PluginModuleType'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import SimpleEventPlugin from './SimpleEventPlugin'; +import {getRawEventName} from './DOMTopLevelEventTypes'; const {isInteractiveTopLevelEventType} = SimpleEventPlugin; @@ -40,7 +46,16 @@ function findRootContainerNode(inst) { } // Used to store ancestor hierarchy in top level callback -function getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst) { +function getTopLevelCallbackBookKeeping( + topLevelType, + nativeEvent, + targetInst, +): { + topLevelType: ?TopLevelType, + nativeEvent: ?AnyNativeEvent, + targetInst: Fiber, + ancestors: Array, +} { if (callbackBookkeepingPool.length) { const instance = callbackBookkeepingPool.pop(); instance.topLevelType = topLevelType; @@ -101,7 +116,7 @@ function handleTopLevel(bookKeeping) { // TODO: can we stop exporting these? export let _enabled = true; -export function setEnabled(enabled) { +export function setEnabled(enabled: ?boolean) { _enabled = !!enabled; } @@ -112,14 +127,16 @@ export function isEnabled() { /** * Traps top-level events by using event bubbling. * - * @param {string} topLevelType Record from `BrowserEventConstants`. - * @param {string} handlerBaseName Event name (e.g. "click"). + * @param {number} topLevelType Number from `TopLevelEventTypes`. * @param {object} element Element on which to attach listener. * @return {?object} An object with a remove function which will forcefully * remove the listener. * @internal */ -export function trapBubbledEvent(topLevelType, handlerBaseName, element) { +export function trapBubbledEvent( + topLevelType: TopLevelType, + element: Document | Element, +) { if (!element) { return null; } @@ -129,7 +146,7 @@ export function trapBubbledEvent(topLevelType, handlerBaseName, element) { addEventBubbleListener( element, - handlerBaseName, + getRawEventName(topLevelType), // Check if interactive and wrap in interactiveUpdates dispatch.bind(null, topLevelType), ); @@ -138,14 +155,16 @@ export function trapBubbledEvent(topLevelType, handlerBaseName, element) { /** * Traps a top-level event by using event capturing. * - * @param {string} topLevelType Record from `BrowserEventConstants`. - * @param {string} handlerBaseName Event name (e.g. "click"). + * @param {number} topLevelType Number from `TopLevelEventTypes`. * @param {object} element Element on which to attach listener. * @return {?object} An object with a remove function which will forcefully * remove the listener. * @internal */ -export function trapCapturedEvent(topLevelType, handlerBaseName, element) { +export function trapCapturedEvent( + topLevelType: TopLevelType, + element: Document | Element, +) { if (!element) { return null; } @@ -155,7 +174,7 @@ export function trapCapturedEvent(topLevelType, handlerBaseName, element) { addEventCaptureListener( element, - handlerBaseName, + getRawEventName(topLevelType), // Check if interactive and wrap in interactiveUpdates dispatch.bind(null, topLevelType), ); @@ -165,7 +184,10 @@ function dispatchInteractiveEvent(topLevelType, nativeEvent) { interactiveUpdates(dispatchEvent, topLevelType, nativeEvent); } -export function dispatchEvent(topLevelType, nativeEvent) { +export function dispatchEvent( + topLevelType: TopLevelType, + nativeEvent: AnyNativeEvent, +) { if (!_enabled) { return; } diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index e6193c7973..81cc5d5186 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -12,6 +12,16 @@ import isTextInputElement from 'shared/isTextInputElement'; import getActiveElement from 'fbjs/lib/getActiveElement'; import shallowEqual from 'fbjs/lib/shallowEqual'; +import { + TOP_BLUR, + TOP_CONTEXT_MENU, + TOP_FOCUS, + TOP_KEY_DOWN, + TOP_KEY_UP, + TOP_MOUSE_DOWN, + TOP_MOUSE_UP, + TOP_SELECTION_CHANGE, +} from './DOMTopLevelEventTypes'; import {isListeningToAllDependencies} from './ReactBrowserEventEmitter'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; import * as ReactInputSelection from '../client/ReactInputSelection'; @@ -29,14 +39,14 @@ const eventTypes = { captured: 'onSelectCapture', }, dependencies: [ - 'topBlur', - 'topContextMenu', - 'topFocus', - 'topKeyDown', - 'topKeyUp', - 'topMouseDown', - 'topMouseUp', - 'topSelectionChange', + TOP_BLUR, + TOP_CONTEXT_MENU, + TOP_FOCUS, + TOP_KEY_DOWN, + TOP_KEY_UP, + TOP_MOUSE_DOWN, + TOP_MOUSE_UP, + TOP_SELECTION_CHANGE, ], }, }; @@ -156,7 +166,7 @@ const SelectEventPlugin = { switch (topLevelType) { // Track the input node that has focus. - case 'topFocus': + case TOP_FOCUS: if ( isTextInputElement(targetNode) || targetNode.contentEditable === 'true' @@ -166,18 +176,18 @@ const SelectEventPlugin = { lastSelection = null; } break; - case 'topBlur': + case TOP_BLUR: activeElement = null; activeElementInst = null; lastSelection = null; break; // Don't fire the event while the user is dragging. This matches the // semantics of the native select event. - case 'topMouseDown': + case TOP_MOUSE_DOWN: mouseDown = true; break; - case 'topContextMenu': - case 'topMouseUp': + case TOP_CONTEXT_MENU: + case TOP_MOUSE_UP: mouseDown = false; return constructSelectEvent(nativeEvent, nativeEventTarget); // Chrome and IE fire non-standard event when selection is changed (and @@ -189,13 +199,13 @@ const SelectEventPlugin = { // keyup, but we check on keydown as well in the case of holding down a // key, when multiple keydown events are fired but only one keyup is. // This is also our approach for IE handling, for the reason above. - case 'topSelectionChange': + case TOP_SELECTION_CHANGE: if (skipSelectionChangeEvent) { break; } // falls through - case 'topKeyDown': - case 'topKeyUp': + case TOP_KEY_DOWN: + case TOP_KEY_UP: return constructSelectEvent(nativeEvent, nativeEventTarget); } diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 602624dd31..6da1d244c9 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -7,7 +7,7 @@ * @flow */ -import type {TopLevelTypes} from './BrowserEventConstants'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; import type { DispatchConfig, ReactSyntheticEvent, @@ -17,6 +17,8 @@ import type {EventTypes, PluginModule} from 'events/PluginModuleType'; import {accumulateTwoPhaseDispatches} from 'events/EventPropagators'; import SyntheticEvent from 'events/SyntheticEvent'; + +import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes'; import warning from 'fbjs/lib/warning'; import SyntheticAnimationEvent from './SyntheticAnimationEvent'; @@ -41,93 +43,96 @@ import getEventCharCode from './getEventCharCode'; * bubbled: 'onAbort', * captured: 'onAbortCapture', * }, - * dependencies: ['topAbort'], + * dependencies: [TOP_ABORT], * }, * ... * }; - * topLevelEventsToDispatchConfig = { - * 'topAbort': { sameConfig } - * }; + * topLevelEventsToDispatchConfig = new Map([ + * [TOP_ABORT, { sameConfig }], + * ]); */ -const interactiveEventTypeNames: Array = [ - 'blur', - 'cancel', - 'click', - 'close', - 'contextMenu', - 'copy', - 'cut', - 'doubleClick', - 'dragEnd', - 'dragStart', - 'drop', - 'focus', - 'input', - 'invalid', - 'keyDown', - 'keyPress', - 'keyUp', - 'mouseDown', - 'mouseUp', - 'paste', - 'pause', - 'play', - 'rateChange', - 'reset', - 'seeked', - 'submit', - 'touchCancel', - 'touchEnd', - 'touchStart', - 'volumeChange', +type EventTuple = [TopLevelType, string]; +const interactiveEventTypeNames: Array = [ + [DOMTopLevelEventTypes.TOP_BLUR, 'blur'], + [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'], + [DOMTopLevelEventTypes.TOP_CLICK, 'click'], + [DOMTopLevelEventTypes.TOP_CLOSE, 'close'], + [DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu'], + [DOMTopLevelEventTypes.TOP_COPY, 'copy'], + [DOMTopLevelEventTypes.TOP_CUT, 'cut'], + [DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick'], + [DOMTopLevelEventTypes.TOP_DRAG_END, 'dragEnd'], + [DOMTopLevelEventTypes.TOP_DRAG_START, 'dragStart'], + [DOMTopLevelEventTypes.TOP_DROP, 'drop'], + [DOMTopLevelEventTypes.TOP_FOCUS, 'focus'], + [DOMTopLevelEventTypes.TOP_INPUT, 'input'], + [DOMTopLevelEventTypes.TOP_INVALID, 'invalid'], + [DOMTopLevelEventTypes.TOP_KEY_DOWN, 'keyDown'], + [DOMTopLevelEventTypes.TOP_KEY_PRESS, 'keyPress'], + [DOMTopLevelEventTypes.TOP_KEY_UP, 'keyUp'], + [DOMTopLevelEventTypes.TOP_MOUSE_DOWN, 'mouseDown'], + [DOMTopLevelEventTypes.TOP_MOUSE_UP, 'mouseUp'], + [DOMTopLevelEventTypes.TOP_PASTE, 'paste'], + [DOMTopLevelEventTypes.TOP_PAUSE, 'pause'], + [DOMTopLevelEventTypes.TOP_PLAY, 'play'], + [DOMTopLevelEventTypes.TOP_RATE_CHANGE, 'rateChange'], + [DOMTopLevelEventTypes.TOP_RESET, 'reset'], + [DOMTopLevelEventTypes.TOP_SEEKED, 'seeked'], + [DOMTopLevelEventTypes.TOP_SUBMIT, 'submit'], + [DOMTopLevelEventTypes.TOP_TOUCH_CANCEL, 'touchCancel'], + [DOMTopLevelEventTypes.TOP_TOUCH_END, 'touchEnd'], + [DOMTopLevelEventTypes.TOP_TOUCH_START, 'touchStart'], + [DOMTopLevelEventTypes.TOP_VOLUME_CHANGE, 'volumeChange'], ]; -const nonInteractiveEventTypeNames: Array = [ - 'abort', - 'animationEnd', - 'animationIteration', - 'animationStart', - 'canPlay', - 'canPlayThrough', - 'drag', - 'dragEnter', - 'dragExit', - 'dragLeave', - 'dragOver', - 'durationChange', - 'emptied', - 'encrypted', - 'ended', - 'error', - 'load', - 'loadedData', - 'loadedMetadata', - 'loadStart', - 'mouseMove', - 'mouseOut', - 'mouseOver', - 'playing', - 'progress', - 'scroll', - 'seeking', - 'stalled', - 'suspend', - 'timeUpdate', - 'toggle', - 'touchMove', - 'transitionEnd', - 'waiting', - 'wheel', +const nonInteractiveEventTypeNames: Array = [ + [DOMTopLevelEventTypes.TOP_ABORT, 'abort'], + [DOMTopLevelEventTypes.TOP_ANIMATION_END, 'animationEnd'], + [DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION, 'animationIteration'], + [DOMTopLevelEventTypes.TOP_ANIMATION_START, 'animationStart'], + [DOMTopLevelEventTypes.TOP_CAN_PLAY, 'canPlay'], + [DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH, 'canPlayThrough'], + [DOMTopLevelEventTypes.TOP_DRAG, 'drag'], + [DOMTopLevelEventTypes.TOP_DRAG_ENTER, 'dragEnter'], + [DOMTopLevelEventTypes.TOP_DRAG_EXIT, 'dragExit'], + [DOMTopLevelEventTypes.TOP_DRAG_LEAVE, 'dragLeave'], + [DOMTopLevelEventTypes.TOP_DRAG_OVER, 'dragOver'], + [DOMTopLevelEventTypes.TOP_DURATION_CHANGE, 'durationChange'], + [DOMTopLevelEventTypes.TOP_EMPTIED, 'emptied'], + [DOMTopLevelEventTypes.TOP_ENCRYPTED, 'encrypted'], + [DOMTopLevelEventTypes.TOP_ENDED, 'ended'], + [DOMTopLevelEventTypes.TOP_ERROR, 'error'], + [DOMTopLevelEventTypes.TOP_LOAD, 'load'], + [DOMTopLevelEventTypes.TOP_LOADED_DATA, 'loadedData'], + [DOMTopLevelEventTypes.TOP_LOADED_METADATA, 'loadedMetadata'], + [DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'], + [DOMTopLevelEventTypes.TOP_MOUSE_MOVE, 'mouseMove'], + [DOMTopLevelEventTypes.TOP_MOUSE_OUT, 'mouseOut'], + [DOMTopLevelEventTypes.TOP_MOUSE_OVER, 'mouseOver'], + [DOMTopLevelEventTypes.TOP_PLAYING, 'playing'], + [DOMTopLevelEventTypes.TOP_PROGRESS, 'progress'], + [DOMTopLevelEventTypes.TOP_SCROLL, 'scroll'], + [DOMTopLevelEventTypes.TOP_SEEKING, 'seeking'], + [DOMTopLevelEventTypes.TOP_STALLED, 'stalled'], + [DOMTopLevelEventTypes.TOP_SUSPEND, 'suspend'], + [DOMTopLevelEventTypes.TOP_TIME_UPDATE, 'timeUpdate'], + [DOMTopLevelEventTypes.TOP_TOGGLE, 'toggle'], + [DOMTopLevelEventTypes.TOP_TOUCH_MOVE, 'touchMove'], + [DOMTopLevelEventTypes.TOP_TRANSITION_END, 'transitionEnd'], + [DOMTopLevelEventTypes.TOP_WAITING, 'waiting'], + [DOMTopLevelEventTypes.TOP_WHEEL, 'wheel'], ]; const eventTypes: EventTypes = {}; const topLevelEventsToDispatchConfig: { - [key: TopLevelTypes]: DispatchConfig, + [key: TopLevelType]: DispatchConfig, } = {}; -function addEventTypeNameToConfig(event: string, isInteractive: boolean) { +function addEventTypeNameToConfig( + [topEvent, event]: EventTuple, + isInteractive: boolean, +) { const capitalizedEvent = event[0].toUpperCase() + event.slice(1); const onEvent = 'on' + capitalizedEvent; - const topEvent = 'top' + capitalizedEvent; const type = { phasedRegistrationNames: { @@ -141,58 +146,60 @@ function addEventTypeNameToConfig(event: string, isInteractive: boolean) { topLevelEventsToDispatchConfig[topEvent] = type; } -interactiveEventTypeNames.forEach(eventTypeName => { - addEventTypeNameToConfig(eventTypeName, true); +interactiveEventTypeNames.forEach(eventTuple => { + addEventTypeNameToConfig(eventTuple, true); }); -nonInteractiveEventTypeNames.forEach(eventTypeName => { - addEventTypeNameToConfig(eventTypeName, false); +nonInteractiveEventTypeNames.forEach(eventTuple => { + addEventTypeNameToConfig(eventTuple, false); }); // Only used in DEV for exhaustiveness validation. -const knownHTMLTopLevelTypes = [ - 'topAbort', - 'topCancel', - 'topCanPlay', - 'topCanPlayThrough', - 'topClose', - 'topDurationChange', - 'topEmptied', - 'topEncrypted', - 'topEnded', - 'topError', - 'topInput', - 'topInvalid', - 'topLoad', - 'topLoadedData', - 'topLoadedMetadata', - 'topLoadStart', - 'topPause', - 'topPlay', - 'topPlaying', - 'topProgress', - 'topRateChange', - 'topReset', - 'topSeeked', - 'topSeeking', - 'topStalled', - 'topSubmit', - 'topSuspend', - 'topTimeUpdate', - 'topToggle', - 'topVolumeChange', - 'topWaiting', +const knownHTMLTopLevelTypes: Array = [ + DOMTopLevelEventTypes.TOP_ABORT, + DOMTopLevelEventTypes.TOP_CANCEL, + DOMTopLevelEventTypes.TOP_CAN_PLAY, + DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH, + DOMTopLevelEventTypes.TOP_CLOSE, + DOMTopLevelEventTypes.TOP_DURATION_CHANGE, + DOMTopLevelEventTypes.TOP_EMPTIED, + DOMTopLevelEventTypes.TOP_ENCRYPTED, + DOMTopLevelEventTypes.TOP_ENDED, + DOMTopLevelEventTypes.TOP_ERROR, + DOMTopLevelEventTypes.TOP_INPUT, + DOMTopLevelEventTypes.TOP_INVALID, + DOMTopLevelEventTypes.TOP_LOAD, + DOMTopLevelEventTypes.TOP_LOADED_DATA, + DOMTopLevelEventTypes.TOP_LOADED_METADATA, + DOMTopLevelEventTypes.TOP_LOAD_START, + DOMTopLevelEventTypes.TOP_PAUSE, + DOMTopLevelEventTypes.TOP_PLAY, + DOMTopLevelEventTypes.TOP_PLAYING, + DOMTopLevelEventTypes.TOP_PROGRESS, + DOMTopLevelEventTypes.TOP_RATE_CHANGE, + DOMTopLevelEventTypes.TOP_RESET, + DOMTopLevelEventTypes.TOP_SEEKED, + DOMTopLevelEventTypes.TOP_SEEKING, + DOMTopLevelEventTypes.TOP_STALLED, + DOMTopLevelEventTypes.TOP_SUBMIT, + DOMTopLevelEventTypes.TOP_SUSPEND, + DOMTopLevelEventTypes.TOP_TIME_UPDATE, + DOMTopLevelEventTypes.TOP_TOGGLE, + DOMTopLevelEventTypes.TOP_VOLUME_CHANGE, + DOMTopLevelEventTypes.TOP_WAITING, ]; -const SimpleEventPlugin: PluginModule = { +const SimpleEventPlugin: PluginModule & { + isInteractiveTopLevelEventType: (topLevelType: TopLevelType) => boolean, +} = { eventTypes: eventTypes, - isInteractiveTopLevelEventType(topLevelType: TopLevelTypes): boolean { + isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean { const config = topLevelEventsToDispatchConfig[topLevelType]; return config !== undefined && config.isInteractive === true; }, extractEvents: function( - topLevelType: TopLevelTypes, + topLevelType: TopLevelType, targetInst: Fiber, nativeEvent: MouseEvent, nativeEventTarget: EventTarget, @@ -203,7 +210,7 @@ const SimpleEventPlugin: PluginModule = { } let EventConstructor; switch (topLevelType) { - case 'topKeyPress': + case DOMTopLevelEventTypes.TOP_KEY_PRESS: // Firefox creates a keypress event for function keys too. This removes // the unwanted keypress events. Enter is however both printable and // non-printable. One would expect Tab to be as well (but it isn't). @@ -211,65 +218,65 @@ const SimpleEventPlugin: PluginModule = { return null; } /* falls through */ - case 'topKeyDown': - case 'topKeyUp': + case DOMTopLevelEventTypes.TOP_KEY_DOWN: + case DOMTopLevelEventTypes.TOP_KEY_UP: EventConstructor = SyntheticKeyboardEvent; break; - case 'topBlur': - case 'topFocus': + case DOMTopLevelEventTypes.TOP_BLUR: + case DOMTopLevelEventTypes.TOP_FOCUS: EventConstructor = SyntheticFocusEvent; break; - case 'topClick': + case DOMTopLevelEventTypes.TOP_CLICK: // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return null; } /* falls through */ - case 'topDoubleClick': - case 'topMouseDown': - case 'topMouseMove': - case 'topMouseUp': + case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK: + case DOMTopLevelEventTypes.TOP_MOUSE_DOWN: + case DOMTopLevelEventTypes.TOP_MOUSE_MOVE: + case DOMTopLevelEventTypes.TOP_MOUSE_UP: // TODO: Disabled elements should not respond to mouse events /* falls through */ - case 'topMouseOut': - case 'topMouseOver': - case 'topContextMenu': + case DOMTopLevelEventTypes.TOP_MOUSE_OUT: + case DOMTopLevelEventTypes.TOP_MOUSE_OVER: + case DOMTopLevelEventTypes.TOP_CONTEXT_MENU: EventConstructor = SyntheticMouseEvent; break; - case 'topDrag': - case 'topDragEnd': - case 'topDragEnter': - case 'topDragExit': - case 'topDragLeave': - case 'topDragOver': - case 'topDragStart': - case 'topDrop': + case DOMTopLevelEventTypes.TOP_DRAG: + case DOMTopLevelEventTypes.TOP_DRAG_END: + case DOMTopLevelEventTypes.TOP_DRAG_ENTER: + case DOMTopLevelEventTypes.TOP_DRAG_EXIT: + case DOMTopLevelEventTypes.TOP_DRAG_LEAVE: + case DOMTopLevelEventTypes.TOP_DRAG_OVER: + case DOMTopLevelEventTypes.TOP_DRAG_START: + case DOMTopLevelEventTypes.TOP_DROP: EventConstructor = SyntheticDragEvent; break; - case 'topTouchCancel': - case 'topTouchEnd': - case 'topTouchMove': - case 'topTouchStart': + case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL: + case DOMTopLevelEventTypes.TOP_TOUCH_END: + case DOMTopLevelEventTypes.TOP_TOUCH_MOVE: + case DOMTopLevelEventTypes.TOP_TOUCH_START: EventConstructor = SyntheticTouchEvent; break; - case 'topAnimationEnd': - case 'topAnimationIteration': - case 'topAnimationStart': + case DOMTopLevelEventTypes.TOP_ANIMATION_END: + case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION: + case DOMTopLevelEventTypes.TOP_ANIMATION_START: EventConstructor = SyntheticAnimationEvent; break; - case 'topTransitionEnd': + case DOMTopLevelEventTypes.TOP_TRANSITION_END: EventConstructor = SyntheticTransitionEvent; break; - case 'topScroll': + case DOMTopLevelEventTypes.TOP_SCROLL: EventConstructor = SyntheticUIEvent; break; - case 'topWheel': + case DOMTopLevelEventTypes.TOP_WHEEL: EventConstructor = SyntheticWheelEvent; break; - case 'topCopy': - case 'topCut': - case 'topPaste': + case DOMTopLevelEventTypes.TOP_COPY: + case DOMTopLevelEventTypes.TOP_CUT: + case DOMTopLevelEventTypes.TOP_PASTE: EventConstructor = SyntheticClipboardEvent; break; default: diff --git a/packages/react-dom/src/events/TapEventPlugin.js b/packages/react-dom/src/events/TapEventPlugin.js index e1e67a5d70..772e5d405f 100644 --- a/packages/react-dom/src/events/TapEventPlugin.js +++ b/packages/react-dom/src/events/TapEventPlugin.js @@ -7,12 +7,33 @@ * @flow */ -import {isStartish, isEndish} from 'events/EventPluginUtils'; import {accumulateTwoPhaseDispatches} from 'events/EventPropagators'; import TouchEventUtils from 'fbjs/lib/TouchEventUtils'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; +import { + TOP_MOUSE_DOWN, + TOP_MOUSE_MOVE, + TOP_MOUSE_UP, + TOP_TOUCH_CANCEL, + TOP_TOUCH_END, + TOP_TOUCH_MOVE, + TOP_TOUCH_START, +} from './DOMTopLevelEventTypes'; import SyntheticUIEvent from './SyntheticUIEvent'; +function isStartish(topLevelType) { + return topLevelType === TOP_MOUSE_DOWN || topLevelType === TOP_TOUCH_START; +} + +function isEndish(topLevelType) { + return ( + topLevelType === TOP_MOUSE_UP || + topLevelType === TOP_TOUCH_END || + topLevelType === TOP_TOUCH_CANCEL + ); +} + /** * We are extending the Flow 'Touch' declaration to enable using bracket * notation to access properties. @@ -75,13 +96,13 @@ function getDistance(coords: CoordinatesType, nativeEvent: _Touch): number { } const touchEvents = [ - 'topTouchStart', - 'topTouchCancel', - 'topTouchEnd', - 'topTouchMove', + TOP_TOUCH_START, + TOP_TOUCH_CANCEL, + TOP_TOUCH_END, + TOP_TOUCH_MOVE, ]; -const dependencies = ['topMouseDown', 'topMouseMove', 'topMouseUp'].concat( +const dependencies = [TOP_MOUSE_DOWN, TOP_MOUSE_MOVE, TOP_MOUSE_UP].concat( touchEvents, ); @@ -105,7 +126,7 @@ const TapEventPlugin = { eventTypes: eventTypes, extractEvents: function( - topLevelType: mixed, + topLevelType: TopLevelType, targetInst: mixed, nativeEvent: _Touch, nativeEventTarget: EventTarget, diff --git a/packages/react-dom/src/__tests__/TapEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/TapEventPlugin-test.internal.js similarity index 100% rename from packages/react-dom/src/__tests__/TapEventPlugin-test.internal.js rename to packages/react-dom/src/events/__tests__/TapEventPlugin-test.internal.js diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index e7cf69b550..d9948291b3 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -18,7 +18,7 @@ import { import SyntheticEvent from 'events/SyntheticEvent'; import invariant from 'fbjs/lib/invariant'; -import {topLevelTypes, mediaEventTypes} from '../events/BrowserEventConstants'; +import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes'; const {findDOMNode} = ReactDOM; const { @@ -36,6 +36,33 @@ function Event(suffix) {} * @class ReactTestUtils */ +/** + * Simulates a top level event being dispatched from a raw event that occurred + * on an `Element` node. + * @param {number} topLevelType A number from `TopLevelEventTypes` + * @param {!Element} node The dom to simulate an event occurring on. + * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. + */ +function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) { + fakeNativeEvent.target = node; + ReactDOMEventListener.dispatchEvent(topLevelType, fakeNativeEvent); +} + +/** + * Simulates a top level event being dispatched from a raw event that occurred + * on the `ReactDOMComponent` `comp`. + * @param {Object} topLevelType A type from `BrowserEventConstants.topLevelTypes`. + * @param {!ReactDOMComponent} comp + * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. + */ +function simulateNativeEventOnDOMComponent( + topLevelType, + comp, + fakeNativeEvent, +) { + simulateNativeEventOnNode(topLevelType, findDOMNode(comp), fakeNativeEvent); +} + function findAllInRenderedFiberTreeInternal(fiber, test) { if (!fiber) { return []; @@ -291,37 +318,6 @@ const ReactTestUtils = { return this; }, - /** - * Simulates a top level event being dispatched from a raw event that occurred - * on an `Element` node. - * @param {Object} topLevelType A type from `BrowserEventConstants.topLevelTypes` - * @param {!Element} node The dom to simulate an event occurring on. - * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. - */ - simulateNativeEventOnNode: function(topLevelType, node, fakeNativeEvent) { - fakeNativeEvent.target = node; - ReactDOMEventListener.dispatchEvent(topLevelType, fakeNativeEvent); - }, - - /** - * Simulates a top level event being dispatched from a raw event that occurred - * on the `ReactDOMComponent` `comp`. - * @param {Object} topLevelType A type from `BrowserEventConstants.topLevelTypes`. - * @param {!ReactDOMComponent} comp - * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. - */ - simulateNativeEventOnDOMComponent: function( - topLevelType, - comp, - fakeNativeEvent, - ) { - ReactTestUtils.simulateNativeEventOnNode( - topLevelType, - findDOMNode(comp), - fakeNativeEvent, - ); - }, - nativeTouchData: function(x, y) { return { touches: [{pageX: x, pageY: y}], @@ -436,20 +432,20 @@ buildSimulators(); * to dispatch synthetic events. */ -function makeNativeSimulator(eventType) { +function makeNativeSimulator(eventType, topLevelType) { return function(domComponentOrNode, nativeEventData) { const fakeNativeEvent = new Event(eventType); Object.assign(fakeNativeEvent, nativeEventData); if (ReactTestUtils.isDOMComponent(domComponentOrNode)) { - ReactTestUtils.simulateNativeEventOnDOMComponent( - eventType, + simulateNativeEventOnDOMComponent( + topLevelType, domComponentOrNode, fakeNativeEvent, ); } else if (domComponentOrNode.tagName) { // Will allow on actual dom nodes. - ReactTestUtils.simulateNativeEventOnNode( - eventType, + simulateNativeEventOnNode( + topLevelType, domComponentOrNode, fakeNativeEvent, ); @@ -457,23 +453,84 @@ function makeNativeSimulator(eventType) { }; } -const eventKeys = [].concat( - Object.keys(topLevelTypes), - Object.keys(mediaEventTypes), -); - -eventKeys.forEach(function(eventType) { - // Event type is stored as 'topClick' - we transform that to 'click' - const convenienceName = - eventType.indexOf('top') === 0 - ? eventType.charAt(3).toLowerCase() + eventType.substr(4) - : eventType; +[ + [DOMTopLevelEventTypes.TOP_ABORT, 'abort'], + [DOMTopLevelEventTypes.TOP_ANIMATION_END, 'animationEnd'], + [DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION, 'animationIteration'], + [DOMTopLevelEventTypes.TOP_ANIMATION_START, 'animationStart'], + [DOMTopLevelEventTypes.TOP_BLUR, 'blur'], + [DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH, 'canPlayThrough'], + [DOMTopLevelEventTypes.TOP_CAN_PLAY, 'canPlay'], + [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'], + [DOMTopLevelEventTypes.TOP_CHANGE, 'change'], + [DOMTopLevelEventTypes.TOP_CLICK, 'click'], + [DOMTopLevelEventTypes.TOP_CLOSE, 'close'], + [DOMTopLevelEventTypes.TOP_COMPOSITION_END, 'compositionEnd'], + [DOMTopLevelEventTypes.TOP_COMPOSITION_START, 'compositionStart'], + [DOMTopLevelEventTypes.TOP_COMPOSITION_UPDATE, 'compositionUpdate'], + [DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu'], + [DOMTopLevelEventTypes.TOP_COPY, 'copy'], + [DOMTopLevelEventTypes.TOP_CUT, 'cut'], + [DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick'], + [DOMTopLevelEventTypes.TOP_DRAG_END, 'dragEnd'], + [DOMTopLevelEventTypes.TOP_DRAG_ENTER, 'dragEnter'], + [DOMTopLevelEventTypes.TOP_DRAG_EXIT, 'dragExit'], + [DOMTopLevelEventTypes.TOP_DRAG_LEAVE, 'dragLeave'], + [DOMTopLevelEventTypes.TOP_DRAG_OVER, 'dragOver'], + [DOMTopLevelEventTypes.TOP_DRAG_START, 'dragStart'], + [DOMTopLevelEventTypes.TOP_DRAG, 'drag'], + [DOMTopLevelEventTypes.TOP_DROP, 'drop'], + [DOMTopLevelEventTypes.TOP_DURATION_CHANGE, 'durationChange'], + [DOMTopLevelEventTypes.TOP_EMPTIED, 'emptied'], + [DOMTopLevelEventTypes.TOP_ENCRYPTED, 'encrypted'], + [DOMTopLevelEventTypes.TOP_ENDED, 'ended'], + [DOMTopLevelEventTypes.TOP_ERROR, 'error'], + [DOMTopLevelEventTypes.TOP_FOCUS, 'focus'], + [DOMTopLevelEventTypes.TOP_INPUT, 'input'], + [DOMTopLevelEventTypes.TOP_KEY_DOWN, 'keyDown'], + [DOMTopLevelEventTypes.TOP_KEY_PRESS, 'keyPress'], + [DOMTopLevelEventTypes.TOP_KEY_UP, 'keyUp'], + [DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'], + [DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'], + [DOMTopLevelEventTypes.TOP_LOAD, 'load'], + [DOMTopLevelEventTypes.TOP_LOADED_DATA, 'loadedData'], + [DOMTopLevelEventTypes.TOP_LOADED_METADATA, 'loadedMetadata'], + [DOMTopLevelEventTypes.TOP_MOUSE_DOWN, 'mouseDown'], + [DOMTopLevelEventTypes.TOP_MOUSE_MOVE, 'mouseMove'], + [DOMTopLevelEventTypes.TOP_MOUSE_OUT, 'mouseOut'], + [DOMTopLevelEventTypes.TOP_MOUSE_OVER, 'mouseOver'], + [DOMTopLevelEventTypes.TOP_MOUSE_UP, 'mouseUp'], + [DOMTopLevelEventTypes.TOP_PASTE, 'paste'], + [DOMTopLevelEventTypes.TOP_PAUSE, 'pause'], + [DOMTopLevelEventTypes.TOP_PLAY, 'play'], + [DOMTopLevelEventTypes.TOP_PLAYING, 'playing'], + [DOMTopLevelEventTypes.TOP_PROGRESS, 'progress'], + [DOMTopLevelEventTypes.TOP_RATE_CHANGE, 'rateChange'], + [DOMTopLevelEventTypes.TOP_SCROLL, 'scroll'], + [DOMTopLevelEventTypes.TOP_SEEKED, 'seeked'], + [DOMTopLevelEventTypes.TOP_SEEKING, 'seeking'], + [DOMTopLevelEventTypes.TOP_SELECTION_CHANGE, 'selectionChange'], + [DOMTopLevelEventTypes.TOP_STALLED, 'stalled'], + [DOMTopLevelEventTypes.TOP_SUSPEND, 'suspend'], + [DOMTopLevelEventTypes.TOP_TEXT_INPUT, 'textInput'], + [DOMTopLevelEventTypes.TOP_TIME_UPDATE, 'timeUpdate'], + [DOMTopLevelEventTypes.TOP_TOGGLE, 'toggle'], + [DOMTopLevelEventTypes.TOP_TOUCH_CANCEL, 'touchCancel'], + [DOMTopLevelEventTypes.TOP_TOUCH_END, 'touchEnd'], + [DOMTopLevelEventTypes.TOP_TOUCH_MOVE, 'touchMove'], + [DOMTopLevelEventTypes.TOP_TOUCH_START, 'touchStart'], + [DOMTopLevelEventTypes.TOP_TRANSITION_END, 'transitionEnd'], + [DOMTopLevelEventTypes.TOP_VOLUME_CHANGE, 'volumeChange'], + [DOMTopLevelEventTypes.TOP_WAITING, 'waiting'], + [DOMTopLevelEventTypes.TOP_WHEEL, 'wheel'], +].forEach(([topLevelType, eventType]) => { /** * @param {!Element|ReactDOMComponent} domComponentOrNode * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent. */ - ReactTestUtils.SimulateNative[convenienceName] = makeNativeSimulator( + ReactTestUtils.SimulateNative[eventType] = makeNativeSimulator( eventType, + topLevelType, ); }); diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 69bc7a260a..59cd7c8f28 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -12,6 +12,7 @@ import { accumulateTwoPhaseDispatches, accumulateDirectDispatches, } from 'events/EventPropagators'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; import * as ReactNativeViewConfigRegistry from 'ReactNativeViewConfigRegistry'; import SyntheticEvent from 'events/SyntheticEvent'; import invariant from 'fbjs/lib/invariant'; @@ -29,7 +30,7 @@ const ReactNativeBridgeEventPlugin = { * @see {EventPluginHub.extractEvents} */ extractEvents: function( - topLevelType: string, + topLevelType: TopLevelType, targetInst: Object, nativeEvent: AnyNativeEvent, nativeEventTarget: Object, diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index e43644a620..6b61de6331 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -15,6 +15,7 @@ import warning from 'fbjs/lib/warning'; import {getInstanceFromNode} from './ReactNativeComponentTree'; import type {AnyNativeEvent} from 'events/PluginModuleType'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; export {getListener, registrationNameModules as registrationNames}; @@ -88,7 +89,7 @@ const removeTouchesAtIndices = function( */ export function _receiveRootNodeIDEvent( rootNodeID: number, - topLevelType: string, + topLevelType: TopLevelType, nativeEventParam: ?AnyNativeEvent, ) { const nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; @@ -114,7 +115,7 @@ export function _receiveRootNodeIDEvent( */ export function receiveEvent( rootNodeID: number, - topLevelType: string, + topLevelType: TopLevelType, nativeEventParam: AnyNativeEvent, ) { _receiveRootNodeIDEvent(rootNodeID, topLevelType, nativeEventParam); @@ -145,7 +146,7 @@ export function receiveEvent( * identifier 0, also abandoning traditional click handlers. */ export function receiveTouches( - eventTopLevelType: string, + eventTopLevelType: TopLevelType, touches: Array, changedIndices: Array, ) { diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 0a223c59ae..28de8651c1 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -143,6 +143,14 @@ const forks = Object.freeze({ return null; } }, + + // React DOM uses different top level event names and supports mouse events. + 'events/ResponderTopLevelEventTypes': (bundleType, entry) => { + if (entry === 'react-dom' || entry.startsWith('react-dom/')) { + return 'events/forks/ResponderTopLevelEventTypes.dom.js'; + } + return null; + }, }); module.exports = forks; diff --git a/yarn.lock b/yarn.lock index 08eb3a49a2..2488a5f252 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,11 @@ # yarn lockfile v1 -"@babel/code-frame@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz#2349d7ec04b3a06945ae173280ef8579b63728e4" +"@babel/code-frame@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^3.0.0" + "@babel/highlight" "7.0.0-beta.44" "@babel/code-frame@^7.0.0-beta.35": version "7.0.0-beta.38" @@ -18,45 +16,71 @@ esutils "^2.0.2" js-tokens "^3.0.0" -"@babel/helper-function-name@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.36.tgz#366e3bc35147721b69009f803907c4d53212e88d" +"@babel/generator@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" dependencies: - "@babel/helper-get-function-arity" "7.0.0-beta.36" - "@babel/template" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" + "@babel/types" "7.0.0-beta.44" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" -"@babel/helper-get-function-arity@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.36.tgz#f5383bac9a96b274828b10d98900e84ee43e32b8" +"@babel/helper-function-name@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" dependencies: - "@babel/types" "7.0.0-beta.36" + "@babel/helper-get-function-arity" "7.0.0-beta.44" + "@babel/template" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" -"@babel/template@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.36.tgz#02e903de5d68bd7899bce3c5b5447e59529abb00" +"@babel/helper-get-function-arity@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" dependencies: - "@babel/code-frame" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - babylon "7.0.0-beta.36" + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-split-export-declaration@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/highlight@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/template@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" lodash "^4.2.0" -"@babel/traverse@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.36.tgz#1dc6f8750e89b6b979de5fe44aa993b1a2192261" +"@babel/traverse@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" dependencies: - "@babel/code-frame" "7.0.0-beta.36" - "@babel/helper-function-name" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - babylon "7.0.0-beta.36" - debug "^3.0.1" + "@babel/code-frame" "7.0.0-beta.44" + "@babel/generator" "7.0.0-beta.44" + "@babel/helper-function-name" "7.0.0-beta.44" + "@babel/helper-split-export-declaration" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + debug "^3.1.0" globals "^11.1.0" invariant "^2.2.0" lodash "^4.2.0" -"@babel/types@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.36.tgz#64f2004353de42adb72f9ebb4665fc35b5499d23" +"@babel/types@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" dependencies: esutils "^2.0.2" lodash "^4.2.0" @@ -78,8 +102,8 @@ abab@^1.0.3: resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" abbrev@1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" acorn-globals@^4.0.0: version "4.1.0" @@ -123,13 +147,6 @@ ajv-keywords@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" -ajv@^4.9.1: - version "4.11.7" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.7.tgz#8655a5d86d0824985cc471a1d913fb6729a0ec48" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" @@ -173,11 +190,11 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.1.0.tgz#09c202d5c917ec23188caa5c9cb9179cd9547750" +ansi-styles@^3.1.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" dependencies: - color-convert "^1.0.0" + color-convert "^1.9.0" ansi-styles@^3.2.0: version "3.2.0" @@ -186,11 +203,11 @@ ansi-styles@^3.2.0: color-convert "^1.9.0" anymatch@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" dependencies: - arrify "^1.0.0" micromatch "^2.1.5" + normalize-path "^2.0.0" append-transform@^0.4.0: version "0.4.0" @@ -199,8 +216,8 @@ append-transform@^0.4.0: default-require-extensions "^1.0.0" aproba@^1.0.3: - version "1.1.1" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab" + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" are-we-there-yet@~1.1.2: version "1.1.4" @@ -221,9 +238,17 @@ arr-diff@^2.0.0: dependencies: arr-flatten "^1.0.1" -arr-flatten@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" array-differ@^1.0.0: version "1.0.0" @@ -247,6 +272,10 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + array.prototype.find@2.0.4, array.prototype.find@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90" @@ -278,6 +307,10 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + ast-traverse@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6" @@ -312,6 +345,10 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +atob@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -320,40 +357,36 @@ aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" -aws4@^1.2.1, aws4@^1.6.0: +aws4@^1.2.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + +aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" babel-cli@^6.6.5: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1" dependencies: - babel-core "^6.24.1" - babel-polyfill "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - commander "^2.8.1" - convert-source-map "^1.1.0" + babel-core "^6.26.0" + babel-polyfill "^6.26.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + commander "^2.11.0" + convert-source-map "^1.5.0" fs-readdir-recursive "^1.0.0" - glob "^7.0.0" - lodash "^4.2.0" - output-file-sync "^1.1.0" - path-is-absolute "^1.0.0" + glob "^7.1.2" + lodash "^4.17.4" + output-file-sync "^1.1.2" + path-is-absolute "^1.0.1" slash "^1.0.0" - source-map "^0.5.0" - v8flags "^2.0.10" + source-map "^0.5.6" + v8flags "^2.1.1" optionalDependencies: chokidar "^1.6.1" -babel-code-frame@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^3.0.0" - -babel-code-frame@^6.26.0: +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -412,52 +445,52 @@ babel-core@^5.6.21: trim-right "^1.0.0" try-resolve "^1.0.0" -babel-core@^6.0.0, babel-core@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.1.tgz#8c428564dce1e1f41fb337ec34f4c3b022b5ad83" +babel-core@^6.0.0, babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" dependencies: - babel-code-frame "^6.22.0" - babel-generator "^6.24.1" + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" babel-helpers "^6.24.1" babel-messages "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - convert-source-map "^1.1.0" - debug "^2.1.1" - json5 "^0.5.0" - lodash "^4.2.0" - minimatch "^3.0.2" - path-is-absolute "^1.0.0" - private "^0.1.6" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" slash "^1.0.0" - source-map "^0.5.0" + source-map "^0.5.6" babel-eslint@^8.0.0: - version "8.2.1" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.1.tgz#136888f3c109edc65376c23ebf494f36a3e03951" + version "8.2.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf" dependencies: - "@babel/code-frame" "7.0.0-beta.36" - "@babel/traverse" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - babylon "7.0.0-beta.36" + "@babel/code-frame" "7.0.0-beta.44" + "@babel/traverse" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" eslint-scope "~3.7.1" eslint-visitor-keys "^1.0.0" -babel-generator@^6.18.0, babel-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" +babel-generator@^6.18.0, babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" dependencies: babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" detect-indent "^4.0.0" jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" + lodash "^4.17.4" + source-map "^0.5.7" trim-right "^1.0.1" babel-helper-builder-react-jsx@^6.24.1: @@ -546,11 +579,11 @@ babel-helpers@^6.24.1: babel-template "^6.24.1" babel-jest@^22.0.6: - version "22.0.6" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.0.6.tgz#807a2a5f5fad7789c57174a955cd14b11045299f" + version "22.4.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.4.3.tgz#4b7a0b6041691bbd422ab49b3b73654a49a6627a" dependencies: babel-plugin-istanbul "^4.1.5" - babel-preset-jest "^22.0.6" + babel-preset-jest "^22.4.3" babel-messages@^6.23.0: version "6.23.0" @@ -587,16 +620,17 @@ babel-plugin-inline-environment-variables@^1.0.1: resolved "https://registry.yarnpkg.com/babel-plugin-inline-environment-variables/-/babel-plugin-inline-environment-variables-1.0.1.tgz#1f58ce91207ad6a826a8bf645fafe68ff5fe3ffe" babel-plugin-istanbul@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e" + version "4.1.6" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" dependencies: + babel-plugin-syntax-object-rest-spread "^6.13.0" find-up "^2.1.0" - istanbul-lib-instrument "^1.7.5" - test-exclude "^4.1.1" + istanbul-lib-instrument "^1.10.1" + test-exclude "^4.2.1" -babel-plugin-jest-hoist@^22.0.6: - version "22.0.6" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.0.6.tgz#551269ded350a15d6585da35d16d449df30d66c4" +babel-plugin-jest-hoist@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.3.tgz#7d8bcccadc2667f96a0dcc6afe1891875ee6c14a" babel-plugin-jscript@^1.0.4: version "1.0.4" @@ -690,14 +724,14 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.5.0: babel-runtime "^6.22.0" babel-plugin-transform-es2015-block-scoping@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" babel-plugin-transform-es2015-classes@^6.5.2: version "6.24.1" @@ -739,13 +773,13 @@ babel-plugin-transform-es2015-literals@^6.5.0: babel-runtime "^6.22.0" babel-plugin-transform-es2015-modules-commonjs@^6.5.2: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe" + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" dependencies: babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-types "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" babel-plugin-transform-es2015-object-super@^6.5.0: version "6.24.1" @@ -791,20 +825,13 @@ babel-plugin-transform-flow-strip-types@^6.22.0: babel-plugin-syntax-flow "^6.18.0" babel-runtime "^6.22.0" -babel-plugin-transform-object-rest-spread@^6.20.2: +babel-plugin-transform-object-rest-spread@^6.20.2, babel-plugin-transform-object-rest-spread@^6.6.5: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" dependencies: babel-plugin-syntax-object-rest-spread "^6.8.0" babel-runtime "^6.26.0" -babel-plugin-transform-object-rest-spread@^6.6.5: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" - dependencies: - babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.22.0" - babel-plugin-transform-react-display-name@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.23.0.tgz#4398910c358441dc4cef18787264d0412ed36b37" @@ -856,13 +883,13 @@ babel-plugin-undefined-to-void@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/babel-plugin-undefined-to-void/-/babel-plugin-undefined-to-void-1.1.6.tgz#7f578ef8b78dfae6003385d8417a61eda06e2f81" -babel-polyfill@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" +babel-polyfill@^6.23.0, babel-polyfill@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" dependencies: - babel-runtime "^6.22.0" - core-js "^2.4.0" - regenerator-runtime "^0.10.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" babel-preset-flow@^6.23.0: version "6.23.0" @@ -870,11 +897,11 @@ babel-preset-flow@^6.23.0: dependencies: babel-plugin-transform-flow-strip-types "^6.22.0" -babel-preset-jest@^22.0.6: - version "22.0.6" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.0.6.tgz#d13202533db9495c98663044d9f51b273d3984c8" +babel-preset-jest@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.4.3.tgz#e92eef9813b7026ab4ca675799f37419b5a44156" dependencies: - babel-plugin-jest-hoist "^22.0.6" + babel-plugin-jest-hoist "^22.4.3" babel-plugin-syntax-object-rest-spread "^6.13.0" babel-preset-react@^6.5.0: @@ -888,66 +915,57 @@ babel-preset-react@^6.5.0: babel-plugin-transform-react-jsx-source "^6.22.0" babel-preset-flow "^6.23.0" -babel-register@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" dependencies: - babel-core "^6.24.1" - babel-runtime "^6.22.0" - core-js "^2.4.0" + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" home-or-tmp "^2.0.0" - lodash "^4.2.0" + lodash "^4.17.4" mkdirp "^0.5.1" - source-map-support "^0.4.2" + source-map-support "^0.4.15" -babel-runtime@6.23.0, babel-runtime@^6.22.0: +babel-runtime@6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" dependencies: core-js "^2.4.0" regenerator-runtime "^0.10.0" -babel-runtime@^6.18.0, babel-runtime@^6.26.0: +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.16.0, babel-template@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" -babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.9.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" +babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0, babel-traverse@^6.9.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" dependencies: - babel-code-frame "^6.22.0" + babel-code-frame "^6.26.0" babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" -babel-types@^6.18.0, babel-types@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" - dependencies: - babel-runtime "^6.22.0" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babel-types@^6.19.0: +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" dependencies: @@ -973,34 +991,34 @@ babel@^5.4.7: slash "^1.0.0" source-map "^0.5.0" -babylon@6.15.0, babylon@^6.11.0: - version "6.15.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@6.18.0, babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" -babylon@7.0.0-beta.36: - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.36.tgz#3a3683ba6a9a1e02b0aa507c8e63435e39305b9e" +babylon@7.0.0-beta.44: + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" babylon@^5.8.38: version "5.8.38" resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd" -babylon@^6.15.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - -balanced-match@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -1012,8 +1030,8 @@ beeper@^1.0.0: resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" binary-extensions@^1.0.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" bl@^1.0.0: version "1.2.1" @@ -1050,10 +1068,10 @@ boom@5.x.x: hoek "4.x.x" brace-expansion@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: - balanced-match "^0.4.1" + balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^1.1.7: @@ -1071,6 +1089,21 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + breakable@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/breakable/-/breakable-1.0.0.tgz#784a797915a38ead27bad456b5572cb4bbaa78c1" @@ -1129,6 +1162,20 @@ bundle-collapser@^1.1.1: minimist "^1.1.1" through2 "^2.0.0" +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -1170,7 +1217,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@*, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@*, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1180,13 +1227,13 @@ chalk@*, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" +chalk@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52" dependencies: - ansi-styles "^3.1.0" + ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" - supports-color "^4.0.0" + supports-color "^5.3.0" chalk@^2.0.1: version "2.0.1" @@ -1196,11 +1243,19 @@ chalk@^2.0.1: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chalk@^2.1.0, chalk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" -chokidar@^1.0.0, chokidar@^1.6.1: +chokidar@^1.0.0: version "1.6.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" dependencies: @@ -1215,6 +1270,21 @@ chokidar@^1.0.0, chokidar@^1.6.1: optionalDependencies: fsevents "^1.0.0" +chokidar@^1.6.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" @@ -1227,6 +1297,15 @@ circular-json@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -1307,15 +1386,22 @@ coffee-script@^1.8.0: version "1.12.5" resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.5.tgz#809f4585419112bbfe46a073ad7543af18c27346" -color-convert@^1.0.0, color-convert@^1.9.0: +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: color-name "^1.1.1" color-name@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.2.tgz#5c8ab72b64bd2215d617ae9559ebb148475cf98d" + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" colors@1.0.3: version "1.0.3" @@ -1335,16 +1421,20 @@ combine-source-map@~0.6.1: source-map "~0.4.2" combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" +commander@^2.11.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + commander@^2.12.2: version "2.12.2" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" -commander@^2.5.0, commander@^2.6.0, commander@^2.8.1, commander@^2.9.0: +commander@^2.5.0, commander@^2.6.0, commander@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: @@ -1364,6 +1454,10 @@ commoner@~0.10.3: q "^1.1.2" recast "^0.11.17" +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1392,7 +1486,11 @@ content-type-parser@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" -convert-source-map@^1.1.0, convert-source-map@^1.4.0: +convert-source-map@^1.1.0, convert-source-map@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +convert-source-map@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" @@ -1400,15 +1498,23 @@ convert-source-map@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.2.1, core-js@^2.4.0: +core-js@^2.2.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" -core-util-is@~1.0.0: +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" + +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1541,11 +1647,11 @@ dateformat@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17" -debug@^2.1.1, debug@^2.2.0: - version "2.6.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.6.tgz#a9fa6fbe9ca43cf1e79f73b75c0189cbb7d6db5a" +debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: - ms "0.7.3" + ms "2.0.0" debug@^2.6.3: version "2.6.8" @@ -1553,7 +1659,7 @@ debug@^2.6.3: dependencies: ms "2.0.0" -debug@^3.0.1, debug@^3.1.0: +debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -1563,9 +1669,13 @@ decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + deep-extend@~0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" deep-is@~0.1.3: version "0.1.3" @@ -1584,6 +1694,25 @@ define-properties@^1.1.2: foreach "^2.0.5" object-keys "^1.0.8" +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" @@ -1651,6 +1780,10 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -1972,20 +2105,15 @@ esquery@^1.0.0: estraverse "^4.0.0" esrecurse@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" dependencies: - estraverse "~4.1.0" - object-assign "^4.0.1" + estraverse "^4.1.0" -estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estraverse@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" - estree-walker@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" @@ -2033,6 +2161,18 @@ expand-brackets@^0.1.4: dependencies: is-posix-bracket "^0.1.0" +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" @@ -2062,6 +2202,13 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -2080,6 +2227,19 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extract-banner@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/extract-banner/-/extract-banner-0.1.2.tgz#61d1ed5cce3acdadb35f4323910b420364241a7f" @@ -2087,9 +2247,13 @@ extract-banner@0.1.2: strip-bom-string "^0.1.2" strip-use-strict "^0.1.0" -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" falafel@^1.2.0: version "1.2.0" @@ -2175,8 +2339,8 @@ file-entry-cache@^2.0.0: object-assign "^4.0.1" filename-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" fileset@^2.0.2: version "2.0.3" @@ -2199,6 +2363,15 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + find-file-up@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/find-file-up/-/find-file-up-0.1.3.tgz#cf68091bcf9f300a40da411b37da5cce5a2fbea0" @@ -2265,7 +2438,7 @@ flow-coverage-report@^0.4.0: terminal-table "0.0.12" yargs "8.0.1" -for-in@^1.0.1: +for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -2299,28 +2472,40 @@ form-data@~2.3.1: combined-stream "^1.0.5" mime-types "^2.1.12" +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + fs-exists-sync@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + fs-readdir-recursive@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz#315b4fb8c1ca5b8c47defef319d073dad3568059" fs-readdir-recursive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" fsevents@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.1.tgz#f19fd28f43eeaf761680e519a203c4d0b3d31aff" + version "1.2.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.2.tgz#4f598f0f69b273188ef4a62ca4e9e08ace314bbf" dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.29" + nan "^2.9.2" + node-pre-gyp "^0.9.0" fsevents@^1.1.1: version "1.1.2" @@ -2358,7 +2543,7 @@ functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" -gauge@~2.7.1: +gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" dependencies: @@ -2397,6 +2582,10 @@ get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -2461,7 +2650,7 @@ glob-stream@^6.1.0: to-absolute-glob "^2.0.0" unique-stream "^2.0.2" -glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: +glob@7.1.1, glob@^7.0.3, glob@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -2492,7 +2681,7 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.2: +glob@^7.0.5, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2519,17 +2708,21 @@ global-prefix@^0.1.4: is-windows "^0.2.0" which "^1.2.12" -globals@^11.0.1, globals@^11.1.0: +globals@^11.0.1: version "11.1.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4" +globals@^11.1.0: + version "11.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.4.0.tgz#b85c793349561c16076a3c13549238a27945f1bc" + globals@^6.4.0: version "6.4.1" resolved "https://registry.yarnpkg.com/globals/-/globals-6.4.1.tgz#8498032b3b6d1cc81eebc5f79690d8fe29fabf4f" -globals@^9.0.0: - version "9.17.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" globby@^5.0.0: version "5.0.0" @@ -2620,10 +2813,6 @@ handlebars@^4.0.3: optionalDependencies: uglify-js "^2.6" -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -2637,13 +2826,6 @@ har-validator@~2.0.6: is-my-json-valid "^2.12.4" pinkie-promise "^2.0.0" -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - har-validator@~5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" @@ -2665,6 +2847,10 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + has-gulplog@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" @@ -2675,6 +2861,33 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + has@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" @@ -2728,8 +2941,8 @@ homedir-polyfill@^1.0.0: parse-passwd "^1.0.0" hosted-git-info@^2.1.4: - version "2.4.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67" + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" html-encoding-sniffer@^1.0.1: version "1.0.1" @@ -2772,16 +2985,22 @@ iconv-lite@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@^0.4.5: - version "0.4.16" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.16.tgz#65de3beeb39e2960d67f049f1634ffcbcde9014b" - -iconv-lite@~0.4.13: +iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.21" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" dependencies: safer-buffer "^2.1.0" +iconv-lite@^0.4.5: + version "0.4.16" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.16.tgz#65de3beeb39e2960d67f049f1634ffcbcde9014b" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + ignore@^3.3.3: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" @@ -2801,14 +3020,10 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" -ini@^1.3.4: +ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" -ini@~1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" - inline-source-map@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.5.0.tgz#4a4c5dd8e4fb5e9b3cda60c822dfadcaee66e0af" @@ -2834,9 +3049,9 @@ inquirer@^3.0.6: strip-ansi "^4.0.0" through "^2.3.6" -invariant@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" +invariant@^2.2.0, invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: loose-envify "^1.0.0" @@ -2851,6 +3066,18 @@ is-absolute@^0.2.5: is-relative "^0.2.1" is-windows "^0.2.0" +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -2862,8 +3089,8 @@ is-binary-path@^1.0.0: binary-extensions "^1.0.0" is-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" is-builtin-module@^1.0.0: version "1.0.0" @@ -2881,13 +3108,41 @@ is-ci@^1.0.10: dependencies: ci-info "^1.0.0" +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" -is-dotfile@^1.0.0: +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" is-equal-shallow@^0.1.3: version "0.1.3" @@ -2899,6 +3154,12 @@ is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" @@ -2958,12 +3219,28 @@ is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" -is-number@^2.0.2, is-number@^2.1.0: +is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" dependencies: kind-of "^3.0.2" +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + +is-odd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" + dependencies: + is-number "^4.0.0" + is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" @@ -2980,6 +3257,12 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" @@ -3044,6 +3327,10 @@ is-windows@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -3062,6 +3349,10 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + isomorphic-fetch@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" @@ -3089,9 +3380,9 @@ istanbul-api@^1.1.14: mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" +istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341" istanbul-lib-hook@^1.1.0: version "1.1.0" @@ -3099,7 +3390,19 @@ istanbul-lib-hook@^1.1.0: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0, istanbul-lib-instrument@^1.9.1: +istanbul-lib-instrument@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.2.0" + semver "^5.3.0" + +istanbul-lib-instrument@^1.8.0, istanbul-lib-instrument@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e" dependencies: @@ -3411,12 +3714,6 @@ jest@^22.0.6: dependencies: jest-cli "^22.0.6" -jodid25519@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" - dependencies: - jsbn "~0.1.0" - js-tokens@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.1.tgz#cc435a5c8b94ad15acb7983140fc80182c89aeae" @@ -3483,6 +3780,10 @@ jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -3521,7 +3822,7 @@ json5@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d" -json5@^0.5.0, json5@^0.5.1: +json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -3538,24 +3839,38 @@ jsonpointer@^4.0.0, jsonpointer@^4.0.1: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" jsprim@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" dependencies: assert-plus "1.0.0" - extsprintf "1.0.2" + extsprintf "1.3.0" json-schema "0.2.3" - verror "1.3.6" + verror "1.10.0" jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" -kind-of@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.0.tgz#b58abe4d5c044ad33726a8c1525b48cf891bff07" +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" dependencies: is-buffer "^1.1.5" +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" @@ -3736,10 +4051,14 @@ lodash@^3.10.0, lodash@^3.2.0, lodash@^3.9.3: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: +lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lodash@^4.2.0: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + log-driver@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" @@ -3786,6 +4105,16 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + mem@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" @@ -3820,19 +4149,37 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" +micromatch@^3.1.8: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: - mime-db "~1.27.0" + mime-db "~1.33.0" mime-types@~2.1.17: version "2.1.17" @@ -3844,13 +4191,13 @@ mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: +"minimatch@2 || 3", minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: brace-expansion "^1.0.0" -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -3870,16 +4217,32 @@ minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +minipass@^2.2.1, minipass@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40" + dependencies: + safe-buffer "^5.1.1" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -ms@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3894,9 +4257,26 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" +nan@^2.3.0, nan@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + +nanomatch@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-odd "^2.0.0" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" natural-compare@^1.4.0: version "1.4.0" @@ -3906,6 +4286,14 @@ ncp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" +needle@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.0.tgz#f14efc69cee1024b72c8b21c7bdf94a731dc12fa" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + node-cleanup@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c" @@ -3930,20 +4318,6 @@ node-notifier@^5.1.2: shellwords "^0.1.0" which "^1.2.12" -node-pre-gyp@^0.6.29: - version "0.6.34" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.34.tgz#94ad1c798a11d7fc67381b50d47f8cc18d9799f7" - dependencies: - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "^2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - node-pre-gyp@^0.6.36: version "0.6.36" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" @@ -3958,6 +4332,21 @@ node-pre-gyp@^0.6.36: tar "^2.2.1" tar-pack "^3.4.0" +node-pre-gyp@^0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.9.1.tgz#f11c07516dd92f87199dbc7e1838eab7cd56c9e0" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.0" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.1.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -3966,20 +4355,31 @@ nopt@^4.0.1: osenv "^0.1.4" normalize-package-data@^2.3.2: - version "2.3.8" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.8.tgz#d819eda2a9dedbd1ffa563ea4071d936782295bb" + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: hosted-git-info "^2.1.4" is-builtin-module "^1.0.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.1: +normalize-path@^2.0.0, normalize-path@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: remove-trailing-separator "^1.0.1" +npm-bundled@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" + +npm-packlist@^1.1.6: + version "1.1.10" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -3987,12 +4387,12 @@ npm-run-path@^2.0.0: path-key "^2.0.0" npmlog@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f" + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: are-we-there-yet "~1.1.2" console-control-strings "~1.1.0" - gauge "~2.7.1" + gauge "~2.7.3" set-blocking "~2.0.0" number-is-nan@^1.0.0: @@ -4015,10 +4415,24 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + object-keys@^1.0.10, object-keys@^1.0.6, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + object.assign@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" @@ -4041,6 +4455,12 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4111,13 +4531,13 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" osenv@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -output-file-sync@^1.1.0: +output-file-sync@^1.1.0, output-file-sync@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" dependencies: @@ -4130,8 +4550,10 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" p-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" p-locate@^2.0.0: version "2.0.0" @@ -4139,6 +4561,10 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + parse-diff@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/parse-diff/-/parse-diff-0.4.0.tgz#9ce35bcce8fc0b7c58f46d71113394fc0b4982dd" @@ -4187,6 +4613,10 @@ parse5@^3.0.2: dependencies: "@types/node" "*" +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -4205,7 +4635,7 @@ path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" -path-is-absolute@^1.0.0: +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -4235,10 +4665,6 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -4273,6 +4699,10 @@ pn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -4303,7 +4733,11 @@ pretty-format@^22.1.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -private@^0.1.6, private@~0.1.5: +private@^0.1.6, private@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +private@~0.1.5: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -4311,6 +4745,10 @@ process-nextick-args@^1.0.6, process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" @@ -4383,10 +4821,6 @@ qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -4398,15 +4832,15 @@ random-seed@^0.3.0: json-stringify-safe "^5.0.1" randomatic@^1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" dependencies: - is-number "^2.0.2" - kind-of "^3.0.2" + is-number "^3.0.0" + kind-of "^4.0.0" rc@^1.1.7: - version "1.2.1" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + version "1.2.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" dependencies: deep-extend "~0.4.0" ini "~1.3.0" @@ -4474,7 +4908,7 @@ read-pkg@^2.0.0: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.1.5: version "2.2.9" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8" dependencies: @@ -4486,6 +4920,18 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.0.0" util-deprecate "~1.0.1" +readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^2.0.5, readable-stream@^2.2.2: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" @@ -4550,9 +4996,9 @@ regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" -regenerator-runtime@^0.10.0: - version "0.10.4" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93" +regenerator-runtime@^0.10.0, regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" regenerator-runtime@^0.11.0: version "0.11.0" @@ -4578,11 +5024,17 @@ regenerator@0.8.40: through "~2.3.8" regex-cache@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" dependencies: is-equal-shallow "^0.1.3" - is-primitive "^2.0.0" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" regexpu@^1.3.0: version "1.3.0" @@ -4605,14 +5057,14 @@ regjsparser@^0.1.4: jsesc "~0.5.0" remove-trailing-separator@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4" + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" repeat-element@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" -repeat-string@^1.5.2: +repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -4676,31 +5128,31 @@ request@2.79.0: uuid "^3.0.0" request@^2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" + aws-sign2 "~0.7.0" + aws4 "^1.6.0" caseless "~0.12.0" combined-stream "~1.0.5" - extend "~3.0.0" + extend "~3.0.1" forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" tunnel-agent "^0.6.0" - uuid "^3.0.0" + uuid "^3.1.0" request@^2.83.0: version "2.83.0" @@ -4759,6 +5211,10 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" @@ -4782,6 +5238,10 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + rfc6902@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/rfc6902/-/rfc6902-2.2.2.tgz#518a4e9caac1688f3d94c9df2fdcdb6ce21f29be" @@ -4792,15 +5252,15 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" +rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" -rimraf@^2.5.4: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" +rimraf@^2.2.8, rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" @@ -4903,14 +5363,16 @@ rxjs@^5.5.6: dependencies: symbol-observable "1.0.1" -safe-buffer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" - -safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -4933,14 +5395,18 @@ sax@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" +semver@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4949,6 +5415,24 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -4989,6 +5473,33 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" @@ -5001,15 +5512,25 @@ sntp@2.x.x: dependencies: hoek "4.x.x" +source-map-resolve@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" + dependencies: + atob "^2.0.0" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + source-map-support@^0.2.10: version "0.2.10" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.2.10.tgz#ea5a3900a1c1cb25096a0ae8cc5c2b4b10ded3dc" dependencies: source-map "0.1.32" -source-map-support@^0.4.2: - version "0.4.14" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.14.tgz#9d4463772598b86271b4f523f6c1f4e02a7d6aef" +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" dependencies: source-map "^0.5.6" @@ -5019,6 +5540,10 @@ source-map-support@^0.5.0: dependencies: source-map "^0.6.0" +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + source-map@0.1.32: version "0.1.32" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.32.tgz#c8b6c167797ba4740a8ea33252162ff08591b266" @@ -5031,7 +5556,11 @@ source-map@^0.4.4, source-map@~0.4.0, source-map@~0.4.2: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1: +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.5.3, source-map@~0.5.0, source-map@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -5054,27 +5583,41 @@ spawn-sync@^1.0.15: concat-stream "^1.4.7" os-shim "^0.1.2" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - spdx-license-ids "^1.0.2" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" sshpk@^1.7.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.0.tgz#ff2a3e4fd04497555fed97b39a0fd82fafb3a33c" + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -5083,7 +5626,6 @@ sshpk@^1.7.0: optionalDependencies: bcrypt-pbkdf "^1.0.0" ecc-jsbn "~0.1.1" - jodid25519 "^1.0.0" jsbn "~0.1.0" tweetnacl "~0.14.0" @@ -5095,6 +5637,13 @@ stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + stealthy-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -5136,18 +5685,18 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.0.tgz#f06f41157b664d86069f84bdbdc9b0d8ab281667" - dependencies: - buffer-shims "~1.0.0" - -string_decoder@~1.0.3: +string_decoder@~1.0.0, string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: safe-buffer "~5.1.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + stringmap@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1" @@ -5209,8 +5758,8 @@ supports-color@^3.1.2: has-flag "^1.0.0" supports-color@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.0.tgz#ad986dc7eb2315d009b4d77c8169c2231a684037" + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" dependencies: has-flag "^2.0.0" @@ -5220,6 +5769,12 @@ supports-color@^5.0.0: dependencies: has-flag "^2.0.0" +supports-color@^5.3.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -5256,8 +5811,8 @@ tar-fs@^1.8.1: tar-stream "^1.1.2" tar-pack@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" dependencies: debug "^2.2.0" fstream "^1.0.10" @@ -5285,6 +5840,18 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" +tar@^4: + version "4.4.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.2.4" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.1" + yallist "^3.0.2" + targz@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/targz/-/targz-1.0.1.tgz#8f76a523694cdedfbb5d60a4076ff6eeecc5398f" @@ -5305,12 +5872,12 @@ terminal-table@0.0.12: colors "^1.0.3" eastasianwidth "^0.1.0" -test-exclude@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" +test-exclude@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa" dependencies: arrify "^1.0.1" - micromatch "^2.3.11" + micromatch "^3.1.8" object-assign "^4.1.0" read-pkg-up "^1.0.1" require-main-filename "^1.0.1" @@ -5380,7 +5947,7 @@ to-absolute-glob@^2.0.0: is-absolute "^0.2.5" is-negated-glob "^1.0.0" -to-fast-properties@^1.0.0, to-fast-properties@^1.0.1: +to-fast-properties@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" @@ -5392,6 +5959,28 @@ to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" @@ -5399,8 +5988,8 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.3: punycode "^1.4.1" tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" dependencies: punycode "^1.4.1" @@ -5483,6 +6072,15 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + unique-stream@^2.0.2: version "2.2.1" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369" @@ -5490,10 +6088,27 @@ unique-stream@^2.0.2: json-stable-stringify "^1.0.0" through2-filter "^2.0.0" +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + url-template@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" +use@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" + dependencies: + kind-of "^6.0.2" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -5510,31 +6125,33 @@ util.promisify@^1.0.0: object.getownpropertydescriptors "^2.0.3" uuid@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -v8flags@^2.0.10: +v8flags@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" dependencies: user-home "^1.1.1" validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" dependencies: - extsprintf "1.0.2" + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" vinyl-sourcemaps-apply@^0.2.0: version "0.2.1" @@ -5626,10 +6243,10 @@ which@^1.2.9: isexe "^2.0.0" wide-align@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" dependencies: - string-width "^1.0.1" + string-width "^1.0.2" window-size@0.1.0: version "0.1.0" @@ -5696,6 +6313,10 @@ yallist@^2.0.0, yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + yargs-parser@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" From 7dc1a176b5c50739b200a9d8612a42b110289d22 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 11:11:19 +0100 Subject: [PATCH 051/277] Skip special nodes when reading TestInstance.parent (#12813) --- .../src/ReactTestRenderer.js | 42 +++++++------------ .../ReactTestRendererTraversal-test.js | 39 ++++++++++++++++- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 5fab9af606..029c260580 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -245,10 +245,14 @@ class ReactTestInstance { } get parent(): ?ReactTestInstance { - const parent = this._fiber.return; - return parent === null || parent.tag === HostRoot - ? null - : wrapFiber(parent); + let parent = this._fiber.return; + while (parent !== null) { + if (validWrapperTypes.has(parent.tag)) { + return wrapFiber(parent); + } + parent = parent.return; + } + return null; } get children(): Array { @@ -262,30 +266,12 @@ class ReactTestInstance { node = node.child; outer: while (true) { let descend = false; - switch (node.tag) { - case FunctionalComponent: - case ClassComponent: - case HostComponent: - case ForwardRef: - children.push(wrapFiber(node)); - break; - case HostText: - children.push('' + node.memoizedProps); - break; - case Fragment: - case ContextProvider: - case ContextConsumer: - case Mode: - case Profiler: - descend = true; - break; - default: - invariant( - false, - 'Unsupported component type %s in test renderer. ' + - 'This is probably a bug in React.', - node.tag, - ); + if (validWrapperTypes.has(node.tag)) { + children.push(wrapFiber(node)); + } else if (node.tag === HostText) { + children.push('' + node.memoizedProps); + } else { + descend = true; } if (descend && node.child !== null) { node.child.return = node; diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js index cefef5657c..9fb91e1f35 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererTraversal-test.js @@ -12,6 +12,7 @@ const React = require('react'); let ReactTestRenderer; +let Context; const RCTView = 'RCTView'; const View = props => ; @@ -20,6 +21,7 @@ describe('ReactTestRendererTraversal', () => { beforeEach(() => { jest.resetModules(); ReactTestRenderer = require('react-test-renderer'); + Context = React.createContext(null); }); class Example extends React.Component { @@ -40,6 +42,17 @@ describe('ReactTestRendererTraversal', () => { {}}> + + + + + {() => } + + + + + + ); @@ -61,7 +74,7 @@ describe('ReactTestRendererTraversal', () => { // assert .props, .type and .parent attributes const foo = render.root.find(hasFooProp); - expect(foo.props.children).toHaveLength(8); + expect(foo.props.children).toHaveLength(9); expect(foo.type).toBe(View); expect(render.root.parent).toBe(null); expect(foo.children[0].parent).toBe(foo); @@ -76,6 +89,7 @@ describe('ReactTestRendererTraversal', () => { const hasNullProp = node => node.props.hasOwnProperty('null'); const hasVoidProp = node => node.props.hasOwnProperty('void'); const hasItselfProp = node => node.props.hasOwnProperty('itself'); + const hasNestedProp = node => node.props.hasOwnProperty('nested'); expect(() => render.root.find(hasFooProp)).not.toThrow(); // 1 match expect(() => render.root.find(hasBarProp)).toThrow(); // >1 matches @@ -83,6 +97,7 @@ describe('ReactTestRendererTraversal', () => { expect(() => render.root.find(hasBingProp)).not.toThrow(); // 1 match expect(() => render.root.find(hasNullProp)).not.toThrow(); // 1 match expect(() => render.root.find(hasVoidProp)).toThrow(); // 0 matches + expect(() => render.root.find(hasNestedProp)).toThrow(); // >1 matches // same assertion as .find(), but confirm length expect(render.root.findAll(hasFooProp, {deep: false})).toHaveLength(1); @@ -91,6 +106,7 @@ describe('ReactTestRendererTraversal', () => { expect(render.root.findAll(hasBingProp, {deep: false})).toHaveLength(1); expect(render.root.findAll(hasNullProp, {deep: false})).toHaveLength(1); expect(render.root.findAll(hasVoidProp, {deep: false})).toHaveLength(0); + expect(render.root.findAll(hasNestedProp, {deep: false})).toHaveLength(3); // note: with {deep: true}, .findAll() will continue to // search children, even after finding a match @@ -100,6 +116,7 @@ describe('ReactTestRendererTraversal', () => { expect(render.root.findAll(hasBingProp)).toHaveLength(1); // no spread expect(render.root.findAll(hasNullProp)).toHaveLength(1); // no spread expect(render.root.findAll(hasVoidProp)).toHaveLength(0); + expect(render.root.findAll(hasNestedProp, {deep: false})).toHaveLength(3); const bing = render.root.find(hasBingProp); expect(bing.find(hasBarProp)).toBe(bing); @@ -130,7 +147,7 @@ describe('ReactTestRendererTraversal', () => { expect(render.root.findAllByType(ExampleFn)).toHaveLength(1); expect(render.root.findAllByType(View, {deep: false})).toHaveLength(1); - expect(render.root.findAllByType(View)).toHaveLength(8); + expect(render.root.findAllByType(View)).toHaveLength(11); expect(render.root.findAllByType(ExampleNull)).toHaveLength(2); expect(render.root.findAllByType(ExampleForwardRef)).toHaveLength(1); @@ -164,4 +181,22 @@ describe('ReactTestRendererTraversal', () => { expect(render.root.findAllByProps({baz})).toHaveLength(4); expect(render.root.findAllByProps({qux})).toHaveLength(3); }); + + it('skips special nodes', () => { + const render = ReactTestRenderer.create(); + expect(render.root.findAllByType(React.Fragment)).toHaveLength(0); + expect(render.root.findAllByType(Context.Consumer)).toHaveLength(0); + expect(render.root.findAllByType(Context.Provider)).toHaveLength(0); + + const expectedParent = render.root.findByProps({foo: 'foo'}, {deep: false}) + .children[0]; + const nestedViews = render.root.findAllByProps( + {nested: true}, + {deep: false}, + ); + expect(nestedViews.length).toBe(3); + expect(nestedViews[0].parent).toBe(expectedParent); + expect(nestedViews[1].parent).toBe(expectedParent); + expect(nestedViews[2].parent).toBe(expectedParent); + }); }); From bb44feb05dd258c4b4868f6c3f11bf0a2f491564 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 13:55:01 +0100 Subject: [PATCH 052/277] Try to fix Flow circular dependency --- .../src/events/DOMTopLevelEventTypes.js | 147 +++++++++--------- .../src/events/ReactDOMEventListener.js | 15 +- .../react-dom/src/events/SimpleEventPlugin.js | 5 +- 3 files changed, 84 insertions(+), 83 deletions(-) diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js index 79f13057f9..aac82dbbe7 100644 --- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -7,7 +7,6 @@ * @flow */ -import type {TopLevelType} from 'events/TopLevelEventTypes'; import getVendorPrefixedEventName from './getVendorPrefixedEventName'; /** @@ -94,87 +93,87 @@ export opaque type DOMTopLevelEventType = | 'waiting' | 'wheel'; -export const TOP_ABORT: TopLevelType = 'abort'; -export const TOP_ANIMATION_END: TopLevelType = getVendorPrefixedEventName( +export const TOP_ABORT: DOMTopLevelEventType = 'abort'; +export const TOP_ANIMATION_END: DOMTopLevelEventType = getVendorPrefixedEventName( 'animationend', ); -export const TOP_ANIMATION_ITERATION: TopLevelType = getVendorPrefixedEventName( +export const TOP_ANIMATION_ITERATION: DOMTopLevelEventType = getVendorPrefixedEventName( 'animationiteration', ); -export const TOP_ANIMATION_START: TopLevelType = getVendorPrefixedEventName( +export const TOP_ANIMATION_START: DOMTopLevelEventType = getVendorPrefixedEventName( 'animationstart', ); -export const TOP_BLUR: TopLevelType = 'blur'; -export const TOP_CAN_PLAY: TopLevelType = 'canplay'; -export const TOP_CAN_PLAY_THROUGH: TopLevelType = 'canplaythrough'; -export const TOP_CANCEL: TopLevelType = 'cancel'; -export const TOP_CHANGE: TopLevelType = 'change'; -export const TOP_CLICK: TopLevelType = 'click'; -export const TOP_CLOSE: TopLevelType = 'close'; -export const TOP_COMPOSITION_END: TopLevelType = 'compositionend'; -export const TOP_COMPOSITION_START: TopLevelType = 'compositionstart'; -export const TOP_COMPOSITION_UPDATE: TopLevelType = 'compositionupdate'; -export const TOP_CONTEXT_MENU: TopLevelType = 'contextmenu'; -export const TOP_COPY: TopLevelType = 'copy'; -export const TOP_CUT: TopLevelType = 'cut'; -export const TOP_DOUBLE_CLICK: TopLevelType = 'dblclick'; -export const TOP_DRAG: TopLevelType = 'drag'; -export const TOP_DRAG_END: TopLevelType = 'dragend'; -export const TOP_DRAG_ENTER: TopLevelType = 'dragenter'; -export const TOP_DRAG_EXIT: TopLevelType = 'dragexit'; -export const TOP_DRAG_LEAVE: TopLevelType = 'dragleave'; -export const TOP_DRAG_OVER: TopLevelType = 'dragover'; -export const TOP_DRAG_START: TopLevelType = 'dragstart'; -export const TOP_DROP: TopLevelType = 'drop'; -export const TOP_DURATION_CHANGE: TopLevelType = 'durationchange'; -export const TOP_EMPTIED: TopLevelType = 'emptied'; -export const TOP_ENCRYPTED: TopLevelType = 'encrypted'; -export const TOP_ENDED: TopLevelType = 'ended'; -export const TOP_ERROR: TopLevelType = 'error'; -export const TOP_FOCUS: TopLevelType = 'focus'; -export const TOP_INPUT: TopLevelType = 'input'; -export const TOP_INVALID: TopLevelType = 'invalid'; -export const TOP_KEY_DOWN: TopLevelType = 'keydown'; -export const TOP_KEY_PRESS: TopLevelType = 'keypress'; -export const TOP_KEY_UP: TopLevelType = 'keyup'; -export const TOP_LOAD: TopLevelType = 'load'; -export const TOP_LOAD_START: TopLevelType = 'loadstart'; -export const TOP_LOADED_DATA: TopLevelType = 'loadeddata'; -export const TOP_LOADED_METADATA: TopLevelType = 'loadedmetadata'; -export const TOP_MOUSE_DOWN: TopLevelType = 'mousedown'; -export const TOP_MOUSE_MOVE: TopLevelType = 'mousemove'; -export const TOP_MOUSE_OUT: TopLevelType = 'mouseout'; -export const TOP_MOUSE_OVER: TopLevelType = 'mouseover'; -export const TOP_MOUSE_UP: TopLevelType = 'mouseup'; -export const TOP_PASTE: TopLevelType = 'paste'; -export const TOP_PAUSE: TopLevelType = 'pause'; -export const TOP_PLAY: TopLevelType = 'play'; -export const TOP_PLAYING: TopLevelType = 'playing'; -export const TOP_PROGRESS: TopLevelType = 'progress'; -export const TOP_RATE_CHANGE: TopLevelType = 'ratechange'; -export const TOP_RESET: TopLevelType = 'reset'; -export const TOP_SCROLL: TopLevelType = 'scroll'; -export const TOP_SEEKED: TopLevelType = 'seeked'; -export const TOP_SEEKING: TopLevelType = 'seeking'; -export const TOP_SELECTION_CHANGE: TopLevelType = 'selectionchange'; -export const TOP_STALLED: TopLevelType = 'stalled'; -export const TOP_SUBMIT: TopLevelType = 'submit'; -export const TOP_SUSPEND: TopLevelType = 'suspend'; -export const TOP_TEXT_INPUT: TopLevelType = 'textInput'; -export const TOP_TIME_UPDATE: TopLevelType = 'timeupdate'; -export const TOP_TOGGLE: TopLevelType = 'toggle'; -export const TOP_TOUCH_CANCEL: TopLevelType = 'touchcancel'; -export const TOP_TOUCH_END: TopLevelType = 'touchend'; -export const TOP_TOUCH_MOVE: TopLevelType = 'touchmove'; -export const TOP_TOUCH_START: TopLevelType = 'touchstart'; -export const TOP_TRANSITION_END: TopLevelType = getVendorPrefixedEventName( +export const TOP_BLUR: DOMTopLevelEventType = 'blur'; +export const TOP_CAN_PLAY: DOMTopLevelEventType = 'canplay'; +export const TOP_CAN_PLAY_THROUGH: DOMTopLevelEventType = 'canplaythrough'; +export const TOP_CANCEL: DOMTopLevelEventType = 'cancel'; +export const TOP_CHANGE: DOMTopLevelEventType = 'change'; +export const TOP_CLICK: DOMTopLevelEventType = 'click'; +export const TOP_CLOSE: DOMTopLevelEventType = 'close'; +export const TOP_COMPOSITION_END: DOMTopLevelEventType = 'compositionend'; +export const TOP_COMPOSITION_START: DOMTopLevelEventType = 'compositionstart'; +export const TOP_COMPOSITION_UPDATE: DOMTopLevelEventType = 'compositionupdate'; +export const TOP_CONTEXT_MENU: DOMTopLevelEventType = 'contextmenu'; +export const TOP_COPY: DOMTopLevelEventType = 'copy'; +export const TOP_CUT: DOMTopLevelEventType = 'cut'; +export const TOP_DOUBLE_CLICK: DOMTopLevelEventType = 'dblclick'; +export const TOP_DRAG: DOMTopLevelEventType = 'drag'; +export const TOP_DRAG_END: DOMTopLevelEventType = 'dragend'; +export const TOP_DRAG_ENTER: DOMTopLevelEventType = 'dragenter'; +export const TOP_DRAG_EXIT: DOMTopLevelEventType = 'dragexit'; +export const TOP_DRAG_LEAVE: DOMTopLevelEventType = 'dragleave'; +export const TOP_DRAG_OVER: DOMTopLevelEventType = 'dragover'; +export const TOP_DRAG_START: DOMTopLevelEventType = 'dragstart'; +export const TOP_DROP: DOMTopLevelEventType = 'drop'; +export const TOP_DURATION_CHANGE: DOMTopLevelEventType = 'durationchange'; +export const TOP_EMPTIED: DOMTopLevelEventType = 'emptied'; +export const TOP_ENCRYPTED: DOMTopLevelEventType = 'encrypted'; +export const TOP_ENDED: DOMTopLevelEventType = 'ended'; +export const TOP_ERROR: DOMTopLevelEventType = 'error'; +export const TOP_FOCUS: DOMTopLevelEventType = 'focus'; +export const TOP_INPUT: DOMTopLevelEventType = 'input'; +export const TOP_INVALID: DOMTopLevelEventType = 'invalid'; +export const TOP_KEY_DOWN: DOMTopLevelEventType = 'keydown'; +export const TOP_KEY_PRESS: DOMTopLevelEventType = 'keypress'; +export const TOP_KEY_UP: DOMTopLevelEventType = 'keyup'; +export const TOP_LOAD: DOMTopLevelEventType = 'load'; +export const TOP_LOAD_START: DOMTopLevelEventType = 'loadstart'; +export const TOP_LOADED_DATA: DOMTopLevelEventType = 'loadeddata'; +export const TOP_LOADED_METADATA: DOMTopLevelEventType = 'loadedmetadata'; +export const TOP_MOUSE_DOWN: DOMTopLevelEventType = 'mousedown'; +export const TOP_MOUSE_MOVE: DOMTopLevelEventType = 'mousemove'; +export const TOP_MOUSE_OUT: DOMTopLevelEventType = 'mouseout'; +export const TOP_MOUSE_OVER: DOMTopLevelEventType = 'mouseover'; +export const TOP_MOUSE_UP: DOMTopLevelEventType = 'mouseup'; +export const TOP_PASTE: DOMTopLevelEventType = 'paste'; +export const TOP_PAUSE: DOMTopLevelEventType = 'pause'; +export const TOP_PLAY: DOMTopLevelEventType = 'play'; +export const TOP_PLAYING: DOMTopLevelEventType = 'playing'; +export const TOP_PROGRESS: DOMTopLevelEventType = 'progress'; +export const TOP_RATE_CHANGE: DOMTopLevelEventType = 'ratechange'; +export const TOP_RESET: DOMTopLevelEventType = 'reset'; +export const TOP_SCROLL: DOMTopLevelEventType = 'scroll'; +export const TOP_SEEKED: DOMTopLevelEventType = 'seeked'; +export const TOP_SEEKING: DOMTopLevelEventType = 'seeking'; +export const TOP_SELECTION_CHANGE: DOMTopLevelEventType = 'selectionchange'; +export const TOP_STALLED: DOMTopLevelEventType = 'stalled'; +export const TOP_SUBMIT: DOMTopLevelEventType = 'submit'; +export const TOP_SUSPEND: DOMTopLevelEventType = 'suspend'; +export const TOP_TEXT_INPUT: DOMTopLevelEventType = 'textInput'; +export const TOP_TIME_UPDATE: DOMTopLevelEventType = 'timeupdate'; +export const TOP_TOGGLE: DOMTopLevelEventType = 'toggle'; +export const TOP_TOUCH_CANCEL: DOMTopLevelEventType = 'touchcancel'; +export const TOP_TOUCH_END: DOMTopLevelEventType = 'touchend'; +export const TOP_TOUCH_MOVE: DOMTopLevelEventType = 'touchmove'; +export const TOP_TOUCH_START: DOMTopLevelEventType = 'touchstart'; +export const TOP_TRANSITION_END: DOMTopLevelEventType = getVendorPrefixedEventName( 'transitionend', ); -export const TOP_VOLUME_CHANGE: TopLevelType = 'volumechange'; -export const TOP_WAITING: TopLevelType = 'waiting'; -export const TOP_WHEEL: TopLevelType = 'wheel'; +export const TOP_VOLUME_CHANGE: DOMTopLevelEventType = 'volumechange'; +export const TOP_WAITING: DOMTopLevelEventType = 'waiting'; +export const TOP_WHEEL: DOMTopLevelEventType = 'wheel'; -export const mediaEventTypes: Array = [ +export const mediaEventTypes: Array = [ TOP_ABORT, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, @@ -200,6 +199,6 @@ export const mediaEventTypes: Array = [ TOP_WAITING, ]; -export function getRawEventName(topLevelType: TopLevelType): string { +export function getRawEventName(topLevelType: DOMTopLevelEventType): string { return topLevelType; } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 0125a8437b..8878d447d2 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -7,13 +7,14 @@ * @flow */ +import type {AnyNativeEvent} from 'events/PluginModuleType'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; + import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; import {isFiberMounted} from 'react-reconciler/reflection'; import {HostRoot} from 'shared/ReactTypeOfWork'; -import type {AnyNativeEvent} from 'events/PluginModuleType'; -import type {TopLevelType} from 'events/TopLevelEventTypes'; -import type {Fiber} from 'react-reconciler/src/ReactFiber'; import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; @@ -51,7 +52,7 @@ function getTopLevelCallbackBookKeeping( nativeEvent, targetInst, ): { - topLevelType: ?TopLevelType, + topLevelType: ?DOMTopLevelEventType, nativeEvent: ?AnyNativeEvent, targetInst: Fiber, ancestors: Array, @@ -134,7 +135,7 @@ export function isEnabled() { * @internal */ export function trapBubbledEvent( - topLevelType: TopLevelType, + topLevelType: DOMTopLevelEventType, element: Document | Element, ) { if (!element) { @@ -162,7 +163,7 @@ export function trapBubbledEvent( * @internal */ export function trapCapturedEvent( - topLevelType: TopLevelType, + topLevelType: DOMTopLevelEventType, element: Document | Element, ) { if (!element) { @@ -185,7 +186,7 @@ function dispatchInteractiveEvent(topLevelType, nativeEvent) { } export function dispatchEvent( - topLevelType: TopLevelType, + topLevelType: DOMTopLevelEventType, nativeEvent: AnyNativeEvent, ) { if (!_enabled) { diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 6da1d244c9..8c9a4bad1c 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -8,6 +8,7 @@ */ import type {TopLevelType} from 'events/TopLevelEventTypes'; +import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; import type { DispatchConfig, ReactSyntheticEvent, @@ -51,7 +52,7 @@ import getEventCharCode from './getEventCharCode'; * [TOP_ABORT, { sameConfig }], * ]); */ -type EventTuple = [TopLevelType, string]; +type EventTuple = [DOMTopLevelEventType, string]; const interactiveEventTypeNames: Array = [ [DOMTopLevelEventTypes.TOP_BLUR, 'blur'], [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'], @@ -154,7 +155,7 @@ nonInteractiveEventTypeNames.forEach(eventTuple => { }); // Only used in DEV for exhaustiveness validation. -const knownHTMLTopLevelTypes: Array = [ +const knownHTMLTopLevelTypes: Array = [ DOMTopLevelEventTypes.TOP_ABORT, DOMTopLevelEventTypes.TOP_CANCEL, DOMTopLevelEventTypes.TOP_CAN_PLAY, From 7631024722eae7b03599ba0eeaf9abcd505da13f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 14:07:01 +0100 Subject: [PATCH 053/277] Try to fix Flow issue on Windows --- packages/react-dom/src/events/SimpleEventPlugin.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 8c9a4bad1c..6da1d244c9 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -8,7 +8,6 @@ */ import type {TopLevelType} from 'events/TopLevelEventTypes'; -import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; import type { DispatchConfig, ReactSyntheticEvent, @@ -52,7 +51,7 @@ import getEventCharCode from './getEventCharCode'; * [TOP_ABORT, { sameConfig }], * ]); */ -type EventTuple = [DOMTopLevelEventType, string]; +type EventTuple = [TopLevelType, string]; const interactiveEventTypeNames: Array = [ [DOMTopLevelEventTypes.TOP_BLUR, 'blur'], [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'], @@ -155,7 +154,7 @@ nonInteractiveEventTypeNames.forEach(eventTuple => { }); // Only used in DEV for exhaustiveness validation. -const knownHTMLTopLevelTypes: Array = [ +const knownHTMLTopLevelTypes: Array = [ DOMTopLevelEventTypes.TOP_ABORT, DOMTopLevelEventTypes.TOP_CANCEL, DOMTopLevelEventTypes.TOP_CAN_PLAY, From b998357f9d1a4d933758dd10412e9ff99def36df Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 14:26:32 +0100 Subject: [PATCH 054/277] Try to fix Flow issue on Windows (part 3) --- packages/events/TopLevelEventTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/events/TopLevelEventTypes.js b/packages/events/TopLevelEventTypes.js index f9d4722abb..a2e322babd 100644 --- a/packages/events/TopLevelEventTypes.js +++ b/packages/events/TopLevelEventTypes.js @@ -7,7 +7,7 @@ * @flow */ -import type {DOMTopLevelEventType} from 'react-dom/src/events/DOMTopLevelEventTypes'; +import type {DOMTopLevelEventType} from '../react-dom/src/events/DOMTopLevelEventTypes'; type RNTopLevelEventType = | 'topMouseDown' From f2252a2ad4e47d5bdc1d7778c7d6eb2d318fed0d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 14:46:58 +0100 Subject: [PATCH 055/277] Try to fix Flow issue on Windows (part 4) --- packages/events/PluginModuleType.js | 2 +- packages/events/TopLevelEventTypes.js | 2 +- packages/react-dom/src/events/SimpleEventPlugin.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index 22d53b1ccb..4b5b9b67a3 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -12,7 +12,7 @@ import type { DispatchConfig, ReactSyntheticEvent, } from './ReactSyntheticEventType'; -import type {TopLevelType} from './TopLevelEventTypes'; +import type {TopLevelType} from 'events/TopLevelEventTypes'; export type EventTypes = {[key: string]: DispatchConfig}; diff --git a/packages/events/TopLevelEventTypes.js b/packages/events/TopLevelEventTypes.js index a2e322babd..f9d4722abb 100644 --- a/packages/events/TopLevelEventTypes.js +++ b/packages/events/TopLevelEventTypes.js @@ -7,7 +7,7 @@ * @flow */ -import type {DOMTopLevelEventType} from '../react-dom/src/events/DOMTopLevelEventTypes'; +import type {DOMTopLevelEventType} from 'react-dom/src/events/DOMTopLevelEventTypes'; type RNTopLevelEventType = | 'topMouseDown' diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 6da1d244c9..b087c0788e 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -18,7 +18,7 @@ import type {EventTypes, PluginModule} from 'events/PluginModuleType'; import {accumulateTwoPhaseDispatches} from 'events/EventPropagators'; import SyntheticEvent from 'events/SyntheticEvent'; -import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes'; +import * as DOMTopLevelEventTypes from 'react-dom/src/events/DOMTopLevelEventTypes'; import warning from 'fbjs/lib/warning'; import SyntheticAnimationEvent from './SyntheticAnimationEvent'; From 7ba1abecaa7a1a9a7a5456a66521f2869ba83c1f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 14:55:38 +0100 Subject: [PATCH 056/277] Try to fix Flow issue on Windows (part 5) --- packages/react-dom/src/events/ReactDOMEventListener.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 8878d447d2..f14b420e70 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -9,7 +9,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; +import type {DOMTopLevelEventType} from 'react-dom/src/events/DOMTopLevelEventTypes'; import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; @@ -20,7 +20,7 @@ import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import SimpleEventPlugin from './SimpleEventPlugin'; -import {getRawEventName} from './DOMTopLevelEventTypes'; +import {getRawEventName} from 'react-dom/src/events/DOMTopLevelEventTypes'; const {isInteractiveTopLevelEventType} = SimpleEventPlugin; From fe7890d569c5aa94c371bdb27c5db7bde4ec385a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 15:03:07 +0100 Subject: [PATCH 057/277] Revert recent Flow changes --- packages/events/PluginModuleType.js | 2 +- packages/react-dom/src/events/ReactDOMEventListener.js | 4 ++-- packages/react-dom/src/events/SimpleEventPlugin.js | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/events/PluginModuleType.js b/packages/events/PluginModuleType.js index 4b5b9b67a3..22d53b1ccb 100644 --- a/packages/events/PluginModuleType.js +++ b/packages/events/PluginModuleType.js @@ -12,7 +12,7 @@ import type { DispatchConfig, ReactSyntheticEvent, } from './ReactSyntheticEventType'; -import type {TopLevelType} from 'events/TopLevelEventTypes'; +import type {TopLevelType} from './TopLevelEventTypes'; export type EventTypes = {[key: string]: DispatchConfig}; diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index f14b420e70..8878d447d2 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -9,7 +9,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {DOMTopLevelEventType} from 'react-dom/src/events/DOMTopLevelEventTypes'; +import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; @@ -20,7 +20,7 @@ import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import SimpleEventPlugin from './SimpleEventPlugin'; -import {getRawEventName} from 'react-dom/src/events/DOMTopLevelEventTypes'; +import {getRawEventName} from './DOMTopLevelEventTypes'; const {isInteractiveTopLevelEventType} = SimpleEventPlugin; diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index b087c0788e..8c9a4bad1c 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -8,6 +8,7 @@ */ import type {TopLevelType} from 'events/TopLevelEventTypes'; +import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; import type { DispatchConfig, ReactSyntheticEvent, @@ -18,7 +19,7 @@ import type {EventTypes, PluginModule} from 'events/PluginModuleType'; import {accumulateTwoPhaseDispatches} from 'events/EventPropagators'; import SyntheticEvent from 'events/SyntheticEvent'; -import * as DOMTopLevelEventTypes from 'react-dom/src/events/DOMTopLevelEventTypes'; +import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes'; import warning from 'fbjs/lib/warning'; import SyntheticAnimationEvent from './SyntheticAnimationEvent'; @@ -51,7 +52,7 @@ import getEventCharCode from './getEventCharCode'; * [TOP_ABORT, { sameConfig }], * ]); */ -type EventTuple = [TopLevelType, string]; +type EventTuple = [DOMTopLevelEventType, string]; const interactiveEventTypeNames: Array = [ [DOMTopLevelEventTypes.TOP_BLUR, 'blur'], [DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'], @@ -154,7 +155,7 @@ nonInteractiveEventTypeNames.forEach(eventTuple => { }); // Only used in DEV for exhaustiveness validation. -const knownHTMLTopLevelTypes: Array = [ +const knownHTMLTopLevelTypes: Array = [ DOMTopLevelEventTypes.TOP_ABORT, DOMTopLevelEventTypes.TOP_CANCEL, DOMTopLevelEventTypes.TOP_CAN_PLAY, From 025d867dceccfa54cfa12122a982ad3c3eff995a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 15:20:15 +0100 Subject: [PATCH 058/277] Try another approach at fixing Windows Flow issues --- packages/events/TopLevelEventTypes.js | 16 +- .../src/events/DOMTopLevelEventTypes.js | 262 +++++++----------- .../src/events/ReactDOMEventListener.js | 2 +- .../react-dom/src/events/SimpleEventPlugin.js | 6 +- 4 files changed, 126 insertions(+), 160 deletions(-) diff --git a/packages/events/TopLevelEventTypes.js b/packages/events/TopLevelEventTypes.js index f9d4722abb..e53ba71e4d 100644 --- a/packages/events/TopLevelEventTypes.js +++ b/packages/events/TopLevelEventTypes.js @@ -7,8 +7,6 @@ * @flow */ -import type {DOMTopLevelEventType} from 'react-dom/src/events/DOMTopLevelEventTypes'; - type RNTopLevelEventType = | 'topMouseDown' | 'topMouseMove' @@ -20,4 +18,18 @@ type RNTopLevelEventType = | 'topTouchMove' | 'topTouchStart'; +export opaque type DOMTopLevelEventType = string; + +export function unsafeCastStringToDOMTopLevelType( + topLevelType: string, +): DOMTopLevelEventType { + return topLevelType; +} + +export function unsafeCastDOMTopLevelTypeToString( + topLevelType: DOMTopLevelEventType, +): string { + return topLevelType; +} + export type TopLevelType = DOMTopLevelEventType | RNTopLevelEventType; diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js index aac82dbbe7..e6d09ee990 100644 --- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -7,6 +7,12 @@ * @flow */ +import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes'; + +import { + unsafeCastStringToDOMTopLevelType, + unsafeCastDOMTopLevelTypeToString, +} from 'events/TopLevelEventTypes'; import getVendorPrefixedEventName from './getVendorPrefixedEventName'; /** @@ -19,161 +25,107 @@ import getVendorPrefixedEventName from './getVendorPrefixedEventName'; * of a constant in this module. */ -// eslint-disable-next-line no-undef -export opaque type DOMTopLevelEventType = - | 'abort' - | 'animationend' - | 'animationiteration' - | 'animationstart' - | 'blur' - | 'canplay' - | 'canplaythrough' - | 'cancel' - | 'change' - | 'click' - | 'close' - | 'compositionend' - | 'compositionstart' - | 'compositionupdate' - | 'contextmenu' - | 'copy' - | 'cut' - | 'dblclick' - | 'drag' - | 'dragend' - | 'dragenter' - | 'dragexit' - | 'dragleave' - | 'dragover' - | 'dragstart' - | 'drop' - | 'durationchange' - | 'emptied' - | 'encrypted' - | 'ended' - | 'error' - | 'focus' - | 'input' - | 'invalid' - | 'keydown' - | 'keypress' - | 'keyup' - | 'load' - | 'loadstart' - | 'loadeddata' - | 'loadedmetadata' - | 'mousedown' - | 'mousemove' - | 'mouseout' - | 'mouseover' - | 'mouseup' - | 'paste' - | 'pause' - | 'play' - | 'playing' - | 'progress' - | 'ratechange' - | 'reset' - | 'scroll' - | 'seeked' - | 'seeking' - | 'selectionchange' - | 'stalled' - | 'submit' - | 'suspend' - | 'textInput' - | 'timeupdate' - | 'toggle' - | 'touchcancel' - | 'touchend' - | 'touchmove' - | 'touchstart' - | 'transitionend' - | 'volumechange' - | 'waiting' - | 'wheel'; +export const TOP_ABORT = unsafeCastStringToDOMTopLevelType('abort'); +export const TOP_ANIMATION_END = unsafeCastStringToDOMTopLevelType( + getVendorPrefixedEventName('animationend'), +); +export const TOP_ANIMATION_ITERATION = unsafeCastStringToDOMTopLevelType( + getVendorPrefixedEventName('animationiteration'), +); +export const TOP_ANIMATION_START = unsafeCastStringToDOMTopLevelType( + getVendorPrefixedEventName('animationstart'), +); +export const TOP_BLUR = unsafeCastStringToDOMTopLevelType('blur'); +export const TOP_CAN_PLAY = unsafeCastStringToDOMTopLevelType('canplay'); +export const TOP_CAN_PLAY_THROUGH = unsafeCastStringToDOMTopLevelType( + 'canplaythrough', +); +export const TOP_CANCEL = unsafeCastStringToDOMTopLevelType('cancel'); +export const TOP_CHANGE = unsafeCastStringToDOMTopLevelType('change'); +export const TOP_CLICK = unsafeCastStringToDOMTopLevelType('click'); +export const TOP_CLOSE = unsafeCastStringToDOMTopLevelType('close'); +export const TOP_COMPOSITION_END = unsafeCastStringToDOMTopLevelType( + 'compositionend', +); +export const TOP_COMPOSITION_START = unsafeCastStringToDOMTopLevelType( + 'compositionstart', +); +export const TOP_COMPOSITION_UPDATE = unsafeCastStringToDOMTopLevelType( + 'compositionupdate', +); +export const TOP_CONTEXT_MENU = unsafeCastStringToDOMTopLevelType( + 'contextmenu', +); +export const TOP_COPY = unsafeCastStringToDOMTopLevelType('copy'); +export const TOP_CUT = unsafeCastStringToDOMTopLevelType('cut'); +export const TOP_DOUBLE_CLICK = unsafeCastStringToDOMTopLevelType('dblclick'); +export const TOP_DRAG = unsafeCastStringToDOMTopLevelType('drag'); +export const TOP_DRAG_END = unsafeCastStringToDOMTopLevelType('dragend'); +export const TOP_DRAG_ENTER = unsafeCastStringToDOMTopLevelType('dragenter'); +export const TOP_DRAG_EXIT = unsafeCastStringToDOMTopLevelType('dragexit'); +export const TOP_DRAG_LEAVE = unsafeCastStringToDOMTopLevelType('dragleave'); +export const TOP_DRAG_OVER = unsafeCastStringToDOMTopLevelType('dragover'); +export const TOP_DRAG_START = unsafeCastStringToDOMTopLevelType('dragstart'); +export const TOP_DROP = unsafeCastStringToDOMTopLevelType('drop'); +export const TOP_DURATION_CHANGE = unsafeCastStringToDOMTopLevelType( + 'durationchange', +); +export const TOP_EMPTIED = unsafeCastStringToDOMTopLevelType('emptied'); +export const TOP_ENCRYPTED = unsafeCastStringToDOMTopLevelType('encrypted'); +export const TOP_ENDED = unsafeCastStringToDOMTopLevelType('ended'); +export const TOP_ERROR = unsafeCastStringToDOMTopLevelType('error'); +export const TOP_FOCUS = unsafeCastStringToDOMTopLevelType('focus'); +export const TOP_INPUT = unsafeCastStringToDOMTopLevelType('input'); +export const TOP_INVALID = unsafeCastStringToDOMTopLevelType('invalid'); +export const TOP_KEY_DOWN = unsafeCastStringToDOMTopLevelType('keydown'); +export const TOP_KEY_PRESS = unsafeCastStringToDOMTopLevelType('keypress'); +export const TOP_KEY_UP = unsafeCastStringToDOMTopLevelType('keyup'); +export const TOP_LOAD = unsafeCastStringToDOMTopLevelType('load'); +export const TOP_LOAD_START = unsafeCastStringToDOMTopLevelType('loadstart'); +export const TOP_LOADED_DATA = unsafeCastStringToDOMTopLevelType('loadeddata'); +export const TOP_LOADED_METADATA = unsafeCastStringToDOMTopLevelType( + 'loadedmetadata', +); +export const TOP_MOUSE_DOWN = unsafeCastStringToDOMTopLevelType('mousedown'); +export const TOP_MOUSE_MOVE = unsafeCastStringToDOMTopLevelType('mousemove'); +export const TOP_MOUSE_OUT = unsafeCastStringToDOMTopLevelType('mouseout'); +export const TOP_MOUSE_OVER = unsafeCastStringToDOMTopLevelType('mouseover'); +export const TOP_MOUSE_UP = unsafeCastStringToDOMTopLevelType('mouseup'); +export const TOP_PASTE = unsafeCastStringToDOMTopLevelType('paste'); +export const TOP_PAUSE = unsafeCastStringToDOMTopLevelType('pause'); +export const TOP_PLAY = unsafeCastStringToDOMTopLevelType('play'); +export const TOP_PLAYING = unsafeCastStringToDOMTopLevelType('playing'); +export const TOP_PROGRESS = unsafeCastStringToDOMTopLevelType('progress'); +export const TOP_RATE_CHANGE = unsafeCastStringToDOMTopLevelType('ratechange'); +export const TOP_RESET = unsafeCastStringToDOMTopLevelType('reset'); +export const TOP_SCROLL = unsafeCastStringToDOMTopLevelType('scroll'); +export const TOP_SEEKED = unsafeCastStringToDOMTopLevelType('seeked'); +export const TOP_SEEKING = unsafeCastStringToDOMTopLevelType('seeking'); +export const TOP_SELECTION_CHANGE = unsafeCastStringToDOMTopLevelType( + 'selectionchange', +); +export const TOP_STALLED = unsafeCastStringToDOMTopLevelType('stalled'); +export const TOP_SUBMIT = unsafeCastStringToDOMTopLevelType('submit'); +export const TOP_SUSPEND = unsafeCastStringToDOMTopLevelType('suspend'); +export const TOP_TEXT_INPUT = unsafeCastStringToDOMTopLevelType('textInput'); +export const TOP_TIME_UPDATE = unsafeCastStringToDOMTopLevelType('timeupdate'); +export const TOP_TOGGLE = unsafeCastStringToDOMTopLevelType('toggle'); +export const TOP_TOUCH_CANCEL = unsafeCastStringToDOMTopLevelType( + 'touchcancel', +); +export const TOP_TOUCH_END = unsafeCastStringToDOMTopLevelType('touchend'); +export const TOP_TOUCH_MOVE = unsafeCastStringToDOMTopLevelType('touchmove'); +export const TOP_TOUCH_START = unsafeCastStringToDOMTopLevelType('touchstart'); +export const TOP_TRANSITION_END = unsafeCastStringToDOMTopLevelType( + getVendorPrefixedEventName('transitionend'), +); +export const TOP_VOLUME_CHANGE = unsafeCastStringToDOMTopLevelType( + 'volumechange', +); +export const TOP_WAITING = unsafeCastStringToDOMTopLevelType('waiting'); +export const TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel'); -export const TOP_ABORT: DOMTopLevelEventType = 'abort'; -export const TOP_ANIMATION_END: DOMTopLevelEventType = getVendorPrefixedEventName( - 'animationend', -); -export const TOP_ANIMATION_ITERATION: DOMTopLevelEventType = getVendorPrefixedEventName( - 'animationiteration', -); -export const TOP_ANIMATION_START: DOMTopLevelEventType = getVendorPrefixedEventName( - 'animationstart', -); -export const TOP_BLUR: DOMTopLevelEventType = 'blur'; -export const TOP_CAN_PLAY: DOMTopLevelEventType = 'canplay'; -export const TOP_CAN_PLAY_THROUGH: DOMTopLevelEventType = 'canplaythrough'; -export const TOP_CANCEL: DOMTopLevelEventType = 'cancel'; -export const TOP_CHANGE: DOMTopLevelEventType = 'change'; -export const TOP_CLICK: DOMTopLevelEventType = 'click'; -export const TOP_CLOSE: DOMTopLevelEventType = 'close'; -export const TOP_COMPOSITION_END: DOMTopLevelEventType = 'compositionend'; -export const TOP_COMPOSITION_START: DOMTopLevelEventType = 'compositionstart'; -export const TOP_COMPOSITION_UPDATE: DOMTopLevelEventType = 'compositionupdate'; -export const TOP_CONTEXT_MENU: DOMTopLevelEventType = 'contextmenu'; -export const TOP_COPY: DOMTopLevelEventType = 'copy'; -export const TOP_CUT: DOMTopLevelEventType = 'cut'; -export const TOP_DOUBLE_CLICK: DOMTopLevelEventType = 'dblclick'; -export const TOP_DRAG: DOMTopLevelEventType = 'drag'; -export const TOP_DRAG_END: DOMTopLevelEventType = 'dragend'; -export const TOP_DRAG_ENTER: DOMTopLevelEventType = 'dragenter'; -export const TOP_DRAG_EXIT: DOMTopLevelEventType = 'dragexit'; -export const TOP_DRAG_LEAVE: DOMTopLevelEventType = 'dragleave'; -export const TOP_DRAG_OVER: DOMTopLevelEventType = 'dragover'; -export const TOP_DRAG_START: DOMTopLevelEventType = 'dragstart'; -export const TOP_DROP: DOMTopLevelEventType = 'drop'; -export const TOP_DURATION_CHANGE: DOMTopLevelEventType = 'durationchange'; -export const TOP_EMPTIED: DOMTopLevelEventType = 'emptied'; -export const TOP_ENCRYPTED: DOMTopLevelEventType = 'encrypted'; -export const TOP_ENDED: DOMTopLevelEventType = 'ended'; -export const TOP_ERROR: DOMTopLevelEventType = 'error'; -export const TOP_FOCUS: DOMTopLevelEventType = 'focus'; -export const TOP_INPUT: DOMTopLevelEventType = 'input'; -export const TOP_INVALID: DOMTopLevelEventType = 'invalid'; -export const TOP_KEY_DOWN: DOMTopLevelEventType = 'keydown'; -export const TOP_KEY_PRESS: DOMTopLevelEventType = 'keypress'; -export const TOP_KEY_UP: DOMTopLevelEventType = 'keyup'; -export const TOP_LOAD: DOMTopLevelEventType = 'load'; -export const TOP_LOAD_START: DOMTopLevelEventType = 'loadstart'; -export const TOP_LOADED_DATA: DOMTopLevelEventType = 'loadeddata'; -export const TOP_LOADED_METADATA: DOMTopLevelEventType = 'loadedmetadata'; -export const TOP_MOUSE_DOWN: DOMTopLevelEventType = 'mousedown'; -export const TOP_MOUSE_MOVE: DOMTopLevelEventType = 'mousemove'; -export const TOP_MOUSE_OUT: DOMTopLevelEventType = 'mouseout'; -export const TOP_MOUSE_OVER: DOMTopLevelEventType = 'mouseover'; -export const TOP_MOUSE_UP: DOMTopLevelEventType = 'mouseup'; -export const TOP_PASTE: DOMTopLevelEventType = 'paste'; -export const TOP_PAUSE: DOMTopLevelEventType = 'pause'; -export const TOP_PLAY: DOMTopLevelEventType = 'play'; -export const TOP_PLAYING: DOMTopLevelEventType = 'playing'; -export const TOP_PROGRESS: DOMTopLevelEventType = 'progress'; -export const TOP_RATE_CHANGE: DOMTopLevelEventType = 'ratechange'; -export const TOP_RESET: DOMTopLevelEventType = 'reset'; -export const TOP_SCROLL: DOMTopLevelEventType = 'scroll'; -export const TOP_SEEKED: DOMTopLevelEventType = 'seeked'; -export const TOP_SEEKING: DOMTopLevelEventType = 'seeking'; -export const TOP_SELECTION_CHANGE: DOMTopLevelEventType = 'selectionchange'; -export const TOP_STALLED: DOMTopLevelEventType = 'stalled'; -export const TOP_SUBMIT: DOMTopLevelEventType = 'submit'; -export const TOP_SUSPEND: DOMTopLevelEventType = 'suspend'; -export const TOP_TEXT_INPUT: DOMTopLevelEventType = 'textInput'; -export const TOP_TIME_UPDATE: DOMTopLevelEventType = 'timeupdate'; -export const TOP_TOGGLE: DOMTopLevelEventType = 'toggle'; -export const TOP_TOUCH_CANCEL: DOMTopLevelEventType = 'touchcancel'; -export const TOP_TOUCH_END: DOMTopLevelEventType = 'touchend'; -export const TOP_TOUCH_MOVE: DOMTopLevelEventType = 'touchmove'; -export const TOP_TOUCH_START: DOMTopLevelEventType = 'touchstart'; -export const TOP_TRANSITION_END: DOMTopLevelEventType = getVendorPrefixedEventName( - 'transitionend', -); -export const TOP_VOLUME_CHANGE: DOMTopLevelEventType = 'volumechange'; -export const TOP_WAITING: DOMTopLevelEventType = 'waiting'; -export const TOP_WHEEL: DOMTopLevelEventType = 'wheel'; - -export const mediaEventTypes: Array = [ +export const mediaEventTypes = [ TOP_ABORT, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, @@ -200,5 +152,5 @@ export const mediaEventTypes: Array = [ ]; export function getRawEventName(topLevelType: DOMTopLevelEventType): string { - return topLevelType; + return unsafeCastDOMTopLevelTypeToString(topLevelType); } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 8878d447d2..b7bd8a46e3 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -9,7 +9,7 @@ import type {AnyNativeEvent} from 'events/PluginModuleType'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; +import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes'; import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching'; import {runExtractedEventsInBatch} from 'events/EventPluginHub'; diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 8c9a4bad1c..193aff57b5 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -7,8 +7,10 @@ * @flow */ -import type {TopLevelType} from 'events/TopLevelEventTypes'; -import type {DOMTopLevelEventType} from './DOMTopLevelEventTypes'; +import type { + TopLevelType, + DOMTopLevelEventType, +} from 'events/TopLevelEventTypes'; import type { DispatchConfig, ReactSyntheticEvent, From d758960116b3ea3a3bd3dfe2f297d5e680fe9ff5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 15:42:43 +0100 Subject: [PATCH 059/277] Tweak comments --- packages/events/TopLevelEventTypes.js | 4 ++++ .../react-dom/src/events/DOMTopLevelEventTypes.js | 13 +++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/events/TopLevelEventTypes.js b/packages/events/TopLevelEventTypes.js index e53ba71e4d..3f9b8ab7e9 100644 --- a/packages/events/TopLevelEventTypes.js +++ b/packages/events/TopLevelEventTypes.js @@ -20,6 +20,10 @@ type RNTopLevelEventType = export opaque type DOMTopLevelEventType = string; +// Do not uses the below two methods directly! +// Instead use constants exported from DOMTopLevelEventTypes in ReactDOM. +// (It is the only module that is allowed to access these methods.) + export function unsafeCastStringToDOMTopLevelType( topLevelType: string, ): DOMTopLevelEventType { diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js index e6d09ee990..1898b0f965 100644 --- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -16,15 +16,12 @@ import { import getVendorPrefixedEventName from './getVendorPrefixedEventName'; /** - * To identify top level events in react-dom, we use constants defined by this - * module. Those are completely opaque to every other module but we rely on them - * being the raw DOM event names inside this module. This allows us to build a - * very efficient mapping from top level identifiers to the raw event type. - * - * The use of an `opaque` flow type makes sure that we can only access the value - * of a constant in this module. + * To identify top level events in ReactDOM, we use constants defined by this + * module. This is the only module that uses the unsafe* methods to express + * that the constants actually correspond to the browser event names. This lets + * us save some bundle size by avoiding a top level type -> event name map. + * The rest of ReactDOM code should import top level types from this file. */ - export const TOP_ABORT = unsafeCastStringToDOMTopLevelType('abort'); export const TOP_ANIMATION_END = unsafeCastStringToDOMTopLevelType( getVendorPrefixedEventName('animationend'), From 9097f3cdf089a34d873f3d349e3bfbed90852fad Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 15 May 2018 19:16:29 +0100 Subject: [PATCH 060/277] Delete React Call/Return experiment (#12820) --- packages/react-call-return/README.md | 32 -- packages/react-call-return/index.js | 12 - packages/react-call-return/npm/index.js | 7 - packages/react-call-return/package.json | 19 - .../react-call-return/src/ReactCallReturn.js | 95 ----- .../ReactCallReturn-test.internal.js | 326 ------------------ .../ReactServerRendering-test.internal.js | 21 -- .../src/server/ReactPartialRenderer.js | 9 - .../src/ReactDebugFiberPerf.js | 4 - packages/react-reconciler/src/ReactFiber.js | 10 - .../src/ReactFiberBeginWork.js | 55 --- .../src/ReactFiberCommitWork.js | 5 - .../src/ReactFiberCompleteWork.js | 87 ----- .../ReactIncrementalPerf-test.internal.js | 49 --- ...ReactIncrementalPerf-test.internal.js.snap | 19 - .../react/src/__tests__/ReactChildren-test.js | 51 --- packages/shared/ReactSymbols.js | 4 - packages/shared/ReactTypeOfWork.js | 6 +- packages/shared/getComponentName.js | 6 - scripts/rollup/bundles.js | 10 - 20 files changed, 3 insertions(+), 824 deletions(-) delete mode 100644 packages/react-call-return/README.md delete mode 100644 packages/react-call-return/index.js delete mode 100644 packages/react-call-return/npm/index.js delete mode 100644 packages/react-call-return/package.json delete mode 100644 packages/react-call-return/src/ReactCallReturn.js delete mode 100644 packages/react-call-return/src/__tests__/ReactCallReturn-test.internal.js diff --git a/packages/react-call-return/README.md b/packages/react-call-return/README.md deleted file mode 100644 index 6cfd1265aa..0000000000 --- a/packages/react-call-return/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# react-call-return - -This is an experimental package for multi-pass rendering in React. - -**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.** - -**Use it at your own risk.** - -# No, Really, It Is Unstable - -This is **an experiment**. - -We **will** replace this with a different API in the future. -It can break between patch versions of React. - -We also know that **it has bugs**. - -Don't rely on this for anything except experiments. -Even in experiments, make sure to lock the versions so that an update doesn't break your app. - -Don't publish third party components relying on this unless you clearly mark them as experimental too. -They will break. - -Have fun! Let us know if you find interesting use cases for it. - -# API - -See the test case in `src/__tests__/ReactCallReturn.js` for an example. - -# What and Why - -The API is not very intuitive right now, but [this is a good overview](https://cdb.reacttraining.com/react-call-return-what-and-why-7e7761f81843) of why it might be useful in some cases. We are very open to better API ideas for this concept. diff --git a/packages/react-call-return/index.js b/packages/react-call-return/index.js deleted file mode 100644 index 8d3798337c..0000000000 --- a/packages/react-call-return/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -'use strict'; - -module.exports = require('./src/ReactCallReturn'); diff --git a/packages/react-call-return/npm/index.js b/packages/react-call-return/npm/index.js deleted file mode 100644 index 0856ded523..0000000000 --- a/packages/react-call-return/npm/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react-call-return.production.min.js'); -} else { - module.exports = require('./cjs/react-call-return.development.js'); -} diff --git a/packages/react-call-return/package.json b/packages/react-call-return/package.json deleted file mode 100644 index 52256ef0f9..0000000000 --- a/packages/react-call-return/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "react-call-return", - "description": "Experimental APIs for multi-pass rendering in React.", - "version": "0.8.0", - "repository": "facebook/react", - "files": [ - "LICENSE", - "README.md", - "index.js", - "cjs/" - ], - "dependencies": { - "fbjs": "^0.8.16", - "object-assign": "^4.1.1" - }, - "peerDependencies": { - "react": "^16.0.0" - } -} diff --git a/packages/react-call-return/src/ReactCallReturn.js b/packages/react-call-return/src/ReactCallReturn.js deleted file mode 100644 index 432ff8f192..0000000000 --- a/packages/react-call-return/src/ReactCallReturn.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import { - REACT_CALL_TYPE, - REACT_RETURN_TYPE, - REACT_ELEMENT_TYPE, -} from 'shared/ReactSymbols'; - -import type {ReactCall, ReactNodeList, ReactReturn} from 'shared/ReactTypes'; - -type CallHandler = (props: T, returns: Array) => ReactNodeList; - -export function unstable_createCall( - children: ReactNodeList, - handler: CallHandler, - props: T, - key: ?string = null, -): ReactCall { - const call = { - // This tag allow us to uniquely identify this as a React Call - $$typeof: REACT_ELEMENT_TYPE, - type: REACT_CALL_TYPE, - key: key == null ? null : '' + key, - ref: null, - props: { - props, - handler, - children: children, - }, - }; - - if (__DEV__) { - // TODO: Add _store property for marking this as validated. - if (Object.freeze) { - Object.freeze(call.props); - Object.freeze(call); - } - } - - return call; -} - -export function unstable_createReturn(value: V): ReactReturn { - const returnNode = { - // This tag allow us to uniquely identify this as a React Call - $$typeof: REACT_ELEMENT_TYPE, - type: REACT_RETURN_TYPE, - key: null, - ref: null, - props: { - value, - }, - }; - - if (__DEV__) { - // TODO: Add _store property for marking this as validated. - if (Object.freeze) { - Object.freeze(returnNode); - } - } - - return returnNode; -} - -/** - * Verifies the object is a call object. - */ -export function unstable_isCall(object: mixed): boolean { - return ( - typeof object === 'object' && - object !== null && - object.type === REACT_CALL_TYPE - ); -} - -/** - * Verifies the object is a return object. - */ -export function unstable_isReturn(object: mixed): boolean { - return ( - typeof object === 'object' && - object !== null && - object.type === REACT_RETURN_TYPE - ); -} - -export const unstable_REACT_RETURN_TYPE = REACT_RETURN_TYPE; -export const unstable_REACT_CALL_TYPE = REACT_CALL_TYPE; diff --git a/packages/react-call-return/src/__tests__/ReactCallReturn-test.internal.js b/packages/react-call-return/src/__tests__/ReactCallReturn-test.internal.js deleted file mode 100644 index 84018bac30..0000000000 --- a/packages/react-call-return/src/__tests__/ReactCallReturn-test.internal.js +++ /dev/null @@ -1,326 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * 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'; - -let React; -let ReactFeatureFlags; -let ReactNoop; -let ReactCallReturn; - -describe('ReactCallReturn', () => { - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - React = require('react'); - ReactNoop = require('react-noop-renderer'); - ReactCallReturn = require('react-call-return'); - }); - - function div(...children) { - children = children.map(c => (typeof c === 'string' ? {text: c} : c)); - return {type: 'div', children, prop: undefined}; - } - - function span(prop) { - return {type: 'span', children: [], prop}; - } - - it('should render a call', () => { - const ops = []; - - function Continuation({isSame}) { - ops.push(['Continuation', isSame]); - return ; - } - - // An alternative API could mark Continuation as something that needs - // returning. E.g. Continuation.returnType = 123; - function Child({bar}) { - ops.push(['Child', bar]); - return ReactCallReturn.unstable_createReturn({ - props: { - bar: bar, - }, - continuation: Continuation, - }); - } - - function Indirection() { - ops.push('Indirection'); - return [, ]; - } - - function HandleReturns(props, returns) { - ops.push('HandleReturns'); - return returns.map((y, i) => ( - - )); - } - - // An alternative API could mark Parent as something that needs - // returning. E.g. Parent.handler = HandleReturns; - function Parent(props) { - ops.push('Parent'); - return ReactCallReturn.unstable_createCall( - props.children, - HandleReturns, - props, - ); - } - - function App() { - return ( -
    - - - -
    - ); - } - - ReactNoop.render(); - ReactNoop.flush(); - - expect(ops).toEqual([ - 'Parent', - 'Indirection', - ['Child', true], - // Return - ['Child', false], - // Return - 'HandleReturns', - // Call continuations - ['Continuation', true], - ['Continuation', false], - ]); - expect(ReactNoop.getChildren()).toEqual([ - div(span('foo==bar'), span('foo!=bar')), - ]); - }); - - it('should update a call', () => { - function Continuation({isSame}) { - return ; - } - - function Child({bar}) { - return ReactCallReturn.unstable_createReturn({ - props: { - bar: bar, - }, - continuation: Continuation, - }); - } - - function Indirection() { - return [, ]; - } - - function HandleReturns(props, returns) { - return returns.map((y, i) => ( - - )); - } - - function Parent(props) { - return ReactCallReturn.unstable_createCall( - props.children, - HandleReturns, - props, - ); - } - - function App(props) { - return ( -
    - - - -
    - ); - } - - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - div(span('foo==bar'), span('foo!=bar')), - ]); - - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - div(span('foo!=bar'), span('foo==bar')), - ]); - }); - - it('should unmount a composite in a call', () => { - let ops = []; - - class Continuation extends React.Component { - render() { - ops.push('Continuation'); - return
    ; - } - componentWillUnmount() { - ops.push('Unmount Continuation'); - } - } - - class Child extends React.Component { - render() { - ops.push('Child'); - return ReactCallReturn.unstable_createReturn(Continuation); - } - componentWillUnmount() { - ops.push('Unmount Child'); - } - } - - function HandleReturns(props, returns) { - ops.push('HandleReturns'); - return returns.map((ContinuationComponent, i) => ( - - )); - } - - class Parent extends React.Component { - render() { - ops.push('Parent'); - return ReactCallReturn.unstable_createCall( - this.props.children, - HandleReturns, - this.props, - ); - } - componentWillUnmount() { - ops.push('Unmount Parent'); - } - } - - ReactNoop.render( - - - , - ); - ReactNoop.flush(); - - expect(ops).toEqual(['Parent', 'Child', 'HandleReturns', 'Continuation']); - - ops = []; - - ReactNoop.render(
    ); - ReactNoop.flush(); - - expect(ops).toEqual([ - 'Unmount Parent', - 'Unmount Child', - 'Unmount Continuation', - ]); - }); - - it('should handle deep updates in call', () => { - let instances = {}; - - class Counter extends React.Component { - state = {value: 5}; - render() { - instances[this.props.id] = this; - return ReactCallReturn.unstable_createReturn(this.state.value); - } - } - - function App(props) { - return ReactCallReturn.unstable_createCall( - [ - , - , - , - ], - (p, returns) => returns.map((y, i) => ), - {}, - ); - } - - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span(500), span(500), span(500)]); - - instances.a.setState({value: 1}); - instances.b.setState({value: 2}); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span(100), span(200), span(500)]); - }); - - it('should unmount and remount children', () => { - let ops = []; - - class Call extends React.Component { - render() { - return ReactCallReturn.unstable_createCall( - this.props.children, - (p, returns) => returns, - {}, - ); - } - } - - class Return extends React.Component { - render() { - ops.push(`Return ${this.props.value}`); - return ReactCallReturn.unstable_createReturn(this.props.children); - } - - UNSAFE_componentWillMount() { - ops.push(`Mount Return ${this.props.value}`); - } - - componentWillUnmount() { - ops.push(`Unmount Return ${this.props.value}`); - } - } - - ReactNoop.render( - - - - , - ); - expect(ReactNoop.flush).toWarnDev( - 'componentWillMount: Please update the following components ' + - 'to use componentDidMount instead: Return', - ); - - expect(ops).toEqual([ - 'Mount Return 1', - 'Return 1', - 'Mount Return 2', - 'Return 2', - ]); - - ops = []; - - ReactNoop.render(); - ReactNoop.flush(); - - expect(ops).toEqual(['Unmount Return 1', 'Unmount Return 2']); - - ops = []; - - ReactNoop.render( - - - , - ); - ReactNoop.flush(); - - expect(ops).toEqual(['Mount Return 3', 'Return 3']); - }); -}); diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js index 05eb32ba03..b7c6eca3de 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js @@ -11,7 +11,6 @@ 'use strict'; let React; -let ReactCallReturn; let ReactDOMServer; let PropTypes; @@ -23,7 +22,6 @@ describe('ReactDOMServer', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - ReactCallReturn = require('react-call-return'); PropTypes = require('prop-types'); ReactDOMServer = require('react-dom/server'); }); @@ -623,25 +621,6 @@ describe('ReactDOMServer', () => { ); }); - it('should throw rendering call/return on the server', () => { - expect(() => { - ReactDOMServer.renderToString( -
    {ReactCallReturn.unstable_createReturn(42)}
    , - ); - }).toThrow( - 'The experimental Call and Return types are not currently supported by the server renderer.', - ); - expect(() => { - ReactDOMServer.renderToString( -
    - {ReactCallReturn.unstable_createCall(null, function() {}, {})} -
    , - ); - }).toThrow( - 'The experimental Call and Return types are not currently supported by the server renderer.', - ); - }); - it('should warn when server rendering a class with a render method that does not extend React.Component', () => { class ClassWithRenderNotExtended { render() { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index d0cef9c60a..59ea170b93 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -31,8 +31,6 @@ import { REACT_FRAGMENT_TYPE, REACT_STRICT_MODE_TYPE, REACT_ASYNC_MODE_TYPE, - REACT_CALL_TYPE, - REACT_RETURN_TYPE, REACT_PORTAL_TYPE, REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, @@ -831,13 +829,6 @@ class ReactDOMServerRenderer { this.stack.push(frame); return ''; } - case REACT_CALL_TYPE: - case REACT_RETURN_TYPE: - invariant( - false, - 'The experimental Call and Return types are not currently ' + - 'supported by the server renderer.', - ); // eslint-disable-next-line-no-fallthrough default: break; diff --git a/packages/react-reconciler/src/ReactDebugFiberPerf.js b/packages/react-reconciler/src/ReactDebugFiberPerf.js index c2889986bf..7c2226f9a8 100644 --- a/packages/react-reconciler/src/ReactDebugFiberPerf.js +++ b/packages/react-reconciler/src/ReactDebugFiberPerf.js @@ -16,8 +16,6 @@ import { HostComponent, HostText, HostPortal, - CallComponent, - ReturnComponent, Fragment, ContextProvider, ContextConsumer, @@ -171,8 +169,6 @@ const shouldIgnoreFiber = (fiber: Fiber): boolean => { case HostComponent: case HostText: case HostPortal: - case CallComponent: - case ReturnComponent: case Fragment: case ContextProvider: case ContextConsumer: diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 720fb9ffe1..acfedb266c 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -24,8 +24,6 @@ import { HostComponent, HostText, HostPortal, - CallComponent, - ReturnComponent, ForwardRef, Fragment, Mode, @@ -41,8 +39,6 @@ import {NoContext, AsyncMode, ProfileMode, StrictMode} from './ReactTypeOfMode'; import { REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, - REACT_RETURN_TYPE, - REACT_CALL_TYPE, REACT_STRICT_MODE_TYPE, REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, @@ -364,12 +360,6 @@ export function createFiberFromElement( break; case REACT_PROFILER_TYPE: return createFiberFromProfiler(pendingProps, mode, expirationTime, key); - case REACT_CALL_TYPE: - fiberTag = CallComponent; - break; - case REACT_RETURN_TYPE: - fiberTag = ReturnComponent; - break; case REACT_TIMEOUT_TYPE: fiberTag = TimeoutComponent; // Suspense does not require async, but its children should be strict diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d08ebd1330..d344ce0998 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -27,9 +27,6 @@ import { HostComponent, HostText, HostPortal, - CallComponent, - CallHandlerPhase, - ReturnComponent, ForwardRef, Fragment, Mode, @@ -741,44 +738,6 @@ export default function( } } - function updateCallComponent(current, workInProgress, renderExpirationTime) { - let nextProps = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextProps) { - nextProps = workInProgress.memoizedProps; - // TODO: When bailing out, we might need to return the stateNode instead - // of the child. To check it for work. - // return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - - const nextChildren = nextProps.children; - - // The following is a fork of reconcileChildrenAtExpirationTime but using - // stateNode to store the child. - if (current === null) { - workInProgress.stateNode = mountChildFibers( - workInProgress, - workInProgress.stateNode, - nextChildren, - renderExpirationTime, - ); - } else { - workInProgress.stateNode = reconcileChildFibers( - workInProgress, - current.stateNode, - nextChildren, - renderExpirationTime, - ); - } - - memoizeProps(workInProgress, nextProps); - // This doesn't take arbitrary time so we could synchronously just begin - // eagerly do the work of workInProgress.child as an optimization. - return workInProgress.stateNode; - } - function updateTimeoutComponent( current, workInProgress, @@ -1260,20 +1219,6 @@ export default function( ); case HostText: return updateHostText(current, workInProgress); - case CallHandlerPhase: - // This is a restart. Reset the tag to the initial phase. - workInProgress.tag = CallComponent; - // Intentionally fall through since this is now the same. - case CallComponent: - return updateCallComponent( - current, - workInProgress, - renderExpirationTime, - ); - case ReturnComponent: - // A return component is just a placeholder, we can just run through the - // next one immediately. - return null; case TimeoutComponent: return updateTimeoutComponent( current, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index f3184ced83..ebe37ef5a0 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -25,7 +25,6 @@ import { HostComponent, HostText, HostPortal, - CallComponent, Profiler, TimeoutComponent, } from 'shared/ReactTypeOfWork'; @@ -393,10 +392,6 @@ export default function( safelyDetachRef(current); return; } - case CallComponent: { - commitNestedUnmounts(current.stateNode); - return; - } case HostPortal: { // TODO: this is recursive. // We are also not using this parent because diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index ab55061b12..9d2cec1f35 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -31,9 +31,6 @@ import { HostComponent, HostText, HostPortal, - CallComponent, - CallHandlerPhase, - ReturnComponent, ContextProvider, ContextConsumer, ForwardRef, @@ -45,8 +42,6 @@ import { import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; -import {reconcileChildFibers} from './ReactChildFiber'; - export default function( config: HostConfig, hostContext: HostContext, @@ -97,75 +92,6 @@ export default function( workInProgress.effectTag |= Ref; } - function appendAllReturns(returns: Array, workInProgress: Fiber) { - let node = workInProgress.stateNode; - if (node) { - node.return = workInProgress; - } - while (node !== null) { - if ( - node.tag === HostComponent || - node.tag === HostText || - node.tag === HostPortal - ) { - invariant(false, 'A call cannot have host component children.'); - } else if (node.tag === ReturnComponent) { - returns.push(node.pendingProps.value); - } else if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - while (node.sibling === null) { - if (node.return === null || node.return === workInProgress) { - return; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - } - } - - function moveCallToHandlerPhase( - current: Fiber | null, - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, - ) { - const props = workInProgress.memoizedProps; - invariant( - props, - 'Should be resolved by now. This error is likely caused by a bug in ' + - 'React. Please file an issue.', - ); - - // First step of the call has completed. Now we need to do the second. - // TODO: It would be nice to have a multi stage call represented by a - // single component, or at least tail call optimize nested ones. Currently - // that requires additional fields that we don't want to add to the fiber. - // So this requires nested handlers. - // Note: This doesn't mutate the alternate node. I don't think it needs to - // since this stage is reset for every pass. - workInProgress.tag = CallHandlerPhase; - - // Build up the returns. - // TODO: Compare this to a generator or opaque helpers like Children. - const returns: Array = []; - appendAllReturns(returns, workInProgress); - const fn = props.handler; - const childProps = props.props; - const nextChildren = fn(childProps, returns); - - const currentFirstChild = current !== null ? current.child : null; - workInProgress.child = reconcileChildFibers( - workInProgress, - currentFirstChild, - nextChildren, - renderExpirationTime, - ); - return workInProgress.child; - } - function appendAllChildren(parent: I, workInProgress: Fiber) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. @@ -579,19 +505,6 @@ export default function( } return null; } - case CallComponent: - return moveCallToHandlerPhase( - current, - workInProgress, - renderExpirationTime, - ); - case CallHandlerPhase: - // Reset the tag to now be a first phase call. - workInProgress.tag = CallComponent; - return null; - case ReturnComponent: - // Does nothing. - return null; case ForwardRef: return null; case TimeoutComponent: diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index 1e33804d12..cf89594aaf 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -12,7 +12,6 @@ describe('ReactDebugFiberPerf', () => { let React; - let ReactCallReturn; let ReactNoop; let PropTypes; @@ -121,7 +120,6 @@ describe('ReactDebugFiberPerf', () => { // Import after the polyfill is set up: React = require('react'); ReactNoop = require('react-noop-renderer'); - ReactCallReturn = require('react-call-return'); PropTypes = require('prop-types'); }); @@ -535,53 +533,6 @@ describe('ReactDebugFiberPerf', () => { expect(getFlameChart()).toMatchSnapshot(); }); - it('supports returns', () => { - function Continuation({isSame}) { - return ; - } - - function CoChild({bar}) { - return ReactCallReturn.unstable_createReturn({ - props: { - bar: bar, - }, - continuation: Continuation, - }); - } - - function Indirection() { - return [, ]; - } - - function HandleReturns(props, returns) { - return returns.map((y, i) => ( - - )); - } - - function CoParent(props) { - return ReactCallReturn.unstable_createCall( - props.children, - HandleReturns, - props, - ); - } - - function App() { - return ( -
    - - - -
    - ); - } - - ReactNoop.render(); - ReactNoop.flush(); - expect(getFlameChart()).toMatchSnapshot(); - }); - it('supports portals', () => { const noopContainer = {children: []}; ReactNoop.render( diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index 4ef5ab8201..a1e4a1cd83 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -370,25 +370,6 @@ exports[`ReactDebugFiberPerf supports portals 1`] = ` " `; -exports[`ReactDebugFiberPerf supports returns 1`] = ` -"⚛ (Waiting for async callback... will force flush in 5250 ms) - -⚛ (React Tree Reconciliation: Completed Root) - ⚛ App [mount] - ⚛ CoParent [mount] - ⚛ Indirection [mount] - ⚛ CoChild [mount] - ⚛ CoChild [mount] - ⚛ Continuation [mount] - ⚛ Continuation [mount] - -⚛ (Committing Changes) - ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 3 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) -" -`; - exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index ce63925434..cf5edb52bd 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -69,57 +69,6 @@ describe('ReactChildren', () => { expect(mappedChildren[0]).toEqual(reactPortal); }); - it('should support Call components', () => { - const context = {}; - const callback = jasmine.createSpy().and.callFake(function(kid, index) { - expect(this).toBe(context); - return kid; - }); - const ReactCallReturn = require('react-call-return'); - const reactCall = ReactCallReturn.unstable_createCall( - , - () => {}, - ); - - const parentInstance =
    {reactCall}
    ; - React.Children.forEach(parentInstance.props.children, callback, context); - expect(callback).toHaveBeenCalledWith(reactCall, 0); - callback.calls.reset(); - const mappedChildren = React.Children.map( - parentInstance.props.children, - callback, - context, - ); - expect(callback).toHaveBeenCalledWith(reactCall, 0); - expect(mappedChildren[0].type).toEqual(reactCall.type); - expect(mappedChildren[0].props).toEqual(reactCall.props); - }); - - it('should support Return components', () => { - const context = {}; - const callback = jasmine.createSpy().and.callFake(function(kid, index) { - expect(this).toBe(context); - return kid; - }); - const ReactCallReturn = require('react-call-return'); - const reactReturn = ReactCallReturn.unstable_createReturn( - , - ); - - const parentInstance =
    {reactReturn}
    ; - React.Children.forEach(parentInstance.props.children, callback, context); - expect(callback).toHaveBeenCalledWith(reactReturn, 0); - callback.calls.reset(); - const mappedChildren = React.Children.map( - parentInstance.props.children, - callback, - context, - ); - expect(callback).toHaveBeenCalledWith(reactReturn, 0); - expect(mappedChildren[0].props).toEqual(reactReturn.props); - expect(mappedChildren[0].type).toEqual(reactReturn.type); - }); - it('should treat single arrayless child as being in array', () => { const context = {}; const callback = jasmine.createSpy().and.callFake(function(kid, index) { diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 9dc1d3627a..bd3af4a903 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -14,10 +14,6 @@ const hasSymbol = typeof Symbol === 'function' && Symbol.for; export const REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7; -export const REACT_CALL_TYPE = hasSymbol ? Symbol.for('react.call') : 0xeac8; -export const REACT_RETURN_TYPE = hasSymbol - ? Symbol.for('react.return') - : 0xeac9; export const REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca; diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index e6c1ace1d9..5ac284818a 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -33,9 +33,9 @@ export const HostRoot = 3; // Root of a host tree. Could be nested inside anothe export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer. export const HostComponent = 5; export const HostText = 6; -export const CallComponent = 7; -export const CallHandlerPhase = 8; -export const ReturnComponent = 9; +export const CallComponent_UNUSED = 7; +export const CallHandlerPhase_UNUSED = 8; +export const ReturnComponent_UNUSED = 9; export const Fragment = 10; export const Mode = 11; export const ContextConsumer = 12; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index a5f61c5efc..b79a2d1c9b 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -11,14 +11,12 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import { REACT_ASYNC_MODE_TYPE, - REACT_CALL_TYPE, REACT_CONTEXT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, - REACT_RETURN_TYPE, REACT_STRICT_MODE_TYPE, } from 'shared/ReactSymbols'; @@ -33,8 +31,6 @@ function getComponentName(fiber: Fiber): string | null { switch (type) { case REACT_ASYNC_MODE_TYPE: return 'AsyncMode'; - case REACT_CALL_TYPE: - return 'ReactCall'; case REACT_CONTEXT_TYPE: return 'Context.Consumer'; case REACT_FRAGMENT_TYPE: @@ -45,8 +41,6 @@ function getComponentName(fiber: Fiber): string | null { return `Profiler(${fiber.pendingProps.id})`; case REACT_PROVIDER_TYPE: return 'Context.Provider'; - case REACT_RETURN_TYPE: - return 'ReactReturn'; case REACT_STRICT_MODE_TYPE: return 'StrictMode'; } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index e9f54a11de..ae5729a748 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -328,16 +328,6 @@ const bundles = [ externals: [], }, - /******* React Call Return (experimental) *******/ - { - label: 'react-call-return', - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: ISOMORPHIC, - entry: 'react-call-return', - global: 'ReactCallReturn', - externals: [], - }, - /******* React Is *******/ { label: 'react-is', From 103503eb69d899de4ed127b2473201db1c30d027 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 12:43:42 -0700 Subject: [PATCH 061/277] Only measure "base" times within ProfileMode (#12821) * Conditionally start/stop base timer only within Profile mode tree * Added test to ensure ProfilerTimer not called outside of Profiler root --- .../src/ReactFiberScheduler.js | 13 ++++++++---- .../__tests__/ReactProfiler-test.internal.js | 20 ++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 562039393d..225c66c827 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -962,12 +962,17 @@ export default function( let next; if (enableProfilerTimer) { - startBaseRenderTimer(); + if (workInProgress.mode & ProfileMode) { + startBaseRenderTimer(); + } + next = beginWork(current, workInProgress, nextRenderExpirationTime); - // Update "base" time if the render wasn't bailed out on. - recordElapsedBaseRenderTimeIfRunning(workInProgress); - stopBaseRenderTimerIfRunning(); + if (workInProgress.mode & ProfileMode) { + // Update "base" time if the render wasn't bailed out on. + recordElapsedBaseRenderTimeIfRunning(workInProgress); + stopBaseRenderTimerIfRunning(); + } } else { next = beginWork(current, workInProgress, nextRenderExpirationTime); } diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index e97d8eeef9..f3b12109b6 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -108,10 +108,14 @@ describe('Profiler', () => { describe('onRender callback', () => { let AdvanceTime; let advanceTimeBy; + let mockNow; const mockNowForTests = () => { let currentTime = 0; - ReactTestRenderer.unstable_setNowImplementation(() => currentTime); + + mockNow = jest.fn().mockImplementation(() => currentTime); + + ReactTestRenderer.unstable_setNowImplementation(mockNow); advanceTimeBy = amount => { currentTime += amount; }; @@ -164,6 +168,20 @@ describe('Profiler', () => { expect(callback).toHaveBeenCalledTimes(1); }); + it('does not record times for components outside of Profiler tree', () => { + ReactTestRenderer.create( +
    + + + +
    , + ); + + // Should only be called twice, for normal expiration time purposes. + // No additional calls from ProfilerTimer are expected. + expect(mockNow).toHaveBeenCalledTimes(2); + }); + it('logs render times for both mount and update', () => { const callback = jest.fn(); From a5184b215daeb7004025dfef223eda9236457358 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 15 May 2018 13:21:07 -0700 Subject: [PATCH 062/277] Add FB www build of simple-cache-provider (#12822) --- scripts/rollup/bundles.js | 2 +- scripts/rollup/results.json | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index ae5729a748..baad2fd0b0 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -348,7 +348,7 @@ const bundles = [ /******* Simple Cache Provider (experimental) *******/ { label: 'simple-cache-provider', - bundleTypes: [NODE_DEV, NODE_PROD], + bundleTypes: [FB_WWW_DEV, FB_WWW_PROD, NODE_DEV, NODE_PROD], moduleType: ISOMORPHIC, entry: 'simple-cache-provider', global: 'SimpleCacheProvider', diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 01863fae3d..afe398d959 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -685,6 +685,20 @@ "packageName": "react-scheduler", "size": 2068, "gzip": 1051 + }, + { + "filename": "SimpleCacheProvider-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "simple-cache-provider", + "size": 6227, + "gzip": 1926 + }, + { + "filename": "SimpleCacheProvider-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "simple-cache-provider", + "size": 2752, + "gzip": 854 } ] } \ No newline at end of file From f79227597202336b5a6e62642dc42646d9639cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 15 May 2018 14:35:13 -0700 Subject: [PATCH 063/277] Pass instance handle to all Fabric clone methods (#12824) We might need this in the future if we want to ensure event handler consistency when an event handler target has been removed before it is called. --- .../src/ReactFabricHostConfig.js | 41 +++++++++++++------ .../src/__mocks__/FabricUIManager.js | 14 +++++-- .../__tests__/ReactFabric-test.internal.js | 6 ++- scripts/flow/react-native-host-hooks.js | 9 +++- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 921067e516..d6d55186a0 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -25,7 +25,17 @@ import invariant from 'fbjs/lib/invariant'; // Modules provided by RN: import TextInputState from 'TextInputState'; -import FabricUIManager from 'FabricUIManager'; +import { + createNode, + cloneNode, + cloneNodeWithNewChildren, + cloneNodeWithNewChildrenAndProps, + cloneNodeWithNewProps, + createChildSet, + appendChild, + appendChildToSet, + completeRoot, +} from 'FabricUIManager'; import UIManager from 'UIManager'; // Counter for uniquely identifying views. @@ -126,12 +136,12 @@ type TextInstance = { node: Node, }; -const ReacFabricHostConfig = { +const ReactFabricHostConfig = { appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, ): void { - FabricUIManager.appendChild(parentInstance.node, child.node); + appendChild(parentInstance.node, child.node); }, createInstance( @@ -164,7 +174,7 @@ const ReacFabricHostConfig = { viewConfig.validAttributes, ); - const node = FabricUIManager.createNode( + const node = createNode( tag, // reactTag viewConfig.uiViewClassName, // viewName rootContainerInstance, // rootTag @@ -194,7 +204,7 @@ const ReacFabricHostConfig = { const tag = nextReactTag; nextReactTag += 2; - const node = FabricUIManager.createNode( + const node = createNode( tag, // reactTag 'RCTRawText', // viewName rootContainerInstance, // rootTag @@ -307,18 +317,23 @@ const ReacFabricHostConfig = { let clone; if (keepChildren) { if (updatePayload !== null) { - clone = FabricUIManager.cloneNodeWithNewProps(node, updatePayload); + clone = cloneNodeWithNewProps( + node, + updatePayload, + internalInstanceHandle, + ); } else { - clone = FabricUIManager.cloneNode(node); + clone = cloneNode(node, internalInstanceHandle); } } else { if (updatePayload !== null) { - clone = FabricUIManager.cloneNodeWithNewChildrenAndProps( + clone = cloneNodeWithNewChildrenAndProps( node, updatePayload, + internalInstanceHandle, ); } else { - clone = FabricUIManager.cloneNodeWithNewChildren(node); + clone = cloneNodeWithNewChildren(node, internalInstanceHandle); } } return { @@ -328,21 +343,21 @@ const ReacFabricHostConfig = { }, createContainerChildSet(container: Container): ChildSet { - return FabricUIManager.createChildSet(container); + return createChildSet(container); }, appendChildToContainerChildSet( childSet: ChildSet, child: Instance | TextInstance, ): void { - FabricUIManager.appendChildToSet(childSet, child.node); + appendChildToSet(childSet, child.node); }, finalizeContainerChildren( container: Container, newChildren: ChildSet, ): void { - FabricUIManager.completeRoot(container, newChildren); + completeRoot(container, newChildren); }, replaceContainerChildren( @@ -352,4 +367,4 @@ const ReacFabricHostConfig = { }, }; -export default ReacFabricHostConfig; +export default ReactFabricHostConfig; diff --git a/packages/react-native-renderer/src/__mocks__/FabricUIManager.js b/packages/react-native-renderer/src/__mocks__/FabricUIManager.js index 4e39e95848..41bb928034 100644 --- a/packages/react-native-renderer/src/__mocks__/FabricUIManager.js +++ b/packages/react-native-renderer/src/__mocks__/FabricUIManager.js @@ -65,7 +65,7 @@ const RCTFabricUIManager = { children: [], }; }), - cloneNode: jest.fn(function cloneNode(node) { + cloneNode: jest.fn(function cloneNode(node, instanceHandle) { return { reactTag: node.reactTag, viewName: node.viewName, @@ -73,7 +73,10 @@ const RCTFabricUIManager = { children: node.children, }; }), - cloneNodeWithNewChildren: jest.fn(function cloneNodeWithNewChildren(node) { + cloneNodeWithNewChildren: jest.fn(function cloneNodeWithNewChildren( + node, + instanceHandle, + ) { return { reactTag: node.reactTag, viewName: node.viewName, @@ -84,6 +87,7 @@ const RCTFabricUIManager = { cloneNodeWithNewProps: jest.fn(function cloneNodeWithNewProps( node, newPropsDiff, + instanceHandle, ) { return { reactTag: node.reactTag, @@ -93,7 +97,11 @@ const RCTFabricUIManager = { }; }), cloneNodeWithNewChildrenAndProps: jest.fn( - function cloneNodeWithNewChildrenAndProps(node, newPropsDiff) { + function cloneNodeWithNewChildrenAndProps( + node, + newPropsDiff, + instanceHandle, + ) { return { reactTag: node.reactTag, viewName: node.viewName, diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index cc697f4ddb..5c4da3a4f8 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -61,7 +61,11 @@ describe('ReactFabric', () => { ReactFabric.render(, 11); expect(FabricUIManager.createNode.mock.calls.length).toBe(1); - expect(FabricUIManager.cloneNodeWithNewProps).toBeCalledWith(firstNode, { + expect(FabricUIManager.cloneNodeWithNewProps.mock.calls.length).toBe(1); + expect(FabricUIManager.cloneNodeWithNewProps.mock.calls[0][0]).toBe( + firstNode, + ); + expect(FabricUIManager.cloneNodeWithNewProps.mock.calls[0][1]).toEqual({ foo: 'bar', }); }); diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index de0bdbf012..5d3971b1c3 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -101,15 +101,20 @@ declare module 'FabricUIManager' { props: ?Object, instanceHandle: Object, ): Object; - declare function cloneNode(node: Object): Object; - declare function cloneNodeWithNewChildren(node: Object): Object; + declare function cloneNode(node: Object, instanceHandle: Object): Object; + declare function cloneNodeWithNewChildren( + node: Object, + instanceHandle: Object, + ): Object; declare function cloneNodeWithNewProps( node: Object, newProps: ?Object, + instanceHandle: Object, ): Object; declare function cloneNodeWithNewChildrenAndProps( node: Object, newProps: ?Object, + instanceHandle: Object, ): Object; declare function appendChild(node: Object, childNode: Object): void; From de84d5c1079b12455058ee177fb3ff97cc0fb8d0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 15:26:46 -0700 Subject: [PATCH 064/277] Enable Profiler timing for DOM and RN dev bundles (#12823) * Enable Profiler timing for DOM and RN dev bundles * Disable enableProfilerTimer feature flag for ReactIncrementalPerf-test --- .../src/__tests__/ReactIncrementalPerf-test.internal.js | 1 + packages/shared/ReactFeatureFlags.js | 2 +- packages/shared/forks/ReactFeatureFlags.native-oss.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index cf89594aaf..ef4631a81d 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -115,6 +115,7 @@ describe('ReactDebugFiberPerf', () => { global.performance = createUserTimingPolyfill(); require('shared/ReactFeatureFlags').enableUserTimingAPI = true; + require('shared/ReactFeatureFlags').enableProfilerTimer = false; require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false; // Import after the polyfill is set up: diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index beefc20c0e..e5863ab9a7 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -40,7 +40,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = false; // Gather advanced timing metrics for Profiler subtrees. -export const enableProfilerTimer = false; +export const enableProfilerTimer = __DEV__; // Fires getDerivedStateFromProps for state *or* props changes export const fireGetDerivedStateFromPropsOnStateUpdates = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 0b93bcbd3a..425709c924 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -22,7 +22,7 @@ export const enablePersistentReconciler = false; export const enableUserTimingAPI = __DEV__; export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = false; -export const enableProfilerTimer = false; +export const enableProfilerTimer = __DEV__; export const fireGetDerivedStateFromPropsOnStateUpdates = true; // Only used in www builds. From 49979bbf521102d0fb55c77323bcec6fd6871a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Spie=C3=9F?= Date: Wed, 16 May 2018 15:34:33 +0200 Subject: [PATCH 065/277] Support Pointer Events (#12507) * Support Pointer Events * Add Pointer Events DOM Fixture --- fixtures/dom/src/components/Header.js | 1 + fixtures/dom/src/components/fixtures/index.js | 3 + .../fixtures/pointer-events/drag-box.js | 90 +++++++++++++++++++ .../fixtures/pointer-events/drag.js | 25 ++++++ .../fixtures/pointer-events/hover-box.js | 34 +++++++ .../fixtures/pointer-events/hover.js | 51 +++++++++++ .../fixtures/pointer-events/index.js | 20 +++++ .../__snapshots__/ReactTestUtils-test.js.snap | 10 +++ .../src/events/DOMTopLevelEventTypes.js | 26 ++++++ .../src/events/EnterLeaveEventPlugin.js | 62 ++++++++++--- .../react-dom/src/events/SimpleEventPlugin.js | 19 ++++ .../src/events/SyntheticPointerEvent.js | 25 ++++++ .../react-dom/src/events/TapEventPlugin.js | 24 ++++- 13 files changed, 373 insertions(+), 17 deletions(-) create mode 100644 fixtures/dom/src/components/fixtures/pointer-events/drag-box.js create mode 100644 fixtures/dom/src/components/fixtures/pointer-events/drag.js create mode 100644 fixtures/dom/src/components/fixtures/pointer-events/hover-box.js create mode 100644 fixtures/dom/src/components/fixtures/pointer-events/hover.js create mode 100644 fixtures/dom/src/components/fixtures/pointer-events/index.js create mode 100644 packages/react-dom/src/events/SyntheticPointerEvent.js diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js index fbb2e6e505..4a7b1513e6 100644 --- a/fixtures/dom/src/components/Header.js +++ b/fixtures/dom/src/components/Header.js @@ -64,6 +64,7 @@ class Header extends React.Component { +