Compare commits

..

35 Commits

Author SHA1 Message Date
Brian Vaughn
906b9fa597 Update bundle sizes for 16.7.0-alpha.1 release 2018-11-13 11:23:40 -08:00
Brian Vaughn
da863c380c Update error codes for 16.7.0-alpha.1 release 2018-11-13 11:23:40 -08:00
Brian Vaughn
2790634a5a Updating dependencies for react-noop-renderer 2018-11-13 11:17:37 -08:00
Brian Vaughn
43dd968939 Updating package versions for release 16.7.0-alpha.1 2018-11-13 11:17:37 -08:00
Brian Vaughn
8354ff81a8 Updating yarn.lock file for 16.7.0-alpha.1 release 2018-11-13 11:15:07 -08:00
Brian Vaughn
85f5a81eb7 Save CI-built node_modules as build artifacts (#14205)
* Store node_modules generated by CI script as an artifact
* NPM pack artifacts before archiving
2018-11-13 11:00:30 -08:00
Dan Abramov
f55795c8ee Add regression test for #14188 (#14197) 2018-11-13 11:23:01 +00:00
Maksim Markelov
d204747bef Update Readme (#14176)
Replace core with react, dom-client with react-dom
2018-11-12 16:25:32 +00:00
Sophie Alpert
b98adb648a Simplify CSS shorthand property warning (#14183)
I figured out a simpler way to do #14181. It does allocate some but I think that's OK. Time complexity might even be better since we avoid the nested loops the old one had.
2018-11-09 16:56:51 -08:00
Sebastian Markbage
f8bfd58680 fix typo 2018-11-09 16:16:25 -08:00
Sebastian Markbåge
961eb65b4b Use unique thread ID for each partial render to access Context (#14182)
* BUG: ReactPartialRenderer / New Context polutes mutable global state

The new context API stores the provided values on the shared context instance. When used in a synchronous context, this is not an issue. However when used in an concurrent context this can cause a "push provider" from one react render to have an effect on an unrelated concurrent react render.

I've encountered this bug in production when using renderToNodeStream, which asks ReactPartialRenderer for bytes up to a high water mark before yielding. If two Node Streams are created and read from in parallel, the state of one can polute the other.

I wrote a failing test to illustrate the conditions under which this happens.

I'm also concerned that the experimental concurrent/async React rendering on the client could suffer from the same issue.

* Use unique thread ID for each partial render to access Context

This first adds an allocator that keeps track of a unique ThreadID index
for each currently executing partial renderer. IDs are not just growing
but are reused as streams are destroyed.

This ensures that IDs are kept nice and compact.

This lets us use an "array" for each Context object to store the current
values. The look up for these are fast because they're just looking up
an offset in a tightly packed "array".

I don't use an actual Array object to store the values. Instead, I rely
on that VMs (notably V8) treat storage of numeric index property access
as a separate "elements" allocation.

This lets us avoid an extra indirection.

However, we must ensure that these arrays are not holey to preserve this
feature.

To do that I store the _threadCount on each context (effectively it takes
the place of the .length property on an array).

This lets us first validate that the context has enough slots before we
access the slot. If not, we fill in the slots with the default value.
2018-11-09 15:38:20 -08:00
Sophie Alpert
1a6ab1e9b5 SimpleMemoComponent should warn if a ref is given (#14178)
Fixes #13964.
2018-11-09 15:29:41 -08:00
Sophie Alpert
8ae867e6b5 Warn about conflicting style values during updates (#14181)
This is one of the most insidious quirks of React DOM that people run into. Now we warn when we think an update is dangerous.

We still allow rendering `{background, backgroundSize}` with unchanging values, for example. But once you remove either one or change `background` (without changing `backgroundSize` at the same time), that's bad news. So we warn.

Fixes #6348.
2018-11-09 15:21:47 -08:00
Brian Vaughn
d5e1bf07d0 Renamed outdated schedule/tracing referecnes (#14177) 2018-11-09 12:37:03 -08:00
Andrew Clark
2dd4ba11e0 ESlint -> ESLint 2018-11-09 10:22:18 -08:00
Sophie Alpert
9cc631a539 Don't run danger on bad build (#14143)
sizebot comments can be confusing when not based on reality.

If results.json doesn't exist, danger will fail. This is what we want.
2018-11-09 10:21:39 -08:00
Heaven
1034e26fe5 Fix typos (#14124) 2018-11-09 10:17:49 -08:00
Bartosz Gordon
5618da49d8 Fix comment typo (#14156) 2018-11-09 10:17:19 -08:00
Andrew Clark
9fb9199455 Add global to ESLint plugin bundle config 2018-11-08 18:49:03 -08:00
Andrew Clark
c174f85924 Add fb build of ESLint plugin (#14165) 2018-11-08 18:44:08 -08:00
Alex Taylor
02e4848e3a Improved suspense support in ReactDOMServer (#14161) 2018-11-08 18:15:06 -08:00
Andrew Clark
4b163fee1c Remove errant return assignment (#14164)
Oopsie!

This could have been avoided if our types were modeled correctly with
Flow (using a disjoint union).

Fuzz tester didn't catch it because it does not generate cases where
a Suspense component mounts with no children. I'll update it.
2018-11-08 18:13:42 -08:00
Andrew Clark
e58ecda9a2 Suspense fuzz tester (#14147)
* Don't warn if an unmounted component is pinged

* Suspense fuzz tester

The fuzzer works by generating a random tree of React elements. The tree
two types of custom components:

- A Text component suspends rendering on initial mount for a fuzzy
  duration of time. It may update a fuzzy number of times; each update
  supsends for a fuzzy duration of time.
- A Container component wraps some children. It may remount its children
  a fuzzy number of times, by updating its key.

The tree may also include nested Suspense components.

After this tree is generated, the tester sets a flag to temporarily
disable Text components from suspending. The tree is rendered
synchronously. The output of this render is the expected output.

Then the tester flips the flag back to enable suspending. It renders the
tree again. This time the Text components will suspend for the amount of
time configured by the props. The tester waits until everything has
resolved. The resolved output is then compared to the expected output
generated in the previous step.

Finally, we render once more, but this time in concurrent mode. Once
again, the resolved output is compared to the expected output.

I tested by commenting out various parts of the Suspense implementation
to see if broke in the expected way. I also confirmed that it would have
caught #14133, a recent bug related to deletions.

* When a generated test case fails, log its input

* Moar fuzziness

Adds more fuzziness to the generated tests. Specifcally, introduces
nested Suspense cases, where the fallback of a Suspense component
also suspends.

This flushed out a bug (yay!) whose test case I've hard coded.

* Use seeded random number generator

So if there's a failure, we can bisect.
2018-11-08 17:26:43 -08:00
Andrew Clark
7fd1661f80 Don't warn if an unmounted component is pinged (#14158)
* Add failing test for ping on unmounted component

We had a test for this, but not outside of concurrent mode :)

* Don't warn if an unmounted component is pinged
2018-11-08 17:24:32 -08:00
Andrew Clark
f9e9913f0e [Synchronous Suspense] Don't delete children of suspended component (#14157)
Vestigial behavior that should have been removed in #13823.

Found using the Suspense fuzz tester in #14147.
2018-11-08 11:38:38 -08:00
Nathan Schloss
7c560131bf Adding logger pri (#14155) 2018-11-08 11:30:38 -08:00
Minh Nguyen
3d8bda70e5 Refactor ESLint configuration to enable better IDE integration (#13914)
* Refactor ESLint configuration to enable better IDE integration

* Minor tweaks
2018-11-08 17:56:35 +00:00
Sebastian Markbåge
051272f201 Use Entry in yarn build ... Instead of Label (#14148)
* Parse build script type and package names

This ensures that `yarn build core dom` includes DOM.

It also ensures that spaces like `yarn build "core, dom"` doesn't build EVERYTHING.

* Get rid of label in bundles config

Instead we just use the name from entry using fuzzy search.

There is one special case. If you put in `/index` or `/index.js`.

That allows to build things like `react/index` to only build isomorphic
where as `react` would build everything. Or `react-dom/index` to exclude
the server renderers.

* Instead of matching `/index.js` just append it to the search string

That way things like `yarn build react/` works too.
2018-11-07 20:46:41 -08:00
Brian Vaughn
3ff2c7ccd4 Invalid actualDuration+treeBaseDuration for hidden+suspended trees (#14065)
* Fixed `treeBaseDuration` by propagating its value from the suspended tree to the Fragment React temporarily wraps around it when showing the fallback UI.
* Fixed `actualDuration` by recording elapsed profiler time in the event of an error.
* Fixed `actualDuration` in concurrent mode by propagating the time spent rendering the suspending component to its parent.

Also updated ReactSuspensePlaceholder-test.internal to cover these new cases.
2018-11-07 15:46:30 -08:00
Brian Vaughn
5afa1c4eda Tag MemoComponent with PerformedWork effectTag for DevTools Profiler (#14141) 2018-11-07 13:56:12 -08:00
Brian Vaughn
be63473004 Release script supports interleaved stable and alpha releases (#14138) 2018-11-07 12:33:13 -08:00
Andrew Clark
e27720d7f5 [Synchronous Suspense] Reuse deletions from primary tree (#14133)
Fixes a bug where deletion effects in the primary tree were dropped
before entering the second render pass.

Because we no longer reset the effect list after the first render pass,
I've also moved the deletion of the fallback children to the complete
phase, after the tree successfully renders without suspending.

Will need to revisit this heuristic when we implement resuming.
2018-11-07 10:56:57 -08:00
Dominic Gannaway
aa1ffe4e77 Show deprecated context object warnings usage in ReactDOM server (#14033)
* Applies context object warnings to ReactDOM server
2018-11-07 17:19:38 +00:00
Andrew Clark
e3a7b96455 Make react-debug-tools a private package 2018-11-06 18:45:30 -08:00
Andrew Clark
ff29de4029 Updating CHANGELOG.md for 16.6.1 release 2018-11-06 18:32:29 -08:00
76 changed files with 2437 additions and 690 deletions

View File

@@ -40,3 +40,6 @@ jobs:
key: v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:
- node_modules
- store_artifacts:
path: ./node_modules.tgz

View File

@@ -1,5 +1,10 @@
'use strict';
const {
es5Paths,
esNextPaths,
} = require('./scripts/shared/pathsByLanguageVersion');
const OFF = 0;
const ERROR = 2;
@@ -16,6 +21,15 @@ module.exports = {
'react-internal',
],
parser: 'espree',
parserOptions: {
ecmaVersion: 2017,
sourceType: 'script',
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
},
// We're stricter than the default config, mostly. We'll override a few rules
// and then enable some React specific ones.
rules: {
@@ -44,6 +58,13 @@ module.exports = {
'space-before-function-paren': OFF,
'valid-typeof': [ERROR, {requireStringLiterals: true}],
// We apply these settings to files that should run on Node.
// They can't use JSX or ES6 modules, and must be in strict mode.
// They can, however, use other ES6 features.
// (Note these rules are overridden later for source files.)
'no-var': ERROR,
strict: ERROR,
// React & JSX
// Our transforms set this automatically
'react/jsx-boolean-value': [ERROR, 'always'],
@@ -71,6 +92,33 @@ module.exports = {
},
overrides: [
{
// We apply these settings to files that we ship through npm.
// They must be ES5.
files: es5Paths,
parser: 'espree',
parserOptions: {
ecmaVersion: 5,
sourceType: 'script',
},
rules: {
'no-var': OFF,
strict: ERROR,
},
},
{
// We apply these settings to the source files that get compiled.
// They can use all features including JSX (but shouldn't use `var`).
files: esNextPaths,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module',
},
rules: {
'no-var': ERROR,
strict: OFF,
},
},
{
files: ['**/__tests__/*.js'],
rules: {

View File

@@ -4,7 +4,9 @@
Changes that have landed in master but are not yet released.
Click to see more.
</summary>
</details>
## 16.6.1 (November 6, 2018)
### React DOM
@@ -17,8 +19,6 @@
* Switch from deadline object to `shouldYield` API. ([@acdlite](https://github.com/acdlite) in [#14025](https://github.com/facebook/react/pull/14025))
</details>
## 16.6.0 (October 23, 2018)

View File

@@ -860,7 +860,7 @@ class App extends React.Component {
});
break;
default:
throw new Error('Switch statement should be exhuastive');
throw new Error('Switch statement should be exhaustive');
}
// Sort
@@ -887,7 +887,7 @@ class App extends React.Component {
});
}
default:
throw new Error('Switch statement should be exhuastive');
throw new Error('Switch statement should be exhaustive');
}
}

View File

@@ -20,7 +20,7 @@ Follow these steps:
# 1: Build react from source
cd /path/to/react
yarn
yarn build dom-client,core,react-cache,scheduler --type=NODE
yarn build react-dom/index,react/index,react-cache,scheduler --type=NODE
# 2: Install fixture dependencies
cd fixtures/unstable-async/suspense/

View File

@@ -18,7 +18,7 @@ There are also known bugs and inefficiencies in master so **don't use this fixtu
# 1: Build react from source
cd /path/to/react
yarn
yarn build dom-client,core,react-cache,scheduler --type=NODE
yarn build react-dom/index,react/index,react-cache,scheduler --type=NODE
# 2: Install fixture dependencies
cd fixtures/unstable-async/time-slicing/

View File

@@ -1,6 +1,6 @@
{
"private": true,
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"workspaces": [
"packages/*"
],

View File

@@ -1,7 +1,7 @@
{
"name": "create-subscription",
"description": "utility for subscribing to external data sources inside React components",
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"repository": "facebook/react",
"files": [
"LICENSE",
@@ -10,7 +10,7 @@
"cjs/"
],
"peerDependencies": {
"react": "^16.3.0"
"react": "^16.3.0 || 16.7.0-alpha.1"
},
"devDependencies": {
"rxjs": "^5.5.6"

View File

@@ -1,4 +1,5 @@
{
"private": true,
"name": "eslint-plugin-react-hooks",
"description": "ESLint rules for React Hooks",
"version": "0.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "jest-react",
"version": "0.4.0",
"version": "0.5.0-alpha.1",
"description": "Jest matchers and utilities for testing React components.",
"main": "index.js",
"repository": "facebook/react",
@@ -16,7 +16,7 @@
"homepage": "https://reactjs.org/",
"peerDependencies": {
"jest": "^23.0.1",
"react": "^16.0.0",
"react": "^16.0.0 || 16.7.0-alpha.1",
"react-test-renderer": "^16.0.0"
},
"files": [

View File

@@ -1,7 +1,7 @@
{
"name": "react-art",
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"main": "index.js",
"repository": "facebook/react",
"keywords": [
@@ -23,10 +23,10 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.0"
"scheduler": "^0.12.0-alpha.1"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
},
"files": [
"LICENSE",

View File

@@ -12,6 +12,6 @@
"umd/"
],
"peerDependencies": {
"react": "^16.3.0-alpha.1"
"react": "^16.3.0-alpha.1 || 16.7.0-alpha.1"
}
}

View File

@@ -21,7 +21,7 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
},
"dependencies": {
"error-stack-parser": "^2.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "react-dom",
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"description": "React package for working with the DOM.",
"main": "index.js",
"repository": "facebook/react",
@@ -16,10 +16,10 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.0"
"scheduler": "^0.12.0-alpha.1"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
},
"files": [
"LICENSE",

View File

@@ -146,6 +146,141 @@ describe('ReactDOMComponent', () => {
}
});
it('should warn for conflicting CSS shorthand updates', () => {
const container = document.createElement('div');
ReactDOM.render(
<div style={{font: 'foo', fontStyle: 'bar'}} />,
container,
);
expect(() =>
ReactDOM.render(<div style={{font: 'foo'}} />, container),
).toWarnDev(
'Warning: Removing a style property during rerender (fontStyle) ' +
'when a conflicting property is set (font) can lead to styling ' +
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
'properties for the same value; instead, replace the shorthand ' +
'with separate values.' +
'\n in div (at **)',
);
// These updates are OK and don't warn:
ReactDOM.render(
<div style={{font: 'qux', fontStyle: 'bar'}} />,
container,
);
ReactDOM.render(
<div style={{font: 'foo', fontStyle: 'baz'}} />,
container,
);
expect(() =>
ReactDOM.render(
<div style={{font: 'qux', fontStyle: 'baz'}} />,
container,
),
).toWarnDev(
'Warning: Updating a style property during rerender (font) when ' +
'a conflicting property is set (fontStyle) can lead to styling ' +
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
'properties for the same value; instead, replace the shorthand ' +
'with separate values.' +
'\n in div (at **)',
);
expect(() =>
ReactDOM.render(<div style={{fontStyle: 'baz'}} />, container),
).toWarnDev(
'Warning: Removing a style property during rerender (font) when ' +
'a conflicting property is set (fontStyle) can lead to styling ' +
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
'properties for the same value; instead, replace the shorthand ' +
'with separate values.' +
'\n in div (at **)',
);
// A bit of a special case: backgroundPosition isn't technically longhand
// (it expands to backgroundPosition{X,Y} but so does background)
ReactDOM.render(
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
container,
);
expect(() =>
ReactDOM.render(<div style={{background: 'yellow'}} />, container),
).toWarnDev(
'Warning: Removing a style property during rerender ' +
'(backgroundPosition) when a conflicting property is set ' +
"(background) can lead to styling bugs. To avoid this, don't mix " +
'shorthand and non-shorthand properties for the same value; ' +
'instead, replace the shorthand with separate values.' +
'\n in div (at **)',
);
ReactDOM.render(
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
container,
);
// But setting them at the same time is OK:
ReactDOM.render(
<div style={{background: 'green', backgroundPosition: 'top'}} />,
container,
);
expect(() =>
ReactDOM.render(<div style={{backgroundPosition: 'top'}} />, container),
).toWarnDev(
'Warning: Removing a style property during rerender (background) ' +
'when a conflicting property is set (backgroundPosition) can lead ' +
"to styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
// A bit of an even more special case: borderLeft and borderStyle overlap.
ReactDOM.render(
<div style={{borderStyle: 'dotted', borderLeft: '1px solid red'}} />,
container,
);
expect(() =>
ReactDOM.render(
<div style={{borderLeft: '1px solid red'}} />,
container,
),
).toWarnDev(
'Warning: Removing a style property during rerender (borderStyle) ' +
'when a conflicting property is set (borderLeft) can lead to ' +
"styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
expect(() =>
ReactDOM.render(
<div style={{borderStyle: 'dashed', borderLeft: '1px solid red'}} />,
container,
),
).toWarnDev(
'Warning: Updating a style property during rerender (borderStyle) ' +
'when a conflicting property is set (borderLeft) can lead to ' +
"styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
// But setting them at the same time is OK:
ReactDOM.render(
<div style={{borderStyle: 'dotted', borderLeft: '2px solid red'}} />,
container,
);
expect(() =>
ReactDOM.render(<div style={{borderStyle: 'dotted'}} />, container),
).toWarnDev(
'Warning: Removing a style property during rerender (borderLeft) ' +
'when a conflicting property is set (borderStyle) can lead to ' +
"styling bugs. To avoid this, don't mix shorthand and " +
'non-shorthand properties for the same value; instead, replace the ' +
'shorthand with separate values.' +
'\n in div (at **)',
);
});
it('should warn for unknown prop', () => {
const container = document.createElement('div');
expect(() =>

View File

@@ -249,5 +249,195 @@ describe('ReactDOMServerIntegration', () => {
expect(e.querySelector('#language2').textContent).toBe('sanskrit');
expect(e.querySelector('#language3').textContent).toBe('french');
});
itRenders(
'should warn with an error message when using Context as consumer in DEV',
async render => {
const Theme = React.createContext('dark');
const Language = React.createContext('french');
const App = () => (
<div>
<Theme.Provider value="light">
<Language.Provider value="english">
<Theme.Provider value="dark">
<Theme>{theme => <div id="theme1">{theme}</div>}</Theme>
</Theme.Provider>
</Language.Provider>
</Theme.Provider>
</div>
);
// We expect 1 error.
await render(<App />, 1);
},
);
// False positive regression test.
itRenders(
'should not warn when using Consumer from React < 16.6 with newer renderer',
async render => {
const Theme = React.createContext('dark');
const Language = React.createContext('french');
// React 16.5 and earlier didn't have a separate object.
Theme.Consumer = Theme;
const App = () => (
<div>
<Theme.Provider value="light">
<Language.Provider value="english">
<Theme.Provider value="dark">
<Theme>{theme => <div id="theme1">{theme}</div>}</Theme>
</Theme.Provider>
</Language.Provider>
</Theme.Provider>
</div>
);
// We expect 0 errors.
await render(<App />, 0);
},
);
itRenders(
'should warn with an error message when using nested context consumers in DEV',
async render => {
const App = () => {
const Theme = React.createContext('dark');
const Language = React.createContext('french');
return (
<div>
<Theme.Provider value="light">
<Language.Provider value="english">
<Theme.Provider value="dark">
<Theme.Consumer.Consumer>
{theme => <div id="theme1">{theme}</div>}
</Theme.Consumer.Consumer>
</Theme.Provider>
</Language.Provider>
</Theme.Provider>
</div>
);
};
// We expect 1 error.
await render(<App />, 1);
},
);
itRenders(
'should warn with an error message when using Context.Consumer.Provider DEV',
async render => {
const App = () => {
const Theme = React.createContext('dark');
const Language = React.createContext('french');
return (
<div>
<Theme.Provider value="light">
<Language.Provider value="english">
<Theme.Consumer.Provider value="dark">
<Theme.Consumer>
{theme => <div id="theme1">{theme}</div>}
</Theme.Consumer>
</Theme.Consumer.Provider>
</Language.Provider>
</Theme.Provider>
</div>
);
};
// We expect 1 error.
await render(<App />, 1);
},
);
it('does not pollute parallel node streams', () => {
const LoggedInUser = React.createContext();
const AppWithUser = user => (
<LoggedInUser.Provider value={user}>
<header>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
</header>
<footer>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
</footer>
</LoggedInUser.Provider>
);
const streamAmy = ReactDOMServer.renderToNodeStream(
AppWithUser('Amy'),
).setEncoding('utf8');
const streamBob = ReactDOMServer.renderToNodeStream(
AppWithUser('Bob'),
).setEncoding('utf8');
// Testing by filling the buffer using internal _read() with a small
// number of bytes to avoid a test case which needs to align to a
// highWaterMark boundary of 2^14 chars.
streamAmy._read(20);
streamBob._read(20);
streamAmy._read(20);
streamBob._read(20);
expect(streamAmy.read()).toBe('<header>Amy</header><footer>Amy</footer>');
expect(streamBob.read()).toBe('<header>Bob</header><footer>Bob</footer>');
});
it('does not pollute parallel node streams when many are used', () => {
const CurrentIndex = React.createContext();
const NthRender = index => (
<CurrentIndex.Provider value={index}>
<header>
<CurrentIndex.Consumer>{idx => idx}</CurrentIndex.Consumer>
</header>
<footer>
<CurrentIndex.Consumer>{idx => idx}</CurrentIndex.Consumer>
</footer>
</CurrentIndex.Provider>
);
let streams = [];
// Test with more than 32 streams to test that growing the thread count
// works properly.
let streamCount = 34;
for (let i = 0; i < streamCount; i++) {
streams[i] = ReactDOMServer.renderToNodeStream(
NthRender(i % 2 === 0 ? 'Expected to be recreated' : i),
).setEncoding('utf8');
}
// Testing by filling the buffer using internal _read() with a small
// number of bytes to avoid a test case which needs to align to a
// highWaterMark boundary of 2^14 chars.
for (let i = 0; i < streamCount; i++) {
streams[i]._read(20);
}
// Early destroy every other stream
for (let i = 0; i < streamCount; i += 2) {
streams[i].destroy();
}
// Recreate those same streams.
for (let i = 0; i < streamCount; i += 2) {
streams[i] = ReactDOMServer.renderToNodeStream(
NthRender(i),
).setEncoding('utf8');
}
// Read a bit from all streams again.
for (let i = 0; i < streamCount; i++) {
streams[i]._read(20);
}
// Assert that all stream rendered the expected output.
for (let i = 0; i < streamCount; i++) {
expect(streams[i].read()).toBe(
'<header>' + i + '</header><footer>' + i + '</footer>',
);
}
});
});
});

View File

@@ -43,16 +43,48 @@ describe('ReactDOMServerSuspense', () => {
resetModules();
});
it('should always render the fallback when a placeholder is encountered', async () => {
const Suspended = props => {
throw new Promise(() => {});
};
function Text(props) {
return <div>{props.text}</div>;
}
function AsyncText(props) {
throw new Promise(() => {});
}
it('should render the children when no promise is thrown', async () => {
const e = await serverRender(
<React.Suspense fallback={<div />}>
<Suspended />
<React.Suspense fallback={<Text text="Fallback" />}>
<Text text="Children" />
</React.Suspense>,
);
expect(e.tagName).toBe('DIV');
expect(e.textContent).toBe('Children');
});
it('should render the fallback when a promise thrown', async () => {
const e = await serverRender(
<React.Suspense fallback={<Text text="Fallback" />}>
<AsyncText text="Children" />
</React.Suspense>,
);
expect(e.tagName).toBe('DIV');
expect(e.textContent).toBe('Fallback');
});
it('should work with nested suspense components', async () => {
const e = await serverRender(
<React.Suspense fallback={<Text text="Fallback" />}>
<div>
<Text text="Children" />
<React.Suspense fallback={<Text text="Fallback" />}>
<AsyncText text="Children" />
</React.Suspense>
</div>
</React.Suspense>,
);
expect(e.innerHTML).toBe('<div>Children</div><div>Fallback</div>');
});
});

View File

@@ -28,6 +28,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
ReactCache = require('react-cache');
Suspense = React.Suspense;
container = document.createElement('div');
document.body.appendChild(container);
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
return new Promise((resolve, reject) =>
@@ -38,6 +39,10 @@ describe('ReactDOMSuspensePlaceholder', () => {
}, ([text, ms]) => text);
});
afterEach(() => {
document.body.removeChild(container);
});
function advanceTimers(ms) {
// Note: This advances Jest's virtual time but not React's. Use
// ReactNoop.expire for that.
@@ -157,4 +162,64 @@ describe('ReactDOMSuspensePlaceholder', () => {
);
},
);
// Regression test for https://github.com/facebook/react/issues/14188
it('can call findDOMNode() in a suspended component commit phase', async () => {
const log = [];
const Lazy = React.lazy(
() =>
new Promise(resolve =>
resolve({
default() {
return 'lazy';
},
}),
),
);
class Child extends React.Component {
componentDidMount() {
log.push('cDM ' + this.props.id);
ReactDOM.findDOMNode(this);
}
componentDidUpdate() {
log.push('cDU ' + this.props.id);
ReactDOM.findDOMNode(this);
}
render() {
return 'child';
}
}
const buttonRef = React.createRef();
class App extends React.Component {
state = {
suspend: false,
};
handleClick = () => {
this.setState({suspend: true});
};
render() {
return (
<React.Suspense fallback="Loading">
<Child id="first" />
<button ref={buttonRef} onClick={this.handleClick}>
Suspend
</button>
<Child id="second" />
{this.state.suspend && <Lazy />}
</React.Suspense>
);
}
}
ReactDOM.render(<App />, container);
expect(log).toEqual(['cDM first', 'cDM second']);
log.length = 0;
buttonRef.current.dispatchEvent(new MouseEvent('click', {bubbles: true}));
await Lazy;
expect(log).toEqual(['cDU first', 'cDU second']);
});
});

View File

@@ -766,6 +766,12 @@ export function diffProperties(
}
}
if (styleUpdates) {
if (__DEV__) {
CSSPropertyOperations.validateShorthandPropertyCollisionInDev(
styleUpdates,
nextProps[STYLE],
);
}
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
return updatePayload;

View File

@@ -18,11 +18,15 @@ class ReactMarkupReadableStream extends Readable {
this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
}
_destroy() {
this.partialRenderer.destroy();
}
_read(size) {
try {
this.push(this.partialRenderer.read(size));
} catch (err) {
this.emit('error', err);
this.destroy(err);
}
}
}

View File

@@ -14,8 +14,12 @@ import ReactPartialRenderer from './ReactPartialRenderer';
*/
export function renderToString(element) {
const renderer = new ReactPartialRenderer(element, false);
const markup = renderer.read(Infinity);
return markup;
try {
const markup = renderer.read(Infinity);
return markup;
} finally {
renderer.destroy();
}
}
/**
@@ -25,6 +29,10 @@ export function renderToString(element) {
*/
export function renderToStaticMarkup(element) {
const renderer = new ReactPartialRenderer(element, true);
const markup = renderer.read(Infinity);
return markup;
try {
const markup = renderer.read(Infinity);
return markup;
} finally {
renderer.destroy();
}
}

View File

@@ -7,12 +7,9 @@
* @flow
*/
import type {ThreadID} from './ReactThreadIDAllocator';
import type {ReactElement} from 'shared/ReactElementType';
import type {
ReactProvider,
ReactConsumer,
ReactContext,
} from 'shared/ReactTypes';
import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
import React from 'react';
import invariant from 'shared/invariant';
@@ -20,7 +17,6 @@ import getComponentName from 'shared/getComponentName';
import lowPriorityWarning from 'shared/lowPriorityWarning';
import warning from 'shared/warning';
import warningWithoutStack from 'shared/warningWithoutStack';
import checkPropTypes from 'prop-types/checkPropTypes';
import describeComponentFrame from 'shared/describeComponentFrame';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -43,6 +39,12 @@ import {
REACT_MEMO_TYPE,
} from 'shared/ReactSymbols';
import {
emptyObject,
processContext,
validateContextBounds,
} from './ReactPartialRendererContext';
import {allocThreadID, freeThreadID} from './ReactThreadIDAllocator';
import {
createMarkupForCustomAttribute,
createMarkupForProperty,
@@ -54,6 +56,8 @@ import {
finishHooks,
Dispatcher,
DispatcherWithoutHooks,
currentThreadID,
setCurrentThreadID,
} from './ReactPartialRendererHooks';
import {
Namespaces,
@@ -93,6 +97,7 @@ let validatePropertiesInDevelopment = (type, props) => {};
let pushCurrentDebugStack = (stack: Array<Frame>) => {};
let pushElementToDebugStack = (element: ReactElement) => {};
let popCurrentDebugStack = () => {};
let hasWarnedAboutUsingContextAsConsumer = false;
if (__DEV__) {
ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
@@ -179,7 +184,6 @@ const didWarnAboutBadClass = {};
const didWarnAboutDeprecatedWillMount = {};
const didWarnAboutUndefinedDerivedState = {};
const didWarnAboutUninitializedState = {};
const didWarnAboutInvalidateContextType = {};
const valuePropNames = ['value', 'defaultValue'];
const newlineEatingTags = {
listing: true,
@@ -327,65 +331,6 @@ function flattenOptionChildren(children: mixed): ?string {
return content;
}
const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}
function maskContext(type, context) {
const contextTypes = type.contextTypes;
if (!contextTypes) {
return emptyObject;
}
const maskedContext = {};
for (const contextName in contextTypes) {
maskedContext[contextName] = context[contextName];
}
return maskedContext;
}
function checkContextTypes(typeSpecs, values, location: string) {
if (__DEV__) {
checkPropTypes(
typeSpecs,
values,
location,
'Component',
getCurrentServerStackImpl,
);
}
}
function processContext(type, context) {
const contextType = type.contextType;
if (typeof contextType === 'object' && contextType !== null) {
if (__DEV__) {
if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
let name = getComponentName(type) || 'Component';
if (!didWarnAboutInvalidateContextType[name]) {
didWarnAboutInvalidateContextType[type] = true;
warningWithoutStack(
false,
'%s defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext(). ' +
'Did you accidentally pass the Context.Provider instead?',
name,
);
}
}
}
return contextType._currentValue;
} else {
const maskedContext = maskContext(type, context);
if (__DEV__) {
if (type.contextTypes) {
checkContextTypes(type.contextTypes, maskedContext, 'context');
}
}
return maskedContext;
}
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
const STYLE = 'style';
const RESERVED_PROPS = {
@@ -456,6 +401,7 @@ function validateRenderResult(child, type) {
function resolve(
child: mixed,
context: Object,
threadID: ThreadID,
): {|
child: mixed,
context: Object,
@@ -475,7 +421,7 @@ function resolve(
// Extra closure so queue and replace can be captured properly
function processChild(element, Component) {
let publicContext = processContext(Component, context);
let publicContext = processContext(Component, context, threadID);
let queue = [];
let replace = false;
@@ -710,6 +656,7 @@ type Frame = {
type: mixed,
domNamespace: string,
children: FlatReactChildren,
fallbackFrame?: Frame,
childIndex: number,
context: Object,
footer: string,
@@ -720,12 +667,14 @@ type FrameDev = Frame & {
};
class ReactDOMServerRenderer {
threadID: ThreadID;
stack: Array<Frame>;
exhausted: boolean;
// TODO: type this more strictly:
currentSelectValue: any;
previousWasTextNode: boolean;
makeStaticMarkup: boolean;
suspenseDepth: number;
contextIndex: number;
contextStack: Array<ReactContext<any>>;
@@ -748,11 +697,13 @@ class ReactDOMServerRenderer {
if (__DEV__) {
((topFrame: any): FrameDev).debugElementStack = [];
}
this.threadID = allocThreadID();
this.stack = [topFrame];
this.exhausted = false;
this.currentSelectValue = null;
this.previousWasTextNode = false;
this.makeStaticMarkup = makeStaticMarkup;
this.suspenseDepth = 0;
// Context (new API)
this.contextIndex = -1;
@@ -763,6 +714,13 @@ class ReactDOMServerRenderer {
}
}
destroy() {
if (!this.exhausted) {
this.exhausted = true;
freeThreadID(this.threadID);
}
}
/**
* Note: We use just two stacks regardless of how many context providers you have.
* Providers are always popped in the reverse order to how they were pushed
@@ -776,7 +734,9 @@ class ReactDOMServerRenderer {
pushProvider<T>(provider: ReactProvider<T>): void {
const index = ++this.contextIndex;
const context: ReactContext<any> = provider.type._context;
const previousValue = context._currentValue;
const threadID = this.threadID;
validateContextBounds(context, threadID);
const previousValue = context[threadID];
// Remember which value to restore this context to on our way up.
this.contextStack[index] = context;
@@ -787,7 +747,7 @@ class ReactDOMServerRenderer {
}
// Mutate the current value.
context._currentValue = provider.props.value;
context[threadID] = provider.props.value;
}
popProvider<T>(provider: ReactProvider<T>): void {
@@ -813,7 +773,9 @@ class ReactDOMServerRenderer {
this.contextIndex--;
// Restore to the previous value we stored as we were walking down.
context._currentValue = previousValue;
// We've already verified that this context has been expanded to accommodate
// this thread id, so we don't need to do it again.
context[this.threadID] = previousValue;
}
read(bytes: number): string | null {
@@ -821,6 +783,8 @@ class ReactDOMServerRenderer {
return null;
}
const prevThreadID = currentThreadID;
setCurrentThreadID(this.threadID);
const prevDispatcher = ReactCurrentOwner.currentDispatcher;
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
@@ -828,16 +792,19 @@ class ReactDOMServerRenderer {
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
}
try {
let out = '';
while (out.length < bytes) {
// Markup generated within <Suspense> ends up buffered until we know
// nothing in that boundary suspended
let out = [''];
let suspended = false;
while (out[0].length < bytes) {
if (this.stack.length === 0) {
this.exhausted = true;
freeThreadID(this.threadID);
break;
}
const frame: Frame = this.stack[this.stack.length - 1];
if (frame.childIndex >= frame.children.length) {
if (suspended || frame.childIndex >= frame.children.length) {
const footer = frame.footer;
out += footer;
if (footer !== '') {
this.previousWasTextNode = false;
}
@@ -851,28 +818,60 @@ class ReactDOMServerRenderer {
) {
const provider: ReactProvider<any> = (frame.type: any);
this.popProvider(provider);
} else if (frame.type === REACT_SUSPENSE_TYPE) {
this.suspenseDepth--;
const buffered = out.pop();
if (suspended) {
suspended = false;
// If rendering was suspended at this boundary, render the fallbackFrame
const fallbackFrame = frame.fallbackFrame;
invariant(
fallbackFrame,
'suspense fallback not found, something is broken',
);
this.stack.push(fallbackFrame);
// Skip flushing output since we're switching to the fallback
continue;
} else {
out[this.suspenseDepth] += buffered;
}
}
// Flush output
out[this.suspenseDepth] += footer;
continue;
}
const child = frame.children[frame.childIndex++];
let outBuffer = '';
if (__DEV__) {
pushCurrentDebugStack(this.stack);
// We're starting work on this frame, so reset its inner stack.
((frame: any): FrameDev).debugElementStack.length = 0;
try {
// Be careful! Make sure this matches the PROD path below.
out += this.render(child, frame.context, frame.domNamespace);
} finally {
}
try {
outBuffer += this.render(child, frame.context, frame.domNamespace);
} catch (err) {
if (enableSuspenseServerRenderer && typeof err.then === 'function') {
suspended = true;
} else {
throw err;
}
} finally {
if (__DEV__) {
popCurrentDebugStack();
}
} else {
// Be careful! Make sure this matches the DEV path above.
out += this.render(child, frame.context, frame.domNamespace);
}
if (out.length <= this.suspenseDepth) {
out.push('');
}
out[this.suspenseDepth] += outBuffer;
}
return out;
return out[0];
} finally {
ReactCurrentOwner.currentDispatcher = prevDispatcher;
setCurrentThreadID(prevThreadID);
}
}
@@ -896,7 +895,7 @@ class ReactDOMServerRenderer {
return escapeTextForBrowser(text);
} else {
let nextChild;
({child: nextChild, context} = resolve(child, context));
({child: nextChild, context} = resolve(child, context, this.threadID));
if (nextChild === null || nextChild === false) {
return '';
} else if (!React.isValidElement(nextChild)) {
@@ -963,13 +962,25 @@ class ReactDOMServerRenderer {
}
case REACT_SUSPENSE_TYPE: {
if (enableSuspenseServerRenderer) {
const nextChildren = toArray(
// Always use the fallback when synchronously rendering to string.
const fallbackChildren = toArray(
((nextChild: any): ReactElement).props.fallback,
);
const frame: Frame = {
const nextChildren = toArray(
((nextChild: any): ReactElement).props.children,
);
const fallbackFrame: Frame = {
type: null,
domNamespace: parentNamespace,
children: fallbackChildren,
childIndex: 0,
context: context,
footer: '',
out: '',
};
const frame: Frame = {
fallbackFrame,
type: REACT_SUSPENSE_TYPE,
domNamespace: parentNamespace,
children: nextChildren,
childIndex: 0,
context: context,
@@ -977,8 +988,10 @@ class ReactDOMServerRenderer {
};
if (__DEV__) {
((frame: any): FrameDev).debugElementStack = [];
((fallbackFrame: any): FrameDev).debugElementStack = [];
}
this.stack.push(frame);
this.suspenseDepth++;
return '';
} else {
invariant(false, 'ReactDOMServer does not yet support Suspense.');
@@ -1061,9 +1074,37 @@ class ReactDOMServerRenderer {
return '';
}
case REACT_CONTEXT_TYPE: {
const consumer: ReactConsumer<any> = (nextChild: any);
const nextProps: any = consumer.props;
const nextValue = consumer.type._currentValue;
let reactContext = (nextChild: any).type;
// The logic below for Context differs depending on PROD or DEV mode. In
// DEV mode, we create a separate object for Context.Consumer that acts
// like a proxy to Context. This proxy object adds unnecessary code in PROD
// so we use the old behaviour (Context.Consumer references Context) to
// reduce size and overhead. The separate object references context via
// a property called "_context", which also gives us the ability to check
// in DEV mode if this property exists or not and warn if it does not.
if (__DEV__) {
if ((reactContext: any)._context === undefined) {
// This may be because it's a Context (rather than a Consumer).
// Or it may be because it's older React where they're the same thing.
// We only want to warn if we're sure it's a new React.
if (reactContext !== reactContext.Consumer) {
if (!hasWarnedAboutUsingContextAsConsumer) {
hasWarnedAboutUsingContextAsConsumer = true;
warning(
false,
'Rendering <Context> directly is not supported and will be removed in ' +
'a future major release. Did you mean to render <Context.Consumer> instead?',
);
}
}
} else {
reactContext = (reactContext: any)._context;
}
}
const nextProps: any = (nextChild: any).props;
const threadID = this.threadID;
validateContextBounds(reactContext, threadID);
const nextValue = reactContext[threadID];
const nextChildren = toArray(nextProps.children(nextValue));
const frame: Frame = {

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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 {ThreadID} from './ReactThreadIDAllocator';
import type {ReactContext} from 'shared/ReactTypes';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import getComponentName from 'shared/getComponentName';
import warningWithoutStack from 'shared/warningWithoutStack';
import checkPropTypes from 'prop-types/checkPropTypes';
let ReactDebugCurrentFrame;
if (__DEV__) {
ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
}
const didWarnAboutInvalidateContextType = {};
export const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}
function maskContext(type, context) {
const contextTypes = type.contextTypes;
if (!contextTypes) {
return emptyObject;
}
const maskedContext = {};
for (const contextName in contextTypes) {
maskedContext[contextName] = context[contextName];
}
return maskedContext;
}
function checkContextTypes(typeSpecs, values, location: string) {
if (__DEV__) {
checkPropTypes(
typeSpecs,
values,
location,
'Component',
ReactDebugCurrentFrame.getCurrentStack,
);
}
}
export function validateContextBounds(
context: ReactContext<any>,
threadID: ThreadID,
) {
// If we don't have enough slots in this context to store this threadID,
// fill it in without leaving any holes to ensure that the VM optimizes
// this as non-holey index properties.
for (let i = context._threadCount; i <= threadID; i++) {
// We assume that this is the same as the defaultValue which might not be
// true if we're rendering inside a secondary renderer but they are
// secondary because these use cases are very rare.
context[i] = context._currentValue2;
context._threadCount = i + 1;
}
}
export function processContext(
type: Function,
context: Object,
threadID: ThreadID,
) {
const contextType = type.contextType;
if (typeof contextType === 'object' && contextType !== null) {
if (__DEV__) {
if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
let name = getComponentName(type) || 'Component';
if (!didWarnAboutInvalidateContextType[name]) {
didWarnAboutInvalidateContextType[name] = true;
warningWithoutStack(
false,
'%s defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext(). ' +
'Did you accidentally pass the Context.Provider instead?',
name,
);
}
}
}
validateContextBounds(contextType, threadID);
return contextType[threadID];
} else {
const maskedContext = maskContext(type, context);
if (__DEV__) {
if (type.contextTypes) {
checkContextTypes(type.contextTypes, maskedContext, 'context');
}
}
return maskedContext;
}
}

View File

@@ -6,9 +6,13 @@
*
* @flow
*/
import type {ThreadID} from './ReactThreadIDAllocator';
import type {ReactContext} from 'shared/ReactTypes';
import areHookInputsEqual from 'shared/areHookInputsEqual';
import {validateContextBounds} from './ReactPartialRendererContext';
import invariant from 'shared/invariant';
import warning from 'shared/warning';
@@ -139,7 +143,9 @@ function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
return context._currentValue;
let threadID = currentThreadID;
validateContextBounds(context, threadID);
return context[threadID];
}
function useContext<T>(
@@ -147,7 +153,9 @@ function useContext<T>(
observedBits: void | number | boolean,
): T {
resolveCurrentlyRenderingComponent();
return context._currentValue;
let threadID = currentThreadID;
validateContextBounds(context, threadID);
return context[threadID];
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
@@ -334,6 +342,12 @@ function dispatchAction<A>(
function noop(): void {}
export let currentThreadID: ThreadID = 0;
export function setCurrentThreadID(threadID: ThreadID) {
currentThreadID = threadID;
}
export const Dispatcher = {
readContext,
useContext,

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// Allocates a new index for each request. Tries to stay as compact as possible so that these
// indices can be used to reference a tightly packaged array. As opposed to being used in a Map.
// The first allocated index is 1.
import invariant from 'shared/invariant';
export type ThreadID = number;
let nextAvailableThreadIDs = new Uint16Array(16);
for (let i = 0; i < 15; i++) {
nextAvailableThreadIDs[i] = i + 1;
}
nextAvailableThreadIDs[15] = 0;
function growThreadCountAndReturnNextAvailable() {
let oldArray = nextAvailableThreadIDs;
let oldSize = oldArray.length;
let newSize = oldSize * 2;
invariant(
newSize <= 0x10000,
'Maximum number of concurrent React renderers exceeded. ' +
'This can happen if you are not properly destroying the Readable provided by React. ' +
'Ensure that you call .destroy() on it if you no longer want to read from it, ' +
'and did not read to the end. If you use .pipe() this should be automatic.',
);
let newArray = new Uint16Array(newSize);
newArray.set(oldArray);
nextAvailableThreadIDs = newArray;
nextAvailableThreadIDs[0] = oldSize + 1;
for (let i = oldSize; i < newSize - 1; i++) {
nextAvailableThreadIDs[i] = i + 1;
}
nextAvailableThreadIDs[newSize - 1] = 0;
return oldSize;
}
export function allocThreadID(): ThreadID {
let nextID = nextAvailableThreadIDs[0];
if (nextID === 0) {
return growThreadCountAndReturnNextAvailable();
}
nextAvailableThreadIDs[0] = nextAvailableThreadIDs[nextID];
return nextID;
}
export function freeThreadID(id: ThreadID) {
nextAvailableThreadIDs[id] = nextAvailableThreadIDs[0];
nextAvailableThreadIDs[0] = id;
}

View File

@@ -56,7 +56,7 @@ function escapeHtml(string) {
let escape;
let html = '';
let index = 0;
let index;
let lastIndex = 0;
for (index = match.index; index < str.length; index++) {

View File

@@ -5,9 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {shorthandToLonghand} from './CSSShorthandProperty';
import dangerousStyleValue from './dangerousStyleValue';
import hyphenateStyleName from './hyphenateStyleName';
import warnValidStyle from './warnValidStyle';
import warning from 'shared/warning';
/**
* Operations for dealing with CSS properties.
@@ -78,3 +81,75 @@ export function setValueForStyles(node, styles) {
}
}
}
function isValueEmpty(value) {
return value == null || typeof value === 'boolean' || value === '';
}
/**
* Given {color: 'red', overflow: 'hidden'} returns {
* color: 'color',
* overflowX: 'overflow',
* overflowY: 'overflow',
* }. This can be read as "the overflowY property was set by the overflow
* shorthand". That is, the values are the property that each was derived from.
*/
function expandShorthandMap(styles) {
const expanded = {};
for (const key in styles) {
const longhands = shorthandToLonghand[key] || [key];
for (let i = 0; i < longhands.length; i++) {
expanded[longhands[i]] = key;
}
}
return expanded;
}
/**
* When mixing shorthand and longhand property names, we warn during updates if
* we expect an incorrect result to occur. In particular, we warn for:
*
* Updating a shorthand property (longhand gets overwritten):
* {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}
* becomes .style.font = 'baz'
* Removing a shorthand property (longhand gets lost too):
* {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}
* becomes .style.font = ''
* Removing a longhand property (should revert to shorthand; doesn't):
* {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}
* becomes .style.fontVariant = ''
*/
export function validateShorthandPropertyCollisionInDev(
styleUpdates,
nextStyles,
) {
if (!nextStyles) {
return;
}
const expandedUpdates = expandShorthandMap(styleUpdates);
const expandedStyles = expandShorthandMap(nextStyles);
const warnedAbout = {};
for (const key in expandedUpdates) {
const originalKey = expandedUpdates[key];
const correctOriginalKey = expandedStyles[key];
if (correctOriginalKey && originalKey !== correctOriginalKey) {
const warningKey = originalKey + ',' + correctOriginalKey;
if (warnedAbout[warningKey]) {
continue;
}
warnedAbout[warningKey] = true;
warning(
false,
'%s a style property during rerender (%s) when a ' +
'conflicting property is set (%s) can lead to styling bugs. To ' +
"avoid this, don't mix shorthand and non-shorthand properties " +
'for the same value; instead, replace the shorthand with ' +
'separate values.',
isValueEmpty(styleUpdates[originalKey]) ? 'Removing' : 'Updating',
originalKey,
correctOriginalKey,
);
}
}
}

View File

@@ -0,0 +1,192 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// List derived from Gecko source code:
// https://github.com/mozilla/gecko-dev/blob/4e638efc71/layout/style/test/property_database.js
export const shorthandToLonghand = {
animation: [
'animationDelay',
'animationDirection',
'animationDuration',
'animationFillMode',
'animationIterationCount',
'animationName',
'animationPlayState',
'animationTimingFunction',
],
background: [
'backgroundAttachment',
'backgroundClip',
'backgroundColor',
'backgroundImage',
'backgroundOrigin',
'backgroundPositionX',
'backgroundPositionY',
'backgroundRepeat',
'backgroundSize',
],
backgroundPosition: ['backgroundPositionX', 'backgroundPositionY'],
border: [
'borderBottomColor',
'borderBottomStyle',
'borderBottomWidth',
'borderImageOutset',
'borderImageRepeat',
'borderImageSlice',
'borderImageSource',
'borderImageWidth',
'borderLeftColor',
'borderLeftStyle',
'borderLeftWidth',
'borderRightColor',
'borderRightStyle',
'borderRightWidth',
'borderTopColor',
'borderTopStyle',
'borderTopWidth',
],
borderBlockEnd: [
'borderBlockEndColor',
'borderBlockEndStyle',
'borderBlockEndWidth',
],
borderBlockStart: [
'borderBlockStartColor',
'borderBlockStartStyle',
'borderBlockStartWidth',
],
borderBottom: ['borderBottomColor', 'borderBottomStyle', 'borderBottomWidth'],
borderColor: [
'borderBottomColor',
'borderLeftColor',
'borderRightColor',
'borderTopColor',
],
borderImage: [
'borderImageOutset',
'borderImageRepeat',
'borderImageSlice',
'borderImageSource',
'borderImageWidth',
],
borderInlineEnd: [
'borderInlineEndColor',
'borderInlineEndStyle',
'borderInlineEndWidth',
],
borderInlineStart: [
'borderInlineStartColor',
'borderInlineStartStyle',
'borderInlineStartWidth',
],
borderLeft: ['borderLeftColor', 'borderLeftStyle', 'borderLeftWidth'],
borderRadius: [
'borderBottomLeftRadius',
'borderBottomRightRadius',
'borderTopLeftRadius',
'borderTopRightRadius',
],
borderRight: ['borderRightColor', 'borderRightStyle', 'borderRightWidth'],
borderStyle: [
'borderBottomStyle',
'borderLeftStyle',
'borderRightStyle',
'borderTopStyle',
],
borderTop: ['borderTopColor', 'borderTopStyle', 'borderTopWidth'],
borderWidth: [
'borderBottomWidth',
'borderLeftWidth',
'borderRightWidth',
'borderTopWidth',
],
columnRule: ['columnRuleColor', 'columnRuleStyle', 'columnRuleWidth'],
columns: ['columnCount', 'columnWidth'],
flex: ['flexBasis', 'flexGrow', 'flexShrink'],
flexFlow: ['flexDirection', 'flexWrap'],
font: [
'fontFamily',
'fontFeatureSettings',
'fontKerning',
'fontLanguageOverride',
'fontSize',
'fontSizeAdjust',
'fontStretch',
'fontStyle',
'fontVariant',
'fontVariantAlternates',
'fontVariantCaps',
'fontVariantEastAsian',
'fontVariantLigatures',
'fontVariantNumeric',
'fontVariantPosition',
'fontWeight',
'lineHeight',
],
fontVariant: [
'fontVariantAlternates',
'fontVariantCaps',
'fontVariantEastAsian',
'fontVariantLigatures',
'fontVariantNumeric',
'fontVariantPosition',
],
gap: ['columnGap', 'rowGap'],
grid: [
'gridAutoColumns',
'gridAutoFlow',
'gridAutoRows',
'gridTemplateAreas',
'gridTemplateColumns',
'gridTemplateRows',
],
gridArea: ['gridColumnEnd', 'gridColumnStart', 'gridRowEnd', 'gridRowStart'],
gridColumn: ['gridColumnEnd', 'gridColumnStart'],
gridColumnGap: ['columnGap'],
gridGap: ['columnGap', 'rowGap'],
gridRow: ['gridRowEnd', 'gridRowStart'],
gridRowGap: ['rowGap'],
gridTemplate: [
'gridTemplateAreas',
'gridTemplateColumns',
'gridTemplateRows',
],
listStyle: ['listStyleImage', 'listStylePosition', 'listStyleType'],
margin: ['marginBottom', 'marginLeft', 'marginRight', 'marginTop'],
marker: ['markerEnd', 'markerMid', 'markerStart'],
mask: [
'maskClip',
'maskComposite',
'maskImage',
'maskMode',
'maskOrigin',
'maskPositionX',
'maskPositionY',
'maskRepeat',
'maskSize',
],
maskPosition: ['maskPositionX', 'maskPositionY'],
outline: ['outlineColor', 'outlineStyle', 'outlineWidth'],
overflow: ['overflowX', 'overflowY'],
padding: ['paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop'],
placeContent: ['alignContent', 'justifyContent'],
placeItems: ['alignItems', 'justifyItems'],
placeSelf: ['alignSelf', 'justifySelf'],
textDecoration: [
'textDecorationColor',
'textDecorationLine',
'textDecorationStyle',
],
textEmphasis: ['textEmphasisColor', 'textEmphasisStyle'],
transition: [
'transitionDelay',
'transitionDuration',
'transitionProperty',
'transitionTimingFunction',
],
wordWrap: ['overflowWrap'],
};

View File

@@ -1,6 +1,6 @@
{
"name": "react-is",
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"description": "Brand checking of React Elements.",
"main": "index.js",
"repository": "facebook/react",

View File

@@ -6,9 +6,9 @@
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.0"
"scheduler": "^0.12.0-alpha.1"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
}
}

View File

@@ -10,10 +10,10 @@
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"regenerator-runtime": "^0.11.0",
"react-reconciler": "*"
"react-reconciler": "* || 0.18.0-alpha.1"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
},
"files": [
"LICENSE",

View File

@@ -1,7 +1,7 @@
{
"name": "react-reconciler",
"description": "React package for creating custom renderers.",
"version": "0.17.0",
"version": "0.18.0-alpha.1",
"keywords": [
"react"
],
@@ -22,13 +22,13 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
},
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.0"
"scheduler": "^0.12.0-alpha.1"
},
"browserify": {
"transform": [

View File

@@ -66,7 +66,12 @@ import {
} from './ReactChildFiber';
import {processUpdateQueue} from './ReactUpdateQueue';
import {NoWork, Never} from './ReactFiberExpirationTime';
import {ConcurrentMode, StrictMode, NoContext} from './ReactTypeOfMode';
import {
ConcurrentMode,
NoContext,
ProfileMode,
StrictMode,
} from './ReactTypeOfMode';
import {
shouldSetTextContent,
shouldDeprioritizeSubtree,
@@ -245,6 +250,9 @@ function updateMemoComponent(
// to a SimpleMemoComponent to allow fast path updates.
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = type;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, type);
}
return updateSimpleMemoComponent(
current,
workInProgress,
@@ -283,6 +291,8 @@ function updateMemoComponent(
);
}
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
let newChild = createWorkInProgress(
currentChild,
nextProps,
@@ -741,7 +751,7 @@ function mountLazyComponent(
) {
if (_current !== null) {
// An lazy component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to tree it like
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
@@ -804,7 +814,7 @@ function mountLazyComponent(
break;
}
default: {
// This message intentionally doesn't metion ForwardRef or MemoComponent
// This message intentionally doesn't mention ForwardRef or MemoComponent
// because the fact that it's a separate type of work is an
// implementation detail.
invariant(
@@ -827,7 +837,7 @@ function mountIncompleteClassComponent(
) {
if (_current !== null) {
// An incomplete component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to tree it like
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
@@ -884,7 +894,7 @@ function mountIndeterminateComponent(
) {
if (_current !== null) {
// An indeterminate component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to tree it like
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
@@ -983,71 +993,75 @@ function mountIndeterminateComponent(
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;
value = finishHooks(Component, props, value, context);
if (__DEV__) {
if (Component) {
warningWithoutStack(
!Component.childContextTypes,
'%s(...): childContextTypes cannot be defined on a function component.',
Component.displayName || Component.name || 'Component',
);
}
if (workInProgress.ref !== null) {
let info = '';
const ownerName = ReactCurrentFiber.getCurrentFiberOwnerNameInDevOrNull();
if (ownerName) {
info += '\n\nCheck the render method of `' + ownerName + '`.';
}
let warningKey = ownerName || workInProgress._debugID || '';
const debugSource = workInProgress._debugSource;
if (debugSource) {
warningKey = debugSource.fileName + ':' + debugSource.lineNumber;
}
if (!didWarnAboutFunctionRefs[warningKey]) {
didWarnAboutFunctionRefs[warningKey] = true;
warning(
false,
'Function components cannot be given refs. ' +
'Attempts to access this ref will fail.%s',
info,
);
}
}
if (typeof Component.getDerivedStateFromProps === 'function') {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
warningWithoutStack(
false,
'%s: Function components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
}
}
if (
typeof Component.contextType === 'object' &&
Component.contextType !== null
) {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
warningWithoutStack(
false,
'%s: Function components do not support contextType.',
componentName,
);
didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
}
}
}
reconcileChildren(null, workInProgress, value, renderExpirationTime);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
}
return workInProgress.child;
}
}
function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
if (Component) {
warningWithoutStack(
!Component.childContextTypes,
'%s(...): childContextTypes cannot be defined on a function component.',
Component.displayName || Component.name || 'Component',
);
}
if (workInProgress.ref !== null) {
let info = '';
const ownerName = ReactCurrentFiber.getCurrentFiberOwnerNameInDevOrNull();
if (ownerName) {
info += '\n\nCheck the render method of `' + ownerName + '`.';
}
let warningKey = ownerName || workInProgress._debugID || '';
const debugSource = workInProgress._debugSource;
if (debugSource) {
warningKey = debugSource.fileName + ':' + debugSource.lineNumber;
}
if (!didWarnAboutFunctionRefs[warningKey]) {
didWarnAboutFunctionRefs[warningKey] = true;
warning(
false,
'Function components cannot be given refs. ' +
'Attempts to access this ref will fail.%s',
info,
);
}
}
if (typeof Component.getDerivedStateFromProps === 'function') {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
warningWithoutStack(
false,
'%s: Function components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
}
}
if (
typeof Component.contextType === 'object' &&
Component.contextType !== null
) {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
warningWithoutStack(
false,
'%s: Function components do not support contextType.',
componentName,
);
didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
}
}
}
function updateSuspenseComponent(
current,
workInProgress,
@@ -1127,11 +1141,7 @@ function updateSuspenseComponent(
progressedState !== null
? (workInProgress.child: any).child
: (workInProgress.child: any);
reuseProgressedPrimaryChild(
workInProgress,
primaryChildFragment,
progressedPrimaryChild,
);
primaryChildFragment.child = progressedPrimaryChild;
}
const fallbackChildFragment = createFiberFromFragment(
@@ -1175,7 +1185,6 @@ function updateSuspenseComponent(
currentPrimaryChildFragment.pendingProps,
NoWork,
);
primaryChildFragment.effectTag |= Placement;
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
// Outside of concurrent mode, we commit the effects from the
@@ -1186,14 +1195,23 @@ function updateSuspenseComponent(
? (workInProgress.child: any).child
: (workInProgress.child: any);
if (progressedPrimaryChild !== currentPrimaryChildFragment.child) {
reuseProgressedPrimaryChild(
workInProgress,
primaryChildFragment,
progressedPrimaryChild,
);
primaryChildFragment.child = progressedPrimaryChild;
}
}
// Because primaryChildFragment is a new fiber that we're inserting as the
// parent of a new tree, we need to set its treeBaseDuration.
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
let treeBaseDuration = 0;
let hiddenChild = primaryChildFragment.child;
while (hiddenChild !== null) {
treeBaseDuration += hiddenChild.treeBaseDuration;
hiddenChild = hiddenChild.sibling;
}
primaryChildFragment.treeBaseDuration = treeBaseDuration;
}
// Clone the fallback child fragment, too. These we'll continue
// working on.
const fallbackChildFragment = (primaryChildFragment.sibling = createWorkInProgress(
@@ -1201,7 +1219,6 @@ function updateSuspenseComponent(
nextFallbackChildren,
currentFallbackChildFragment.expirationTime,
));
fallbackChildFragment.effectTag |= Placement;
child = primaryChildFragment;
primaryChildFragment.childExpirationTime = NoWork;
// Skip the primary children, and continue working on the
@@ -1213,27 +1230,26 @@ function updateSuspenseComponent(
// and remove the intermediate fragment fiber.
const nextPrimaryChildren = nextProps.children;
const currentPrimaryChild = currentPrimaryChildFragment.child;
const currentFallbackChild = currentFallbackChildFragment.child;
const primaryChild = reconcileChildFibers(
workInProgress,
currentPrimaryChild,
nextPrimaryChildren,
renderExpirationTime,
);
// Delete the fallback children.
reconcileChildFibers(
workInProgress,
currentFallbackChild,
null,
renderExpirationTime,
);
// If this render doesn't suspend, we need to delete the fallback
// children. Wait until the complete phase, after we've confirmed the
// fallback is no longer needed.
// TODO: Would it be better to store the fallback fragment on
// the stateNode?
// Continue rendering the children, like we normally do.
child = next = primaryChild;
}
} else {
// The current tree has not already timed out. That means the primary
// children are not wrapped in a fragment fiber.
const currentPrimaryChild: Fiber = (current.child: any);
const currentPrimaryChild = current.child;
if (nextDidTimeout) {
// Timed out. Wrap the children in a fragment fiber to keep them
// separate from the fallback children.
@@ -1246,9 +1262,12 @@ function updateSuspenseComponent(
NoWork,
null,
);
primaryChildFragment.effectTag |= Placement;
primaryChildFragment.child = currentPrimaryChild;
currentPrimaryChild.return = primaryChildFragment;
// Even though we're creating a new fiber, there are no new children,
// because we're reusing an already mounted tree. So we don't need to
// schedule a placement.
// primaryChildFragment.effectTag |= Placement;
if ((workInProgress.mode & ConcurrentMode) === NoContext) {
// Outside of concurrent mode, we commit the effects from the
@@ -1258,11 +1277,20 @@ function updateSuspenseComponent(
progressedState !== null
? (workInProgress.child: any).child
: (workInProgress.child: any);
reuseProgressedPrimaryChild(
workInProgress,
primaryChildFragment,
progressedPrimaryChild,
);
primaryChildFragment.child = progressedPrimaryChild;
}
// Because primaryChildFragment is a new fiber that we're inserting as the
// parent of a new tree, we need to set its treeBaseDuration.
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
let treeBaseDuration = 0;
let hiddenChild = primaryChildFragment.child;
while (hiddenChild !== null) {
treeBaseDuration += hiddenChild.treeBaseDuration;
hiddenChild = hiddenChild.sibling;
}
primaryChildFragment.treeBaseDuration = treeBaseDuration;
}
// Create a fragment from the fallback children, too.
@@ -1298,49 +1326,6 @@ function updateSuspenseComponent(
return next;
}
function reuseProgressedPrimaryChild(
workInProgress: Fiber,
primaryChildFragment: Fiber,
progressedChild: Fiber | null,
) {
// This function is only called outside concurrent mode. Usually, if a work-
// in-progress primary tree suspends, we throw it out and revert back to
// current. Outside concurrent mode, though, we commit the suspended work-in-
// progress, even though it didn't complete. This function reuses the children
// and transfers the effects.
let child = (primaryChildFragment.child = progressedChild);
while (child !== null) {
// Ensure that the first and last effect of the parent corresponds
// to the children's first and last effect.
if (primaryChildFragment.firstEffect === null) {
primaryChildFragment.firstEffect = child.firstEffect;
}
if (child.lastEffect !== null) {
if (primaryChildFragment.lastEffect !== null) {
primaryChildFragment.lastEffect.nextEffect = child.firstEffect;
}
primaryChildFragment.lastEffect = child.lastEffect;
}
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (child.effectTag > PerformedWork) {
if (primaryChildFragment.lastEffect !== null) {
primaryChildFragment.lastEffect.nextEffect = child;
} else {
primaryChildFragment.firstEffect = child;
}
primaryChildFragment.lastEffect = child;
}
child.return = primaryChildFragment;
child = child.sibling;
}
workInProgress.firstEffect = primaryChildFragment.firstEffect;
workInProgress.lastEffect = primaryChildFragment.lastEffect;
}
function updatePortalComponent(
current: Fiber | null,
workInProgress: Fiber,

View File

@@ -558,6 +558,16 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
} else {
unhideTextInstance(instance, node.memoizedProps);
}
} else if (
node.tag === SuspenseComponent &&
node.memoizedState !== null
) {
// Found a nested Suspense component that timed out. Skip over the
// primary child fragment, which should remain hidden.
const fallbackChildFragment: Fiber = (node.child: any).sibling;
fallbackChildFragment.return = node;
node = fallbackChildFragment;
continue;
} else if (node.child !== null) {
node.child.return = node;
node = node.child;

View File

@@ -45,6 +45,7 @@ import {
Update,
NoEffect,
DidCapture,
Deletion,
} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';
@@ -700,12 +701,33 @@ function completeWork(
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime = renderExpirationTime;
workInProgress.firstEffect = workInProgress.lastEffect = null;
// Do not reset the effect list.
return workInProgress;
}
const nextDidTimeout = nextState !== null;
const prevDidTimeout = current !== null && current.memoizedState !== null;
if (current !== null && !nextDidTimeout && prevDidTimeout) {
// We just switched from the fallback to the normal children. Delete
// the fallback.
// TODO: Would it be better to store the fallback fragment on
// the stateNode during the begin phase?
const currentFallbackChild: Fiber | null = (current.child: any).sibling;
if (currentFallbackChild !== null) {
// Deletions go at the beginning of the return fiber's effect list
const first = workInProgress.firstEffect;
if (first !== null) {
workInProgress.firstEffect = currentFallbackChild;
currentFallbackChild.nextEffect = first;
} else {
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;
currentFallbackChild.nextEffect = null;
}
currentFallbackChild.effectTag = Deletion;
}
}
// The children either timed out after previously being visible, or
// were restored after previously being hidden. Schedule an effect
// to update their visiblity.

View File

@@ -180,11 +180,11 @@ let warnAboutInvalidUpdates;
if (enableSchedulerTracing) {
// Provide explicit error message when production+profiling bundle of e.g. react-dom
// is used with production (non-profiling) bundle of schedule/tracing
// is used with production (non-profiling) bundle of scheduler/tracing
invariant(
__interactionsRef != null && __interactionsRef.current != null,
'It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) ' +
'without also replacing the `schedule/tracing` module with `schedule/tracing-profiling`. ' +
'without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. ' +
'Your bundler might have a setting for aliasing both modules. ' +
'Learn more at http://fb.me/react-profiling',
);
@@ -993,7 +993,6 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
if (nextUnitOfWork !== null) {
// Completing this fiber spawned new work. Work on that next.
nextUnitOfWork.firstEffect = nextUnitOfWork.lastEffect = null;
return nextUnitOfWork;
}
@@ -1050,9 +1049,18 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
return null;
}
} else {
if (workInProgress.mode & ProfileMode) {
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
// Include the time spent working on failed children before continuing.
let actualDuration = workInProgress.actualDuration;
let child = workInProgress.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
}
// This fiber did not complete because something threw. Pop values off
@@ -1077,19 +1085,6 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
}
if (enableProfilerTimer) {
// Include the time spent working on failed children before continuing.
if (next.mode & ProfileMode) {
let actualDuration = next.actualDuration;
let child = next.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
next.actualDuration = actualDuration;
}
}
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
@@ -1315,6 +1310,12 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
didFatal = true;
onUncaughtError(thrownValue);
} else {
if (enableProfilerTimer && nextUnitOfWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown.
// This avoids inaccurate Profiler durations in the case of a suspended render.
stopProfilerTimerIfRunningAndRecordDelta(nextUnitOfWork, true);
}
if (__DEV__) {
// Reset global debug state
// We assume this is defined in DEV
@@ -1746,6 +1747,46 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
}
}
if (enableSchedulerTracing) {
if (root !== null) {
const interactions = __interactionsRef.current;
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(expirationTime);
if (pendingInteractions != null) {
interactions.forEach(interaction => {
if (!pendingInteractions.has(interaction)) {
// Update the pending async work count for previously unscheduled interaction.
interaction.__count++;
}
pendingInteractions.add(interaction);
});
} else {
pendingInteractionMap.set(expirationTime, new Set(interactions));
// Update the pending async work count for the current interactions.
interactions.forEach(interaction => {
interaction.__count++;
});
}
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(
expirationTime,
root.interactionThreadID,
);
subscriber.onWorkScheduled(interactions, threadID);
}
}
}
}
return root;
}
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
if (__DEV__) {
switch (fiber.tag) {
@@ -1760,49 +1801,6 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
break;
}
}
return null;
}
if (enableSchedulerTracing) {
const interactions = __interactionsRef.current;
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(expirationTime);
if (pendingInteractions != null) {
interactions.forEach(interaction => {
if (!pendingInteractions.has(interaction)) {
// Update the pending async work count for previously unscheduled interaction.
interaction.__count++;
}
pendingInteractions.add(interaction);
});
} else {
pendingInteractionMap.set(expirationTime, new Set(interactions));
// Update the pending async work count for the current interactions.
interactions.forEach(interaction => {
interaction.__count++;
});
}
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(
expirationTime,
root.interactionThreadID,
);
subscriber.onWorkScheduled(interactions, threadID);
}
}
}
return root;
}
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
return;
}

View File

@@ -70,7 +70,6 @@ import {
LOW_PRIORITY_EXPIRATION,
} from './ReactFiberExpirationTime';
import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
import {reconcileChildren} from './ReactFiberBeginWork';
function createRootErrorUpdate(
fiber: Fiber,
@@ -238,20 +237,10 @@ function throwException(
if ((workInProgress.mode & ConcurrentMode) === NoEffect) {
workInProgress.effectTag |= DidCapture;
// Unmount the source fiber's children
const nextChildren = null;
reconcileChildren(
sourceFiber.alternate,
sourceFiber,
nextChildren,
renderExpirationTime,
);
sourceFiber.effectTag &= ~Incomplete;
// We're going to commit this fiber even though it didn't complete.
// But we shouldn't call any lifecycle methods or callbacks. Remove
// all lifecycle effect tags.
sourceFiber.effectTag &= ~LifecycleEffectMask;
sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete);
if (sourceFiber.tag === ClassComponent) {
const current = sourceFiber.alternate;

View File

@@ -15,6 +15,7 @@
let React;
let ReactFeatureFlags;
let ReactNoop;
let Suspense;
describe('memo', () => {
beforeEach(() => {
@@ -23,6 +24,7 @@ describe('memo', () => {
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
({Suspense} = React);
});
function span(prop) {
@@ -38,6 +40,42 @@ describe('memo', () => {
return {default: result};
}
it('warns when giving a ref (simple)', async () => {
// This test lives outside sharedTests because the wrappers don't forward
// refs properly, and they end up affecting the current owner which is used
// by the warning (making the messages not line up).
function App() {
return null;
}
App = React.memo(App);
function Outer() {
return <App ref={() => {}} />;
}
ReactNoop.render(<Outer />);
expect(ReactNoop.flush).toWarnDev([
'Warning: Function components cannot be given refs. Attempts to access ' +
'this ref will fail.',
]);
});
it('warns when giving a ref (complex)', async () => {
// defaultProps means this won't use SimpleMemoComponent (as of this writing)
// SimpleMemoComponent is unobservable tho, so we can't check :)
function App() {
return null;
}
App.defaultProps = {};
App = React.memo(App);
function Outer() {
return <App ref={() => {}} />;
}
ReactNoop.render(<Outer />);
expect(ReactNoop.flush).toWarnDev([
'Warning: Function components cannot be given refs. Attempts to access ' +
'this ref will fail.',
]);
});
// Tests should run against both the lazy and non-lazy versions of `memo`.
// To make the tests work for both versions, we wrap the non-lazy version in
// a lazy function component.
@@ -56,8 +94,6 @@ describe('memo', () => {
function sharedTests(label, memo) {
describe(`${label}`, () => {
it('bails out on props equality', async () => {
const {Suspense} = React;
function Counter({count}) {
return <Text text={count} />;
}
@@ -93,8 +129,6 @@ describe('memo', () => {
});
it("does not bail out if there's a context change", async () => {
const {Suspense} = React;
const CountContext = React.createContext(0);
function readContext(Context) {
@@ -142,8 +176,6 @@ describe('memo', () => {
});
it('accepts custom comparison function', async () => {
const {Suspense} = React;
function Counter({count}) {
return <Text text={count} />;
}
@@ -184,8 +216,6 @@ describe('memo', () => {
});
it('supports non-pure class components', async () => {
const {Suspense} = React;
class CounterInner extends React.Component {
static defaultProps = {suffix: '!'};
render() {

View File

@@ -779,5 +779,149 @@ describe('ReactSuspense', () => {
expect(root).toFlushAndYield(['Child 1', 'Child 2']);
expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
});
it('reuses effects, including deletions, from the suspended tree', () => {
const {useState} = React;
let setTab;
function App() {
const [tab, _setTab] = useState(0);
setTab = _setTab;
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText key={tab} text={'Tab: ' + tab} ms={1000} />
<Text key={tab + 'sibling'} text=" + sibling" />
</Suspense>
);
}
const root = ReactTestRenderer.create(<App />);
expect(ReactTestRenderer).toHaveYielded([
'Suspend! [Tab: 0]',
' + sibling',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Tab: 0]',
'Tab: 0',
]);
expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
setTab(1);
expect(ReactTestRenderer).toHaveYielded([
'Suspend! [Tab: 1]',
' + sibling',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Tab: 1]',
'Tab: 1',
]);
expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
setTab(2);
expect(ReactTestRenderer).toHaveYielded([
'Suspend! [Tab: 2]',
' + sibling',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Tab: 2]',
'Tab: 2',
]);
expect(root).toMatchRenderedOutput('Tab: 2 + sibling');
});
it('does not warn if an mounted component is pinged', () => {
const {useState} = React;
const root = ReactTestRenderer.create(null);
let setStep;
function UpdatingText({text, ms}) {
const [step, _setStep] = useState(0);
setStep = _setStep;
const fullText = `${text}:${step}`;
try {
TextResource.read([fullText, ms]);
ReactTestRenderer.unstable_yield(fullText);
return fullText;
} catch (promise) {
if (typeof promise.then === 'function') {
ReactTestRenderer.unstable_yield(`Suspend! [${fullText}]`);
} else {
ReactTestRenderer.unstable_yield(`Error! [${fullText}]`);
}
throw promise;
}
}
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<UpdatingText text="A" ms={1000} />
</Suspense>,
);
expect(ReactTestRenderer).toHaveYielded(['Suspend! [A:0]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [A:0]',
'A:0',
]);
expect(root).toMatchRenderedOutput('A:0');
setStep(1);
expect(ReactTestRenderer).toHaveYielded(['Suspend! [A:1]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
root.update(null);
expect(root).toFlushWithoutYielding();
jest.advanceTimersByTime(1000);
});
it('#14162', () => {
const {lazy} = React;
function Hello() {
return <span>hello</span>;
}
async function fetchComponent() {
return new Promise(r => {
// simulating a delayed import() call
setTimeout(r, 1000, {default: Hello});
});
}
const LazyHello = lazy(fetchComponent);
class App extends React.Component {
state = {render: false};
componentDidMount() {
setTimeout(() => this.setState({render: true}));
}
render() {
return (
<Suspense fallback={<span>loading...</span>}>
{this.state.render && <LazyHello />}
</Suspense>
);
}
}
const root = ReactTestRenderer.create(null);
root.update(<App name="world" />);
jest.advanceTimersByTime(1000);
});
});
});

View File

@@ -0,0 +1,385 @@
let React;
let Suspense;
let ReactTestRenderer;
let ReactFeatureFlags;
let Random;
const SEED = 0;
const prettyFormatPkg = require('pretty-format');
function prettyFormat(thing) {
return prettyFormatPkg(thing, {
plugins: [
prettyFormatPkg.plugins.ReactElement,
prettyFormatPkg.plugins.ReactTestComponent,
],
});
}
describe('ReactSuspenseFuzz', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
ReactFeatureFlags.enableHooks = true;
React = require('react');
Suspense = React.Suspense;
ReactTestRenderer = require('react-test-renderer');
Random = require('random-seed');
});
function createFuzzer() {
const {useState, useContext, useLayoutEffect} = React;
const ShouldSuspendContext = React.createContext(true);
let pendingTasks = new Set();
let cache = new Map();
function resetCache() {
pendingTasks = new Set();
cache = new Map();
}
function Container({children, updates}) {
const [step, setStep] = useState(0);
useLayoutEffect(
() => {
if (updates !== undefined) {
const cleanUps = new Set();
updates.forEach(({remountAfter}, i) => {
const task = {
label: `Remount childen after ${remountAfter}ms`,
};
const timeoutID = setTimeout(() => {
pendingTasks.delete(task);
ReactTestRenderer.unstable_yield(task.label);
setStep(i + 1);
}, remountAfter);
pendingTasks.add(task);
cleanUps.add(() => {
pendingTasks.delete(task);
clearTimeout(timeoutID);
});
});
return () => {
cleanUps.forEach(cleanUp => cleanUp());
};
}
},
[updates],
);
return <React.Fragment key={step}>{children}</React.Fragment>;
}
function Text({text, initialDelay = 0, updates}) {
const [[step, delay], setStep] = useState([0, initialDelay]);
useLayoutEffect(
() => {
if (updates !== undefined) {
const cleanUps = new Set();
updates.forEach(({beginAfter, suspendFor}, i) => {
const task = {
label: `Update ${beginAfter}ms after mount and suspend for ${suspendFor}ms [${text}]`,
};
const timeoutID = setTimeout(() => {
pendingTasks.delete(task);
ReactTestRenderer.unstable_yield(task.label);
setStep([i + 1, suspendFor]);
}, beginAfter);
pendingTasks.add(task);
cleanUps.add(() => {
pendingTasks.delete(task);
clearTimeout(timeoutID);
});
});
return () => {
cleanUps.forEach(cleanUp => cleanUp());
};
}
},
[updates],
);
const fullText = `${text}:${step}`;
const shouldSuspend = useContext(ShouldSuspendContext);
let resolvedText;
if (shouldSuspend && delay > 0) {
resolvedText = cache.get(fullText);
if (resolvedText === undefined) {
const thenable = {
then(resolve) {
const task = {label: `Promise resolved [${fullText}]`};
pendingTasks.add(task);
setTimeout(() => {
cache.set(fullText, fullText);
pendingTasks.delete(task);
ReactTestRenderer.unstable_yield(task.label);
resolve();
}, delay);
},
};
cache.set(fullText, thenable);
ReactTestRenderer.unstable_yield(`Suspended! [${fullText}]`);
throw thenable;
} else if (typeof resolvedText.then === 'function') {
const thenable = resolvedText;
ReactTestRenderer.unstable_yield(`Suspended! [${fullText}]`);
throw thenable;
}
} else {
resolvedText = fullText;
}
ReactTestRenderer.unstable_yield(resolvedText);
return resolvedText;
}
function renderToRoot(
root,
children,
{shouldSuspend} = {shouldSuspend: true},
) {
root.update(
<ShouldSuspendContext.Provider value={shouldSuspend}>
{children}
</ShouldSuspendContext.Provider>,
);
root.unstable_flushAll();
let elapsedTime = 0;
while (pendingTasks && pendingTasks.size > 0) {
if ((elapsedTime += 1000) > 1000000) {
throw new Error('Something did not resolve properly.');
}
jest.advanceTimersByTime(1000);
root.unstable_flushAll();
}
return root.toJSON();
}
function testResolvedOutput(unwrappedChildren) {
const children = (
<Suspense fallback="Loading...">{unwrappedChildren}</Suspense>
);
const expectedRoot = ReactTestRenderer.create(null);
const expectedOutput = renderToRoot(expectedRoot, children, {
shouldSuspend: false,
});
expectedRoot.unmount();
resetCache();
const syncRoot = ReactTestRenderer.create(null);
const syncOutput = renderToRoot(syncRoot, children);
expect(syncOutput).toEqual(expectedOutput);
syncRoot.unmount();
resetCache();
const concurrentRoot = ReactTestRenderer.create(null, {
unstable_isConcurrent: true,
});
const concurrentOutput = renderToRoot(concurrentRoot, children);
expect(concurrentOutput).toEqual(expectedOutput);
concurrentRoot.unmount();
concurrentRoot.unstable_flushAll();
ReactTestRenderer.unstable_clearYields();
}
function pickRandomWeighted(rand, options) {
let totalWeight = 0;
for (let i = 0; i < options.length; i++) {
totalWeight += options[i].weight;
}
let remainingWeight = rand.floatBetween(0, totalWeight);
for (let i = 0; i < options.length; i++) {
const {value, weight} = options[i];
remainingWeight -= weight;
if (remainingWeight <= 0) {
return value;
}
}
}
function generateTestCase(rand, numberOfElements) {
let remainingElements = numberOfElements;
function createRandomChild(hasSibling) {
const possibleActions = [
{value: 'return', weight: 1},
{value: 'text', weight: 1},
];
if (hasSibling) {
possibleActions.push({value: 'container', weight: 1});
possibleActions.push({value: 'suspense', weight: 1});
}
const action = pickRandomWeighted(rand, possibleActions);
switch (action) {
case 'text': {
remainingElements--;
const numberOfUpdates = pickRandomWeighted(rand, [
{value: 0, weight: 8},
{value: 1, weight: 4},
{value: 2, weight: 1},
]);
let updates = [];
for (let i = 0; i < numberOfUpdates; i++) {
updates.push({
beginAfter: rand.intBetween(0, 10000),
suspendFor: rand.intBetween(0, 10000),
});
}
return (
<Text
text={(remainingElements + 9).toString(36).toUpperCase()}
initialDelay={rand.intBetween(0, 10000)}
updates={updates}
/>
);
}
case 'container': {
const numberOfUpdates = pickRandomWeighted(rand, [
{value: 0, weight: 8},
{value: 1, weight: 4},
{value: 2, weight: 1},
]);
let updates = [];
for (let i = 0; i < numberOfUpdates; i++) {
updates.push({
remountAfter: rand.intBetween(0, 10000),
});
}
remainingElements--;
const children = createRandomChildren(3);
return React.createElement(Container, {updates}, ...children);
}
case 'suspense': {
remainingElements--;
const children = createRandomChildren(3);
const maxDuration = pickRandomWeighted(rand, [
{value: undefined, weight: 1},
{value: rand.intBetween(0, 5000), weight: 1},
]);
const fallbackType = pickRandomWeighted(rand, [
{value: 'none', weight: 1},
{value: 'normal', weight: 1},
{value: 'nested suspense', weight: 1},
]);
let fallback;
if (fallbackType === 'normal') {
fallback = 'Loading...';
} else if (fallbackType === 'nested suspense') {
fallback = React.createElement(
React.Fragment,
null,
...createRandomChildren(3),
);
}
return React.createElement(
Suspense,
{maxDuration, fallback},
...children,
);
}
case 'return':
default:
return null;
}
}
function createRandomChildren(limit) {
const children = [];
while (remainingElements > 0 && children.length < limit) {
children.push(createRandomChild(children.length > 0));
}
return children;
}
const children = createRandomChildren(Infinity);
return React.createElement(React.Fragment, null, ...children);
}
return {Container, Text, testResolvedOutput, generateTestCase};
}
it('basic cases', () => {
// This demonstrates that the testing primitives work
const {Container, Text, testResolvedOutput} = createFuzzer();
testResolvedOutput(
<Container updates={[{remountAfter: 150}]}>
<Text
text="Hi"
initialDelay={2000}
updates={[{beginAfter: 100, suspendFor: 200}]}
/>
</Container>,
);
});
it('hard-coded cases', () => {
const {Text, testResolvedOutput} = createFuzzer();
testResolvedOutput(
<React.Fragment>
<Text
initialDelay={20}
text="A"
updates={[{beginAfter: 10, suspendFor: 20}]}
/>
<Suspense fallback="Loading... (B)">
<Text
initialDelay={10}
text="B"
updates={[{beginAfter: 30, suspendFor: 50}]}
/>
<Text text="C" />
</Suspense>
</React.Fragment>,
);
});
it('generative tests', () => {
const {generateTestCase, testResolvedOutput} = createFuzzer();
const rand = Random.create(SEED);
const NUMBER_OF_TEST_CASES = 500;
const ELEMENTS_PER_CASE = 12;
for (let i = 0; i < NUMBER_OF_TEST_CASES; i++) {
const randomTestCase = generateTestCase(rand, ELEMENTS_PER_CASE);
try {
testResolvedOutput(randomTestCase);
} catch (e) {
console.log(`
Failed fuzzy test case:
${prettyFormat(randomTestCase)}
`);
throw e;
}
}
});
});

View File

@@ -16,6 +16,9 @@ runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
);
function runPlaceholderTests(suiteLabel, loadReactNoop) {
let advanceTimeBy;
let mockNow;
let Profiler;
let React;
let ReactTestRenderer;
let ReactFeatureFlags;
@@ -27,13 +30,24 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
describe(suiteLabel, () => {
beforeEach(() => {
jest.resetModules();
let currentTime = 0;
mockNow = jest.fn().mockImplementation(() => currentTime);
global.Date.now = mockNow;
advanceTimeBy = amount => {
currentTime += amount;
};
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableProfilerTimer = true;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
ReactTestRenderer.unstable_setNowImplementation(mockNow);
ReactCache = require('react-cache');
Profiler = React.unstable_Profiler;
Suspense = React.Suspense;
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
@@ -83,15 +97,16 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
textResourceShouldFail = false;
});
function Text(props) {
ReactTestRenderer.unstable_yield(props.text);
return props.text;
function Text({fakeRenderDuration = 0, text = 'Text'}) {
advanceTimeBy(fakeRenderDuration);
ReactTestRenderer.unstable_yield(text);
return text;
}
function AsyncText(props) {
const text = props.text;
function AsyncText({fakeRenderDuration = 0, ms, text}) {
advanceTimeBy(fakeRenderDuration);
try {
TextResource.read([props.text, props.ms]);
TextResource.read([text, ms]);
ReactTestRenderer.unstable_yield(text);
return text;
} catch (promise) {
@@ -215,5 +230,229 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
// given a `hidden` prop.
expect(root).toMatchRenderedOutput('AB2C');
});
describe('profiler durations', () => {
let App;
let onRender;
beforeEach(() => {
// Order of parameters: id, phase, actualDuration, treeBaseDuration
onRender = jest.fn();
const Fallback = () => {
ReactTestRenderer.unstable_yield('Fallback');
advanceTimeBy(10);
return 'Loading...';
};
const Suspending = () => {
ReactTestRenderer.unstable_yield('Suspending');
advanceTimeBy(2);
return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
};
App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
ReactTestRenderer.unstable_yield('App');
return (
<Profiler id="root" onRender={onRender}>
<Suspense maxDuration={500} fallback={<Fallback />}>
{shouldSuspend && <Suspending />}
<Text fakeRenderDuration={textRenderDuration} text={text} />
</Suspense>
</Profiler>
);
};
});
describe('when suspending during mount', () => {
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(18);
expect(onRender.mock.calls[0][3]).toBe(10);
jest.advanceTimersByTime(1000);
expect(root.toJSON()).toEqual(['Loaded', 'Text']);
expect(onRender).toHaveBeenCalledTimes(2);
// When the suspending data is resolved and our final UI is rendered,
// the baseDuration should only include the 1ms re-rendering AsyncText,
// but the treeBaseDuration should include the full 8ms spent in the tree.
expect(onRender.mock.calls[1][2]).toBe(1);
expect(onRender.mock.calls[1][3]).toBe(8);
});
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(<App shouldSuspend={true} />, {
unstable_isConcurrent: true,
});
expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'Text',
'Fallback',
]);
expect(root).toMatchRenderedOutput(null);
// Show the fallback UI.
jest.advanceTimersByTime(750);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
expect(onRender.mock.calls[0][2]).toBe(18);
expect(onRender.mock.calls[0][3]).toBe(10);
// Resolve the pending promise.
jest.advanceTimersByTime(250);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Loaded]',
]);
expect(root).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
expect(root).toMatchRenderedOutput('LoadedText');
expect(onRender).toHaveBeenCalledTimes(2);
// When the suspending data is resolved and our final UI is rendered,
// both times should include the 8ms re-rendering Suspending and AsyncText.
expect(onRender.mock.calls[1][2]).toBe(8);
expect(onRender.mock.calls[1][3]).toBe(8);
});
});
describe('when suspending during update', () => {
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(
<App shouldSuspend={false} textRenderDuration={5} />,
);
expect(root.toJSON()).toEqual('Text');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Text" text.
// It should take 5ms to render.
expect(onRender.mock.calls[0][2]).toBe(5);
expect(onRender.mock.calls[0][3]).toBe(5);
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(2);
// The suspense update should only show the "Loading..." Fallback.
// Both durations should include 10ms spent rendering Fallback
// plus the 8ms rendering the (hidden) components.
expect(onRender.mock.calls[1][2]).toBe(18);
expect(onRender.mock.calls[1][3]).toBe(18);
root.update(
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
);
expect(root.toJSON()).toEqual(['Loading...']);
expect(onRender).toHaveBeenCalledTimes(3);
// If we force another update while still timed out,
// but this time the Text component took 1ms longer to render.
// This should impact both actualDuration and treeBaseDuration.
expect(onRender.mock.calls[2][2]).toBe(19);
expect(onRender.mock.calls[2][3]).toBe(19);
jest.advanceTimersByTime(1000);
// TODO Change expected onRender count to 4.
// At the moment, every time we suspended while rendering will cause a commit.
// This will probably change in the future, but that's why there are two new ones.
expect(root.toJSON()).toEqual(['Loaded', 'New']);
expect(onRender).toHaveBeenCalledTimes(5);
// When the suspending data is resolved and our final UI is rendered,
// the baseDuration should only include the 1ms re-rendering AsyncText,
// but the treeBaseDuration should include the full 9ms spent in the tree.
expect(onRender.mock.calls[3][2]).toBe(1);
expect(onRender.mock.calls[3][3]).toBe(9);
// TODO Remove these assertions once this commit is gone.
// For now, there was no actual work done during this commit; see above comment.
expect(onRender.mock.calls[4][2]).toBe(0);
expect(onRender.mock.calls[4][3]).toBe(9);
});
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(
<App shouldSuspend={false} textRenderDuration={5} />,
{
unstable_isConcurrent: true,
},
);
expect(root).toFlushAndYield(['App', 'Text']);
expect(root).toMatchRenderedOutput('Text');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Text" text.
// It should take 5ms to render.
expect(onRender.mock.calls[0][2]).toBe(5);
expect(onRender.mock.calls[0][3]).toBe(5);
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'Text',
'Fallback',
]);
expect(root).toMatchRenderedOutput('Text');
// Show the fallback UI.
jest.advanceTimersByTime(750);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);
// The suspense update should only show the "Loading..." Fallback.
// The actual duration should include 10ms spent rendering Fallback,
// plus the 8ms render all of the hidden, suspended subtree.
// But the tree base duration should only include 10ms spent rendering Fallback,
// plus the 5ms rendering the previously committed version of the hidden tree.
expect(onRender.mock.calls[1][2]).toBe(18);
expect(onRender.mock.calls[1][3]).toBe(15);
// Update again while timed out.
root.update(
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
);
expect(root).toFlushAndYield([
'App',
'Suspending',
'Suspend! [Loaded]',
'New',
'Fallback',
]);
expect(root).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);
// Resolve the pending promise.
jest.advanceTimersByTime(250);
expect(ReactTestRenderer).toHaveYielded([
'Promise resolved [Loaded]',
]);
expect(root).toFlushAndYield(['App', 'Suspending', 'Loaded', 'New']);
expect(onRender).toHaveBeenCalledTimes(3);
// When the suspending data is resolved and our final UI is rendered,
// both times should include the 6ms rendering Text,
// the 2ms rendering Suspending, and the 1ms rendering AsyncText.
expect(onRender.mock.calls[2][2]).toBe(9);
expect(onRender.mock.calls[2][3]).toBe(9);
});
});
});
});
}

View File

@@ -937,6 +937,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
]);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<React.Fragment>
<span hidden={true} prop="Step: 1" />
<span hidden={true} prop="Sibling" />
<span prop="Loading (1)" />
<span prop="Loading (2)" />
@@ -1060,6 +1061,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<React.Fragment>
<span hidden={true} prop="Before" />
<span hidden={true} prop="Async: 1" />
<span hidden={true} prop="After" />
<span prop="Loading..." />
@@ -1194,6 +1196,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<React.Fragment>
<span hidden={true} prop="Before" />
<span hidden={true} prop="Async: 1" />
<span hidden={true} prop="After" />
<span prop="Loading..." />

View File

@@ -11,7 +11,7 @@
'use strict';
describe('ReactTracing', () => {
it('should error if profiling renderer and non-profiling schedule/tracing bundles are combined', () => {
it('should error if profiling renderer and non-profiling scheduler/tracing bundles are combined', () => {
jest.resetModules();
const ReactFeatureFlags = require('shared/ReactFeatureFlags');

View File

@@ -1,6 +1,6 @@
{
"name": "react-test-renderer",
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"description": "React package for snapshot testing.",
"main": "index.js",
"repository": "facebook/react",
@@ -17,11 +17,11 @@
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"react-is": "^16.6.1",
"scheduler": "^0.11.0"
"react-is": "^16.7.0-alpha.1",
"scheduler": "^0.12.0-alpha.1"
},
"peerDependencies": {
"react": "^16.0.0"
"react": "^16.0.0 || 16.7.0-alpha.1"
},
"files": [
"LICENSE",

View File

@@ -4,7 +4,7 @@
"keywords": [
"react"
],
"version": "16.6.1",
"version": "16.7.0-alpha.1",
"homepage": "https://reactjs.org/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",
@@ -24,7 +24,7 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.0"
"scheduler": "^0.12.0-alpha.1"
},
"browserify": {
"transform": [

View File

@@ -42,6 +42,9 @@ export function createContext<T>(
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
@@ -98,6 +101,14 @@ export function createContext<T>(
context._currentValue2 = _currentValue2;
},
},
_threadCount: {
get() {
return context._threadCount;
},
set(_threadCount) {
context._threadCount = _threadCount;
},
},
Consumer: {
get() {
if (!hasWarnedAboutUsingNestedContextConsumers) {

View File

@@ -10,6 +10,7 @@
'use strict';
(function(global, factory) {
// eslint-disable-next-line no-unused-expressions
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory(require('react')))
: typeof define === 'function' && define.amd // eslint-disable-line no-undef

View File

@@ -10,6 +10,7 @@
'use strict';
(function(global, factory) {
// eslint-disable-next-line no-unused-expressions
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory(require('react')))
: typeof define === 'function' && define.amd // eslint-disable-line no-undef

View File

@@ -10,6 +10,7 @@
'use strict';
(function(global, factory) {
// eslint-disable-next-line no-unused-expressions
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory(require('react')))
: typeof define === 'function' && define.amd // eslint-disable-line no-undef

View File

@@ -12,6 +12,7 @@
'use strict';
(function(global, factory) {
// eslint-disable-next-line no-unused-expressions
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory(require('react')))
: typeof define === 'function' && define.amd // eslint-disable-line no-undef

View File

@@ -12,6 +12,7 @@
'use strict';
(function(global, factory) {
// eslint-disable-next-line no-unused-expressions
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory(require('react')))
: typeof define === 'function' && define.amd // eslint-disable-line no-undef

View File

@@ -12,6 +12,7 @@
'use strict';
(function(global, factory) {
// eslint-disable-next-line no-unused-expressions
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory(require('react')))
: typeof define === 'function' && define.amd // eslint-disable-line no-undef

View File

@@ -1,6 +1,6 @@
{
"name": "scheduler",
"version": "0.11.0",
"version": "0.12.0-alpha.1",
"description": "Cooperative scheduler for the browser environment.",
"main": "index.js",
"repository": "facebook/react",

View File

@@ -12,7 +12,8 @@
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var IdlePriority = 4;
var LowPriority = 4;
var IdlePriority = 5;
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
@@ -24,6 +25,7 @@ var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
@@ -220,6 +222,7 @@ function unstable_runWithPriority(priorityLevel, eventHandler) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
@@ -284,6 +287,9 @@ function unstable_scheduleCallback(callback, deprecated_options) {
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
@@ -655,6 +661,7 @@ export {
UserBlockingPriority as unstable_UserBlockingPriority,
NormalPriority as unstable_NormalPriority,
IdlePriority as unstable_IdlePriority,
LowPriority as unstable_LowPriority,
unstable_runWithPriority,
unstable_scheduleCallback,
unstable_cancelCallback,

View File

@@ -15,7 +15,7 @@ import invariant from 'shared/invariant';
function shim(...args: any) {
invariant(
false,
'The current renderer does not support hyration. ' +
'The current renderer does not support hydration. ' +
'This error is likely caused by a bug in React. ' +
'Please file an issue.',
);

View File

@@ -59,6 +59,7 @@ export type ReactContext<T> = {
_currentValue: T,
_currentValue2: T,
_threadCount: number,
// DEV only
_currentRenderer?: Object | null,

View File

@@ -8,4 +8,4 @@
'use strict';
// TODO: this is special because it gets imported during build.
module.exports = '16.6.1';
module.exports = '16.7.0-alpha.1';

View File

@@ -5,7 +5,7 @@
In most cases, the only two commands you might want to use are:
- `yarn start`
- `yarn --cwd=../../ build core,dom-client --type=UMD_PROD && yarn start --skip-build`
- `yarn --cwd=../../ build react/index,react-dom/index --type=UMD_PROD && yarn start --skip-build`
The first command will run benchmarks with all the default settings. A local and remote build will occur on React and ReactDOM UMD bundles, both local and remote repos will be run against all benchmarks.

View File

@@ -104,7 +104,7 @@ async function buildBenchmarkBundlesFromGitRepo(
async function buildReactBundles(reactPath = getDefaultReactPath(), skipBuild) {
if (!skipBuild) {
await executeCommand(
`cd ${reactPath} && yarn && yarn build core,dom-client --type=UMD_PROD`
`cd ${reactPath} && yarn && yarn build react/index,react-dom/index --type=UMD_PROD`
);
}
}

View File

@@ -8,6 +8,9 @@ set -e
# the merge base by Dangerfile instead. See https://github.com/facebook/react/pull/12606.
if [ -z "$CI_PULL_REQUEST" ]; then
curl -o scripts/rollup/results.json http://react.zpao.com/builds/master/latest/results.json
else
# If build fails, cause danger to fail/abort too
rm scripts/rollup/results.json
fi
yarn build --extract-errors

View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e
# NPM pack all modules to ensure we archive the correct set of files
for dir in ./build/node_modules/* ; do
npm pack "$dir"
done
# Wrap everything in a single zip file for easy download by the publish script
tar -zcvf ./node_modules.tgz ./*.tgz

View File

@@ -30,6 +30,7 @@ if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('yarn test-build-prod --maxWorkers=2')
COMMANDS_TO_RUN+=('node ./scripts/tasks/danger')
COMMANDS_TO_RUN+=('./scripts/circleci/upload_build.sh')
COMMANDS_TO_RUN+=('./scripts/circleci/pack_and_store_artifact.sh')
fi
if [ $((3 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then

View File

@@ -300,5 +300,9 @@
"298": "Hooks can only be called inside the body of a function component.",
"299": "%s(...): Target container is not a DOM element.",
"300": "Rendered fewer hooks than expected. This may be caused by an accidental early return statement.",
"301": "Too many re-renders. React limits the number of renders to prevent an infinite loop."
"301": "Too many re-renders. React limits the number of renders to prevent an infinite loop.",
"302": "It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at http://fb.me/react-profiling",
"303": "suspense fallback not found, something is broken",
"304": "Maximum number of concurrent React renderers exceeded. This can happen if you are not properly destroying the Readable provided by React. Ensure that you call .destroy() on it if you no longer want to read from it, and did not read to the end. If you use .pipe() this should be automatic.",
"305": "The current renderer does not support hydration. This error is likely caused by a bug in React. Please file an issue."
}

View File

@@ -1,31 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const eslintrc = require('../../.eslintrc');
const ERROR = 2;
// We apply these settings to files that should run on Node.
// They can't use JSX or ES6 modules, and must be in strict mode.
// They can, however, use other ES6 features.
module.exports = Object.assign({}, eslintrc, {
parser: 'espree',
parserOptions: {
ecmaVersion: 2017,
sourceType: 'script',
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
},
rules: Object.assign({}, eslintrc.rules, {
'no-var': ERROR,
strict: ERROR,
}),
});

View File

@@ -1,26 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const eslintrc = require('../../.eslintrc');
const ERROR = 2;
// We apply these settings to files that we ship through npm.
// They must be ES5.
module.exports = Object.assign({}, eslintrc, {
parser: 'espree',
parserOptions: {
ecmaVersion: 5,
sourceType: 'script',
},
rules: {
strict: ERROR,
},
});

View File

@@ -1,21 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const eslintrc = require('../../.eslintrc');
const ERROR = 2;
// We apply these settings to the source files that get compiled.
// They can use all features including JSX (but shouldn't use `var`).
module.exports = Object.assign({}, eslintrc, {
rules: Object.assign({}, eslintrc.rules, {
'no-var': ERROR,
}),
});

View File

@@ -10,7 +10,6 @@
const minimatch = require('minimatch');
const CLIEngine = require('eslint').CLIEngine;
const listChangedFiles = require('../shared/listChangedFiles');
const {es5Paths, esNextPaths} = require('../shared/pathsByLanguageVersion');
const allPaths = ['**/*.js'];
@@ -65,25 +64,10 @@ function runESLint({onlyChanged}) {
if (typeof onlyChanged !== 'boolean') {
throw new Error('Pass options.onlyChanged as a boolean.');
}
let errorCount = 0;
let warningCount = 0;
let output = '';
[
runESLintOnFilesWithOptions(allPaths, onlyChanged, {
configFile: `${__dirname}/eslintrc.default.js`,
ignorePattern: [...es5Paths, ...esNextPaths],
}),
runESLintOnFilesWithOptions(esNextPaths, onlyChanged, {
configFile: `${__dirname}/eslintrc.esnext.js`,
}),
runESLintOnFilesWithOptions(es5Paths, onlyChanged, {
configFile: `${__dirname}/eslintrc.es5.js`,
}),
].forEach(result => {
errorCount += result.errorCount;
warningCount += result.warningCount;
output += result.output;
});
const {errorCount, warningCount, output} = runESLintOnFilesWithOptions(
allPaths,
onlyChanged
);
console.log(output);
return errorCount === 0 && warningCount === 0;
}

View File

@@ -11,10 +11,12 @@ const {execRead, execUnlessDry, logPromise} = require('../utils');
const push = async ({cwd, dry, otp, packages, version, tag}) => {
const errors = [];
const isPrerelease = semver.prerelease(version);
let resolvedTag = tag;
if (tag === undefined) {
// No tag was provided. Default to `latest` for stable releases and `next`
// for prereleases
tag = isPrerelease ? 'next' : 'latest';
resolvedTag = isPrerelease ? 'next' : 'latest';
} else if (tag === 'latest' && isPrerelease) {
throw new Error('The tag `latest` can only be used for stable versions.');
}
@@ -26,7 +28,7 @@ const push = async ({cwd, dry, otp, packages, version, tag}) => {
const publishProject = async project => {
try {
const path = join(cwd, 'build', 'node_modules', project);
await execUnlessDry(`npm publish --tag ${tag} ${twoFactorAuth}`, {
await execUnlessDry(`npm publish --tag ${resolvedTag} ${twoFactorAuth}`, {
cwd: path,
dry,
});
@@ -49,7 +51,7 @@ const push = async ({cwd, dry, otp, packages, version, tag}) => {
const status = JSON.parse(
await execRead(`npm info ${project} dist-tags --json`)
);
const remoteVersion = status[tag];
const remoteVersion = status[resolvedTag];
// Compare remote version to package.json version,
// To better handle the case of pre-release versions.
@@ -62,7 +64,9 @@ const push = async ({cwd, dry, otp, packages, version, tag}) => {
// If we've just published a stable release,
// Update the @next tag to also point to it (so @next doesn't lag behind).
if (!isPrerelease) {
// Skip this step if we have a manually specified tag.
// This is an escape hatch for us to interleave alpha and stable releases.
if (tag === undefined && !isPrerelease) {
await execUnlessDry(
`npm dist-tag add ${project}@${packageVersion} next ${twoFactorAuth}`,
{cwd: path, dry}

View File

@@ -53,12 +53,30 @@ const {
RN_FB_PROFILING,
} = Bundles.bundleTypes;
const requestedBundleTypes = (argv.type || '')
.split(',')
.map(type => type.toUpperCase());
const requestedBundleNames = (argv._[0] || '')
.split(',')
.map(type => type.toLowerCase());
function parseRequestedNames(names, toCase) {
let result = [];
for (let i = 0; i < names.length; i++) {
let splitNames = names[i].split(',');
for (let j = 0; j < splitNames.length; j++) {
let name = splitNames[j].trim();
if (!name) {
continue;
}
if (toCase === 'uppercase') {
name = name.toUpperCase();
} else if (toCase === 'lowercase') {
name = name.toLowerCase();
}
result.push(name);
}
}
return result;
}
const requestedBundleTypes = argv.type
? parseRequestedNames([argv.type], 'uppercase')
: [];
const requestedBundleNames = parseRequestedNames(argv._, 'lowercase');
const forcePrettyOutput = argv.pretty;
const syncFBSourcePath = argv['sync-fbsource'];
const syncWWWPath = argv['sync-www'];
@@ -407,7 +425,12 @@ function shouldSkipBundle(bundle, bundleType) {
}
if (requestedBundleNames.length > 0) {
const isAskingForDifferentNames = requestedBundleNames.every(
requestedName => bundle.label.indexOf(requestedName) === -1
// If the name ends with `something/index` we only match if the
// entry ends in something. Such as `react-dom/index` only matches
// `react-dom` but not `react-dom/server`. Everything else is fuzzy
// search.
requestedName =>
(bundle.entry + '/index.js').indexOf(requestedName) === -1
);
if (isAskingForDifferentNames) {
return true;

View File

@@ -56,7 +56,6 @@ const NON_FIBER_RENDERER = moduleTypes.NON_FIBER_RENDERER;
const bundles = [
/******* Isomorphic *******/
{
label: 'core',
bundleTypes: [
UMD_DEV,
UMD_PROD,
@@ -75,7 +74,6 @@ const bundles = [
/******* React DOM *******/
{
label: 'dom-client',
bundleTypes: [
UMD_DEV,
UMD_PROD,
@@ -95,7 +93,6 @@ const bundles = [
//******* Test Utils *******/
{
label: 'dom-test-utils',
moduleType: RENDERER_UTILS,
bundleTypes: [FB_WWW_DEV, NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
entry: 'react-dom/test-utils',
@@ -105,7 +102,6 @@ const bundles = [
/* React DOM internals required for react-native-web (e.g., to shim native events from react-dom) */
{
label: 'dom-unstable-native-dependencies',
bundleTypes: [
UMD_DEV,
UMD_PROD,
@@ -122,7 +118,6 @@ const bundles = [
/******* React DOM Server *******/
{
label: 'dom-server-browser',
bundleTypes: [
UMD_DEV,
UMD_PROD,
@@ -138,7 +133,6 @@ const bundles = [
},
{
label: 'dom-server-node',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: NON_FIBER_RENDERER,
entry: 'react-dom/server.node',
@@ -147,7 +141,6 @@ const bundles = [
/******* React ART *******/
{
label: 'art',
bundleTypes: [
UMD_DEV,
UMD_PROD,
@@ -169,7 +162,6 @@ const bundles = [
/******* React Native *******/
{
label: 'native-fb',
bundleTypes: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING],
moduleType: RENDERER,
entry: 'react-native-renderer',
@@ -189,7 +181,6 @@ const bundles = [
},
{
label: 'native',
bundleTypes: [RN_OSS_DEV, RN_OSS_PROD, RN_OSS_PROFILING],
moduleType: RENDERER,
entry: 'react-native-renderer',
@@ -210,7 +201,6 @@ const bundles = [
/******* React Native Fabric *******/
{
label: 'native-fabric-fb',
bundleTypes: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING],
moduleType: RENDERER,
entry: 'react-native-renderer/fabric',
@@ -231,7 +221,6 @@ const bundles = [
},
{
label: 'native-fabric',
bundleTypes: [RN_OSS_DEV, RN_OSS_PROD, RN_OSS_PROFILING],
moduleType: RENDERER,
entry: 'react-native-renderer/fabric',
@@ -253,7 +242,6 @@ const bundles = [
/******* React Test Renderer *******/
{
label: 'test',
bundleTypes: [FB_WWW_DEV, NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
moduleType: RENDERER,
entry: 'react-test-renderer',
@@ -262,7 +250,6 @@ const bundles = [
},
{
label: 'test-shallow',
bundleTypes: [FB_WWW_DEV, NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
moduleType: NON_FIBER_RENDERER,
entry: 'react-test-renderer/shallow',
@@ -272,7 +259,6 @@ const bundles = [
/******* React Noop Renderer (used for tests) *******/
{
label: 'noop',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry: 'react-noop-renderer',
@@ -295,7 +281,6 @@ const bundles = [
/******* React Noop Persistent Renderer (used for tests) *******/
{
label: 'noop-persistent',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry: 'react-noop-renderer/persistent',
@@ -318,7 +303,6 @@ const bundles = [
/******* React Reconciler *******/
{
label: 'react-reconciler',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RECONCILER,
entry: 'react-reconciler',
@@ -328,7 +312,6 @@ const bundles = [
/******* React Persistent Reconciler *******/
{
label: 'react-reconciler-persistent',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RECONCILER,
entry: 'react-reconciler/persistent',
@@ -338,7 +321,6 @@ const bundles = [
/******* Reflection *******/
{
label: 'reconciler-reflection',
moduleType: RENDERER_UTILS,
bundleTypes: [NODE_DEV, NODE_PROD],
entry: 'react-reconciler/reflection',
@@ -348,7 +330,6 @@ const bundles = [
/******* React Is *******/
{
label: 'react-is',
bundleTypes: [
NODE_DEV,
NODE_PROD,
@@ -365,7 +346,6 @@ const bundles = [
/******* React Debug Tools *******/
{
label: 'react-debug-tools',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: ISOMORPHIC,
entry: 'react-debug-tools',
@@ -375,7 +355,6 @@ const bundles = [
/******* React Cache (experimental) *******/
{
label: 'react-cache',
bundleTypes: [
FB_WWW_DEV,
FB_WWW_PROD,
@@ -392,7 +371,6 @@ const bundles = [
/******* createComponentWithSubscriptions (experimental) *******/
{
label: 'create-subscription',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: ISOMORPHIC,
entry: 'create-subscription',
@@ -402,7 +380,6 @@ const bundles = [
/******* React Scheduler (experimental) *******/
{
label: 'scheduler',
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: ISOMORPHIC,
entry: 'scheduler',
@@ -412,7 +389,6 @@ const bundles = [
/******* Jest React (experimental) *******/
{
label: 'jest-react',
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: ISOMORPHIC,
entry: 'jest-react',
@@ -422,21 +398,20 @@ const bundles = [
/******* ESLint Plugin for Hooks (proposal) *******/
{
label: 'eslint-plugin-react-hooks',
// TODO: it's awkward to create a bundle for this
// but if we don't, the package won't get copied.
// We also can't create just DEV bundle because
// it contains a NODE_ENV check inside.
// We should probably tweak our build process
// to allow "raw" packages that don't get bundled.
bundleTypes: [NODE_DEV, NODE_PROD],
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV],
moduleType: ISOMORPHIC,
entry: 'eslint-plugin-react-hooks',
global: 'ESLintPluginReactHooks',
externals: [],
},
{
label: 'scheduler-tracing',
bundleTypes: [
FB_WWW_DEV,
FB_WWW_PROD,

View File

@@ -4,29 +4,29 @@
"filename": "react.development.js",
"bundleType": "UMD_DEV",
"packageName": "react",
"size": 97676,
"gzip": 25688
"size": 98246,
"gzip": 25819
},
{
"filename": "react.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react",
"size": 11771,
"gzip": 4678
"size": 11830,
"gzip": 4700
},
{
"filename": "react.development.js",
"bundleType": "NODE_DEV",
"packageName": "react",
"size": 60944,
"gzip": 16507
"size": 61339,
"gzip": 16608
},
{
"filename": "react.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react",
"size": 6223,
"gzip": 2655
"size": 6254,
"gzip": 2669
},
{
"filename": "React-dev.js",
@@ -46,29 +46,29 @@
"filename": "react-dom.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 725515,
"gzip": 167737
"size": 733309,
"gzip": 169744
},
{
"filename": "react-dom.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 100307,
"gzip": 32617
"size": 100018,
"gzip": 32571
},
{
"filename": "react-dom.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 720713,
"gzip": 166331
"size": 728507,
"gzip": 168337
},
{
"filename": "react-dom.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 100301,
"gzip": 32146
"size": 100013,
"gzip": 32076
},
{
"filename": "ReactDOM-dev.js",
@@ -88,29 +88,29 @@
"filename": "react-dom-test-utils.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 45937,
"gzip": 12585
"size": 45945,
"gzip": 12592
},
{
"filename": "react-dom-test-utils.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 10199,
"gzip": 3787
"size": 10207,
"gzip": 3794
},
{
"filename": "react-dom-test-utils.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 45651,
"gzip": 12521
"size": 45659,
"gzip": 12528
},
{
"filename": "react-dom-test-utils.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 9969,
"gzip": 3726
"size": 9977,
"gzip": 3733
},
{
"filename": "ReactTestUtils-dev.js",
@@ -123,29 +123,29 @@
"filename": "react-dom-unstable-native-dependencies.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 62059,
"gzip": 16289
"size": 62067,
"gzip": 16297
},
{
"filename": "react-dom-unstable-native-dependencies.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 11263,
"gzip": 3892
"size": 11271,
"gzip": 3899
},
{
"filename": "react-dom-unstable-native-dependencies.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 61723,
"gzip": 16156
"size": 61731,
"gzip": 16163
},
{
"filename": "react-dom-unstable-native-dependencies.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 10998,
"gzip": 3785
"size": 11006,
"gzip": 3791
},
{
"filename": "ReactDOMUnstableNativeDependencies-dev.js",
@@ -165,29 +165,29 @@
"filename": "react-dom-server.browser.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 120524,
"gzip": 32011
"size": 127102,
"gzip": 33823
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 16402,
"gzip": 6237
"size": 17381,
"gzip": 6667
},
{
"filename": "react-dom-server.browser.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 116562,
"gzip": 31037
"size": 123140,
"gzip": 32865
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 16301,
"gzip": 6233
"size": 17289,
"gzip": 6661
},
{
"filename": "ReactDOMServer-dev.js",
@@ -207,43 +207,43 @@
"filename": "react-dom-server.node.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 118530,
"gzip": 31584
"size": 125215,
"gzip": 33415
},
{
"filename": "react-dom-server.node.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 17126,
"gzip": 6544
"size": 18173,
"gzip": 6976
},
{
"filename": "react-art.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-art",
"size": 507548,
"gzip": 112066
"size": 508568,
"gzip": 112248
},
{
"filename": "react-art.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-art",
"size": 92284,
"gzip": 28330
"size": 91997,
"gzip": 28283
},
{
"filename": "react-art.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-art",
"size": 437685,
"gzip": 94610
"size": 438705,
"gzip": 94790
},
{
"filename": "react-art.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-art",
"size": 56431,
"gzip": 17393
"size": 56144,
"gzip": 17322
},
{
"filename": "ReactART-dev.js",
@@ -291,29 +291,29 @@
"filename": "react-test-renderer.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-test-renderer",
"size": 450653,
"gzip": 97378
"size": 451673,
"gzip": 97563
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-test-renderer",
"size": 57674,
"gzip": 17698
"size": 57391,
"gzip": 17618
},
{
"filename": "react-test-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 445754,
"gzip": 96206
"size": 446774,
"gzip": 96385
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
"size": 57342,
"gzip": 17539
"size": 57053,
"gzip": 17471
},
{
"filename": "ReactTestRenderer-dev.js",
@@ -326,29 +326,29 @@
"filename": "react-test-renderer-shallow.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-test-renderer",
"size": 26400,
"gzip": 7200
"size": 26408,
"gzip": 7207
},
{
"filename": "react-test-renderer-shallow.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-test-renderer",
"size": 7442,
"gzip": 2425
"size": 7450,
"gzip": 2431
},
{
"filename": "react-test-renderer-shallow.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 20656,
"gzip": 5736
"size": 20664,
"gzip": 5742
},
{
"filename": "react-test-renderer-shallow.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
"size": 8141,
"gzip": 2697
"size": 8149,
"gzip": 2703
},
{
"filename": "ReactShallowRenderer-dev.js",
@@ -361,57 +361,57 @@
"filename": "react-noop-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 28829,
"gzip": 6286
"size": 28837,
"gzip": 6293
},
{
"filename": "react-noop-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 10889,
"gzip": 3632
"size": 10897,
"gzip": 3639
},
{
"filename": "react-reconciler.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 435479,
"gzip": 93068
"size": 436490,
"gzip": 93261
},
{
"filename": "react-reconciler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 57593,
"gzip": 17242
"size": 57292,
"gzip": 17184
},
{
"filename": "react-reconciler-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 433889,
"gzip": 92426
"size": 434900,
"gzip": 92614
},
{
"filename": "react-reconciler-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 57604,
"gzip": 17248
"size": 57303,
"gzip": 17190
},
{
"filename": "react-reconciler-reflection.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 15764,
"gzip": 4943
"size": 15772,
"gzip": 4950
},
{
"filename": "react-reconciler-reflection.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 2614,
"gzip": 1153
"size": 2622,
"gzip": 1159
},
{
"filename": "react-call-return.development.js",
@@ -431,29 +431,29 @@
"filename": "react-is.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-is",
"size": 7691,
"gzip": 2393
"size": 7699,
"gzip": 2400
},
{
"filename": "react-is.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-is",
"size": 2171,
"gzip": 854
"size": 2179,
"gzip": 860
},
{
"filename": "react-is.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-is",
"size": 7502,
"gzip": 2344
"size": 7510,
"gzip": 2351
},
{
"filename": "react-is.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-is",
"size": 2132,
"gzip": 793
"size": 2140,
"gzip": 799
},
{
"filename": "ReactIs-dev.js",
@@ -487,43 +487,43 @@
"filename": "create-subscription.development.js",
"bundleType": "NODE_DEV",
"packageName": "create-subscription",
"size": 8535,
"gzip": 2957
"size": 8543,
"gzip": 2964
},
{
"filename": "create-subscription.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "create-subscription",
"size": 2886,
"gzip": 1346
"size": 2894,
"gzip": 1353
},
{
"filename": "React-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react",
"size": 58338,
"gzip": 15703
"size": 58723,
"gzip": 15806
},
{
"filename": "React-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react",
"size": 15346,
"gzip": 4131
"size": 15377,
"gzip": 4148
},
{
"filename": "ReactDOM-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 742105,
"gzip": 167549
"size": 750157,
"gzip": 169494
},
{
"filename": "ReactDOM-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 318358,
"gzip": 58562
"size": 317074,
"gzip": 58435
},
{
"filename": "ReactTestUtils-dev.js",
@@ -550,92 +550,92 @@
"filename": "ReactDOMServer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 117649,
"gzip": 30639
"size": 124347,
"gzip": 32492
},
{
"filename": "ReactDOMServer-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 41626,
"gzip": 9808
"size": 45482,
"gzip": 10536
},
{
"filename": "ReactART-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-art",
"size": 445159,
"gzip": 93560
"size": 445813,
"gzip": 93658
},
{
"filename": "ReactART-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-art",
"size": 189135,
"gzip": 32266
"size": 187820,
"gzip": 32111
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
"size": 573357,
"gzip": 124963
"size": 574011,
"gzip": 125117
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
"size": 245987,
"gzip": 43227
"size": 244579,
"gzip": 43055
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
"size": 573046,
"gzip": 124866
"size": 573700,
"gzip": 125022
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
"size": 231080,
"gzip": 40077
"size": 229672,
"gzip": 39906
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
"size": 563440,
"gzip": 122415
"size": 564094,
"gzip": 122574
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
"size": 225188,
"gzip": 38730
"size": 223518,
"gzip": 38538
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
"size": 563475,
"gzip": 122429
"size": 564129,
"gzip": 122588
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
"size": 225224,
"gzip": 38745
"size": 223554,
"gzip": 38553
},
{
"filename": "ReactTestRenderer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-test-renderer",
"size": 453421,
"gzip": 95442
"size": 454075,
"gzip": 95541
},
{
"filename": "ReactShallowRenderer-dev.js",
@@ -676,15 +676,15 @@
"filename": "scheduler.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 22073,
"gzip": 5976
"size": 22300,
"gzip": 6022
},
{
"filename": "scheduler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "scheduler",
"size": 4755,
"gzip": 1865
"size": 4822,
"gzip": 1892
},
{
"filename": "SimpleCacheProvider-dev.js",
@@ -704,50 +704,50 @@
"filename": "react-noop-renderer-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 28948,
"gzip": 6299
"size": 28956,
"gzip": 6306
},
{
"filename": "react-noop-renderer-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 10911,
"gzip": 3639
"size": 10919,
"gzip": 3645
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
"size": 103137,
"gzip": 32687
"size": 103086,
"gzip": 32708
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
"size": 236392,
"gzip": 41298
"size": 235527,
"gzip": 41288
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
"size": 229595,
"gzip": 40005
"size": 228448,
"gzip": 39999
},
{
"filename": "Scheduler-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
"size": 22314,
"gzip": 6027
"size": 22533,
"gzip": 6066
},
{
"filename": "Scheduler-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "scheduler",
"size": 13375,
"gzip": 2927
"size": 13496,
"gzip": 2945
},
{
"filename": "react.profiling.min.js",
@@ -760,64 +760,64 @@
"filename": "React-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react",
"size": 15346,
"gzip": 4131
"size": 15377,
"gzip": 4148
},
{
"filename": "ReactDOM-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
"size": 324711,
"gzip": 59847
"size": 324138,
"gzip": 59866
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
"size": 251555,
"gzip": 44455
"size": 250690,
"gzip": 44441
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
"size": 229554,
"gzip": 39988
"size": 228407,
"gzip": 39983
},
{
"filename": "react.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react",
"size": 13977,
"gzip": 5211
"size": 14036,
"gzip": 5234
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
"size": 103046,
"gzip": 33256
"size": 102993,
"gzip": 33291
},
{
"filename": "scheduler-tracing.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 10480,
"gzip": 2403
"size": 10488,
"gzip": 2408
},
{
"filename": "scheduler-tracing.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "scheduler",
"size": 719,
"gzip": 374
"size": 727,
"gzip": 379
},
{
"filename": "scheduler-tracing.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "scheduler",
"size": 3334,
"gzip": 991
"size": 3342,
"gzip": 997
},
{
"filename": "SchedulerTracing-dev.js",
@@ -844,15 +844,15 @@
"filename": "react-cache.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-cache",
"size": 9015,
"gzip": 3022
"size": 9023,
"gzip": 3029
},
{
"filename": "react-cache.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-cache",
"size": 2204,
"gzip": 1128
"size": 2212,
"gzip": 1135
},
{
"filename": "ReactCache-dev.js",
@@ -872,29 +872,29 @@
"filename": "react-cache.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-cache",
"size": 9246,
"gzip": 3094
"size": 9254,
"gzip": 3102
},
{
"filename": "react-cache.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-cache",
"size": 2403,
"gzip": 1219
"size": 2411,
"gzip": 1226
},
{
"filename": "jest-react.development.js",
"bundleType": "NODE_DEV",
"packageName": "jest-react",
"size": 9286,
"gzip": 3053
"size": 9294,
"gzip": 3059
},
{
"filename": "jest-react.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "jest-react",
"size": 3716,
"gzip": 1655
"size": 3724,
"gzip": 1661
},
{
"filename": "JestReact-dev.js",
@@ -914,29 +914,36 @@
"filename": "react-debug-tools.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-debug-tools",
"size": 16717,
"gzip": 4965
"size": 16725,
"gzip": 4972
},
{
"filename": "react-debug-tools.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-debug-tools",
"size": 5402,
"gzip": 2187
"size": 5410,
"gzip": 2194
},
{
"filename": "eslint-plugin-react-hooks.development.js",
"bundleType": "NODE_DEV",
"packageName": "eslint-plugin-react-hooks",
"size": 25592,
"gzip": 5885
"size": 25600,
"gzip": 5891
},
{
"filename": "eslint-plugin-react-hooks.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "eslint-plugin-react-hooks",
"size": 4943,
"gzip": 1815
"size": 4951,
"gzip": 1821
},
{
"filename": "ESLintPluginReactHooks-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "eslint-plugin-react-hooks",
"size": 27469,
"gzip": 6036
}
]
}

View File

@@ -5,7 +5,9 @@ const filesize = require('filesize');
const chalk = require('chalk');
const join = require('path').join;
const fs = require('fs');
const prevBuildResults = require('./results.json');
const prevBuildResults = fs.existsSync(__dirname + '/results.json')
? require('./results.json')
: {bundleSizes: []};
const currentBuildResults = {
// Mutated inside build.js during a build run.

View File

@@ -12,6 +12,7 @@ module.exports = {
Proxy: true,
Symbol: true,
WeakMap: true,
Uint16Array: true,
// Vendor specific
MSApp: true,
__REACT_DEVTOOLS_GLOBAL_HOOK__: true,

View File

@@ -12,6 +12,7 @@ module.exports = {
Symbol: true,
Proxy: true,
WeakMap: true,
Uint16Array: true,
// Vendor specific
MSApp: true,
__REACT_DEVTOOLS_GLOBAL_HOOK__: true,

View File

@@ -11,6 +11,7 @@ module.exports = {
Symbol: true,
Proxy: true,
WeakMap: true,
Uint16Array: true,
// Vendor specific
MSApp: true,
__REACT_DEVTOOLS_GLOBAL_HOOK__: true,

View File

@@ -3382,6 +3382,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0:
loose-envify@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
@@ -4021,6 +4022,7 @@ promise@^7.1.1:
prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"