Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
487f4bf2ee | ||
|
|
297165f1e1 | ||
|
|
cd5b8950a0 | ||
|
|
f00be84b81 | ||
|
|
e0c2c56dfd | ||
|
|
aa8736a3a9 | ||
|
|
d9fa20d52f | ||
|
|
84cc8a31fa | ||
|
|
f9e41e3a51 | ||
|
|
fb572afc14 | ||
|
|
a0a2e846ce | ||
|
|
1fc13e4b35 | ||
|
|
b5cb9d345c | ||
|
|
342fa78ed4 | ||
|
|
62f5d4a057 | ||
|
|
6b86a6e039 | ||
|
|
8f7335875c | ||
|
|
d822d4bbe7 | ||
|
|
13a3788c54 | ||
|
|
d8a73b5eb6 | ||
|
|
741aa17a33 | ||
|
|
95c2b49543 | ||
|
|
55cf14f98e | ||
|
|
29b7b775f2 | ||
|
|
b668168d4d | ||
|
|
619cdfc624 | ||
|
|
3e55560438 | ||
|
|
dfabb77a97 | ||
|
|
c555c008b6 | ||
|
|
ff188d666b | ||
|
|
c4d8ef6430 | ||
|
|
08e9554357 | ||
|
|
0e4135e8c2 | ||
|
|
6d4038f0a6 | ||
|
|
fa6205d522 | ||
|
|
c6bee765ba | ||
|
|
3ae94e1885 | ||
|
|
f3a14951ab | ||
|
|
f24a0da6e0 | ||
|
|
1fecba9230 | ||
|
|
e15542ee0f | ||
|
|
c11015ff4f | ||
|
|
3e295edd52 | ||
|
|
1d48b4a684 | ||
|
|
aa9423701e | ||
|
|
45fc46bfa0 | ||
|
|
b7cc6b2e6f | ||
|
|
f2e2637c8e | ||
|
|
1107b9673c | ||
|
|
0975ea3278 | ||
|
|
d827445a50 | ||
|
|
bc9818f24d | ||
|
|
b5398a9130 | ||
|
|
ce6ecd3fbf | ||
|
|
008a2ab9cd | ||
|
|
d1326f466a | ||
|
|
267ed98146 | ||
|
|
fb3f7bfde9 | ||
|
|
81470a0027 | ||
|
|
e602b5291c | ||
|
|
e489c3f9c1 | ||
|
|
c21c41ecfa | ||
|
|
fec00a869c | ||
|
|
66eb293742 | ||
|
|
51c07912ac | ||
|
|
70d4075832 | ||
|
|
ba6477aa3c | ||
|
|
cb1ff430e8 | ||
|
|
9d483dcfd6 | ||
|
|
6d5ea54eee | ||
|
|
e19c9e1064 | ||
|
|
f11a9c1cb0 | ||
|
|
e679a4b6e2 | ||
|
|
8bcc88f2e7 | ||
|
|
6cb26774e2 | ||
|
|
73962c3664 | ||
|
|
9944392285 | ||
|
|
f0befae657 | ||
|
|
a129259ad6 | ||
|
|
c068d31cca | ||
|
|
db695c4d32 | ||
|
|
38247cba36 | ||
|
|
3f0bcaf0db | ||
|
|
ecd919a2f9 | ||
|
|
3fbebb2a0b | ||
|
|
5fce6488ce | ||
|
|
fe2ecd276e | ||
|
|
8f45a7fdc4 | ||
|
|
1fcbd22431 | ||
|
|
2a084f51a9 | ||
|
|
b5a3df6e88 | ||
|
|
9c146e6751 | ||
|
|
baa6d40fc8 | ||
|
|
a1414e8949 | ||
|
|
10a7a5b5ce | ||
|
|
a2fa6eb98d | ||
|
|
9120f6c2d8 | ||
|
|
b66e6e41e6 | ||
|
|
8c1614a2fd | ||
|
|
177fb76353 | ||
|
|
d17d0b99c1 | ||
|
|
4f332885a2 | ||
|
|
e1cd83e49d | ||
|
|
be457ca685 | ||
|
|
17d70df919 | ||
|
|
4846809370 | ||
|
|
4feab7fc92 | ||
|
|
1454a8be03 | ||
|
|
71b64d5211 | ||
|
|
790c8ef041 | ||
|
|
8a120095bf | ||
|
|
7ab8a8e979 | ||
|
|
4392e3821d | ||
|
|
153a0b5980 | ||
|
|
0ca628b216 | ||
|
|
7ad9806d11 | ||
|
|
0fc1547513 | ||
|
|
edb1f59564 | ||
|
|
3e15b1c690 | ||
|
|
0005d1e3f5 | ||
|
|
f290138d32 | ||
|
|
b4ad8e9471 | ||
|
|
ab03e3d651 | ||
|
|
19ef0ec116 | ||
|
|
a9b035b0c2 | ||
|
|
547e059f0b | ||
|
|
3494ee57e6 | ||
|
|
fef40c061e | ||
|
|
659c13963e | ||
|
|
84b86471ea | ||
|
|
c695b2384f | ||
|
|
1c5aa2f23a | ||
|
|
653bc582f9 | ||
|
|
8bfef0da55 | ||
|
|
4a1072194f | ||
|
|
535804f5c8 | ||
|
|
2743fb7b23 | ||
|
|
7325ebe4d6 | ||
|
|
a22880e5e5 | ||
|
|
947bddd5cf | ||
|
|
ce43a8cd07 | ||
|
|
f64906fba1 | ||
|
|
8df4d59be5 | ||
|
|
5bb4ad7376 | ||
|
|
98eb5ae531 | ||
|
|
39489e7674 | ||
|
|
1dc108e582 | ||
|
|
d9871729c8 | ||
|
|
6bf5e85986 | ||
|
|
7a48c900b7 | ||
|
|
e382b0ba95 | ||
|
|
8482cbe22d | ||
|
|
f00c2755b9 | ||
|
|
52bea95cfc | ||
|
|
1d25aa5787 | ||
|
|
f1bf281605 | ||
|
|
16e120438c | ||
|
|
d14ba87b1b | ||
|
|
88ada98198 | ||
|
|
4f964f09c1 | ||
|
|
c2a2d8a539 | ||
|
|
48f1e5b3ce | ||
|
|
f93f3402f7 | ||
|
|
ee3ef3a079 | ||
|
|
33f6f5e532 | ||
|
|
409066a0a1 | ||
|
|
a7f270c550 | ||
|
|
ed4c4a51cd | ||
|
|
686f1060ad | ||
|
|
7475120ce7 | ||
|
|
0c7189d923 | ||
|
|
14be29b2b9 | ||
|
|
dc0dd4bbff | ||
|
|
dd8205cef9 | ||
|
|
1da310809e | ||
|
|
a9fdf8a326 | ||
|
|
327cf0ee33 | ||
|
|
c954efa70f | ||
|
|
ccb14e270c | ||
|
|
0e9cb3f5d0 | ||
|
|
592676503c | ||
|
|
9b2fb24f99 | ||
|
|
a22fabc2a1 | ||
|
|
21d5f7d32d | ||
|
|
5f06576f51 | ||
|
|
8feeed10d8 | ||
|
|
5bce0ef10a | ||
|
|
d7fd679a31 | ||
|
|
3cd89daed9 | ||
|
|
85f5a81eb7 | ||
|
|
f55795c8ee | ||
|
|
d204747bef | ||
|
|
b98adb648a | ||
|
|
f8bfd58680 | ||
|
|
961eb65b4b | ||
|
|
1a6ab1e9b5 | ||
|
|
8ae867e6b5 | ||
|
|
d5e1bf07d0 | ||
|
|
2dd4ba11e0 | ||
|
|
9cc631a539 | ||
|
|
1034e26fe5 | ||
|
|
5618da49d8 | ||
|
|
9fb9199455 | ||
|
|
c174f85924 | ||
|
|
02e4848e3a | ||
|
|
4b163fee1c | ||
|
|
e58ecda9a2 | ||
|
|
7fd1661f80 | ||
|
|
f9e9913f0e | ||
|
|
7c560131bf | ||
|
|
3d8bda70e5 | ||
|
|
051272f201 | ||
|
|
3ff2c7ccd4 | ||
|
|
5afa1c4eda | ||
|
|
be63473004 | ||
|
|
e27720d7f5 | ||
|
|
aa1ffe4e77 | ||
|
|
e3a7b96455 | ||
|
|
ff29de4029 | ||
|
|
ba19844236 | ||
|
|
a24d510287 | ||
|
|
b50e63ef53 | ||
|
|
fd4527dbcd | ||
|
|
bd5a6d3914 | ||
|
|
8f2c89e963 | ||
|
|
2aecbcd6f1 | ||
|
|
b4608dd24c | ||
|
|
3c69a18814 | ||
|
|
b020fb1148 | ||
|
|
b67c1a2ee1 | ||
|
|
f777d196e0 | ||
|
|
e4512991c9 | ||
|
|
affb2b50ca | ||
|
|
0a0f503d57 | ||
|
|
600651e68e | ||
|
|
e9a2ec9156 | ||
|
|
9d47143e85 | ||
|
|
ebdb47d2c1 | ||
|
|
8b87ebf5b0 | ||
|
|
da04058a91 | ||
|
|
fd1256a561 | ||
|
|
b305c4e034 | ||
|
|
ce90ffd045 | ||
|
|
d34d1c3bae | ||
|
|
6c404d82aa | ||
|
|
8eca0eff87 | ||
|
|
293fed8993 | ||
|
|
ae196e84b6 | ||
|
|
c898020e01 | ||
|
|
c84b9bf828 | ||
|
|
595b4f945b | ||
|
|
d5d10d140e | ||
|
|
cdbfa6b5dd | ||
|
|
1ae3f29c20 | ||
|
|
3db8b80e15 | ||
|
|
169f935f78 | ||
|
|
bf9fadfcf4 | ||
|
|
8c67bbf183 | ||
|
|
e02086bfcc | ||
|
|
b92cdef641 | ||
|
|
6efbbe0685 | ||
|
|
5045763064 | ||
|
|
ddbfe2ed50 | ||
|
|
acb4899637 | ||
|
|
933b64710a | ||
|
|
5fc84efacc | ||
|
|
9f34eb79a3 | ||
|
|
f7cb9d2b22 | ||
|
|
63cc7d2b31 | ||
|
|
3a7c6da8d4 | ||
|
|
75a1c2e72a | ||
|
|
55a4b1f377 | ||
|
|
30aa4ad554 | ||
|
|
b772e0e26b | ||
|
|
9e9e3970e4 | ||
|
|
6514697f0c | ||
|
|
dd019d34db | ||
|
|
11d0781eea | ||
|
|
105f2de545 | ||
|
|
7bee9fbdd4 | ||
|
|
37c7fe0a5f | ||
|
|
ae4f3f07e5 | ||
|
|
95f98a1873 | ||
|
|
e217f2f1ac | ||
|
|
275e76e83b | ||
|
|
b5539ad628 | ||
|
|
0cc50b675a | ||
|
|
8b97a9c36f | ||
|
|
c8ade996e9 | ||
|
|
6c29eabf78 | ||
|
|
d520b358d6 | ||
|
|
8f1ec7649e | ||
|
|
5055a83fa4 | ||
|
|
3e8b4a5b8b | ||
|
|
ff5efb0390 | ||
|
|
f4488bee51 | ||
|
|
d42ed60026 | ||
|
|
d8e03de4aa | ||
|
|
fefa1269e2 | ||
|
|
80a0c05ce3 | ||
|
|
915e4eab53 | ||
|
|
508b5fba0e | ||
|
|
c285ea2700 | ||
|
|
eac092ecac | ||
|
|
cbbc2b6c4d | ||
|
|
4947fcd762 | ||
|
|
d75c69e0cf | ||
|
|
c8ef2feda9 | ||
|
|
55444a6f49 | ||
|
|
04c4f2fcea | ||
|
|
409e472fca | ||
|
|
663835a43a | ||
|
|
82710097f6 | ||
|
|
7ebd90c2c3 | ||
|
|
420001cb4e | ||
|
|
b753f76a74 | ||
|
|
d37f595595 | ||
|
|
b5c0852fdd | ||
|
|
769b1f270e | ||
|
|
8ecd4bd4f0 | ||
|
|
15b11d23f9 | ||
|
|
e770af7a3a | ||
|
|
95a313ec0b | ||
|
|
e16cdd5b17 |
@@ -40,3 +40,9 @@ jobs:
|
||||
key: v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
- store_artifacts:
|
||||
path: ./node_modules.tgz
|
||||
|
||||
- store_artifacts:
|
||||
path: ./scripts/error-codes/codes.json
|
||||
48
.eslintrc.js
48
.eslintrc.js
@@ -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: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
|
||||
|
||||
module.exports = {
|
||||
|
||||
186
CHANGELOG.md
186
CHANGELOG.md
@@ -4,9 +4,189 @@
|
||||
Changes that have landed in master but are not yet released.
|
||||
Click to see more.
|
||||
</summary>
|
||||
|
||||
</details>
|
||||
|
||||
## 16.8.6 (March 27, 2019)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fix an incorrect bailout in `useReducer()`. ([@acdlite](https://github.com/acdlite) in [#15124](https://github.com/facebook/react/pull/15124))
|
||||
* Fix iframe warnings in Safari DevTools. ([@renanvalentin](https://github.com/renanvalentin) in [#15099](https://github.com/facebook/react/pull/15099))
|
||||
* Warn if `contextType` is set to `Context.Consumer` instead of `Context`. ([@aweary](https://github.com/aweary) in [#14831](https://github.com/facebook/react/pull/14831))
|
||||
* Warn if `contextType` is set to invalid values. ([@gaearon](https://github.com/gaearon) in [#15142](https://github.com/facebook/react/pull/15142))
|
||||
|
||||
## 16.8.5 (March 22, 2019)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Don't set the first option as selected in select tag with `size` attribute. ([@kulek1](https://github.com/kulek1) in [#14242](https://github.com/facebook/react/pull/14242))
|
||||
* Improve the `useEffect(async () => ...)` warning message. ([@gaearon](https://github.com/gaearon) in [#15118](https://github.com/facebook/react/pull/15118))
|
||||
* Improve the error message sometimes caused by duplicate React. ([@jaredpalmer](https://github.com/jaredpalmer) in [#15139](https://github.com/facebook/react/pull/15139))
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Improve the `useLayoutEffect` warning message when server rendering. ([@gaearon](https://github.com/gaearon) in [#15158](https://github.com/facebook/react/pull/15158))
|
||||
|
||||
### React Shallow Renderer
|
||||
|
||||
* Fix `setState` in shallow renderer to work with Hooks. ([@gaearon](https://github.com/gaearon) in [#15120](https://github.com/facebook/react/pull/15120))
|
||||
* Fix shallow renderer to support `React.memo`. ([@aweary](https://github.com/aweary) in [#14816](https://github.com/facebook/react/pull/14816))
|
||||
* Fix shallow renderer to support Hooks inside `forwardRef`. ([@eps1lon](https://github.com/eps1lon) in [#15100](https://github.com/facebook/react/pull/15100))
|
||||
|
||||
## 16.8.4 (March 5, 2019)
|
||||
|
||||
### React DOM and other renderers
|
||||
|
||||
- Fix a bug where DevTools caused a runtime error when inspecting a component that used a `useContext` hook. ([@bvaughn](https://github.com/bvaughn) in [#14940](https://github.com/facebook/react/pull/14940))
|
||||
|
||||
## 16.8.3 (February 21, 2019)
|
||||
|
||||
### React DOM
|
||||
|
||||
- Fix a bug that caused inputs to behave incorrectly in UMD builds. ([@gaearon](https://github.com/gaearon) in [#14914](https://github.com/facebook/react/pull/14914))
|
||||
- Fix a bug that caused render phase updates to be discarded. ([@gaearon](https://github.com/gaearon) in [#14852](https://github.com/facebook/react/pull/14852))
|
||||
|
||||
### React DOM Server
|
||||
- Unwind the context stack when a stream is destroyed without completing, to prevent incorrect values during a subsequent render. ([@overlookmotel](https://github.com/overlookmotel) in [#14706](https://github.com/facebook/react/pull/14706/))
|
||||
|
||||
## 16.8.2 (February 14, 2019)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fix `ReactDOM.render` being ignored inside `useEffect`. ([@gaearon](https://github.com/gaearon) in [#14799](https://github.com/facebook/react/pull/14799))
|
||||
* Fix a crash when unmounting empty portals. ([@gaearon](https://github.com/gaearon) in [#14820](https://github.com/facebook/react/pull/14820))
|
||||
* Fix `useImperativeHandle` to work correctly when no deps are specified. ([@gaearon](https://github.com/gaearon) in [#14801](https://github.com/facebook/react/pull/14801))
|
||||
* Fix `crossOrigin` attribute to work in SVG `image` elements. ([@aweary](https://github.com/aweary) in [#14832](https://github.com/facebook/react/pull/14832))
|
||||
* Fix a false positive warning when using Suspense with Hooks. ([@gaearon](https://github.com/gaearon) in [#14821](https://github.com/facebook/react/pull/14821))
|
||||
|
||||
### React Test Utils and React Test Renderer
|
||||
|
||||
* Include component stack into the `act()` warning. ([@threepointone](https://github.com/threepointone) in [#14855](https://github.com/facebook/react/pull/14855))
|
||||
|
||||
## 16.8.1 (February 6, 2019)
|
||||
|
||||
### React DOM and React Test Renderer
|
||||
|
||||
* Fix a crash when used together with an older version of React. ([@bvaughn](https://github.com/bvaughn) in [#14770](https://github.com/facebook/react/pull/14770))
|
||||
|
||||
### React Test Utils
|
||||
|
||||
* Fix a crash in Node environment. ([@threepointone](https://github.com/threepointone) in [#14768](https://github.com/facebook/react/pull/14768))
|
||||
|
||||
## 16.8.0 (February 6, 2019)
|
||||
|
||||
### React
|
||||
|
||||
* Add [Hooks](https://reactjs.org/docs/hooks-intro.html) — a way to use state and other React features without writing a class. ([@acdlite](https://github.com/acdlite) et al. in [#13968](https://github.com/facebook/react/pull/13968))
|
||||
* Improve the `useReducer` Hook lazy initialization API. ([@acdlite](https://github.com/acdlite) in [#14723](https://github.com/facebook/react/pull/14723))
|
||||
|
||||
### React DOM
|
||||
|
||||
* Bail out of rendering on identical values for `useState` and `useReducer` Hooks. ([@acdlite](https://github.com/acdlite) in [#14569](https://github.com/facebook/react/pull/14569))
|
||||
* Use `Object.is` algorithm for comparing `useState` and `useReducer` values. ([@Jessidhia](https://github.com/Jessidhia) in [#14752](https://github.com/facebook/react/pull/14752))
|
||||
* Don’t compare the first argument passed to `useEffect`/`useMemo`/`useCallback` Hooks. ([@acdlite](https://github.com/acdlite) in [#14594](https://github.com/facebook/react/pull/14594))
|
||||
* Support synchronous thenables passed to `React.lazy()`. ([@gaearon](https://github.com/gaearon) in [#14626](https://github.com/facebook/react/pull/14626))
|
||||
* Render components with Hooks twice in Strict Mode (DEV-only) to match class behavior. ([@gaearon](https://github.com/gaearon) in [#14654](https://github.com/facebook/react/pull/14654))
|
||||
* Warn about mismatching Hook order in development. ([@threepointone](https://github.com/threepointone) in [#14585](https://github.com/facebook/react/pull/14585) and [@acdlite](https://github.com/acdlite) in [#14591](https://github.com/facebook/react/pull/14591))
|
||||
* Effect clean-up functions must return either `undefined` or a function. All other values, including `null`, are not allowed. [@acdlite](https://github.com/acdlite) in [#14119](https://github.com/facebook/react/pull/14119)
|
||||
|
||||
### React Test Renderer and Test Utils
|
||||
|
||||
* Support Hooks in the shallow renderer. ([@trueadm](https://github.com/trueadm) in [#14567](https://github.com/facebook/react/pull/14567))
|
||||
* Fix wrong state in `shouldComponentUpdate` in the presence of `getDerivedStateFromProps` for Shallow Renderer. ([@chenesan](https://github.com/chenesan) in [#14613](https://github.com/facebook/react/pull/14613))
|
||||
* Add `ReactTestRenderer.act()` and `ReactTestUtils.act()` for batching updates so that tests more closely match real behavior. ([@threepointone](https://github.com/threepointone) in [#14744](https://github.com/facebook/react/pull/14744))
|
||||
|
||||
|
||||
### ESLint Plugin: React Hooks
|
||||
|
||||
* Initial [release](https://www.npmjs.com/package/eslint-plugin-react-hooks). ([@calebmer](https://github.com/calebmer) in [#13968](https://github.com/facebook/react/pull/13968))
|
||||
* Fix reporting after encountering a loop. ([@calebmer](https://github.com/calebmer) and [@Yurickh](https://github.com/Yurickh) in [#14661](https://github.com/facebook/react/pull/14661))
|
||||
* Don't consider throwing to be a rule violation. ([@sophiebits](https://github.com/sophiebits) in [#14040](https://github.com/facebook/react/pull/14040))
|
||||
|
||||
## 16.7.0 (December 19, 2018)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fix performance of `React.lazy` for large numbers of lazily-loaded components. ([@acdlite](http://github.com/acdlite) in [#14429](https://github.com/facebook/react/pull/14429))
|
||||
* Clear fields on unmount to avoid memory leaks. ([@trueadm](http://github.com/trueadm) in [#14276](https://github.com/facebook/react/pull/14276))
|
||||
* Fix bug with SSR and context when mixing `react-dom/server@16.6` and `react@<16.6`. ([@gaearon](http://github.com/gaearon) in [#14291](https://github.com/facebook/react/pull/14291))
|
||||
* Fix a performance regression in profiling mode. ([@bvaughn](http://github.com/bvaughn) in [#14383](https://github.com/facebook/react/pull/14383))
|
||||
|
||||
### Scheduler (Experimental)
|
||||
|
||||
* Post to MessageChannel instead of window. ([@acdlite](http://github.com/acdlite) in [#14234](https://github.com/facebook/react/pull/14234))
|
||||
* Reduce serialization overhead. ([@developit](http://github.com/developit) in [#14249](https://github.com/facebook/react/pull/14249))
|
||||
* Fix fallback to `setTimeout` in testing environments. ([@bvaughn](http://github.com/bvaughn) in [#14358](https://github.com/facebook/react/pull/14358))
|
||||
* Add methods for debugging. ([@mrkev](http://github.com/mrkev) in [#14053](https://github.com/facebook/react/pull/14053))
|
||||
|
||||
|
||||
## 16.6.3 (November 12, 2018)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fix bugs in `Suspense` and `lazy`. ([@acdlite](https://github.com/acdlite) in [#14133](https://github.com/facebook/react/pull/14133), [#14157](https://github.com/facebook/react/pull/14157), and [#14164](https://github.com/facebook/react/pull/14164))
|
||||
* Fix highlighting of `React.memo` updates in React DevTools. ([@bvaughn](https://github.com/bvaughn) in [#14141](https://github.com/facebook/react/pull/14141))
|
||||
* Fix interaction of Suspense with the React Profiler. ([@bvaughn](https://github.com/bvaughn) in [#14065](https://github.com/facebook/react/pull/14065))
|
||||
* Fix a false positive warning when using Suspense. ([@acdlite](https://github.com/acdlite) in [#14158](https://github.com/facebook/react/pull/14158))
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Fix incorrect sharing of context state between `renderToNodeStream()` calls. ([@sebmarkbage](https://github.com/sebmarkbage) in [#14182](https://github.com/facebook/react/pull/14182))
|
||||
* Add a warning about incorrect usage of the context API. ([@trueadm](https://github.com/trueadm) in [#14033](https://github.com/facebook/react/pull/14033))
|
||||
|
||||
## 16.6.2 (November 12, 2018)
|
||||
|
||||
This release was published in a broken state and should be skipped.
|
||||
|
||||
## 16.6.1 (November 6, 2018)
|
||||
|
||||
### React DOM
|
||||
|
||||
* Fallback should not remount every time a promise resolves. ([@acdlite](https://github.com/acdlite) in [#14083](https://github.com/facebook/react/pull/14083))
|
||||
* Fix bug where Suspense keeps showing fallback even after everything finishes loading. ([@acdlite](https://github.com/acdlite) in [#14083](https://github.com/facebook/react/pull/14083))
|
||||
* Fix a crash when Suspense finishes loading in IE11. ([@sophiebits](https://github.com/sophiebits) in [#14126](https://github.com/facebook/react/pull/14126))
|
||||
* Fix unresolved default props in lifecycle methods of a lazy component. ([@gaearon](https://github.com/gaearon) in [#14112](https://github.com/facebook/react/pull/14112))
|
||||
* Fix bug when recovering from an error thrown during complete phase. ([@gaearon](https://github.com/gaearon) in [#14104](https://github.com/facebook/react/pull/14104))
|
||||
|
||||
### Scheduler (Experimental)
|
||||
|
||||
* Switch from deadline object to `shouldYield` API. ([@acdlite](https://github.com/acdlite) in [#14025](https://github.com/facebook/react/pull/14025))
|
||||
|
||||
|
||||
## 16.6.0 (October 23, 2018)
|
||||
|
||||
### React
|
||||
|
||||
* Add `React.memo()` as an alternative to `PureComponent` for functions. ([@acdlite](https://github.com/acdlite) in [#13748](https://github.com/facebook/react/pull/13748))
|
||||
* Add `React.lazy()` for code splitting components. ([@acdlite](https://github.com/acdlite) in [#13885](https://github.com/facebook/react/pull/13885))
|
||||
* `React.StrictMode` now warns about legacy context API. ([@bvaughn](https://github.com/bvaughn) in [#13760](https://github.com/facebook/react/pull/13760))
|
||||
* `React.StrictMode` now warns about `findDOMNode`. ([@sebmarkbage](https://github.com/sebmarkbage) in [#13841](https://github.com/facebook/react/pull/13841))
|
||||
* Rename `unstable_AsyncMode` to `unstable_ConcurrentMode`. ([@trueadm](https://github.com/trueadm) in [#13732](https://github.com/facebook/react/pull/13732))
|
||||
* Rename `unstable_Placeholder` to `Suspense`, and `delayMs` to `maxDuration`. ([@gaearon](https://github.com/gaearon) in [#13799](https://github.com/facebook/react/pull/13799) and [@sebmarkbage](https://github.com/sebmarkbage) in [#13922](https://github.com/facebook/react/pull/13922))
|
||||
|
||||
### React DOM
|
||||
|
||||
* Add `contextType` as a more ergonomic way to subscribe to context from a class. ([@bvaughn](https://github.com/bvaughn) in [#13728](https://github.com/facebook/react/pull/13728))
|
||||
* Add `getDerivedStateFromError` lifecycle method for catching errors in a future asynchronous server-side renderer. ([@bvaughn](https://github.com/bvaughn) in [#13746](https://github.com/facebook/react/pull/13746))
|
||||
* Warn when `<Context>` is used instead of `<Context.Consumer>`. ([@trueadm](https://github.com/trueadm) in [#13829](https://github.com/facebook/react/pull/13829))
|
||||
* Fix gray overlay on iOS Safari. ([@philipp-spiess](https://github.com/philipp-spiess) in [#13778](https://github.com/facebook/react/pull/13778))
|
||||
* Fix a bug caused by overwriting `window.event` in development. ([@sergei-startsev](https://github.com/sergei-startsev) in [#13697](https://github.com/facebook/react/pull/13697))
|
||||
|
||||
### React DOM Server
|
||||
|
||||
* Add support for `React.memo()`. ([@alexmckenley](https://github.com/alexmckenley) in [#13855](https://github.com/facebook/react/pull/13855))
|
||||
* Add support for `contextType`. ([@alexmckenley](https://github.com/alexmckenley) and [@sebmarkbage](https://github.com/sebmarkbage) in [#13889](https://github.com/facebook/react/pull/13889))
|
||||
|
||||
### Scheduler (Experimental)
|
||||
|
||||
* Rename the package to `scheduler`. ([@gaearon](https://github.com/gaearon) in [#13683](https://github.com/facebook/react/pull/13683))
|
||||
* Support priority levels, continuations, and wrapped callbacks. ([@acdlite](https://github.com/acdlite) in [#13720](https://github.com/facebook/react/pull/13720) and [#13842](https://github.com/facebook/react/pull/13842))
|
||||
* Improve the fallback mechanism in non-DOM environments. ([@acdlite](https://github.com/acdlite) in [#13740](https://github.com/facebook/react/pull/13740))
|
||||
* Schedule `requestAnimationFrame` earlier. ([@acdlite](https://github.com/acdlite) in [#13785](https://github.com/facebook/react/pull/13785))
|
||||
* Fix the DOM detection to be more thorough. ([@trueadm](https://github.com/trueadm) in [#13731](https://github.com/facebook/react/pull/13731))
|
||||
* Fix bugs with interaction tracing. ([@bvaughn](https://github.com/bvaughn) in [#13590](https://github.com/facebook/react/pull/13590))
|
||||
* Add the `envify` transform to the package. ([@mridgway](https://github.com/mridgway) in [#13766](https://github.com/facebook/react/pull/13766))
|
||||
|
||||
## 16.5.2 (September 18, 2018)
|
||||
|
||||
### React DOM
|
||||
@@ -60,7 +240,7 @@
|
||||
* Fix incorrect data in `compositionend` event when typing Korean on IE11 ([@crux153](https://github.com/crux153) in [#12563](https://github.com/facebook/react/issues/12563))
|
||||
* Fix a crash when using dynamic `children` in the `<option>` tag ([@Slowyn](https://github.com/Slowyn) in [#13261](https://github.com/facebook/react/issues/13261), [@gaearon](https://github.com/gaearon) in [#13465](https://github.com/facebook/react/pull/13465))
|
||||
* Fix the `checked` attribute not getting initially set on the `input` ([@dilidili](https://github.com/dilidili) in [#13114](https://github.com/facebook/react/issues/13114))
|
||||
* Fix hydration of `dangerouslySetInnerHTML` when `__html` is not a string ([@gaearon](https://github.com/gaearon) in [#13353](https://github.com/facebook/react/issues/13353))
|
||||
* Fix hydration of `dangerouslySetInnerHTML` when `__html` is not a string ([@gaearon](https://github.com/gaearon) in [#13353](https://github.com/facebook/react/issues/13353))
|
||||
* Fix a warning about missing controlled `onChange` to fire on falsy values too ([@nicolevy](https://github.com/nicolevy) in [#12628](https://github.com/facebook/react/issues/12628))
|
||||
* Fix `submit` and `reset` buttons getting an empty label ([@ellsclytn](https://github.com/ellsclytn) in [#12780](https://github.com/facebook/react/issues/12780))
|
||||
* Fix the `onSelect` event not being triggered after drag and drop ([@gaearon](https://github.com/gaearon) in [#13422](https://github.com/facebook/react/issues/13422))
|
||||
@@ -251,7 +431,7 @@
|
||||
* Deduplicate warning messages about invalid callback. ([@yenshih](https://github.com/yenshih) in [#11833](https://github.com/facebook/react/pull/11833))
|
||||
* Deprecate `ReactDOM.unstable_createPortal()` in favor of `ReactDOM.createPortal()`. ([@prometheansacrifice](https://github.com/prometheansacrifice) in [#11747](https://github.com/facebook/react/pull/11747))
|
||||
* Don't emit User Timing entries for context types. ([@abhaynikam](https://github.com/abhaynikam) in [#12250](https://github.com/facebook/react/pull/12250))
|
||||
* Improve the error message when context consumer child isn't a function. ([@raunofreiberg](https://github.com/raunofreiberg) in [#12267](https://github.com/facebook/react/pull/12267))
|
||||
* Improve the error message when context consumer child isn't a function. ([@raunofreiberg](https://github.com/raunofreiberg) in [#12267](https://github.com/facebook/react/pull/12267))
|
||||
* Improve the error message when adding a ref to a functional component. ([@skiritsis](https://github.com/skiritsis) in [#11782](https://github.com/facebook/react/pull/11782))
|
||||
|
||||
### React DOM Server
|
||||
|
||||
10
README.md
10
README.md
@@ -40,10 +40,8 @@ You can improve it by sending pull requests to [this repository](https://github.
|
||||
We have several examples [on the website](https://reactjs.org/). Here is the first one to get you started:
|
||||
|
||||
```jsx
|
||||
class HelloMessage extends React.Component {
|
||||
render() {
|
||||
return <div>Hello {this.props.name}</div>;
|
||||
}
|
||||
function HelloMessage({ name }) {
|
||||
return <div>Hello {name}</div>;
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
@@ -60,9 +58,9 @@ You'll notice that we used an HTML-like syntax; [we call it JSX](https://reactjs
|
||||
|
||||
The main purpose of this repository is to continue to evolve React core, making it faster and easier to use. Development of React happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving React.
|
||||
|
||||
### [Code of Conduct](https://code.facebook.com/codeofconduct)
|
||||
### [Code of Conduct](https://code.fb.com/codeofconduct)
|
||||
|
||||
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
|
||||
Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.fb.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
|
||||
|
||||
### [Contributing Guide](https://reactjs.org/contributing/how-to-contribute.html)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//
|
||||
// You'll need a GitHub token, you can re-use this one:
|
||||
//
|
||||
// e622517d9f1136ea8900 07c6373666312cdfaa69
|
||||
// 0a7d5c3cad9a6dbec2d9 9a5222cf49062a4c1ef7
|
||||
//
|
||||
// (Just remove the space)
|
||||
//
|
||||
@@ -29,9 +29,16 @@ const {markdown, danger} = require('danger');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const {generateResultsArray} = require('./scripts/rollup/stats');
|
||||
const {readFileSync} = require('fs');
|
||||
const {existsSync, readFileSync} = require('fs');
|
||||
const {exec} = require('child_process');
|
||||
|
||||
if (!existsSync('./scripts/rollup/results.json')) {
|
||||
// This indicates the build failed previously.
|
||||
// In that case, there's nothing for the Dangerfile to do.
|
||||
// Exit early to avoid leaving a redundant (and potentially confusing) PR comment.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const currentBuildResults = JSON.parse(
|
||||
readFileSync('./scripts/rollup/results.json')
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ class VectorWidget extends React.Component {
|
||||
/**
|
||||
* When the component is mounted into the document - this is similar to a
|
||||
* constructor, but invoked when the instance is actually mounted into the
|
||||
* document. Here's, we'll just set up an animation loop that invokes our
|
||||
* document. Here, we'll just set up an animation loop that invokes our
|
||||
* method. Binding of `this.onTick` is not needed because all React methods
|
||||
* are automatically bound before being mounted.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prestart":
|
||||
"cp ../../build/dist/react.development.js public/ && cp ../../build/dist/react-dom.development.js public/ && cp ../../build/dist/react-dom-server.browser.development.js public/",
|
||||
"cp ../../build/node_modules/react/umd/react.development.js public/ && cp ../../build/node_modules/react-dom/umd/react-dom.development.js public/ && cp ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js public/",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
fixtures/dom/.gitignore
vendored
3
fixtures/dom/.gitignore
vendored
@@ -9,8 +9,11 @@ coverage
|
||||
# production
|
||||
build
|
||||
public/react.development.js
|
||||
public/react.production.min.js
|
||||
public/react-dom.development.js
|
||||
public/react-dom.production.min.js
|
||||
public/react-dom-server.browser.development.js
|
||||
public/react-dom-server.browser.production.min.js
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"prestart": "cp ../../build/dist/react.development.js ../../build/dist/react-dom.development.js ../../build/dist/react-dom-server.browser.development.js public/",
|
||||
"prestart": "cp ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js public/",
|
||||
"build": "react-scripts build && cp build/index.html build/200.html",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
|
||||
@@ -7,8 +7,9 @@ class Header extends React.Component {
|
||||
super(props, context);
|
||||
const query = parse(window.location.search);
|
||||
const version = query.version || 'local';
|
||||
const production = query.production || false;
|
||||
const versions = [version];
|
||||
this.state = {version, versions};
|
||||
this.state = {version, versions, production};
|
||||
}
|
||||
componentWillMount() {
|
||||
getVersionTags().then(tags => {
|
||||
@@ -25,6 +26,14 @@ class Header extends React.Component {
|
||||
}
|
||||
window.location.search = stringify(query);
|
||||
}
|
||||
handleProductionChange(event) {
|
||||
const query = parse(window.location.search);
|
||||
query.production = event.target.checked;
|
||||
if (!query.production) {
|
||||
delete query.production;
|
||||
}
|
||||
window.location.search = stringify(query);
|
||||
}
|
||||
handleFixtureChange(event) {
|
||||
window.location.pathname = event.target.value;
|
||||
}
|
||||
@@ -43,6 +52,16 @@ class Header extends React.Component {
|
||||
</span>
|
||||
|
||||
<div className="header-controls">
|
||||
<input
|
||||
id="react_production"
|
||||
className="header__checkbox"
|
||||
type="checkbox"
|
||||
checked={this.state.production}
|
||||
onChange={this.handleProductionChange}
|
||||
/>
|
||||
<label htmlFor="react_production" className="header__label">
|
||||
Production
|
||||
</label>
|
||||
<label htmlFor="example">
|
||||
<span className="sr-only">Select an example</span>
|
||||
<select
|
||||
|
||||
@@ -202,6 +202,34 @@ class SelectFixture extends React.Component {
|
||||
</select>
|
||||
</div>
|
||||
</TestCase>
|
||||
|
||||
<TestCase
|
||||
title="A select with the size attribute should not set first option as selected"
|
||||
relatedIssues="14239"
|
||||
introducedIn="16.0.0">
|
||||
<TestCase.ExpectedResult>
|
||||
No options should be selected.
|
||||
</TestCase.ExpectedResult>
|
||||
|
||||
<div className="test-fixture">
|
||||
<select size="3">
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p className="footnote">
|
||||
<b>Notes:</b> This happens if <code>size</code> is assigned after
|
||||
options are selected. The select element picks the first item by
|
||||
default, then it is expanded to show more options when{' '}
|
||||
<code>size</code> is assigned, preserving the default selection.
|
||||
</p>
|
||||
<p className="footnote">
|
||||
This was introduced in React 16.0.0 when options were added before
|
||||
select attribute assignment.
|
||||
</p>
|
||||
</TestCase>
|
||||
</FixtureSet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import TestCase from '../../TestCase';
|
||||
const React = window.React;
|
||||
const ReactDOM = window.ReactDOM;
|
||||
|
||||
const Suspense = React.unstable_Suspense;
|
||||
const Suspense = React.Suspense;
|
||||
|
||||
let cache = new Set();
|
||||
|
||||
|
||||
36
fixtures/dom/src/react-loader.js
vendored
36
fixtures/dom/src/react-loader.js
vendored
@@ -37,12 +37,15 @@ function loadScript(src) {
|
||||
}
|
||||
|
||||
export function reactPaths() {
|
||||
let reactPath = 'react.development.js';
|
||||
let reactDOMPath = 'react-dom.development.js';
|
||||
let reactDOMServerPath = 'react-dom-server.browser.development.js';
|
||||
|
||||
let query = parseQuery(window.location.search);
|
||||
let version = query.version || 'local';
|
||||
let isProduction = query.production === 'true';
|
||||
|
||||
let environment = isProduction ? 'production.min' : 'development';
|
||||
|
||||
let reactPath = 'react.' + environment + '.js';
|
||||
let reactDOMPath = 'react-dom.' + environment + '.js';
|
||||
let reactDOMServerPath = 'react-dom-server.browser.' + environment + '.js';
|
||||
|
||||
if (version !== 'local') {
|
||||
const {major, minor, prerelease} = semver(version);
|
||||
@@ -51,21 +54,34 @@ export function reactPaths() {
|
||||
// Load the old module location for anything less than 16 RC
|
||||
if (major >= 16 && !(minor === 0 && preReleaseStage === 'alpha')) {
|
||||
reactPath =
|
||||
'https://unpkg.com/react@' + version + '/umd/react.development.js';
|
||||
'https://unpkg.com/react@' +
|
||||
version +
|
||||
'/umd/react.' +
|
||||
environment +
|
||||
'.js';
|
||||
reactDOMPath =
|
||||
'https://unpkg.com/react-dom@' +
|
||||
version +
|
||||
'/umd/react-dom.development.js';
|
||||
'/umd/react-dom.' +
|
||||
environment +
|
||||
'.js';
|
||||
reactDOMServerPath =
|
||||
'https://unpkg.com/react-dom@' +
|
||||
version +
|
||||
'/umd/react-dom-server.browser.development';
|
||||
'/umd/react-dom-server.browser.' +
|
||||
environment +
|
||||
'.js';
|
||||
} else {
|
||||
reactPath = 'https://unpkg.com/react@' + version + '/dist/react.js';
|
||||
let suffix = isProduction ? '.min.js' : '.js';
|
||||
|
||||
reactPath = 'https://unpkg.com/react@' + version + '/dist/react' + suffix;
|
||||
reactDOMPath =
|
||||
'https://unpkg.com/react-dom@' + version + '/dist/react-dom.js';
|
||||
'https://unpkg.com/react-dom@' + version + '/dist/react-dom' + suffix;
|
||||
reactDOMServerPath =
|
||||
'https://unpkg.com/react-dom@' + version + '/dist/react-dom-server.js';
|
||||
'https://unpkg.com/react-dom@' +
|
||||
version +
|
||||
'/dist/react-dom-server' +
|
||||
suffix;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ h3 {
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
h4, h4, h5, h6 {
|
||||
h4, h5, h6 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
@@ -126,6 +126,15 @@ textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header__checkbox {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header__label {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
clip: rect(0, 0, 0, 0);
|
||||
height: 0;
|
||||
|
||||
14
fixtures/eslint/.eslintrc.json
Normal file
14
fixtures/eslint/.eslintrc.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["react-hooks"],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": 2
|
||||
}
|
||||
}
|
||||
7
fixtures/eslint/README.md
Normal file
7
fixtures/eslint/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# ESLint Playground Fixture
|
||||
|
||||
This is an internal playground for quick iteration on our lint rules inside an IDE like VSCode.
|
||||
|
||||
See instructions in `./index.js` in this directory.
|
||||
|
||||

|
||||
11
fixtures/eslint/index.js
Normal file
11
fixtures/eslint/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// This is a testing playground for our lint rules.
|
||||
|
||||
// 1. Run yarn && yarn start
|
||||
// 2. "File > Add Folder to Workspace" this specific folder in VSCode with ESLint extension
|
||||
// 3. Changes to the rule source should get picked up without restarting ESLint server
|
||||
|
||||
function Foo() {
|
||||
if (condition) {
|
||||
useEffect(() => {});
|
||||
}
|
||||
}
|
||||
12
fixtures/eslint/package.json
Normal file
12
fixtures/eslint/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "eslint-playground",
|
||||
"dependencies": {
|
||||
"eslint": "4.1.0",
|
||||
"eslint-plugin-react-hooks": "link:./proxy"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./watch.sh",
|
||||
"lint": "eslint index.js"
|
||||
}
|
||||
}
|
||||
35
fixtures/eslint/proxy/index.js
Normal file
35
fixtures/eslint/proxy/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
// This file is a proxy for our rule definition that will
|
||||
// load the latest built version on every check. This makes
|
||||
// it convenient to test inside IDEs (which would otherwise
|
||||
// load a version of our rule once and never restart the server).
|
||||
// See instructions in ../index.js playground.
|
||||
|
||||
let build;
|
||||
reload();
|
||||
|
||||
function reload() {
|
||||
for (let id in require.cache) {
|
||||
if (/eslint-plugin-react-hooks/.test(id)) {
|
||||
delete require.cache[id];
|
||||
}
|
||||
}
|
||||
// Point to the built version.
|
||||
build = require('../../../build/node_modules/eslint-plugin-react-hooks');
|
||||
}
|
||||
|
||||
let rules = {};
|
||||
for (let key in build.rules) {
|
||||
if (build.rules.hasOwnProperty(key)) {
|
||||
rules[key] = Object.assign({}, build.rules, {
|
||||
create() {
|
||||
// Reload changes to the built rule
|
||||
reload();
|
||||
return build.rules[key].create.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {rules};
|
||||
4
fixtures/eslint/proxy/package.json
Normal file
4
fixtures/eslint/proxy/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "0.0.0"
|
||||
}
|
||||
3
fixtures/eslint/watch.sh
Executable file
3
fixtures/eslint/watch.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
(cd ../.. && yarn build eslint --type=NODE_DEV)
|
||||
(cd ../.. && watchman-make --make 'yarn build eslint --type=NODE_DEV' -p 'packages/eslint-plugin-*/**/*' -t ignored)
|
||||
853
fixtures/eslint/yarn.lock
Normal file
853
fixtures/eslint/yarn.lock
Normal file
@@ -0,0 +1,853 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
acorn-jsx@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
|
||||
integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=
|
||||
dependencies:
|
||||
acorn "^3.0.4"
|
||||
|
||||
acorn@^3.0.4:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
|
||||
integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
|
||||
|
||||
acorn@^5.5.0:
|
||||
version "5.7.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
|
||||
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
|
||||
|
||||
ajv-keywords@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
|
||||
integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
|
||||
|
||||
ajv@^6.0.1:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96"
|
||||
integrity sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==
|
||||
dependencies:
|
||||
fast-deep-equal "^2.0.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-escapes@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
|
||||
integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
|
||||
|
||||
ansi-styles@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
babel-code-frame@^6.22.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
esutils "^2.0.2"
|
||||
js-tokens "^3.0.2"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
|
||||
caller-path@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
|
||||
integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=
|
||||
dependencies:
|
||||
callsites "^0.2.0"
|
||||
|
||||
callsites@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
|
||||
integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=
|
||||
|
||||
chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
|
||||
dependencies:
|
||||
ansi-styles "^2.2.1"
|
||||
escape-string-regexp "^1.0.2"
|
||||
has-ansi "^2.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.0.0, chalk@^2.1.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chardet@^0.4.0:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
|
||||
integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=
|
||||
|
||||
circular-json@^0.3.1:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
|
||||
|
||||
cli-cursor@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
|
||||
integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
|
||||
dependencies:
|
||||
restore-cursor "^2.0.0"
|
||||
|
||||
cli-width@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
dependencies:
|
||||
color-name "1.1.3"
|
||||
|
||||
color-name@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
concat-stream@^1.6.0:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
|
||||
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
debug@^2.6.8:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
deep-is@~0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
||||
|
||||
doctrine@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||
integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
"eslint-plugin-react-hooks@link:./proxy":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
eslint-scope@^3.7.1:
|
||||
version "3.7.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535"
|
||||
integrity sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==
|
||||
dependencies:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.1.0.tgz#bbb55a28220ee08b69da9554d45a6b2ebfd7d913"
|
||||
integrity sha1-u7VaKCIO4Itp2pVU1FprLr/X2RM=
|
||||
dependencies:
|
||||
babel-code-frame "^6.22.0"
|
||||
chalk "^1.1.3"
|
||||
concat-stream "^1.6.0"
|
||||
debug "^2.6.8"
|
||||
doctrine "^2.0.0"
|
||||
eslint-scope "^3.7.1"
|
||||
espree "^3.4.3"
|
||||
esquery "^1.0.0"
|
||||
estraverse "^4.2.0"
|
||||
esutils "^2.0.2"
|
||||
file-entry-cache "^2.0.0"
|
||||
glob "^7.1.2"
|
||||
globals "^9.17.0"
|
||||
ignore "^3.3.3"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^3.0.6"
|
||||
is-my-json-valid "^2.16.0"
|
||||
is-resolvable "^1.0.0"
|
||||
js-yaml "^3.8.4"
|
||||
json-stable-stringify "^1.0.1"
|
||||
levn "^0.3.0"
|
||||
lodash "^4.17.4"
|
||||
minimatch "^3.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.8.2"
|
||||
path-is-inside "^1.0.2"
|
||||
pluralize "^4.0.0"
|
||||
progress "^2.0.0"
|
||||
require-uncached "^1.0.3"
|
||||
strip-json-comments "~2.0.1"
|
||||
table "^4.0.1"
|
||||
text-table "~0.2.0"
|
||||
|
||||
espree@^3.4.3:
|
||||
version "3.5.4"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
|
||||
integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==
|
||||
dependencies:
|
||||
acorn "^5.5.0"
|
||||
acorn-jsx "^3.0.0"
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
||||
integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
|
||||
dependencies:
|
||||
estraverse "^4.0.0"
|
||||
|
||||
esrecurse@^4.1.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
|
||||
integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
|
||||
dependencies:
|
||||
estraverse "^4.1.0"
|
||||
|
||||
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
|
||||
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
|
||||
integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
|
||||
|
||||
external-editor@^2.0.4:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
|
||||
integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==
|
||||
dependencies:
|
||||
chardet "^0.4.0"
|
||||
iconv-lite "^0.4.17"
|
||||
tmp "^0.0.33"
|
||||
|
||||
fast-deep-equal@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||
integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
|
||||
|
||||
fast-levenshtein@~2.0.4:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
|
||||
integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=
|
||||
dependencies:
|
||||
flat-cache "^1.2.1"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
flat-cache@^1.2.1:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f"
|
||||
integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==
|
||||
dependencies:
|
||||
circular-json "^0.3.1"
|
||||
graceful-fs "^4.1.2"
|
||||
rimraf "~2.6.2"
|
||||
write "^0.2.1"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
generate-function@^2.0.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
|
||||
integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
|
||||
dependencies:
|
||||
is-property "^1.0.2"
|
||||
|
||||
generate-object-property@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
|
||||
integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=
|
||||
dependencies:
|
||||
is-property "^1.0.0"
|
||||
|
||||
glob@^7.1.2, glob@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
globals@^9.17.0:
|
||||
version "9.18.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
||||
integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
|
||||
|
||||
graceful-fs@^4.1.2:
|
||||
version "4.1.15"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
||||
|
||||
has-ansi@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||
integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
|
||||
|
||||
iconv-lite@^0.4.17:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
ignore@^3.3.3:
|
||||
version "3.3.10"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
|
||||
integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
|
||||
|
||||
imurmurhash@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
inquirer@^3.0.6:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
||||
integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==
|
||||
dependencies:
|
||||
ansi-escapes "^3.0.0"
|
||||
chalk "^2.0.0"
|
||||
cli-cursor "^2.1.0"
|
||||
cli-width "^2.0.0"
|
||||
external-editor "^2.0.4"
|
||||
figures "^2.0.0"
|
||||
lodash "^4.3.0"
|
||||
mute-stream "0.0.7"
|
||||
run-async "^2.2.0"
|
||||
rx-lite "^4.0.8"
|
||||
rx-lite-aggregates "^4.0.8"
|
||||
string-width "^2.1.0"
|
||||
strip-ansi "^4.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
is-fullwidth-code-point@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
|
||||
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
|
||||
|
||||
is-my-ip-valid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
|
||||
integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==
|
||||
|
||||
is-my-json-valid@^2.16.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175"
|
||||
integrity sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==
|
||||
dependencies:
|
||||
generate-function "^2.0.0"
|
||||
generate-object-property "^1.1.0"
|
||||
is-my-ip-valid "^1.0.0"
|
||||
jsonpointer "^4.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
is-promise@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
|
||||
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
|
||||
|
||||
is-property@^1.0.0, is-property@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
|
||||
integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
|
||||
|
||||
is-resolvable@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
|
||||
integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
|
||||
|
||||
js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||
|
||||
js-yaml@^3.8.4:
|
||||
version "3.12.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600"
|
||||
integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
json-schema-traverse@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-stable-stringify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
||||
integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
|
||||
dependencies:
|
||||
jsonify "~0.0.0"
|
||||
|
||||
jsonify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
|
||||
|
||||
jsonpointer@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
|
||||
integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk=
|
||||
|
||||
levn@^0.3.0, levn@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
|
||||
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
|
||||
dependencies:
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lodash@^4.17.4, lodash@^4.3.0:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
|
||||
mimic-fn@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
|
||||
integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
|
||||
|
||||
minimatch@^3.0.2, minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
mute-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
||||
integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
optionator@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
|
||||
integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
|
||||
dependencies:
|
||||
deep-is "~0.1.3"
|
||||
fast-levenshtein "~2.0.4"
|
||||
levn "~0.3.0"
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
wordwrap "~1.0.0"
|
||||
|
||||
os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
path-is-inside@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
|
||||
|
||||
pluralize@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762"
|
||||
integrity sha1-WbcIwcAZCi9pLxx2GMRGsFL9F2I=
|
||||
|
||||
prelude-ls@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
||||
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
readable-stream@^2.2.2:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
require-uncached@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
|
||||
integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=
|
||||
dependencies:
|
||||
caller-path "^0.1.0"
|
||||
resolve-from "^1.0.0"
|
||||
|
||||
resolve-from@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
|
||||
integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
|
||||
dependencies:
|
||||
onetime "^2.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
rimraf@~2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
run-async@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
|
||||
dependencies:
|
||||
is-promise "^2.1.0"
|
||||
|
||||
rx-lite-aggregates@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
|
||||
integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=
|
||||
dependencies:
|
||||
rx-lite "*"
|
||||
|
||||
rx-lite@*, rx-lite@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
|
||||
integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
signal-exit@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
||||
|
||||
slice-ansi@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
|
||||
integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==
|
||||
dependencies:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||
|
||||
string-width@^2.1.0, string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||
dependencies:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
strip-ansi@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
|
||||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
||||
supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
table@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
|
||||
integrity sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==
|
||||
dependencies:
|
||||
ajv "^6.0.1"
|
||||
ajv-keywords "^3.0.0"
|
||||
chalk "^2.1.0"
|
||||
lodash "^4.17.4"
|
||||
slice-ansi "1.0.0"
|
||||
string-width "^2.1.1"
|
||||
|
||||
text-table@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
||||
through@^2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
type-check@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
||||
integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
|
||||
dependencies:
|
||||
prelude-ls "~1.1.2"
|
||||
|
||||
typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
|
||||
integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
wordwrap@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
write@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
|
||||
integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=
|
||||
dependencies:
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prestart":
|
||||
"cp ../../build/dist/react.development.js public/ && cp ../../build/dist/react-dom.development.js public/",
|
||||
"cp ../../build/node_modules/react/umd/react.development.js public/ && cp ../../build/node_modules/react-dom/umd/react-dom.development.js public/",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
|
||||
@@ -90,17 +90,17 @@ export default function describeFibers(rootFiber, workInProgress) {
|
||||
const workInProgressID = acknowledgeFiber(workInProgress);
|
||||
|
||||
let currentIDs = new Set();
|
||||
function markAsCurent(id) {
|
||||
function markAsCurrent(id) {
|
||||
currentIDs.add(id);
|
||||
const fiber = descriptions[id];
|
||||
if (fiber.sibling) {
|
||||
markAsCurent(fiber.sibling);
|
||||
markAsCurrent(fiber.sibling);
|
||||
}
|
||||
if (fiber.child) {
|
||||
markAsCurent(fiber.child);
|
||||
markAsCurrent(fiber.child);
|
||||
}
|
||||
}
|
||||
markAsCurent(rootID);
|
||||
markAsCurrent(rootID);
|
||||
|
||||
return {
|
||||
descriptions,
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
|
||||
</p>
|
||||
</div>
|
||||
<script src="../../build/dist/react.development.js"></script>
|
||||
<script src="../../build/dist/react-dom.development.js"></script>
|
||||
<script src="../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../build/node_modules/react-dom/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<script type="text/babel">
|
||||
var dotStyle = {
|
||||
|
||||
40
fixtures/fizz-ssr-browser/index.html
Normal file
40
fixtures/fizz-ssr-browser/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="width: 100%; height: 100%; overflow: hidden">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Fizz Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Fizz Example</h1>
|
||||
<div id="container">
|
||||
<p>
|
||||
To install React, follow the instructions on
|
||||
<a href="https://github.com/facebook/react/">GitHub</a>.
|
||||
</p>
|
||||
<p>
|
||||
If you can see this, React is <strong>not</strong> working right.
|
||||
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
|
||||
</p>
|
||||
</div>
|
||||
<script src="../../build/dist/react.development.js"></script>
|
||||
<script src="../../build/dist/react-dom-unstable-fizz.browser.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<script type="text/babel">
|
||||
let stream = ReactDOMFizzServer.renderToReadableStream(<body>Success</body>);
|
||||
let response = new Response(stream, {
|
||||
headers: {'Content-Type': 'text/html'},
|
||||
});
|
||||
display(response);
|
||||
|
||||
async function display(responseToDisplay) {
|
||||
let blob = await responseToDisplay.blob();
|
||||
let url = URL.createObjectURL(blob);
|
||||
let iframe = document.createElement('iframe');
|
||||
iframe.src = url;
|
||||
let container = document.getElementById('container');
|
||||
container.innerHTML = '';
|
||||
container.appendChild(iframe);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,28 +15,18 @@ First, build React and the fixtures:
|
||||
```
|
||||
cd react
|
||||
npm run build
|
||||
|
||||
cd fixtures/packaging
|
||||
node build-all.js
|
||||
node fixtures/packaging/build-all.js
|
||||
```
|
||||
|
||||
Then run a local server at the root of the repo, e.g.
|
||||
Then run a local server, e.g.
|
||||
|
||||
```
|
||||
npm i -g pushstate-server
|
||||
cd ../..
|
||||
pushstate-server .
|
||||
npx pushstate-server .
|
||||
```
|
||||
|
||||
(Too complicated? Send a PR to simplify this :-)).
|
||||
|
||||
Then open the following URL in your browser:
|
||||
|
||||
```
|
||||
open http://localhost:9000/fixtures/packaging/index.html
|
||||
```
|
||||
and open the following URL in your browser: [http://localhost:9000/fixtures/packaging/index.html](http://localhost:9000/fixtures/packaging/index.html)
|
||||
|
||||
You should see two things:
|
||||
|
||||
* "Hello World" is rendered in each iframe.
|
||||
* A number of iframes (corresponding to various builds), with "Hello World" rendered in each iframe.
|
||||
* No errors in the console.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="../../../build/dist/react.development.js"></script>
|
||||
<script src="../../../build/dist/react-dom.development.js"></script>
|
||||
<script src="../../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../../build/node_modules/react-dom/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<div id="container"></div>
|
||||
<script type="text/babel">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="../../../build/dist/react.development.js"></script>
|
||||
<script src="../../../build/dist/react-dom.development.js"></script>
|
||||
<script src="../../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../../build/node_modules/react-dom/umd/react-dom.development.js"></script>
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
ReactDOM.render(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="../../../build/dist/react.production.min.js"></script>
|
||||
<script src="../../../build/dist/react-dom.production.min.js"></script>
|
||||
<script src="../../../build/node_modules/react/umd/react.production.min.js"></script>
|
||||
<script src="../../../build/node_modules/react-dom/umd/react-dom.production.min.js"></script>
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
ReactDOM.render(
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<script>
|
||||
requirejs.config({
|
||||
paths: {
|
||||
react: '../../../build/dist/react.development',
|
||||
'react-dom': '../../../build/dist/react-dom.development'
|
||||
react: '../../../build/node_modules/react/umd/react.development',
|
||||
'react-dom': '../../../build/node_modules/react-dom/umd/react-dom.development'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<script>
|
||||
requirejs.config({
|
||||
paths: {
|
||||
react: '../../../build/dist/react.production.min',
|
||||
'react-dom': '../../../build/dist/react-dom.production.min'
|
||||
react: '../../../build/node_modules/react/umd/react.production.min',
|
||||
'react-dom': '../../../build/node_modules/react-dom/umd/react-dom.production.min'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ module.exports = {
|
||||
out: 'output.js',
|
||||
optimize: 'none',
|
||||
paths: {
|
||||
react: '../../../../build/dist/react.development',
|
||||
'react-dom': '../../../../build/dist/react-dom.development',
|
||||
react: '../../../../build/node_modules/react/umd/react.development',
|
||||
'react-dom':
|
||||
'../../../../build/node_modules/react-dom/umd/react-dom.development',
|
||||
schedule: '../../../../build/dist/schedule.development',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,9 @@ module.exports = {
|
||||
out: 'output.js',
|
||||
optimize: 'none',
|
||||
paths: {
|
||||
react: '../../../../build/dist/react.production.min',
|
||||
'react-dom': '../../../../build/dist/react-dom.production.min',
|
||||
react: '../../../../build/node_modules/react/umd/react.production.min',
|
||||
'react-dom':
|
||||
'../../../../build/node_modules/react-dom/umd/react-dom.production.min',
|
||||
schedule: '../../../../build/dist/schedule.development',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
System.config({
|
||||
paths: {
|
||||
react: '../../../../build/dist/react.development.js',
|
||||
'react-dom': '../../../../build/dist/react-dom.development.js',
|
||||
react: '../../../../build/node_modules/react/umd/react.development.js',
|
||||
'react-dom':
|
||||
'../../../../build/node_modules/react-dom/umd/react-dom.development.js',
|
||||
schedule: '../../../../build/dist/schedule.development',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
System.config({
|
||||
paths: {
|
||||
react: '../../../../build/dist/react.production.min.js',
|
||||
'react-dom': '../../../../build/dist/react-dom.production.min.js',
|
||||
react: '../../../../build/node_modules/react/umd/react.production.min.js',
|
||||
'react-dom':
|
||||
'../../../../build/node_modules/react-dom/umd/react-dom.production.min.js',
|
||||
schedule: '../../../../build/dist/schedule.development',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<script>
|
||||
System.config({
|
||||
paths: {
|
||||
react: '../../../build/dist/react.development.js',
|
||||
'react-dom': '../../../build/dist/react-dom.development.js'
|
||||
react: '../../../build/node_modules/react/umd/react.development.js',
|
||||
'react-dom': '../../../build/node_modules/react-dom/umd/react-dom.development.js'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<script>
|
||||
System.config({
|
||||
paths: {
|
||||
react: '../../../build/dist/react.production.min.js',
|
||||
'react-dom': '../../../build/dist/react-dom.production.min.js'
|
||||
react: '../../../build/node_modules/react/umd/react.production.min.js',
|
||||
'react-dom': '../../../build/node_modules/react-dom/umd/react-dom.production.min.js'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div id="test-2"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>Schedules callbacks in correct order when they use scheduleWork to schedule themselves</p>
|
||||
<p>Schedules callbacks in correct order when they use scheduleCallback to schedule themselves</p>
|
||||
<button onClick="runTestThree()">Run Test 3</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-3-expected">
|
||||
@@ -90,15 +90,33 @@
|
||||
</div>
|
||||
<div> If the counter advanced while you were away from this tab, it's correct.</div>
|
||||
</li>
|
||||
<li>
|
||||
<p>Can pause execution, dump scheduled callbacks, and continue where it left off</p>
|
||||
<button onClick="runTestEight()">Run Test 8</button>
|
||||
<div><b>Click the button above, press "continue" to finish the test after it pauses:</b></div>
|
||||
<button onClick="continueTestEight()">continue</button>
|
||||
<div><b>Expected:</b></div>
|
||||
<div id="test-8-expected">
|
||||
</div>
|
||||
<div> -------------------------------------------------</div>
|
||||
<div> If the test didn't progress until you hit "continue" and </div>
|
||||
<div> you see the same above and below afterwards it's correct.
|
||||
<div> -------------------------------------------------</div>
|
||||
<div><b>Actual:</b></div>
|
||||
<div id="test-8"></div>
|
||||
</li>
|
||||
</ol>
|
||||
<script src="../../build/dist/react.development.js"></script>
|
||||
<script src="../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<script type="text/babel">
|
||||
const {
|
||||
unstable_scheduleCallback: scheduleCallback,
|
||||
unstable_cancelCallback: cancelCallback,
|
||||
unstable_now: now
|
||||
unstable_now: now,
|
||||
unstable_getFirstCallbackNode: getFirstCallbackNode,
|
||||
unstable_pauseExecution: pauseExecution,
|
||||
unstable_continueExecution: continueExecution,
|
||||
} = Scheduler;
|
||||
function displayTestResult(testNumber) {
|
||||
const expectationNode = document.getElementById('test-' + testNumber + '-expected');
|
||||
@@ -215,6 +233,16 @@ const expectedResults = [
|
||||
[
|
||||
// ... TODO
|
||||
],
|
||||
[],
|
||||
[],
|
||||
// Test 8
|
||||
[
|
||||
'Queue size: 0.',
|
||||
'Pausing... press continue to resume.',
|
||||
'Queue size: 2.',
|
||||
'Finishing...',
|
||||
'Done!',
|
||||
],
|
||||
];
|
||||
function runTestOne() {
|
||||
// Test 1
|
||||
@@ -225,7 +253,7 @@ function runTestOne() {
|
||||
const cb1 = (x) => {
|
||||
updateTestResult(1, 'cb1 called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cb1);
|
||||
scheduleCallback(cb1);
|
||||
updateTestResult(1, 'scheduled Cb1');
|
||||
logWhenFramesStart(1, () => {
|
||||
displayTestResult(1);
|
||||
@@ -243,9 +271,9 @@ function runTestTwo() {
|
||||
const cbB = (x) => {
|
||||
updateTestResult(2, 'cbB called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
updateTestResult(2, 'scheduled CbA');
|
||||
scheduleWork(cbB);
|
||||
scheduleCallback(cbB);
|
||||
updateTestResult(2, 'scheduled CbB');
|
||||
logWhenFramesStart(2, () => {
|
||||
displayTestResult(2);
|
||||
@@ -255,12 +283,12 @@ function runTestTwo() {
|
||||
|
||||
function runTestThree() {
|
||||
// Test 3
|
||||
// Schedules callbacks in correct order when they use scheduleWork to schedule themselves
|
||||
// Schedules callbacks in correct order when they use scheduleCallback to schedule themselves
|
||||
clearTestResult(3);
|
||||
let callbackAIterations = 0;
|
||||
const cbA = (x) => {
|
||||
if (callbackAIterations < 1) {
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
updateTestResult(3, 'scheduled CbA again');
|
||||
}
|
||||
updateTestResult(3, 'cbA' + callbackAIterations + ' called with argument of ' + JSON.stringify(x));
|
||||
@@ -269,9 +297,9 @@ function runTestThree() {
|
||||
const cbB = (x) => {
|
||||
updateTestResult(3, 'cbB called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
updateTestResult(3, 'scheduled CbA');
|
||||
scheduleWork(cbB);
|
||||
scheduleCallback(cbB);
|
||||
updateTestResult(3, 'scheduled CbB');
|
||||
logWhenFramesStart(3, () => {
|
||||
displayTestResult(3);
|
||||
@@ -305,13 +333,13 @@ function runTestFour() {
|
||||
const cbD = (x) => {
|
||||
updateTestResult(4, 'cbD called with argument of ' + JSON.stringify(x));
|
||||
}
|
||||
scheduleWork(cbA); // won't time out
|
||||
scheduleCallback(cbA); // won't time out
|
||||
updateTestResult(4, 'scheduled cbA');
|
||||
scheduleWork(cbB, {timeout: 100}); // times out later
|
||||
scheduleCallback(cbB, {timeout: 100}); // times out later
|
||||
updateTestResult(4, 'scheduled cbB');
|
||||
scheduleWork(cbC, {timeout: 1}); // will time out fast
|
||||
scheduleCallback(cbC, {timeout: 1}); // will time out fast
|
||||
updateTestResult(4, 'scheduled cbC');
|
||||
scheduleWork(cbD); // won't time out
|
||||
scheduleCallback(cbD); // won't time out
|
||||
updateTestResult(4, 'scheduled cbD');
|
||||
|
||||
// should have run in order of C, A, B, D
|
||||
@@ -390,15 +418,15 @@ function runTestFive() {
|
||||
});
|
||||
});
|
||||
});
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
console.log('scheduled cbA');
|
||||
scheduleWork(cbB); // will throw error
|
||||
scheduleCallback(cbB); // will throw error
|
||||
console.log('scheduled cbB');
|
||||
scheduleWork(cbC);
|
||||
scheduleCallback(cbC);
|
||||
console.log('scheduled cbC');
|
||||
scheduleWork(cbD); // will throw error
|
||||
scheduleCallback(cbD); // will throw error
|
||||
console.log('scheduled cbD');
|
||||
scheduleWork(cbE);
|
||||
scheduleCallback(cbE);
|
||||
console.log('scheduled cbE');
|
||||
};
|
||||
}
|
||||
@@ -468,15 +496,15 @@ function runTestSix() {
|
||||
});
|
||||
});
|
||||
});
|
||||
scheduleWork(cbA);
|
||||
scheduleCallback(cbA);
|
||||
console.log('scheduled cbA');
|
||||
scheduleWork(cbB); // will throw error
|
||||
scheduleCallback(cbB); // will throw error
|
||||
console.log('scheduled cbB');
|
||||
scheduleWork(cbC, {timeout: 1});
|
||||
scheduleCallback(cbC, {timeout: 1});
|
||||
console.log('scheduled cbC');
|
||||
scheduleWork(cbD, {timeout: 1}); // will throw error
|
||||
scheduleCallback(cbD, {timeout: 1}); // will throw error
|
||||
console.log('scheduled cbD');
|
||||
scheduleWork(cbE, {timeout: 1});
|
||||
scheduleCallback(cbE, {timeout: 1});
|
||||
console.log('scheduled cbE');
|
||||
};
|
||||
}
|
||||
@@ -492,10 +520,55 @@ function runTestSeven() {
|
||||
counter++;
|
||||
counterNode.innerHTML = counter;
|
||||
waitForTimeToPass(100);
|
||||
scheduleWork(incrementCounterAndScheduleNextCallback);
|
||||
scheduleCallback(incrementCounterAndScheduleNextCallback);
|
||||
}
|
||||
scheduleWork(incrementCounterAndScheduleNextCallback);
|
||||
scheduleCallback(incrementCounterAndScheduleNextCallback);
|
||||
}
|
||||
|
||||
function runTestEight() {
|
||||
// Test 8
|
||||
// Pauses execution, dumps the queue, and continues execution
|
||||
clearTestResult(8);
|
||||
|
||||
function countNodesInStack(firstCallbackNode) {
|
||||
var node = firstCallbackNode;
|
||||
var count = 0;
|
||||
if (node !== null) {
|
||||
do {
|
||||
count = count + 1;
|
||||
node = node.next;
|
||||
} while (node !== firstCallbackNode);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
scheduleCallback(() => {
|
||||
|
||||
// size should be 0
|
||||
updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`);
|
||||
updateTestResult(8, 'Pausing... press continue to resume.');
|
||||
pauseExecution();
|
||||
|
||||
scheduleCallback(function () {
|
||||
updateTestResult(8, 'Finishing...');
|
||||
displayTestResult(8);
|
||||
})
|
||||
scheduleCallback(function () {
|
||||
updateTestResult(8, 'Done!');
|
||||
displayTestResult(8);
|
||||
checkTestResult(8);
|
||||
})
|
||||
|
||||
// new size should be 2 now
|
||||
updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`);
|
||||
displayTestResult(8);
|
||||
});
|
||||
}
|
||||
|
||||
function continueTestEight() {
|
||||
continueExecution();
|
||||
}
|
||||
|
||||
</script type="text/babel">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,17 +1,32 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, {useContext, useState, Suspense} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
import Page from './Page';
|
||||
import Page2 from './Page2';
|
||||
import Theme from './Theme';
|
||||
|
||||
export default class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Chrome title="Hello World" assets={this.props.assets}>
|
||||
<div>
|
||||
<h1>Hello World</h1>
|
||||
<Page />
|
||||
</div>
|
||||
</Chrome>
|
||||
);
|
||||
}
|
||||
function LoadingIndicator() {
|
||||
let theme = useContext(Theme);
|
||||
return <div className={theme + '-loading'}>Loading...</div>;
|
||||
}
|
||||
|
||||
export default function App({assets}) {
|
||||
let [CurrentPage, switchPage] = useState(() => Page);
|
||||
return (
|
||||
<Chrome title="Hello World" assets={assets}>
|
||||
<div>
|
||||
<h1>Hello World</h1>
|
||||
<a className="link" onClick={() => switchPage(() => Page)}>
|
||||
Page 1
|
||||
</a>
|
||||
{' | '}
|
||||
<a className="link" onClick={() => switchPage(() => Page2)}>
|
||||
Page 2
|
||||
</a>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<CurrentPage />
|
||||
</Suspense>
|
||||
</div>
|
||||
</Chrome>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,27 @@ body {
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
body.light {
|
||||
background-color: #FFFFFF;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background-color: #000000;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
.light-loading {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.dark-loading {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #333333;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Theme, {ThemeToggleButton} from './Theme';
|
||||
|
||||
import './Chrome.css';
|
||||
|
||||
export default class Chrome extends Component {
|
||||
state = {theme: 'light'};
|
||||
render() {
|
||||
const assets = this.props.assets;
|
||||
return (
|
||||
@@ -14,13 +17,18 @@ export default class Chrome extends Component {
|
||||
<link rel="stylesheet" href={assets['main.css']} />
|
||||
<title>{this.props.title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<body className={this.state.theme}>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<b>Enable JavaScript to run this app.</b>`,
|
||||
}}
|
||||
/>
|
||||
{this.props.children}
|
||||
<Theme.Provider value={this.state.theme}>
|
||||
{this.props.children}
|
||||
<div>
|
||||
<ThemeToggleButton onChange={theme => this.setState({theme})} />
|
||||
</div>
|
||||
</Theme.Provider>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
.bold {
|
||||
.link {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
.light-box {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #CCCCCC;
|
||||
color: #333333;
|
||||
}
|
||||
.dark-box {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #333333;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import Theme from './Theme';
|
||||
import Suspend from './Suspend';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
const autofocusedInputs = [
|
||||
@@ -14,17 +17,22 @@ export default class Page extends Component {
|
||||
};
|
||||
render() {
|
||||
const link = (
|
||||
<a className="bold" onClick={this.handleClick}>
|
||||
<a className="link" onClick={this.handleClick}>
|
||||
Click Here
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<p suppressHydrationWarning={true}>A random number: {Math.random()}</p>
|
||||
<p>Autofocus on page load: {autofocusedInputs}</p>
|
||||
<p>{!this.state.active ? link : 'Thanks!'}</p>
|
||||
{this.state.active && <p>Autofocus on update: {autofocusedInputs}</p>}
|
||||
<div className={this.context + '-box'}>
|
||||
<Suspend>
|
||||
<p suppressHydrationWarning={true}>
|
||||
A random number: {Math.random()}
|
||||
</p>
|
||||
<p>Autofocus on page load: {autofocusedInputs}</p>
|
||||
<p>{!this.state.active ? link : 'Thanks!'}</p>
|
||||
{this.state.active && <p>Autofocus on update: {autofocusedInputs}</p>}
|
||||
</Suspend>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Page.contextType = Theme;
|
||||
|
||||
15
fixtures/ssr/src/components/Page2.js
Normal file
15
fixtures/ssr/src/components/Page2.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, {useContext} from 'react';
|
||||
|
||||
import Theme from './Theme';
|
||||
import Suspend from './Suspend';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
export default function Page2() {
|
||||
let theme = useContext(Theme);
|
||||
return (
|
||||
<div className={theme + '-box'}>
|
||||
<Suspend>Content of a different page</Suspend>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
fixtures/ssr/src/components/Suspend.js
Normal file
21
fixtures/ssr/src/components/Suspend.js
Normal file
@@ -0,0 +1,21 @@
|
||||
let promise = null;
|
||||
let isResolved = false;
|
||||
|
||||
export default function Suspend({children}) {
|
||||
// This will suspend the content from rendering but only on the client.
|
||||
// This is used to demo a slow loading app.
|
||||
if (typeof window === 'object') {
|
||||
if (!isResolved) {
|
||||
if (promise === null) {
|
||||
promise = new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
isResolved = true;
|
||||
resolve();
|
||||
}, 6000);
|
||||
});
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
25
fixtures/ssr/src/components/Theme.js
Normal file
25
fixtures/ssr/src/components/Theme.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, {createContext, useContext, useState} from 'react';
|
||||
|
||||
const Theme = createContext('light');
|
||||
|
||||
export default Theme;
|
||||
|
||||
export function ThemeToggleButton({onChange}) {
|
||||
let theme = useContext(Theme);
|
||||
let [targetTheme, setTargetTheme] = useState(theme);
|
||||
function toggleTheme() {
|
||||
let newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
// High pri, responsive update.
|
||||
setTargetTheme(newTheme);
|
||||
// Perform the actual theme change in a separate update.
|
||||
setTimeout(() => onChange(newTheme), 0);
|
||||
}
|
||||
if (targetTheme !== theme) {
|
||||
return 'Switching to ' + targetTheme + '...';
|
||||
}
|
||||
return (
|
||||
<a className="link" onClick={toggleTheme}>
|
||||
Switch to {theme === 'light' ? 'Dark' : 'Light'} theme
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {hydrate} from 'react-dom';
|
||||
import {unstable_createRoot} from 'react-dom';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
hydrate(<App assets={window.assetManifest} />, document);
|
||||
let root = unstable_createRoot(document, {hydrate: true});
|
||||
root.render(<App assets={window.assetManifest} />);
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</p>
|
||||
<h3>
|
||||
Tests
|
||||
<button onClick="runAllTests()">Run all tests</button>
|
||||
<button id="run-test-button" onClick="runAllTests()">Run all tests</button>
|
||||
</h3>
|
||||
<ol>
|
||||
<li id="checkSchedulerAPI" data-value="...">
|
||||
@@ -62,6 +62,17 @@
|
||||
<strong>Test end-to-end integration</strong>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<script>
|
||||
if (window.location.search.includes('puppeteer=true')) {
|
||||
// Colocated calls to performance.now() often yield different values in Puppeteer.
|
||||
// This causes the Scheduler API test to fail.
|
||||
// For the purposes of our automated release scripts,
|
||||
// Coerce tests to use Date.now() instead to reduce the chances of a false positive.
|
||||
window.performance = {now: Date.now};
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Load the tracing API before react to test that it's lazily evaluated -->
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
|
||||
<script src="../../build/node_modules/scheduler/umd/scheduler-tracing.development.js"></script>
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import {createCache} from 'react-cache';
|
||||
|
||||
export let cache;
|
||||
function initCache() {
|
||||
cache = createCache(initCache);
|
||||
}
|
||||
initCache();
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {lazy, unstable_Suspense as Suspense, PureComponent} from 'react';
|
||||
import React, {lazy, Suspense, PureComponent} from 'react';
|
||||
import {unstable_scheduleCallback} from 'scheduler';
|
||||
import {
|
||||
unstable_trace as trace,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, {Fragment} from 'react';
|
||||
import {createResource} from 'react-cache';
|
||||
import {cache} from '../cache';
|
||||
import {unstable_createResource} from 'react-cache';
|
||||
import Spinner from './Spinner';
|
||||
import {fetchCoreContributorListJSON} from '../api';
|
||||
|
||||
const ContributorListResource = createResource(fetchCoreContributorListJSON);
|
||||
const ContributorListResource = unstable_createResource(
|
||||
fetchCoreContributorListJSON
|
||||
);
|
||||
|
||||
const ContributorListPage = ({loadingId, onUserClick}) => (
|
||||
<Fragment>
|
||||
@@ -17,7 +18,7 @@ const ContributorListPage = ({loadingId, onUserClick}) => (
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
}}>
|
||||
{ContributorListResource.read(cache).map(user => (
|
||||
{ContributorListResource.read().map(user => (
|
||||
<ContributorListItem
|
||||
key={user.id}
|
||||
onClick={() => onUserClick(user.id)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {unstable_Suspense as Suspense} from 'react';
|
||||
import {createResource} from 'react-cache';
|
||||
import React, {Suspense} from 'react';
|
||||
import {unstable_createResource} from 'react-cache';
|
||||
import Spinner from './Spinner';
|
||||
import {cache} from '../cache';
|
||||
import {fetchUserProfileJSON, fetchUserRepositoriesListJSON} from '../api';
|
||||
|
||||
export default function UserPage({id}) {
|
||||
@@ -21,10 +20,10 @@ export default function UserPage({id}) {
|
||||
);
|
||||
}
|
||||
|
||||
const UserDetailsResource = createResource(fetchUserProfileJSON);
|
||||
const UserDetailsResource = unstable_createResource(fetchUserProfileJSON);
|
||||
|
||||
function UserDetails({id}) {
|
||||
const user = UserDetailsResource.read(cache, id);
|
||||
const user = UserDetailsResource.read(id);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -103,7 +102,7 @@ const Email = ({email}) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const ImageResource = createResource(
|
||||
const ImageResource = unstable_createResource(
|
||||
src =>
|
||||
new Promise(resolve => {
|
||||
const img = new Image();
|
||||
@@ -113,7 +112,7 @@ const ImageResource = createResource(
|
||||
);
|
||||
|
||||
function Img({src, alt, ...rest}) {
|
||||
return <img src={ImageResource.read(cache, src)} alt={alt} {...rest} />;
|
||||
return <img src={ImageResource.read(src)} alt={alt} {...rest} />;
|
||||
}
|
||||
|
||||
function UserPicture({source}) {
|
||||
@@ -132,10 +131,12 @@ function UserPicture({source}) {
|
||||
);
|
||||
}
|
||||
|
||||
const UserRepositoriesResource = createResource(fetchUserRepositoriesListJSON);
|
||||
const UserRepositoriesResource = unstable_createResource(
|
||||
fetchUserRepositoriesListJSON
|
||||
);
|
||||
|
||||
function Repositories({id}) {
|
||||
const repos = UserRepositoriesResource.read(cache, id);
|
||||
const repos = UserRepositoriesResource.read(id);
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {Fragment, PureComponent} from 'react';
|
||||
import {unstable_createRoot, render} from 'react-dom';
|
||||
import {unstable_trace as trace} from 'scheduler/tracing';
|
||||
import {cache} from './cache';
|
||||
import {
|
||||
setFakeRequestTime,
|
||||
setPaused,
|
||||
@@ -65,8 +64,9 @@ class Debugger extends PureComponent {
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
trace('Clear cache', () => {
|
||||
cache.invalidate();
|
||||
trace('Clear cache', performance.now(), () => {
|
||||
// TODO: this is not implemented.
|
||||
// cache.invalidate();
|
||||
this.setState(state => ({
|
||||
requests: {},
|
||||
}));
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -124,7 +124,7 @@ class App extends PureComponent {
|
||||
<div className="rendering">
|
||||
{this.renderOption('sync', 'Synchronous')}
|
||||
{this.renderOption('debounced', 'Debounced')}
|
||||
{this.renderOption('async', 'Asynchronous')}
|
||||
{this.renderOption('async', 'Concurrent')}
|
||||
</div>
|
||||
<input
|
||||
className={'input ' + this.state.strategy}
|
||||
|
||||
14
fixtures/unstable-fire/index.html
Normal file
14
fixtures/unstable-fire/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="../../build/node_modules/react/umd/react.development.js"></script>
|
||||
<script src="../../build/node_modules/react-dom/umd/react-dom-unstable-fire.development.js"></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<div id="container"></div>
|
||||
<script type="text/babel">
|
||||
ReactFire.render(
|
||||
<h1>Hello World!</h1>,
|
||||
document.getElementById('container')
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "16.6.0-alpha.8af6728",
|
||||
"version": "16.6.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -44,6 +44,7 @@
|
||||
"create-react-class": "^15.6.3",
|
||||
"cross-env": "^5.1.1",
|
||||
"danger": "^3.0.4",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"eslint": "^4.1.0",
|
||||
"eslint-config-fbjs": "^1.1.1",
|
||||
"eslint-plugin-babel": "^3.3.0",
|
||||
@@ -57,7 +58,7 @@
|
||||
"flow-bin": "^0.72.0",
|
||||
"glob": "^6.0.4",
|
||||
"glob-stream": "^6.1.0",
|
||||
"google-closure-compiler": "20180506.0.0",
|
||||
"google-closure-compiler": "20190106.0.0",
|
||||
"gzip-size": "^3.0.0",
|
||||
"jasmine-check": "^1.0.0-rc.0",
|
||||
"jest": "^23.1.0",
|
||||
@@ -83,16 +84,17 @@
|
||||
"targz": "^1.0.1",
|
||||
"through2": "^2.0.0",
|
||||
"tmp": "~0.0.28",
|
||||
"typescript": "~1.8.10"
|
||||
"typescript": "~1.8.10",
|
||||
"@mattiasbuelens/web-streams-polyfill": "0.1.0"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": "8.x || 9.x || 10.x"
|
||||
"node": "8.x || 9.x || 10.x || 11.x"
|
||||
},
|
||||
"jest": {
|
||||
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run version-check && node ./scripts/rollup/build.js",
|
||||
"build": "node ./scripts/rollup/build.js",
|
||||
"linc": "node ./scripts/tasks/linc.js",
|
||||
"lint": "node ./scripts/tasks/eslint.js",
|
||||
"lint-build": "node ./scripts/rollup/validate/index.js",
|
||||
|
||||
@@ -36,7 +36,7 @@ This abstraction can handle a variety of subscription types, including:
|
||||
yarn add create-subscription
|
||||
|
||||
# NPM
|
||||
npm install create-subscription --save
|
||||
npm install create-subscription
|
||||
```
|
||||
|
||||
# Usage
|
||||
@@ -81,7 +81,7 @@ import React from "react";
|
||||
import { createSubscription } from "create-subscription";
|
||||
|
||||
// Start with a simple component.
|
||||
// In this case, it's a functional component, but it could have been a class.
|
||||
// In this case, it's a function component, but it could have been a class.
|
||||
function FollowerComponent({ followersCount }) {
|
||||
return <div>You have {followersCount} followers!</div>;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ function LoadingComponent({ loadingStatus }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the functional component with a subscriber HOC.
|
||||
// Wrap the function component with a subscriber HOC.
|
||||
// This HOC will manage subscriptions and pass values to the decorated component.
|
||||
// It will add and remove subscriptions in an async-safe way when props change.
|
||||
const PromiseSubscription = createSubscription({
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
{
|
||||
"name": "create-subscription",
|
||||
"description": "utility for subscribing to external data sources inside React components",
|
||||
"version": "16.6.0-alpha.8af6728",
|
||||
"repository": "facebook/react",
|
||||
"version": "16.8.6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/create-subscription"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || 16.6.0-alpha.8af6728"
|
||||
"react": "^16.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rxjs": "^5.5.6"
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('createSubscription', () => {
|
||||
|
||||
// Test a promise that resolves before render
|
||||
// Note that this will require an extra render anyway,
|
||||
// Because there is no way to syncrhonously get a Promise's value
|
||||
// Because there is no way to synchronously get a Promise's value
|
||||
rejectB(false);
|
||||
ReactNoop.render(<Subscription source={promiseB}>{render}</Subscription>);
|
||||
expect(ReactNoop.flush()).toEqual(['loading']);
|
||||
@@ -431,7 +431,7 @@ describe('createSubscription', () => {
|
||||
'Parent.componentDidUpdate',
|
||||
]);
|
||||
|
||||
// Updates from the new subsribable should be ignored.
|
||||
// Updates from the new subscribable should be ignored.
|
||||
observableB.next('b-1');
|
||||
expect(ReactNoop.flush()).toEqual([]);
|
||||
expect(log).toEqual([
|
||||
|
||||
42
packages/eslint-plugin-react-hooks/README.md
Normal file
42
packages/eslint-plugin-react-hooks/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# `eslint-plugin-react-hooks`
|
||||
|
||||
This ESLint plugin enforces the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html).
|
||||
|
||||
It is a part of the [Hooks API](https://reactjs.org/docs/hooks-intro.html) for React.
|
||||
|
||||
## Installation
|
||||
|
||||
**Note: If you're using Create React App, please wait for a corresponding release of `react-scripts` that includes this rule instead of adding it directly.**
|
||||
|
||||
Assuming you already have ESLint installed, run:
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm install eslint-plugin-react-hooks --save-dev
|
||||
|
||||
# yarn
|
||||
yarn add eslint-plugin-react-hooks --dev
|
||||
```
|
||||
|
||||
Then add it to your ESLint configuration:
|
||||
|
||||
```js
|
||||
{
|
||||
"plugins": [
|
||||
// ...
|
||||
"react-hooks"
|
||||
],
|
||||
"rules": {
|
||||
// ...
|
||||
"react-hooks/rules-of-hooks": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Valid and Invalid Examples
|
||||
|
||||
Please refer to the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation and the [Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#what-exactly-do-the-lint-rules-enforce) to learn more about this rule.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,710 @@
|
||||
/**
|
||||
* 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 ESLintTester = require('eslint').RuleTester;
|
||||
const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');
|
||||
const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['rules-of-hooks'];
|
||||
|
||||
ESLintTester.setDefaultConfig({
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
const eslintTester = new ESLintTester();
|
||||
eslintTester.run('react-hooks', ReactHooksESLintRule, {
|
||||
valid: [
|
||||
`
|
||||
// Valid because components can use hooks.
|
||||
function ComponentWithHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
useHook();
|
||||
};
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can use hooks.
|
||||
function useHookWithHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because components can call functions.
|
||||
function ComponentWithNormalFunction() {
|
||||
doSomething();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function normalFunctionWithNormalFunction() {
|
||||
doSomething();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function normalFunctionWithConditionalFunction() {
|
||||
if (cond) {
|
||||
doSomething();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because functions can call functions.
|
||||
function functionThatStartsWithUseButIsntAHook() {
|
||||
if (cond) {
|
||||
userFetch();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid although unconditional return doesn't make sense and would fail other rules.
|
||||
// We could make it invalid but it doesn't matter.
|
||||
function useUnreachable() {
|
||||
return;
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() { useState(); }
|
||||
const whatever = function useHook() { useState(); };
|
||||
const useHook1 = () => { useState(); };
|
||||
let useHook2 = () => useState();
|
||||
useHook2 = () => { useState(); };
|
||||
({useHook: () => { useState(); }});
|
||||
({useHook() { useState(); }});
|
||||
const {useHook = () => { useState(); }} = {};
|
||||
({useHook = () => { useState(); }} = {});
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
useHook1();
|
||||
useHook2();
|
||||
};
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
useState() && a;
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
return useHook1() + useHook2();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because hooks can call hooks.
|
||||
function useHook() {
|
||||
return useHook1(useHook2());
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
m() {
|
||||
this.useHook();
|
||||
super.useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid.
|
||||
// We *could* make this invalid if we want, but it creates false positives
|
||||
// (see the FooStore case).
|
||||
class C {
|
||||
m() {
|
||||
This.useHook();
|
||||
Super.useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid although we *could* consider these invalid.
|
||||
// But it doesn't bring much benefit since it's an immediate runtime error anyway.
|
||||
// So might as well allow it.
|
||||
Hook.use();
|
||||
Hook._use();
|
||||
Hook.useState();
|
||||
Hook._useState();
|
||||
Hook.use42();
|
||||
Hook.useHook();
|
||||
Hook.use_hook();
|
||||
`,
|
||||
`
|
||||
// Valid -- this is a regression test.
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
`,
|
||||
`
|
||||
// Valid because that's a false positive we've seen quite a bit.
|
||||
// This is a regression test.
|
||||
class Foo extends Component {
|
||||
render() {
|
||||
if (cond) {
|
||||
FooStore.useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid because we found this to be a common pattern
|
||||
// for feature flag checks in existing components.
|
||||
// We *could* make it invalid but that produces quite a few false positives.
|
||||
// Why does it make sense to ignore it? Firstly, because using
|
||||
// hooks in a class would cause a runtime error anyway.
|
||||
// But why don't we care about the same kind of false positive in a functional
|
||||
// component? Because even if it was a false positive, it would be confusing
|
||||
// anyway. So it might make sense to rename a feature flag check in that case.
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
render() {
|
||||
if (foo) {
|
||||
useFeatureFlag();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid because we don't check for hooks in classes.
|
||||
// See ClassComponentWithFeatureFlag for rationale.
|
||||
// We *could* make it invalid if we don't regress that false positive.
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
render() {
|
||||
React.useState();
|
||||
}
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Currently valid.
|
||||
// These are variations capturing the current heuristic--
|
||||
// we only allow hooks in PascalCase, useFoo functions,
|
||||
// or classes (due to common false positives and because they error anyway).
|
||||
// We *could* make some of these invalid.
|
||||
// They probably don't matter much.
|
||||
(class {useHook = () => { useState(); }});
|
||||
(class {useHook() { useState(); }});
|
||||
(class {h = () => { useState(); }});
|
||||
(class {i() { useState(); }});
|
||||
`,
|
||||
`
|
||||
// Currently valid although we *could* consider these invalid.
|
||||
// It doesn't make a lot of difference because it would crash early.
|
||||
use();
|
||||
_use();
|
||||
useState();
|
||||
_useState();
|
||||
use42();
|
||||
useHook();
|
||||
use_hook();
|
||||
React.useState();
|
||||
`,
|
||||
`
|
||||
// Regression test for the popular "history" library
|
||||
const {createHistory, useBasename} = require('history-2.1.2');
|
||||
const browserHistory = useBasename(createHistory)({
|
||||
basename: '/',
|
||||
});
|
||||
`,
|
||||
`
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
// a component or a hook.
|
||||
function makeListener(instance) {
|
||||
each(pixelsWithInferredEvents, pixel => {
|
||||
if (useExtendedSelector(pixel.id) && extendedButton) {
|
||||
foo();
|
||||
}
|
||||
});
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Regression test for incorrectly flagged valid code.
|
||||
function RegressionTest() {
|
||||
const foo = cond ? a : b;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because exceptions abort rendering
|
||||
function RegressionTest() {
|
||||
if (page == null) {
|
||||
throw new Error('oh no!');
|
||||
}
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Valid because the loop doesn't change the order of hooks calls.
|
||||
function RegressionTest() {
|
||||
const res = [];
|
||||
const additionalCond = true;
|
||||
for (let i = 0; i !== 10 && additionalCond; ++i ) {
|
||||
res.push(i);
|
||||
}
|
||||
React.useLayoutEffect(() => {});
|
||||
}
|
||||
`,
|
||||
`
|
||||
// Is valid but hard to compute by brute-forcing
|
||||
function MyComponent() {
|
||||
// 40 conditions
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
if (c) {} else {}
|
||||
|
||||
// 10 hooks
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function createComponent() {
|
||||
return function ComponentWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function createHook() {
|
||||
return function useHookWithConditionalHook() {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithTernaryHook() {
|
||||
cond ? useTernaryHook() : null;
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useTernaryHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function ComponentWithHookInsideCallback() {
|
||||
useEffect(() => {
|
||||
useHookInsideCallback();
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [genericError('useHookInsideCallback')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function createComponent() {
|
||||
return function ComponentWithHookInsideCallback() {
|
||||
useEffect(() => {
|
||||
useHookInsideCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [genericError('useHookInsideCallback')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function ComponentWithHookInsideCallback() {
|
||||
function handleClick() {
|
||||
useState();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'handleClick')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's a common misunderstanding.
|
||||
// We *could* make it valid but the runtime error could be confusing.
|
||||
function createComponent() {
|
||||
return function ComponentWithHookInsideCallback() {
|
||||
function handleClick() {
|
||||
useState();
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'handleClick')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function ComponentWithHookInsideLoop() {
|
||||
while (cond) {
|
||||
useHookInsideLoop();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [loopError('useHookInsideLoop')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function renderItem() {
|
||||
useState();
|
||||
}
|
||||
|
||||
function List(props) {
|
||||
return props.items.map(renderItem);
|
||||
}
|
||||
`,
|
||||
errors: [functionError('useState', 'renderItem')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Currently invalid because it violates the convention and removes the "taint"
|
||||
// from a hook. We *could* make it valid to avoid some false positives but let's
|
||||
// ensure that we don't break the "renderItem" and "normalFunctionWithConditionalHook"
|
||||
// cases which must remain invalid.
|
||||
function normalFunctionWithHook() {
|
||||
useHookInsideNormalFunction();
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
functionError('useHookInsideNormalFunction', 'normalFunctionWithHook'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function normalFunctionWithConditionalHook() {
|
||||
if (cond) {
|
||||
useHookInsideNormalFunction();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
functionError(
|
||||
'useHookInsideNormalFunction',
|
||||
'normalFunctionWithConditionalHook'
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookInLoops() {
|
||||
while (a) {
|
||||
useHook1();
|
||||
if (b) return;
|
||||
useHook2();
|
||||
}
|
||||
while (c) {
|
||||
useHook3();
|
||||
if (d) return;
|
||||
useHook4();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
loopError('useHook1'),
|
||||
loopError('useHook2'),
|
||||
loopError('useHook3'),
|
||||
loopError('useHook4'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHookInLoops() {
|
||||
while (a) {
|
||||
useHook1();
|
||||
if (b) continue;
|
||||
useHook2();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
loopError('useHook1'),
|
||||
|
||||
// NOTE: Small imprecision in error reporting due to caching means we
|
||||
// have a conditional error here instead of a loop error. However,
|
||||
// we will always get an error so this is acceptable.
|
||||
conditionalError('useHook2', true),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useLabeledBlock() {
|
||||
label: {
|
||||
if (a) break label;
|
||||
useHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useHook')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Currently invalid.
|
||||
// These are variations capturing the current heuristic--
|
||||
// we only allow hooks in PascalCase or useFoo functions.
|
||||
// We *could* make some of these valid. But before doing it,
|
||||
// consider specific cases documented above that contain reasoning.
|
||||
function a() { useState(); }
|
||||
const whatever = function b() { useState(); };
|
||||
const c = () => { useState(); };
|
||||
let d = () => useState();
|
||||
e = () => { useState(); };
|
||||
({f: () => { useState(); }});
|
||||
({g() { useState(); }});
|
||||
const {j = () => { useState(); }} = {};
|
||||
({k = () => { useState(); }} = {});
|
||||
`,
|
||||
errors: [
|
||||
functionError('useState', 'a'),
|
||||
functionError('useState', 'b'),
|
||||
functionError('useState', 'c'),
|
||||
functionError('useState', 'd'),
|
||||
functionError('useState', 'e'),
|
||||
functionError('useState', 'f'),
|
||||
functionError('useState', 'g'),
|
||||
functionError('useState', 'j'),
|
||||
functionError('useState', 'k'),
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (a) return;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (a) return;
|
||||
if (b) {
|
||||
console.log('true');
|
||||
} else {
|
||||
console.log('false');
|
||||
}
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
if (b) {
|
||||
console.log('true');
|
||||
} else {
|
||||
console.log('false');
|
||||
}
|
||||
if (a) return;
|
||||
useState();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useState', true)],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
a && useHook1();
|
||||
b && useHook2();
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useHook1'), conditionalError('useHook2')],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
// This *must* be invalid.
|
||||
function useHook() {
|
||||
try {
|
||||
f();
|
||||
useState();
|
||||
} catch {}
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
// NOTE: This is an error since `f()` could possibly throw.
|
||||
conditionalError('useState'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function conditionalError(hook, hasPreviousFinalizer = false) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" is called conditionally. React Hooks must be ` +
|
||||
'called in the exact same order in every component render.' +
|
||||
(hasPreviousFinalizer
|
||||
? ' Did you accidentally call a React Hook after an early return?'
|
||||
: ''),
|
||||
};
|
||||
}
|
||||
|
||||
function loopError(hook) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" may be executed more than once. Possibly ` +
|
||||
'because it is called in a loop. React Hooks must be called in the ' +
|
||||
'exact same order in every component render.',
|
||||
};
|
||||
}
|
||||
|
||||
function functionError(hook, fn) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" is called in function "${fn}" which is neither ` +
|
||||
'a React function component or a custom React Hook function.',
|
||||
};
|
||||
}
|
||||
|
||||
function genericError(hook) {
|
||||
return {
|
||||
message:
|
||||
`React Hook "${hook}" cannot be called inside a callback. React Hooks ` +
|
||||
'must be called in a React function component or a custom React ' +
|
||||
'Hook function.',
|
||||
};
|
||||
}
|
||||
10
packages/eslint-plugin-react-hooks/index.js
Normal file
10
packages/eslint-plugin-react-hooks/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
module.exports = require('./src/index');
|
||||
9
packages/eslint-plugin-react-hooks/npm/index.js
Normal file
9
packages/eslint-plugin-react-hooks/npm/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: this doesn't make sense for an ESLint rule.
|
||||
// We need to fix our build process to not create bundles for "raw" packages like this.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
|
||||
}
|
||||
35
packages/eslint-plugin-react-hooks/package.json
Normal file
35
packages/eslint-plugin-react-hooks/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"description": "ESLint rules for React Hooks",
|
||||
"version": "1.1.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/eslint-plugin-react-hooks"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs"
|
||||
],
|
||||
"keywords": [
|
||||
"eslint",
|
||||
"eslint-plugin",
|
||||
"eslintplugin",
|
||||
"react"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/facebook/react/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
}
|
||||
554
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Normal file
554
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
Normal file
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||
* character to exclude identifiers like "user".
|
||||
*/
|
||||
|
||||
function isHookName(s) {
|
||||
return /^use[A-Z0-9].*$/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* We consider hooks to be a hook name identifier or a member expression
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return isHookName(node.name);
|
||||
} else if (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
isHook(node.property)
|
||||
) {
|
||||
// Only consider React.useFoo() to be namespace hooks for now to avoid false positives.
|
||||
// We can expand this check later.
|
||||
const obj = node.object;
|
||||
return obj.type === 'Identifier' && obj.name === 'React';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a React component name. React component names must
|
||||
* always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
|
||||
* are valid component names for instance.
|
||||
*/
|
||||
|
||||
function isComponentName(node) {
|
||||
if (node.type === 'Identifier') {
|
||||
return !/^[a-z]/.test(node.name);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideComponentOrHook(node) {
|
||||
while (node) {
|
||||
const functionName = getFunctionName(node);
|
||||
if (functionName) {
|
||||
if (isComponentName(functionName) || isHook(functionName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
const codePathReactHooksMapStack = [];
|
||||
const codePathSegmentStack = [];
|
||||
return {
|
||||
// Maintain code segment path stack as we traverse.
|
||||
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
|
||||
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
|
||||
|
||||
// Maintain code path stack as we traverse.
|
||||
onCodePathStart: () => codePathReactHooksMapStack.push(new Map()),
|
||||
|
||||
// Process our code path.
|
||||
//
|
||||
// Everything is ok if all React Hooks are both reachable from the initial
|
||||
// segment and reachable from every final segment.
|
||||
onCodePathEnd(codePath, codePathNode) {
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop();
|
||||
if (reactHooksMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the segments which are cyclic are recorded in this set.
|
||||
const cyclic = new Set();
|
||||
|
||||
/**
|
||||
* Count the number of code paths from the start of the function to this
|
||||
* segment. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* } else {
|
||||
* // Segment 2
|
||||
* }
|
||||
* // Segment 3
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 1 and 2 have one path to the beginning of `MyComponent` and
|
||||
* segment 3 has two paths to the beginning of `MyComponent` since we
|
||||
* could have either taken the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsFromStart(segment) {
|
||||
const {cache} = countPathsFromStart;
|
||||
let paths = cache.get(segment.id);
|
||||
|
||||
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
|
||||
// any other segments which are a part of this cycle.
|
||||
if (paths === null) {
|
||||
if (cyclic.has(segment.id)) {
|
||||
return 0;
|
||||
} else {
|
||||
cyclic.add(segment.id);
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
countPathsFromStart(prevSegment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Compute `paths` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = 0;
|
||||
} else if (segment.prevSegments.length === 0) {
|
||||
paths = 1;
|
||||
} else {
|
||||
paths = 0;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
paths += countPathsFromStart(prevSegment);
|
||||
}
|
||||
}
|
||||
|
||||
// If our segment is reachable then there should be at least one path
|
||||
// to it from the start of our code path.
|
||||
if (segment.reachable && paths === 0) {
|
||||
cache.delete(segment.id);
|
||||
} else {
|
||||
cache.set(segment.id, paths);
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of code paths from this segment to the end of the
|
||||
* function. For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* // Segment 1
|
||||
* if (condition) {
|
||||
* // Segment 2
|
||||
* } else {
|
||||
* // Segment 3
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Segments 2 and 3 have one path to the end of `MyComponent` and
|
||||
* segment 1 has two paths to the end of `MyComponent` since we could
|
||||
* either take the path of segment 1 or segment 2.
|
||||
*
|
||||
* Populates `cyclic` with cyclic segments.
|
||||
*/
|
||||
|
||||
function countPathsToEnd(segment) {
|
||||
const {cache} = countPathsToEnd;
|
||||
let paths = cache.get(segment.id);
|
||||
|
||||
// If `paths` is null then we've found a cycle! Add it to `cyclic` and
|
||||
// any other segments which are a part of this cycle.
|
||||
if (paths === null) {
|
||||
if (cyclic.has(segment.id)) {
|
||||
return 0;
|
||||
} else {
|
||||
cyclic.add(segment.id);
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
countPathsToEnd(nextSegment);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a cached `paths`. Return it.
|
||||
if (paths !== undefined) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Compute `paths` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (codePath.thrownSegments.includes(segment)) {
|
||||
paths = 0;
|
||||
} else if (segment.nextSegments.length === 0) {
|
||||
paths = 1;
|
||||
} else {
|
||||
paths = 0;
|
||||
for (const nextSegment of segment.nextSegments) {
|
||||
paths += countPathsToEnd(nextSegment);
|
||||
}
|
||||
}
|
||||
cache.set(segment.id, paths);
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shortest path length to the start of a code path.
|
||||
* For example:
|
||||
*
|
||||
* ```js
|
||||
* function MyComponent() {
|
||||
* if (condition) {
|
||||
* // Segment 1
|
||||
* }
|
||||
* // Segment 2
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* There is only one path from segment 1 to the code path start. Its
|
||||
* length is one so that is the shortest path.
|
||||
*
|
||||
* There are two paths from segment 2 to the code path start. One
|
||||
* through segment 1 with a length of two and another directly to the
|
||||
* start with a length of one. The shortest path has a length of one
|
||||
* so we would return that.
|
||||
*/
|
||||
|
||||
function shortestPathLengthToStart(segment) {
|
||||
const {cache} = shortestPathLengthToStart;
|
||||
let length = cache.get(segment.id);
|
||||
|
||||
// If `length` is null then we found a cycle! Return infinity since
|
||||
// the shortest path is definitely not the one where we looped.
|
||||
if (length === null) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
// We have a cached `length`. Return it.
|
||||
if (length !== undefined) {
|
||||
return length;
|
||||
}
|
||||
|
||||
// Compute `length` and cache it. Guarding against cycles.
|
||||
cache.set(segment.id, null);
|
||||
if (segment.prevSegments.length === 0) {
|
||||
length = 1;
|
||||
} else {
|
||||
length = Infinity;
|
||||
for (const prevSegment of segment.prevSegments) {
|
||||
const prevLength = shortestPathLengthToStart(prevSegment);
|
||||
if (prevLength < length) {
|
||||
length = prevLength;
|
||||
}
|
||||
}
|
||||
length += 1;
|
||||
}
|
||||
cache.set(segment.id, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
countPathsFromStart.cache = new Map();
|
||||
countPathsToEnd.cache = new Map();
|
||||
shortestPathLengthToStart.cache = new Map();
|
||||
|
||||
// Count all code paths to the end of our component/hook. Also primes
|
||||
// the `countPathsToEnd` cache.
|
||||
const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
|
||||
|
||||
// Gets the function name for our code path. If the function name is
|
||||
// `undefined` then we know either that we have an anonymous function
|
||||
// expression or our code path is not in a function. In both cases we
|
||||
// will want to error since neither are React function components or
|
||||
// hook functions.
|
||||
const codePathFunctionName = getFunctionName(codePathNode);
|
||||
|
||||
// This is a valid code path for React hooks if we are directly in a React
|
||||
// function component or we are in a hook function.
|
||||
const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(
|
||||
codePathNode,
|
||||
);
|
||||
const isDirectlyInsideComponentOrHook = codePathFunctionName
|
||||
? isComponentName(codePathFunctionName) ||
|
||||
isHook(codePathFunctionName)
|
||||
: false;
|
||||
|
||||
// Compute the earliest finalizer level using information from the
|
||||
// cache. We expect all reachable final segments to have a cache entry
|
||||
// after calling `visitSegment()`.
|
||||
let shortestFinalPathLength = Infinity;
|
||||
for (const finalSegment of codePath.finalSegments) {
|
||||
if (!finalSegment.reachable) {
|
||||
continue;
|
||||
}
|
||||
const length = shortestPathLengthToStart(finalSegment);
|
||||
if (length < shortestFinalPathLength) {
|
||||
shortestFinalPathLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all React Hooks pass our lint invariants. Log warnings
|
||||
// if not.
|
||||
for (const [segment, reactHooks] of reactHooksMap) {
|
||||
// NOTE: We could report here that the hook is not reachable, but
|
||||
// that would be redundant with more general "no unreachable"
|
||||
// lint rules.
|
||||
if (!segment.reachable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are any final segments with a shorter path to start then
|
||||
// we possibly have an early return.
|
||||
//
|
||||
// If our segment is a final segment itself then siblings could
|
||||
// possibly be early returns.
|
||||
const possiblyHasEarlyReturn =
|
||||
segment.nextSegments.length === 0
|
||||
? shortestFinalPathLength <= shortestPathLengthToStart(segment)
|
||||
: shortestFinalPathLength < shortestPathLengthToStart(segment);
|
||||
|
||||
// Count all the paths from the start of our code path to the end of
|
||||
// our code path that go _through_ this segment. The critical piece
|
||||
// of this is _through_. If we just call `countPathsToEnd(segment)`
|
||||
// then we neglect that we may have gone through multiple paths to get
|
||||
// to this point! Consider:
|
||||
//
|
||||
// ```js
|
||||
// function MyComponent() {
|
||||
// if (a) {
|
||||
// // Segment 1
|
||||
// } else {
|
||||
// // Segment 2
|
||||
// }
|
||||
// // Segment 3
|
||||
// if (b) {
|
||||
// // Segment 4
|
||||
// } else {
|
||||
// // Segment 5
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// In this component we have four code paths:
|
||||
//
|
||||
// 1. `a = true; b = true`
|
||||
// 2. `a = true; b = false`
|
||||
// 3. `a = false; b = true`
|
||||
// 4. `a = false; b = false`
|
||||
//
|
||||
// From segment 3 there are two code paths to the end through segment
|
||||
// 4 and segment 5. However, we took two paths to get here through
|
||||
// segment 1 and segment 2.
|
||||
//
|
||||
// If we multiply the paths from start (two) by the paths to end (two)
|
||||
// for segment 3 we get four. Which is our desired count.
|
||||
const pathsFromStartToEnd =
|
||||
countPathsFromStart(segment) * countPathsToEnd(segment);
|
||||
|
||||
// Is this hook a part of a cyclic segment?
|
||||
const cycled = cyclic.has(segment.id);
|
||||
|
||||
for (const hook of reactHooks) {
|
||||
// Report an error if a hook may be called more then once.
|
||||
if (cycled) {
|
||||
context.report({
|
||||
node: hook,
|
||||
message:
|
||||
`React Hook "${context.getSource(hook)}" may be executed ` +
|
||||
'more than once. Possibly because it is called in a loop. ' +
|
||||
'React Hooks must be called in the exact same order in ' +
|
||||
'every component render.',
|
||||
});
|
||||
}
|
||||
|
||||
// If this is not a valid code path for React hooks then we need to
|
||||
// log a warning for every hook in this code path.
|
||||
//
|
||||
// Pick a special message depending on the scope this hook was
|
||||
// called in.
|
||||
if (isDirectlyInsideComponentOrHook) {
|
||||
// Report an error if a hook does not reach all finalizing code
|
||||
// path segments.
|
||||
//
|
||||
// Special case when we think there might be an early return.
|
||||
if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
|
||||
const message =
|
||||
`React Hook "${context.getSource(hook)}" is called ` +
|
||||
'conditionally. React Hooks must be called in the exact ' +
|
||||
'same order in every component render.' +
|
||||
(possiblyHasEarlyReturn
|
||||
? ' Did you accidentally call a React Hook after an' +
|
||||
' early return?'
|
||||
: '');
|
||||
context.report({node: hook, message});
|
||||
}
|
||||
} else if (
|
||||
codePathNode.parent &&
|
||||
(codePathNode.parent.type === 'MethodDefinition' ||
|
||||
codePathNode.parent.type === 'ClassProperty') &&
|
||||
codePathNode.parent.value === codePathNode
|
||||
) {
|
||||
// Ignore class methods for now because they produce too many
|
||||
// false positives due to feature flag checks. We're less
|
||||
// sensitive to them in classes because hooks would produce
|
||||
// runtime errors in classes anyway, and because a use*()
|
||||
// call in a class, if it works, is unambiguously *not* a hook.
|
||||
} else if (codePathFunctionName) {
|
||||
// Custom message if we found an invalid function name.
|
||||
const message =
|
||||
`React Hook "${context.getSource(hook)}" is called in ` +
|
||||
`function "${context.getSource(codePathFunctionName)}" ` +
|
||||
'which is neither a React function component or a custom ' +
|
||||
'React Hook function.';
|
||||
context.report({node: hook, message});
|
||||
} else if (codePathNode.type === 'Program') {
|
||||
// For now, ignore if it's in top level scope.
|
||||
// We could warn here but there are false positives related
|
||||
// configuring libraries like `history`.
|
||||
} else {
|
||||
// Assume in all other cases the user called a hook in some
|
||||
// random function callback. This should usually be true for
|
||||
// anonymous function expressions. Hopefully this is clarifying
|
||||
// enough in the common case that the incorrect message in
|
||||
// uncommon cases doesn't matter.
|
||||
if (isSomewhereInsideComponentOrHook) {
|
||||
const message =
|
||||
`React Hook "${context.getSource(hook)}" cannot be called ` +
|
||||
'inside a callback. React Hooks must be called in a ' +
|
||||
'React function component or a custom React Hook function.';
|
||||
context.report({node: hook, message});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Missed opportunity...We could visit all `Identifier`s instead of all
|
||||
// `CallExpression`s and check that _every use_ of a hook name is valid.
|
||||
// But that gets complicated and enters type-system territory, so we're
|
||||
// only being strict about hook calls for now.
|
||||
CallExpression(node) {
|
||||
if (isHook(node.callee)) {
|
||||
// Add the hook node to a map keyed by the code path segment. We will
|
||||
// do full code path analysis at the end of our code path.
|
||||
const reactHooksMap = last(codePathReactHooksMapStack);
|
||||
const codePathSegment = last(codePathSegmentStack);
|
||||
let reactHooks = reactHooksMap.get(codePathSegment);
|
||||
if (!reactHooks) {
|
||||
reactHooks = [];
|
||||
reactHooksMap.set(codePathSegment, reactHooks);
|
||||
}
|
||||
reactHooks.push(node.callee);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the static name of a function AST node. For function declarations it is
|
||||
* easy. For anonymous function expressions it is much harder. If you search for
|
||||
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
|
||||
* where JS gives anonymous function expressions names. We roughly detect the
|
||||
* same AST nodes with some exceptions to better fit our usecase.
|
||||
*/
|
||||
|
||||
function getFunctionName(node) {
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
) {
|
||||
// function useHook() {}
|
||||
// const whatever = function useHook() {};
|
||||
//
|
||||
// Function declaration or function expression names win over any
|
||||
// assignment statements or other renames.
|
||||
return node.id;
|
||||
} else if (
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (
|
||||
node.parent.type === 'VariableDeclarator' &&
|
||||
node.parent.init === node
|
||||
) {
|
||||
// const useHook = () => {};
|
||||
return node.parent.id;
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentExpression' &&
|
||||
node.parent.right === node &&
|
||||
node.parent.operator === '='
|
||||
) {
|
||||
// useHook = () => {};
|
||||
return node.parent.left;
|
||||
} else if (
|
||||
node.parent.type === 'Property' &&
|
||||
node.parent.value === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
// {useHook: () => {}}
|
||||
// {useHook() {}}
|
||||
return node.parent.key;
|
||||
|
||||
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
|
||||
// here to be pedantic. However, hooks in a class are an anti-pattern. So
|
||||
// we don't allow it to error early.
|
||||
//
|
||||
// class {useHook = () => {}}
|
||||
// class {useHook() {}}
|
||||
} else if (
|
||||
node.parent.type === 'AssignmentPattern' &&
|
||||
node.parent.right === node &&
|
||||
!node.parent.computed
|
||||
) {
|
||||
// const {useHook = () => {}} = {};
|
||||
// ({useHook = () => {}} = {});
|
||||
//
|
||||
// Kinda clowny, but we'd said we'd follow spec convention for
|
||||
// `IsAnonymousFunctionDefinition()` usage.
|
||||
return node.parent.left;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for peeking the last item in a stack.
|
||||
*/
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
14
packages/eslint-plugin-react-hooks/src/index.js
Normal file
14
packages/eslint-plugin-react-hooks/src/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
import RuleOfHooks from './RulesOfHooks';
|
||||
|
||||
export const rules = {
|
||||
'rules-of-hooks': RuleOfHooks,
|
||||
};
|
||||
@@ -37,26 +37,19 @@ let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
|
||||
* Dispatches an event and releases it back into the pool, unless persistent.
|
||||
*
|
||||
* @param {?object} event Synthetic event to be dispatched.
|
||||
* @param {boolean} simulated If the event is simulated (changes exn behavior)
|
||||
* @private
|
||||
*/
|
||||
const executeDispatchesAndRelease = function(
|
||||
event: ReactSyntheticEvent,
|
||||
simulated: boolean,
|
||||
) {
|
||||
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
|
||||
if (event) {
|
||||
executeDispatchesInOrder(event, simulated);
|
||||
executeDispatchesInOrder(event);
|
||||
|
||||
if (!event.isPersistent()) {
|
||||
event.constructor.release(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
const executeDispatchesAndReleaseSimulated = function(e) {
|
||||
return executeDispatchesAndRelease(e, true);
|
||||
};
|
||||
const executeDispatchesAndReleaseTopLevel = function(e) {
|
||||
return executeDispatchesAndRelease(e, false);
|
||||
return executeDispatchesAndRelease(e);
|
||||
};
|
||||
|
||||
function isInteractive(tag) {
|
||||
@@ -192,7 +185,6 @@ function extractEvents(
|
||||
|
||||
export function runEventsInBatch(
|
||||
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
|
||||
simulated: boolean,
|
||||
) {
|
||||
if (events !== null) {
|
||||
eventQueue = accumulateInto(eventQueue, events);
|
||||
@@ -207,17 +199,7 @@ export function runEventsInBatch(
|
||||
return;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
forEachAccumulated(
|
||||
processingEventQueue,
|
||||
executeDispatchesAndReleaseSimulated,
|
||||
);
|
||||
} else {
|
||||
forEachAccumulated(
|
||||
processingEventQueue,
|
||||
executeDispatchesAndReleaseTopLevel,
|
||||
);
|
||||
}
|
||||
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
|
||||
invariant(
|
||||
!eventQueue,
|
||||
'processEventQueue(): Additional events were enqueued while processing ' +
|
||||
@@ -239,5 +221,5 @@ export function runExtractedEventsInBatch(
|
||||
nativeEvent,
|
||||
nativeEventTarget,
|
||||
);
|
||||
runEventsInBatch(events, false);
|
||||
runEventsInBatch(events);
|
||||
}
|
||||
|
||||
@@ -60,11 +60,10 @@ if (__DEV__) {
|
||||
/**
|
||||
* Dispatch the event to the listener.
|
||||
* @param {SyntheticEvent} event SyntheticEvent to handle
|
||||
* @param {boolean} simulated If the event is simulated (changes exn behavior)
|
||||
* @param {function} listener Application-level callback
|
||||
* @param {*} inst Internal component instance
|
||||
*/
|
||||
function executeDispatch(event, simulated, listener, inst) {
|
||||
function executeDispatch(event, listener, inst) {
|
||||
const type = event.type || 'unknown-event';
|
||||
event.currentTarget = getNodeFromInstance(inst);
|
||||
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
|
||||
@@ -74,7 +73,7 @@ function executeDispatch(event, simulated, listener, inst) {
|
||||
/**
|
||||
* Standard/simple iteration through an event's collected dispatches.
|
||||
*/
|
||||
export function executeDispatchesInOrder(event, simulated) {
|
||||
export function executeDispatchesInOrder(event) {
|
||||
const dispatchListeners = event._dispatchListeners;
|
||||
const dispatchInstances = event._dispatchInstances;
|
||||
if (__DEV__) {
|
||||
@@ -86,15 +85,10 @@ export function executeDispatchesInOrder(event, simulated) {
|
||||
break;
|
||||
}
|
||||
// Listeners and Instances are two parallel arrays that are always in sync.
|
||||
executeDispatch(
|
||||
event,
|
||||
simulated,
|
||||
dispatchListeners[i],
|
||||
dispatchInstances[i],
|
||||
);
|
||||
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
|
||||
}
|
||||
} else if (dispatchListeners) {
|
||||
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
|
||||
executeDispatch(event, dispatchListeners, dispatchInstances);
|
||||
}
|
||||
event._dispatchListeners = null;
|
||||
event._dispatchInstances = null;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -35,11 +37,13 @@ function restoreStateOfTarget(target) {
|
||||
restoreImpl(internalInstance.stateNode, internalInstance.type, props);
|
||||
}
|
||||
|
||||
export function setRestoreImplementation(impl) {
|
||||
export function setRestoreImplementation(
|
||||
impl: (domElement: Element, tag: string, props: Object) => void,
|
||||
): void {
|
||||
restoreImpl = impl;
|
||||
}
|
||||
|
||||
export function enqueueStateRestore(target) {
|
||||
export function enqueueStateRestore(target: EventTarget): void {
|
||||
if (restoreTarget) {
|
||||
if (restoreQueue) {
|
||||
restoreQueue.push(target);
|
||||
|
||||
@@ -321,7 +321,7 @@ const run = function(config, hierarchyConfig, nativeEventConfig) {
|
||||
// At this point the negotiation events have been dispatched as part of the
|
||||
// extraction process, but not the side effectful events. Below, we dispatch
|
||||
// side effectful events.
|
||||
EventPluginHub.runEventsInBatch(extractedEvents, true);
|
||||
EventPluginHub.runEventsInBatch(extractedEvents);
|
||||
|
||||
// Ensure that every event that declared an `order`, was actually dispatched.
|
||||
expect('number of events dispatched:' + runData.dispatchCount).toBe(
|
||||
|
||||
5
packages/jest-mock-scheduler/README.md
Normal file
5
packages/jest-mock-scheduler/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# `jest-mock-scheduler`
|
||||
|
||||
Jest matchers and utilities for testing the `scheduler` package.
|
||||
|
||||
This package is experimental. APIs may change between releases.
|
||||
8
packages/jest-mock-scheduler/index.js
Normal file
8
packages/jest-mock-scheduler/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './src/JestMockScheduler';
|
||||
7
packages/jest-mock-scheduler/npm/index.js
Normal file
7
packages/jest-mock-scheduler/npm/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/jest-mock-scheduler.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/jest-mock-scheduler.development.js');
|
||||
}
|
||||
32
packages/jest-mock-scheduler/package.json
Normal file
32
packages/jest-mock-scheduler/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "jest-mock-scheduler",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Jest matchers and utilities for testing the scheduler package.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"directory": "packages/jest-mock-scheduler"
|
||||
},
|
||||
"keywords": [
|
||||
"jest",
|
||||
"scheduler"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/facebook/react/issues"
|
||||
},
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"jest": "^23.0.1",
|
||||
"scheduler": "^0.11.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
]
|
||||
}
|
||||
61
packages/jest-mock-scheduler/src/JestMockScheduler.js
Normal file
61
packages/jest-mock-scheduler/src/JestMockScheduler.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
|
||||
// Math.pow(2, 30) - 1
|
||||
// 0b111111111111111111111111111111
|
||||
const maxSigned31BitInt = 1073741823;
|
||||
|
||||
export function mockRestore() {
|
||||
delete global._schedMock;
|
||||
}
|
||||
|
||||
let callback = null;
|
||||
let currentTime = -1;
|
||||
|
||||
function flushCallback(didTimeout, ms) {
|
||||
if (callback !== null) {
|
||||
let cb = callback;
|
||||
callback = null;
|
||||
try {
|
||||
currentTime = ms;
|
||||
cb(didTimeout);
|
||||
} finally {
|
||||
currentTime = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestHostCallback(cb, ms) {
|
||||
if (currentTime !== -1) {
|
||||
// Protect against re-entrancy.
|
||||
setTimeout(requestHostCallback, 0, cb, ms);
|
||||
} else {
|
||||
callback = cb;
|
||||
setTimeout(flushCallback, ms, true, ms);
|
||||
setTimeout(flushCallback, maxSigned31BitInt, false, maxSigned31BitInt);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelHostCallback() {
|
||||
callback = null;
|
||||
}
|
||||
|
||||
function shouldYieldToHost() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCurrentTime() {
|
||||
return currentTime === -1 ? 0 : currentTime;
|
||||
}
|
||||
|
||||
global._schedMock = [
|
||||
requestHostCallback,
|
||||
cancelHostCallback,
|
||||
shouldYieldToHost,
|
||||
getCurrentTime,
|
||||
];
|
||||
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"name": "jest-react",
|
||||
"version": "0.3.0-alpha.8af6728",
|
||||
"version": "0.6.6",
|
||||
"description": "Jest matchers and utilities for testing React components.",
|
||||
"main": "index.js",
|
||||
"repository": "facebook/react",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/jest-react"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"jest",
|
||||
@@ -16,12 +20,13 @@
|
||||
"homepage": "https://reactjs.org/",
|
||||
"peerDependencies": {
|
||||
"jest": "^23.0.1",
|
||||
"react": "^16.0.0 || 16.6.0-alpha.8af6728",
|
||||
"react": "^16.0.0",
|
||||
"react-test-renderer": "^16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
]
|
||||
|
||||
@@ -30,33 +30,31 @@ function assertYieldsWereCleared(root) {
|
||||
invariant(
|
||||
actualYields.length === 0,
|
||||
'Log of yielded values is not empty. ' +
|
||||
'Call expect(ReactTestRenderer).toHaveYielded(...) first.',
|
||||
'Call expect(ReactTestRenderer).unstable_toHaveYielded(...) first.',
|
||||
);
|
||||
}
|
||||
|
||||
export function toFlushAndYield(root, expectedYields) {
|
||||
export function unstable_toFlushAndYield(root, expectedYields) {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualYields = root.unstable_flushAll();
|
||||
return captureAssertion(() => {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualYields = root.unstable_flushAll();
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
});
|
||||
}
|
||||
|
||||
export function toFlushAndYieldThrough(root, expectedYields) {
|
||||
export function unstable_toFlushAndYieldThrough(root, expectedYields) {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualYields = root.unstable_flushNumberOfYields(expectedYields.length);
|
||||
return captureAssertion(() => {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualYields = root.unstable_flushNumberOfYields(
|
||||
expectedYields.length,
|
||||
);
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
});
|
||||
}
|
||||
|
||||
export function toFlushWithoutYielding(root) {
|
||||
return toFlushAndYield(root, []);
|
||||
export function unstable_toFlushWithoutYielding(root) {
|
||||
return unstable_toFlushAndYield(root, []);
|
||||
}
|
||||
|
||||
export function toHaveYielded(ReactTestRenderer, expectedYields) {
|
||||
export function unstable_toHaveYielded(ReactTestRenderer, expectedYields) {
|
||||
return captureAssertion(() => {
|
||||
if (
|
||||
ReactTestRenderer === null ||
|
||||
@@ -65,9 +63,9 @@ export function toHaveYielded(ReactTestRenderer, expectedYields) {
|
||||
) {
|
||||
invariant(
|
||||
false,
|
||||
'The matcher `toHaveYielded` expects an instance of React Test ' +
|
||||
'The matcher `unstable_toHaveYielded` expects an instance of React Test ' +
|
||||
'Renderer.\n\nTry: ' +
|
||||
'expect(ReactTestRenderer).toHaveYielded(expectedYields)',
|
||||
'expect(ReactTestRenderer).unstable_toHaveYielded(expectedYields)',
|
||||
);
|
||||
}
|
||||
const actualYields = ReactTestRenderer.unstable_clearYields();
|
||||
@@ -75,16 +73,16 @@ export function toHaveYielded(ReactTestRenderer, expectedYields) {
|
||||
});
|
||||
}
|
||||
|
||||
export function toFlushAndThrow(root, ...rest) {
|
||||
export function unstable_toFlushAndThrow(root, ...rest) {
|
||||
assertYieldsWereCleared(root);
|
||||
return captureAssertion(() => {
|
||||
assertYieldsWereCleared(root);
|
||||
expect(() => {
|
||||
root.unstable_flushAll();
|
||||
}).toThrow(...rest);
|
||||
});
|
||||
}
|
||||
|
||||
export function toMatchRenderedOutput(root, expectedJSX) {
|
||||
export function unstable_toMatchRenderedOutput(root, expectedJSX) {
|
||||
assertYieldsWereCleared(root);
|
||||
const actualJSON = root.toJSON();
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"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.0-alpha.8af6728",
|
||||
"version": "16.8.6",
|
||||
"main": "index.js",
|
||||
"repository": "facebook/react",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-art"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"art",
|
||||
@@ -23,14 +27,15 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.10.0-alpha.8af6728"
|
||||
"scheduler": "^0.13.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || 16.6.0-alpha.8af6728"
|
||||
"react": "^16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs/",
|
||||
"umd/",
|
||||
|
||||
16
packages/react-art/src/ReactART.js
vendored
16
packages/react-art/src/ReactART.js
vendored
@@ -7,7 +7,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
import * as ARTRenderer from 'react-reconciler/inline.art';
|
||||
import {
|
||||
createContainer,
|
||||
updateContainer,
|
||||
injectIntoDevTools,
|
||||
} from 'react-reconciler/inline.art';
|
||||
import Transform from 'art/core/transform';
|
||||
import Mode from 'art/modes/current';
|
||||
import FastNoSideEffects from 'art/modes/fast-noSideEffects';
|
||||
@@ -61,8 +65,8 @@ class Surface extends React.Component {
|
||||
|
||||
this._surface = Mode.Surface(+width, +height, this._tagRef);
|
||||
|
||||
this._mountNode = ARTRenderer.createContainer(this._surface);
|
||||
ARTRenderer.updateContainer(this.props.children, this._mountNode, this);
|
||||
this._mountNode = createContainer(this._surface);
|
||||
updateContainer(this.props.children, this._mountNode, this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -72,7 +76,7 @@ class Surface extends React.Component {
|
||||
this._surface.resize(+props.width, +props.height);
|
||||
}
|
||||
|
||||
ARTRenderer.updateContainer(this.props.children, this._mountNode, this);
|
||||
updateContainer(this.props.children, this._mountNode, this);
|
||||
|
||||
if (this._surface.render) {
|
||||
this._surface.render();
|
||||
@@ -80,7 +84,7 @@ class Surface extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ARTRenderer.updateContainer(null, this._mountNode, this);
|
||||
updateContainer(null, this._mountNode, this);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -132,7 +136,7 @@ class Text extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ARTRenderer.injectIntoDevTools({
|
||||
injectIntoDevTools({
|
||||
findFiberByHostInstance: () => null,
|
||||
bundleType: __DEV__ ? 1 : 0,
|
||||
version: ReactVersion,
|
||||
|
||||
7
packages/react-art/src/ReactARTHostConfig.js
vendored
7
packages/react-art/src/ReactARTHostConfig.js
vendored
@@ -5,9 +5,14 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
unstable_scheduleCallback as scheduleDeferredCallback,
|
||||
unstable_cancelCallback as cancelDeferredCallback,
|
||||
} from 'scheduler';
|
||||
export {
|
||||
unstable_now as now,
|
||||
unstable_scheduleCallback as scheduleDeferredCallback,
|
||||
unstable_shouldYield as shouldYield,
|
||||
unstable_cancelCallback as cancelDeferredCallback,
|
||||
} from 'scheduler';
|
||||
import Transform from 'art/core/transform';
|
||||
@@ -336,6 +341,8 @@ export function getChildHostContext() {
|
||||
export const scheduleTimeout = setTimeout;
|
||||
export const cancelTimeout = clearTimeout;
|
||||
export const noTimeout = -1;
|
||||
export const schedulePassiveEffects = scheduleDeferredCallback;
|
||||
export const cancelPassiveEffects = cancelDeferredCallback;
|
||||
|
||||
export function shouldSetTextContent(type, props) {
|
||||
return (
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "react-cache",
|
||||
"description": "A basic cache for React applications",
|
||||
"version": "16.6.0-alpha.8af6728",
|
||||
"repository": "facebook/react",
|
||||
"version": "2.0.0-alpha.0",
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-cache"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
"cjs/",
|
||||
"umd/"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0-alpha.1 || 16.6.0-alpha.8af6728"
|
||||
"react": "^16.3.0-alpha.1"
|
||||
}
|
||||
}
|
||||
|
||||
142
packages/react-cache/src/LRU.js
vendored
Normal file
142
packages/react-cache/src/LRU.js
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {unstable_scheduleCallback as scheduleCallback} from 'scheduler';
|
||||
|
||||
type Entry<T> = {|
|
||||
value: T,
|
||||
onDelete: () => mixed,
|
||||
previous: Entry<T>,
|
||||
next: Entry<T>,
|
||||
|};
|
||||
|
||||
export function createLRU<T>(limit: number) {
|
||||
let LIMIT = limit;
|
||||
|
||||
// Circular, doubly-linked list
|
||||
let first: Entry<T> | null = null;
|
||||
let size: number = 0;
|
||||
|
||||
let cleanUpIsScheduled: boolean = false;
|
||||
|
||||
function scheduleCleanUp() {
|
||||
if (cleanUpIsScheduled === false && size > LIMIT) {
|
||||
// The cache size exceeds the limit. Schedule a callback to delete the
|
||||
// least recently used entries.
|
||||
cleanUpIsScheduled = true;
|
||||
scheduleCallback(cleanUp);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUp() {
|
||||
cleanUpIsScheduled = false;
|
||||
deleteLeastRecentlyUsedEntries(LIMIT);
|
||||
}
|
||||
|
||||
function deleteLeastRecentlyUsedEntries(targetSize: number) {
|
||||
// Delete entries from the cache, starting from the end of the list.
|
||||
if (first !== null) {
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
let last = resolvedFirst.previous;
|
||||
while (size > targetSize && last !== null) {
|
||||
const onDelete = last.onDelete;
|
||||
const previous = last.previous;
|
||||
last.onDelete = (null: any);
|
||||
|
||||
// Remove from the list
|
||||
last.previous = last.next = (null: any);
|
||||
if (last === first) {
|
||||
// Reached the head of the list.
|
||||
first = last = null;
|
||||
} else {
|
||||
(first: any).previous = previous;
|
||||
previous.next = (first: any);
|
||||
last = previous;
|
||||
}
|
||||
|
||||
size -= 1;
|
||||
|
||||
// Call the destroy method after removing the entry from the list. If it
|
||||
// throws, the rest of cache will not be deleted, but it will be in a
|
||||
// valid state.
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function add(value: T, onDelete: () => mixed): Entry<T> {
|
||||
const entry = {
|
||||
value,
|
||||
onDelete,
|
||||
next: (null: any),
|
||||
previous: (null: any),
|
||||
};
|
||||
if (first === null) {
|
||||
entry.previous = entry.next = entry;
|
||||
first = entry;
|
||||
} else {
|
||||
// Append to head
|
||||
const last = first.previous;
|
||||
last.next = entry;
|
||||
entry.previous = last;
|
||||
|
||||
first.previous = entry;
|
||||
entry.next = first;
|
||||
|
||||
first = entry;
|
||||
}
|
||||
size += 1;
|
||||
return entry;
|
||||
}
|
||||
|
||||
function update(entry: Entry<T>, newValue: T): void {
|
||||
entry.value = newValue;
|
||||
}
|
||||
|
||||
function access(entry: Entry<T>): T {
|
||||
const next = entry.next;
|
||||
if (next !== null) {
|
||||
// Entry already cached
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
if (first !== entry) {
|
||||
// Remove from current position
|
||||
const previous = entry.previous;
|
||||
previous.next = next;
|
||||
next.previous = previous;
|
||||
|
||||
// Append to head
|
||||
const last = resolvedFirst.previous;
|
||||
last.next = entry;
|
||||
entry.previous = last;
|
||||
|
||||
resolvedFirst.previous = entry;
|
||||
entry.next = resolvedFirst;
|
||||
|
||||
first = entry;
|
||||
}
|
||||
} else {
|
||||
// Cannot access a deleted entry
|
||||
// TODO: Error? Warning?
|
||||
}
|
||||
scheduleCleanUp();
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
function setLimit(newLimit: number) {
|
||||
LIMIT = newLimit;
|
||||
scheduleCleanUp();
|
||||
}
|
||||
|
||||
return {
|
||||
add,
|
||||
update,
|
||||
access,
|
||||
setLimit,
|
||||
};
|
||||
}
|
||||
495
packages/react-cache/src/ReactCache.js
vendored
495
packages/react-cache/src/ReactCache.js
vendored
@@ -10,394 +10,179 @@
|
||||
import React from 'react';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
function noop() {}
|
||||
import {createLRU} from './LRU';
|
||||
|
||||
const Empty = 0;
|
||||
const Pending = 1;
|
||||
const Resolved = 2;
|
||||
const Rejected = 3;
|
||||
|
||||
type EmptyRecord<K> = {|
|
||||
status: 0,
|
||||
suspender: null,
|
||||
key: K,
|
||||
value: null,
|
||||
error: null,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type PendingRecord<K, V> = {|
|
||||
status: 1,
|
||||
suspender: Promise<V>,
|
||||
key: K,
|
||||
value: null,
|
||||
error: null,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type ResolvedRecord<K, V> = {|
|
||||
status: 2,
|
||||
suspender: null,
|
||||
key: K,
|
||||
value: V,
|
||||
error: null,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type RejectedRecord<K> = {|
|
||||
status: 3,
|
||||
suspender: null,
|
||||
key: K,
|
||||
value: null,
|
||||
error: Error,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type Record<K, V> =
|
||||
| EmptyRecord<K>
|
||||
| PendingRecord<K, V>
|
||||
| ResolvedRecord<K, V>
|
||||
| RejectedRecord<K>;
|
||||
|
||||
type RecordCache<K, V> = {|
|
||||
map: Map<K, Record<K, V>>,
|
||||
head: Record<K, V> | null,
|
||||
tail: Record<K, V> | null,
|
||||
size: number,
|
||||
|};
|
||||
|
||||
// TODO: How do you express this type with Flow?
|
||||
type ResourceMap = Map<any, RecordCache<any, any>>;
|
||||
type Cache = {
|
||||
invalidate(): void,
|
||||
read<K, V, A>(
|
||||
resourceType: mixed,
|
||||
key: K,
|
||||
miss: (A) => Promise<V>,
|
||||
missArg: A,
|
||||
): V,
|
||||
preload<K, V, A>(
|
||||
resourceType: mixed,
|
||||
key: K,
|
||||
miss: (A) => Promise<V>,
|
||||
missArg: A,
|
||||
): void,
|
||||
|
||||
// DEV-only
|
||||
$$typeof?: Symbol | number,
|
||||
type Thenable<T> = {
|
||||
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
|
||||
};
|
||||
|
||||
let CACHE_TYPE;
|
||||
if (__DEV__) {
|
||||
CACHE_TYPE = 0xcac4e;
|
||||
}
|
||||
type Suspender = {
|
||||
then(resolve: () => mixed, reject: () => mixed): mixed,
|
||||
};
|
||||
|
||||
let isCache;
|
||||
if (__DEV__) {
|
||||
isCache = value =>
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
value.$$typeof === CACHE_TYPE;
|
||||
}
|
||||
type PendingResult = {|
|
||||
status: 0,
|
||||
value: Suspender,
|
||||
|};
|
||||
|
||||
// TODO: Make this configurable per resource
|
||||
const MAX_SIZE = 500;
|
||||
const PAGE_SIZE = 50;
|
||||
type ResolvedResult<V> = {|
|
||||
status: 1,
|
||||
value: V,
|
||||
|};
|
||||
|
||||
function createRecord<K>(key: K): EmptyRecord<K> {
|
||||
return {
|
||||
status: Empty,
|
||||
suspender: null,
|
||||
key,
|
||||
value: null,
|
||||
error: null,
|
||||
next: null,
|
||||
previous: null,
|
||||
};
|
||||
}
|
||||
type RejectedResult = {|
|
||||
status: 2,
|
||||
value: mixed,
|
||||
|};
|
||||
|
||||
function createRecordCache<K, V>(): RecordCache<K, V> {
|
||||
return {
|
||||
map: new Map(),
|
||||
head: null,
|
||||
tail: null,
|
||||
size: 0,
|
||||
};
|
||||
}
|
||||
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
|
||||
|
||||
export function createCache(invalidator: () => mixed): Cache {
|
||||
const resourceMap: ResourceMap = new Map();
|
||||
type Resource<I, V> = {
|
||||
read(I): V,
|
||||
preload(I): void,
|
||||
};
|
||||
|
||||
function accessRecord<K, V>(resourceType: any, key: K): Record<K, V> {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof resourceType !== 'string' && typeof resourceType !== 'number',
|
||||
'Invalid resourceType: Expected a symbol, object, or function, but ' +
|
||||
'instead received: %s. Strings and numbers are not permitted as ' +
|
||||
'resource types.',
|
||||
resourceType,
|
||||
);
|
||||
}
|
||||
const Pending = 0;
|
||||
const Resolved = 1;
|
||||
const Rejected = 2;
|
||||
|
||||
let recordCache = resourceMap.get(resourceType);
|
||||
if (recordCache === undefined) {
|
||||
recordCache = createRecordCache();
|
||||
resourceMap.set(resourceType, recordCache);
|
||||
}
|
||||
const map = recordCache.map;
|
||||
const ReactCurrentDispatcher =
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||
.ReactCurrentDispatcher;
|
||||
|
||||
let record = map.get(key);
|
||||
if (record === undefined) {
|
||||
// This record does not already exist. Create a new one.
|
||||
record = createRecord(key);
|
||||
map.set(key, record);
|
||||
if (recordCache.size >= MAX_SIZE) {
|
||||
// The cache is already at maximum capacity. Remove PAGE_SIZE least
|
||||
// recently used records.
|
||||
// TODO: We assume the max capcity is greater than zero. Otherwise warn.
|
||||
const tail = recordCache.tail;
|
||||
if (tail !== null) {
|
||||
let newTail = tail;
|
||||
for (let i = 0; i < PAGE_SIZE && newTail !== null; i++) {
|
||||
recordCache.size -= 1;
|
||||
map.delete(newTail.key);
|
||||
newTail = newTail.previous;
|
||||
}
|
||||
recordCache.tail = newTail;
|
||||
if (newTail !== null) {
|
||||
newTail.next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This record is already cached. Remove it from its current position in
|
||||
// the list. We'll add it to the front below.
|
||||
const previous = record.previous;
|
||||
const next = record.next;
|
||||
if (previous !== null) {
|
||||
previous.next = next;
|
||||
} else {
|
||||
recordCache.head = next;
|
||||
}
|
||||
if (next !== null) {
|
||||
next.previous = previous;
|
||||
} else {
|
||||
recordCache.tail = previous;
|
||||
}
|
||||
recordCache.size -= 1;
|
||||
}
|
||||
|
||||
// Add the record to the front of the list.
|
||||
const head = recordCache.head;
|
||||
const newHead = record;
|
||||
recordCache.head = newHead;
|
||||
newHead.previous = null;
|
||||
newHead.next = head;
|
||||
if (head !== null) {
|
||||
head.previous = newHead;
|
||||
} else {
|
||||
recordCache.tail = newHead;
|
||||
}
|
||||
recordCache.size += 1;
|
||||
|
||||
return newHead;
|
||||
}
|
||||
|
||||
function load<K, V>(emptyRecord: EmptyRecord<K>, suspender: Promise<V>) {
|
||||
const pendingRecord: PendingRecord<K, V> = (emptyRecord: any);
|
||||
pendingRecord.status = Pending;
|
||||
pendingRecord.suspender = suspender;
|
||||
suspender.then(
|
||||
value => {
|
||||
// Resource loaded successfully.
|
||||
const resolvedRecord: ResolvedRecord<K, V> = (pendingRecord: any);
|
||||
resolvedRecord.status = Resolved;
|
||||
resolvedRecord.suspender = null;
|
||||
resolvedRecord.value = value;
|
||||
},
|
||||
error => {
|
||||
// Resource failed to load. Stash the error for later so we can throw it
|
||||
// the next time it's requested.
|
||||
const rejectedRecord: RejectedRecord<K> = (pendingRecord: any);
|
||||
rejectedRecord.status = Rejected;
|
||||
rejectedRecord.suspender = null;
|
||||
rejectedRecord.error = error;
|
||||
},
|
||||
function readContext(Context, observedBits) {
|
||||
const dispatcher = ReactCurrentDispatcher.current;
|
||||
if (dispatcher === null) {
|
||||
throw new Error(
|
||||
'react-cache: read and preload may only be called from within a ' +
|
||||
"component's render. They are not supported in event handlers or " +
|
||||
'lifecycle methods.',
|
||||
);
|
||||
}
|
||||
|
||||
const cache: Cache = {
|
||||
invalidate() {
|
||||
invalidator();
|
||||
},
|
||||
preload<K, V, A>(
|
||||
resourceType: any,
|
||||
key: K,
|
||||
miss: A => Promise<V>,
|
||||
missArg: A,
|
||||
): void {
|
||||
const record: Record<K, V> = accessRecord(resourceType, key);
|
||||
switch (record.status) {
|
||||
case Empty:
|
||||
// Warm the cache.
|
||||
const suspender = miss(missArg);
|
||||
load(record, suspender);
|
||||
return;
|
||||
case Pending:
|
||||
// There's already a pending request.
|
||||
return;
|
||||
case Resolved:
|
||||
// The resource is already in the cache.
|
||||
return;
|
||||
case Rejected:
|
||||
// The request failed.
|
||||
return;
|
||||
}
|
||||
},
|
||||
read<K, V, A>(
|
||||
resourceType: any,
|
||||
key: K,
|
||||
miss: A => Promise<V>,
|
||||
missArg: A,
|
||||
): V {
|
||||
const record: Record<K, V> = accessRecord(resourceType, key);
|
||||
switch (record.status) {
|
||||
case Empty:
|
||||
// Load the requested resource.
|
||||
const suspender = miss(missArg);
|
||||
load(record, suspender);
|
||||
throw suspender;
|
||||
case Pending:
|
||||
// There's already a pending request.
|
||||
throw record.suspender;
|
||||
case Resolved:
|
||||
return record.value;
|
||||
case Rejected:
|
||||
default:
|
||||
// The requested resource previously failed loading.
|
||||
const error = record.error;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
cache.$$typeof = CACHE_TYPE;
|
||||
}
|
||||
return cache;
|
||||
return dispatcher.readContext(Context, observedBits);
|
||||
}
|
||||
|
||||
let warnIfNonPrimitiveKey;
|
||||
if (__DEV__) {
|
||||
warnIfNonPrimitiveKey = (key, methodName) => {
|
||||
function identityHashFn(input) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof key === 'string' ||
|
||||
typeof key === 'number' ||
|
||||
typeof key === 'boolean' ||
|
||||
key === undefined ||
|
||||
key === null,
|
||||
'%s: Invalid key type. Expected a string, number, symbol, or boolean, ' +
|
||||
typeof input === 'string' ||
|
||||
typeof input === 'number' ||
|
||||
typeof input === 'boolean' ||
|
||||
input === undefined ||
|
||||
input === null,
|
||||
'Invalid key type. Expected a string, number, symbol, or boolean, ' +
|
||||
'but instead received: %s' +
|
||||
'\n\nTo use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
methodName,
|
||||
key,
|
||||
input,
|
||||
);
|
||||
};
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
type primitive = string | number | boolean | void | null;
|
||||
const CACHE_LIMIT = 500;
|
||||
const lru = createLRU(CACHE_LIMIT);
|
||||
|
||||
type Resource<K, V> = {|
|
||||
read(Cache, K): V,
|
||||
preload(cache: Cache, key: K): void,
|
||||
|};
|
||||
const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
|
||||
|
||||
// These declarations are used to express function overloading. I wish there
|
||||
// were a more elegant way to do this in the function definition itself.
|
||||
const CacheContext = React.createContext(null);
|
||||
|
||||
// Primitive keys do not request a hash function.
|
||||
declare function createResource<V, K: primitive, H: primitive>(
|
||||
loadResource: (K) => Promise<V>,
|
||||
hash?: (K) => H,
|
||||
): Resource<K, V>;
|
||||
function accessResult<I, K, V>(
|
||||
resource: any,
|
||||
fetch: I => Thenable<V>,
|
||||
input: I,
|
||||
key: K,
|
||||
): Result<V> {
|
||||
let entriesForResource = entries.get(resource);
|
||||
if (entriesForResource === undefined) {
|
||||
entriesForResource = new Map();
|
||||
entries.set(resource, entriesForResource);
|
||||
}
|
||||
let entry = entriesForResource.get(key);
|
||||
if (entry === undefined) {
|
||||
const thenable = fetch(input);
|
||||
thenable.then(
|
||||
value => {
|
||||
if (newResult.status === Pending) {
|
||||
const resolvedResult: ResolvedResult<V> = (newResult: any);
|
||||
resolvedResult.status = Resolved;
|
||||
resolvedResult.value = value;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if (newResult.status === Pending) {
|
||||
const rejectedResult: RejectedResult = (newResult: any);
|
||||
rejectedResult.status = Rejected;
|
||||
rejectedResult.value = error;
|
||||
}
|
||||
},
|
||||
);
|
||||
const newResult: PendingResult = {
|
||||
status: Pending,
|
||||
value: thenable,
|
||||
};
|
||||
const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
|
||||
entriesForResource.set(key, newEntry);
|
||||
return newResult;
|
||||
} else {
|
||||
return (lru.access(entry): any);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-primitive keys *do* require a hash function.
|
||||
// eslint-disable-next-line no-redeclare
|
||||
declare function createResource<V, K: mixed, H: primitive>(
|
||||
loadResource: (K) => Promise<V>,
|
||||
hash: (K) => H,
|
||||
): Resource<K, V>;
|
||||
function deleteEntry(resource, key) {
|
||||
const entriesForResource = entries.get(resource);
|
||||
if (entriesForResource !== undefined) {
|
||||
entriesForResource.delete(key);
|
||||
if (entriesForResource.size === 0) {
|
||||
entries.delete(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unstable_createResource<I, K: string | number, V>(
|
||||
fetch: I => Thenable<V>,
|
||||
maybeHashInput?: I => K,
|
||||
): Resource<I, V> {
|
||||
const hashInput: I => K =
|
||||
maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export function createResource<V, K, H: primitive>(
|
||||
loadResource: K => Promise<V>,
|
||||
hash: K => H,
|
||||
): Resource<K, V> {
|
||||
const resource = {
|
||||
read(cache, key) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
isCache(cache),
|
||||
'read(): The first argument must be a cache. Instead received: %s',
|
||||
cache,
|
||||
);
|
||||
}
|
||||
if (hash === undefined) {
|
||||
if (__DEV__) {
|
||||
warnIfNonPrimitiveKey(key, 'read');
|
||||
read(input: I): V {
|
||||
// react-cache currently doesn't rely on context, but it may in the
|
||||
// future, so we read anyway to prevent access outside of render.
|
||||
readContext(CacheContext);
|
||||
const key = hashInput(input);
|
||||
const result: Result<V> = accessResult(resource, fetch, input, key);
|
||||
switch (result.status) {
|
||||
case Pending: {
|
||||
const suspender = result.value;
|
||||
throw suspender;
|
||||
}
|
||||
return cache.read(resource, key, loadResource, key);
|
||||
case Resolved: {
|
||||
const value = result.value;
|
||||
return value;
|
||||
}
|
||||
case Rejected: {
|
||||
const error = result.value;
|
||||
throw error;
|
||||
}
|
||||
default:
|
||||
// Should be unreachable
|
||||
return (undefined: any);
|
||||
}
|
||||
const hashedKey = hash(key);
|
||||
return cache.read(resource, hashedKey, loadResource, key);
|
||||
},
|
||||
preload(cache, key) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
isCache(cache),
|
||||
'preload(): The first argument must be a cache. Instead received: %s',
|
||||
cache,
|
||||
);
|
||||
}
|
||||
if (hash === undefined) {
|
||||
if (__DEV__) {
|
||||
warnIfNonPrimitiveKey(key, 'preload');
|
||||
}
|
||||
cache.preload(resource, key, loadResource, key);
|
||||
return;
|
||||
}
|
||||
const hashedKey = hash(key);
|
||||
cache.preload(resource, hashedKey, loadResource, key);
|
||||
|
||||
preload(input: I): void {
|
||||
// react-cache currently doesn't rely on context, but it may in the
|
||||
// future, so we read anyway to prevent access outside of render.
|
||||
readContext(CacheContext);
|
||||
const key = hashInput(input);
|
||||
accessResult(resource, fetch, input, key);
|
||||
},
|
||||
};
|
||||
return resource;
|
||||
}
|
||||
|
||||
// Global cache has no eviction policy (except for, ya know, a browser refresh).
|
||||
const globalCache = createCache(noop);
|
||||
export const ReactCache = React.createContext(globalCache);
|
||||
export function unstable_setGlobalCacheLimit(limit: number) {
|
||||
lru.setLimit(limit);
|
||||
}
|
||||
|
||||
424
packages/react-cache/src/__tests__/ReactCache-test.internal.js
Normal file
424
packages/react-cache/src/__tests__/ReactCache-test.internal.js
Normal file
@@ -0,0 +1,424 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let ReactCache;
|
||||
let createResource;
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactTestRenderer;
|
||||
let Suspense;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
let flushScheduledWork;
|
||||
let evictLRU;
|
||||
|
||||
describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
let currentPriorityLevel = 3;
|
||||
|
||||
jest.mock('scheduler', () => {
|
||||
let callbacks = [];
|
||||
return {
|
||||
unstable_scheduleCallback(callback) {
|
||||
const callbackIndex = callbacks.length;
|
||||
callbacks.push(callback);
|
||||
return {callbackIndex};
|
||||
},
|
||||
flushScheduledWork() {
|
||||
while (callbacks.length) {
|
||||
const callback = callbacks.pop();
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
unstable_ImmediatePriority: 1,
|
||||
unstable_UserBlockingPriority: 2,
|
||||
unstable_NormalPriority: 3,
|
||||
unstable_LowPriority: 4,
|
||||
unstable_IdlePriority: 5,
|
||||
|
||||
unstable_runWithPriority(priorityLevel, fn) {
|
||||
const prevPriorityLevel = currentPriorityLevel;
|
||||
currentPriorityLevel = priorityLevel;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
currentPriorityLevel = prevPriorityLevel;
|
||||
}
|
||||
},
|
||||
|
||||
unstable_getCurrentPriorityLevel() {
|
||||
return currentPriorityLevel;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
Suspense = React.Suspense;
|
||||
ReactCache = require('react-cache');
|
||||
createResource = ReactCache.unstable_createResource;
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
flushScheduledWork = require('scheduler').flushScheduledWork;
|
||||
evictLRU = flushScheduledWork;
|
||||
|
||||
TextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
ReactTestRenderer.unstable_yield(
|
||||
`Promise rejected [${text}]`,
|
||||
);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(
|
||||
`Promise resolved [${text}]`,
|
||||
);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
textResourceShouldFail = false;
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
ReactTestRenderer.unstable_yield(props.text);
|
||||
return props.text;
|
||||
}
|
||||
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
TextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
it('throws a promise if the requested value is not in the cache', () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text="Hi" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Hi]']);
|
||||
expect(root).toFlushAndYield(['Hi']);
|
||||
});
|
||||
|
||||
it('throws an error on the subsequent read if the promise is rejected', async () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text="Hi" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
textResourceShouldFail = true;
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded(['Promise rejected [Hi]']);
|
||||
|
||||
expect(root).toFlushAndThrow('Failed to load: Hi');
|
||||
expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
|
||||
|
||||
// Should throw again on a subsequent read
|
||||
root.update(<App />);
|
||||
expect(root).toFlushAndThrow('Failed to load: Hi');
|
||||
expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
|
||||
});
|
||||
|
||||
it('warns if non-primitive key is passed to a resource without a hash function', () => {
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
resolve(text);
|
||||
}, ms),
|
||||
);
|
||||
});
|
||||
|
||||
function App() {
|
||||
ReactTestRenderer.unstable_yield('App');
|
||||
return BadTextResource.read(['Hi', 100]);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
expect(() => {
|
||||
expect(root).toFlushAndYield(['App', 'Loading...']);
|
||||
}).toWarnDev(
|
||||
[
|
||||
'Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: Hi,100\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
expect(root).toFlushAndYield(['App', 'Loading...']);
|
||||
}
|
||||
});
|
||||
|
||||
it('evicts least recently used values', async () => {
|
||||
ReactCache.unstable_setGlobalCacheLimit(3);
|
||||
|
||||
// Render 1, 2, and 3
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={2} />
|
||||
<AsyncText ms={100} text={3} />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
expect(root).toFlushAndYield([
|
||||
'Suspend! [1]',
|
||||
'Suspend! [2]',
|
||||
'Suspend! [3]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [1]',
|
||||
'Promise resolved [2]',
|
||||
'Promise resolved [3]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 2, 3]);
|
||||
expect(root).toMatchRenderedOutput('123');
|
||||
|
||||
// Render 1, 4, 5
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={4} />
|
||||
<AsyncText ms={100} text={5} />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
1,
|
||||
'Suspend! [4]',
|
||||
'Suspend! [5]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [4]',
|
||||
'Promise resolved [5]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 4, 5]);
|
||||
expect(root).toMatchRenderedOutput('145');
|
||||
|
||||
// We've now rendered values 1, 2, 3, 4, 5, over our limit of 3. The least
|
||||
// recently used values are 2 and 3. They will be evicted during the
|
||||
// next sweep.
|
||||
evictLRU();
|
||||
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={2} />
|
||||
<AsyncText ms={100} text={3} />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
// 1 is still cached
|
||||
1,
|
||||
// 2 and 3 suspend because they were evicted from the cache
|
||||
'Suspend! [2]',
|
||||
'Suspend! [3]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [2]',
|
||||
'Promise resolved [3]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 2, 3]);
|
||||
expect(root).toMatchRenderedOutput('123');
|
||||
});
|
||||
|
||||
it('preloads during the render phase', async () => {
|
||||
function App() {
|
||||
TextResource.preload(['B', 1000]);
|
||||
TextResource.read(['A', 1000]);
|
||||
TextResource.read(['B', 1000]);
|
||||
return <Text text="Result" />;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield(['Loading...']);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [B]',
|
||||
'Promise resolved [A]',
|
||||
]);
|
||||
expect(root).toFlushAndYield(['Result']);
|
||||
expect(root).toMatchRenderedOutput('Result');
|
||||
});
|
||||
|
||||
it('if a thenable resolves multiple times, does not update the first cached value', () => {
|
||||
let resolveThenable;
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
if (value !== null) {
|
||||
resolve(value);
|
||||
} else {
|
||||
if (listeners === null) {
|
||||
listeners = [resolve];
|
||||
resolveThenable = v => {
|
||||
listeners.forEach(listener => listener(v));
|
||||
};
|
||||
} else {
|
||||
listeners.push(resolve);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
function BadAsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
const actualText = BadTextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(actualText);
|
||||
return actualText;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<BadAsyncText text="Hi" />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
resolveThenable('Hi');
|
||||
// This thenable improperly resolves twice. We should not update the
|
||||
// cached value.
|
||||
resolveThenable('Hi muahahaha I am different');
|
||||
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<BadAsyncText text="Hi" />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(ReactTestRenderer).toHaveYielded([]);
|
||||
expect(root).toFlushAndYield(['Hi']);
|
||||
expect(root).toMatchRenderedOutput('Hi');
|
||||
});
|
||||
|
||||
it('throws if read is called outside render', () => {
|
||||
expect(() => TextResource.read(['A', 1000])).toThrow(
|
||||
"read and preload may only be called from within a component's render",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if preload is called outside render', () => {
|
||||
expect(() => TextResource.preload(['A', 1000])).toThrow(
|
||||
"read and preload may only be called from within a component's render",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,235 +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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let ReactCache;
|
||||
|
||||
describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactCache = require('react-cache');
|
||||
});
|
||||
|
||||
it('throws a promise if the requested value is not in the cache', async () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
function loadUpperCase(text) {
|
||||
return Promise.resolve(text.toUpperCase());
|
||||
}
|
||||
const UpperCase = createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
let suspender;
|
||||
try {
|
||||
UpperCase.read(cache, 'hello');
|
||||
} catch (v) {
|
||||
suspender = v;
|
||||
}
|
||||
|
||||
await suspender;
|
||||
const result = UpperCase.read(cache, 'hello');
|
||||
expect(result).toBe('HELLO');
|
||||
});
|
||||
|
||||
it('throws an error on the subsequent read if the promise is rejected', async () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
let shouldFail = true;
|
||||
function loadUpperCase(text) {
|
||||
if (shouldFail) {
|
||||
// Rejects on the first try
|
||||
shouldFail = false;
|
||||
return Promise.reject(new Error('oh no'));
|
||||
} else {
|
||||
// Succeeds the second time
|
||||
return Promise.resolve(text.toUpperCase());
|
||||
}
|
||||
}
|
||||
const UpperCase = createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
let suspender;
|
||||
try {
|
||||
UpperCase.read(cache, 'hello');
|
||||
} catch (v) {
|
||||
suspender = v;
|
||||
}
|
||||
|
||||
let error;
|
||||
try {
|
||||
await suspender;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(() => UpperCase.read(cache, 'hello')).toThrow(error);
|
||||
expect(error.message).toBe('oh no');
|
||||
|
||||
// On a subsequent read, it should still throw.
|
||||
try {
|
||||
UpperCase.read(cache, 'hello');
|
||||
} catch (v) {
|
||||
suspender = v;
|
||||
}
|
||||
await suspender;
|
||||
expect(() => UpperCase.read(cache, 'hello')).toThrow(error);
|
||||
expect(error.message).toBe('oh no');
|
||||
});
|
||||
|
||||
it('can preload data ahead of time', async () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
function loadUpperCase(text) {
|
||||
return Promise.resolve(text.toUpperCase());
|
||||
}
|
||||
const UpperCase = createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
UpperCase.preload(cache, 'hello');
|
||||
// Wait for next tick
|
||||
await Promise.resolve();
|
||||
const result = UpperCase.read(cache, 'hello');
|
||||
expect(result).toBe('HELLO');
|
||||
});
|
||||
|
||||
it('does not throw if preloaded promise rejects', async () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
function loadUpperCase(text) {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
}
|
||||
const UpperCase = createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
UpperCase.preload(cache, 'hello');
|
||||
// Wait for next tick
|
||||
await Promise.resolve();
|
||||
|
||||
expect(() => UpperCase.read(cache, 'hello')).toThrow('uh oh');
|
||||
});
|
||||
|
||||
it('accepts custom hash function', async () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
function loadSum([a, b]) {
|
||||
return Promise.resolve(a + b);
|
||||
}
|
||||
function hash([a, b]) {
|
||||
return `${a + b}`;
|
||||
}
|
||||
const Sum = createResource(loadSum, hash);
|
||||
const cache = createCache();
|
||||
|
||||
Sum.preload(cache, [5, 5]);
|
||||
Sum.preload(cache, [1, 2]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(Sum.read(cache, [5, 5])).toEqual(10);
|
||||
expect(Sum.read(cache, [1, 2])).toEqual(3);
|
||||
// The fact that the next line returns synchronously and doesn't throw, even
|
||||
// though [3, 7] was not preloaded, proves that the hashing function works.
|
||||
expect(Sum.read(cache, [3, 7])).toEqual(10);
|
||||
});
|
||||
|
||||
it('warns if resourceType is a string or number', () => {
|
||||
const {createCache} = ReactCache;
|
||||
|
||||
spyOnDev(console, 'error');
|
||||
const cache = createCache();
|
||||
|
||||
function fn() {
|
||||
cache.preload('foo', 'uppercaseA', () => Promise.resolve('A'));
|
||||
cache.preload(123, 'productOf9And2', () => Promise.resolve(18));
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
expect(fn).toWarnDev(
|
||||
[
|
||||
'Invalid resourceType: Expected a symbol, object, or function, but ' +
|
||||
'instead received: foo. Strings and numbers are not permitted as ' +
|
||||
'resource types.',
|
||||
'Invalid resourceType: Expected a symbol, object, or function, but ' +
|
||||
'instead received: 123. Strings and numbers are not permitted as ' +
|
||||
'resource types.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
|
||||
it('warns if non-primitive key is passed to a resource without a hash function', () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
spyOnDev(console, 'error');
|
||||
|
||||
function loadSum([a, b]) {
|
||||
return Promise.resolve(a + b);
|
||||
}
|
||||
|
||||
const Sum = createResource(loadSum);
|
||||
const cache = createCache();
|
||||
|
||||
function fn() {
|
||||
Sum.preload(cache, [5, 5]);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
expect(fn).toWarnDev(
|
||||
[
|
||||
'preload: Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: 5,5\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
|
||||
it('stays within maximum capacity by evicting the least recently used record', async () => {
|
||||
const {createCache, createResource} = ReactCache;
|
||||
|
||||
function loadIntegerString(int) {
|
||||
return Promise.resolve(int + '');
|
||||
}
|
||||
const IntegerStringResource = createResource(loadIntegerString);
|
||||
const cache = createCache();
|
||||
|
||||
// TODO: This is hard-coded to a maximum size of 500. Make this configurable
|
||||
// per resource.
|
||||
for (let n = 1; n <= 500; n++) {
|
||||
IntegerStringResource.preload(cache, n);
|
||||
}
|
||||
|
||||
// Access 1, 2, and 3 again. The least recently used integer is now 4.
|
||||
IntegerStringResource.preload(cache, 3);
|
||||
IntegerStringResource.preload(cache, 2);
|
||||
IntegerStringResource.preload(cache, 1);
|
||||
|
||||
// Evict older integers from the cache by adding new ones.
|
||||
IntegerStringResource.preload(cache, 501);
|
||||
IntegerStringResource.preload(cache, 502);
|
||||
IntegerStringResource.preload(cache, 503);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
// 1, 2, and 3 should be in the cache. 4, 5, and 6 should have been evicted.
|
||||
expect(IntegerStringResource.read(cache, 1)).toEqual('1');
|
||||
expect(IntegerStringResource.read(cache, 2)).toEqual('2');
|
||||
expect(IntegerStringResource.read(cache, 3)).toEqual('3');
|
||||
|
||||
expect(() => IntegerStringResource.read(cache, 4)).toThrow(Promise);
|
||||
expect(() => IntegerStringResource.read(cache, 5)).toThrow(Promise);
|
||||
expect(() => IntegerStringResource.read(cache, 6)).toThrow(Promise);
|
||||
});
|
||||
});
|
||||
7
packages/react-debug-tools/README.md
Normal file
7
packages/react-debug-tools/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# react-debug-tools
|
||||
|
||||
This is an experimental package for debugging React renderers.
|
||||
|
||||
**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
|
||||
|
||||
**Use it at your own risk.**
|
||||
13
packages/react-debug-tools/index.js
vendored
Normal file
13
packages/react-debug-tools/index.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 ReactDebugTools = require('./src/ReactDebugTools');
|
||||
|
||||
// This is hacky but makes it work with both Rollup and Jest.
|
||||
module.exports = ReactDebugTools.default || ReactDebugTools;
|
||||
7
packages/react-debug-tools/npm/index.js
vendored
Normal file
7
packages/react-debug-tools/npm/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-debug-tools.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-debug-tools.development.js');
|
||||
}
|
||||
34
packages/react-debug-tools/package.json
Normal file
34
packages/react-debug-tools/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "react-debug-tools",
|
||||
"description": "React package for debugging React trees.",
|
||||
"version": "0.16.0",
|
||||
"keywords": [
|
||||
"react"
|
||||
],
|
||||
"homepage": "https://reactjs.org/",
|
||||
"bugs": "https://github.com/facebook/react/issues",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"build-info.json",
|
||||
"index.js",
|
||||
"cjs/"
|
||||
],
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-debug-tools"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"error-stack-parser": "^2.0.2"
|
||||
}
|
||||
}
|
||||
592
packages/react-debug-tools/src/ReactDebugHooks.js
vendored
Normal file
592
packages/react-debug-tools/src/ReactDebugHooks.js
vendored
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* 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 {ReactContext, ReactProviderType} from 'shared/ReactTypes';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||
import type {Hook} from 'react-reconciler/src/ReactFiberHooks';
|
||||
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
|
||||
|
||||
import ErrorStackParser from 'error-stack-parser';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
FunctionComponent,
|
||||
SimpleMemoComponent,
|
||||
ContextProvider,
|
||||
ForwardRef,
|
||||
} from 'shared/ReactWorkTags';
|
||||
|
||||
type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher;
|
||||
|
||||
// Used to track hooks called during a render
|
||||
|
||||
type HookLogEntry = {
|
||||
primitive: string,
|
||||
stackError: Error,
|
||||
value: mixed,
|
||||
};
|
||||
|
||||
let hookLog: Array<HookLogEntry> = [];
|
||||
|
||||
// Primitives
|
||||
|
||||
type BasicStateAction<S> = (S => S) | S;
|
||||
|
||||
type Dispatch<A> = A => void;
|
||||
|
||||
let primitiveStackCache: null | Map<string, Array<any>> = null;
|
||||
|
||||
function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||
// This initializes a cache of all primitive hooks so that the top
|
||||
// most stack frames added by calling the primitive hook can be removed.
|
||||
if (primitiveStackCache === null) {
|
||||
let cache = new Map();
|
||||
let readHookLog;
|
||||
try {
|
||||
// Use all hooks here to add them to the hook log.
|
||||
Dispatcher.useContext(({_currentValue: null}: any));
|
||||
Dispatcher.useState(null);
|
||||
Dispatcher.useReducer((s, a) => s, null);
|
||||
Dispatcher.useRef(null);
|
||||
Dispatcher.useLayoutEffect(() => {});
|
||||
Dispatcher.useEffect(() => {});
|
||||
Dispatcher.useImperativeHandle(undefined, () => null);
|
||||
Dispatcher.useDebugValue(null);
|
||||
Dispatcher.useCallback(() => {});
|
||||
Dispatcher.useMemo(() => null);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
}
|
||||
for (let i = 0; i < readHookLog.length; i++) {
|
||||
let hook = readHookLog[i];
|
||||
cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError));
|
||||
}
|
||||
primitiveStackCache = cache;
|
||||
}
|
||||
return primitiveStackCache;
|
||||
}
|
||||
|
||||
let currentHook: null | Hook = null;
|
||||
function nextHook(): null | Hook {
|
||||
let hook = currentHook;
|
||||
if (hook !== null) {
|
||||
currentHook = hook.next;
|
||||
}
|
||||
return hook;
|
||||
}
|
||||
|
||||
function readContext<T>(
|
||||
context: ReactContext<T>,
|
||||
observedBits: void | number | boolean,
|
||||
): T {
|
||||
// For now we don't expose readContext usage in the hooks debugging info.
|
||||
return context._currentValue;
|
||||
}
|
||||
|
||||
function useContext<T>(
|
||||
context: ReactContext<T>,
|
||||
observedBits: void | number | boolean,
|
||||
): T {
|
||||
hookLog.push({
|
||||
primitive: 'Context',
|
||||
stackError: new Error(),
|
||||
value: context._currentValue,
|
||||
});
|
||||
return context._currentValue;
|
||||
}
|
||||
|
||||
function useState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
let hook = nextHook();
|
||||
let state: S =
|
||||
hook !== null
|
||||
? hook.memoizedState
|
||||
: typeof initialState === 'function'
|
||||
? initialState()
|
||||
: initialState;
|
||||
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
|
||||
return [state, (action: BasicStateAction<S>) => {}];
|
||||
}
|
||||
|
||||
function useReducer<S, I, A>(
|
||||
reducer: (S, A) => S,
|
||||
initialArg: I,
|
||||
init?: I => S,
|
||||
): [S, Dispatch<A>] {
|
||||
let hook = nextHook();
|
||||
let state;
|
||||
if (hook !== null) {
|
||||
state = hook.memoizedState;
|
||||
} else {
|
||||
state = init !== undefined ? init(initialArg) : ((initialArg: any): S);
|
||||
}
|
||||
hookLog.push({
|
||||
primitive: 'Reducer',
|
||||
stackError: new Error(),
|
||||
value: state,
|
||||
});
|
||||
return [state, (action: A) => {}];
|
||||
}
|
||||
|
||||
function useRef<T>(initialValue: T): {current: T} {
|
||||
let hook = nextHook();
|
||||
let ref = hook !== null ? hook.memoizedState : {current: initialValue};
|
||||
hookLog.push({
|
||||
primitive: 'Ref',
|
||||
stackError: new Error(),
|
||||
value: ref.current,
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
|
||||
function useLayoutEffect(
|
||||
create: () => (() => void) | void,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
hookLog.push({
|
||||
primitive: 'LayoutEffect',
|
||||
stackError: new Error(),
|
||||
value: create,
|
||||
});
|
||||
}
|
||||
|
||||
function useEffect(
|
||||
create: () => (() => void) | void,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
|
||||
}
|
||||
|
||||
function useImperativeHandle<T>(
|
||||
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
|
||||
create: () => T,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): void {
|
||||
nextHook();
|
||||
// We don't actually store the instance anywhere if there is no ref callback
|
||||
// and if there is a ref callback it might not store it but if it does we
|
||||
// have no way of knowing where. So let's only enable introspection of the
|
||||
// ref itself if it is using the object form.
|
||||
let instance = undefined;
|
||||
if (ref !== null && typeof ref === 'object') {
|
||||
instance = ref.current;
|
||||
}
|
||||
hookLog.push({
|
||||
primitive: 'ImperativeHandle',
|
||||
stackError: new Error(),
|
||||
value: instance,
|
||||
});
|
||||
}
|
||||
|
||||
function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
|
||||
hookLog.push({
|
||||
primitive: 'DebugValue',
|
||||
stackError: new Error(),
|
||||
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
|
||||
});
|
||||
}
|
||||
|
||||
function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
|
||||
let hook = nextHook();
|
||||
hookLog.push({
|
||||
primitive: 'Callback',
|
||||
stackError: new Error(),
|
||||
value: hook !== null ? hook.memoizedState[0] : callback,
|
||||
});
|
||||
return callback;
|
||||
}
|
||||
|
||||
function useMemo<T>(
|
||||
nextCreate: () => T,
|
||||
inputs: Array<mixed> | void | null,
|
||||
): T {
|
||||
let hook = nextHook();
|
||||
let value = hook !== null ? hook.memoizedState[0] : nextCreate();
|
||||
hookLog.push({primitive: 'Memo', stackError: new Error(), value});
|
||||
return value;
|
||||
}
|
||||
|
||||
const Dispatcher: DispatcherType = {
|
||||
readContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useDebugValue,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
};
|
||||
|
||||
// Inspect
|
||||
|
||||
type HooksNode = {
|
||||
name: string,
|
||||
value: mixed,
|
||||
subHooks: Array<HooksNode>,
|
||||
};
|
||||
type HooksTree = Array<HooksNode>;
|
||||
|
||||
// Don't assume
|
||||
//
|
||||
// We can't assume that stack frames are nth steps away from anything.
|
||||
// E.g. we can't assume that the root call shares all frames with the stack
|
||||
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
|
||||
// in a wrapper constructor like a polyfill. That'll add an extra frame.
|
||||
// Similar things can happen with the call to the dispatcher. The top frame
|
||||
// may not be the primitive. Likewise the primitive can have fewer stack frames
|
||||
// such as when a call to useState got inlined to use dispatcher.useState.
|
||||
//
|
||||
// We also can't assume that the last frame of the root call is the same
|
||||
// frame as the last frame of the hook call because long stack traces can be
|
||||
// truncated to a stack trace limit.
|
||||
|
||||
let mostLikelyAncestorIndex = 0;
|
||||
|
||||
function findSharedIndex(hookStack, rootStack, rootIndex) {
|
||||
let source = rootStack[rootIndex].source;
|
||||
hookSearch: for (let i = 0; i < hookStack.length; i++) {
|
||||
if (hookStack[i].source === source) {
|
||||
// This looks like a match. Validate that the rest of both stack match up.
|
||||
for (
|
||||
let a = rootIndex + 1, b = i + 1;
|
||||
a < rootStack.length && b < hookStack.length;
|
||||
a++, b++
|
||||
) {
|
||||
if (hookStack[b].source !== rootStack[a].source) {
|
||||
// If not, give up and try a different match.
|
||||
continue hookSearch;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findCommonAncestorIndex(rootStack, hookStack) {
|
||||
let rootIndex = findSharedIndex(
|
||||
hookStack,
|
||||
rootStack,
|
||||
mostLikelyAncestorIndex,
|
||||
);
|
||||
if (rootIndex !== -1) {
|
||||
return rootIndex;
|
||||
}
|
||||
// If the most likely one wasn't a hit, try any other frame to see if it is shared.
|
||||
// If that takes more than 5 frames, something probably went wrong.
|
||||
for (let i = 0; i < rootStack.length && i < 5; i++) {
|
||||
rootIndex = findSharedIndex(hookStack, rootStack, i);
|
||||
if (rootIndex !== -1) {
|
||||
mostLikelyAncestorIndex = i;
|
||||
return rootIndex;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isReactWrapper(functionName, primitiveName) {
|
||||
if (!functionName) {
|
||||
return false;
|
||||
}
|
||||
let expectedPrimitiveName = 'use' + primitiveName;
|
||||
if (functionName.length < expectedPrimitiveName.length) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
functionName.lastIndexOf(expectedPrimitiveName) ===
|
||||
functionName.length - expectedPrimitiveName.length
|
||||
);
|
||||
}
|
||||
|
||||
function findPrimitiveIndex(hookStack, hook) {
|
||||
let stackCache = getPrimitiveStackCache();
|
||||
let primitiveStack = stackCache.get(hook.primitive);
|
||||
if (primitiveStack === undefined) {
|
||||
return -1;
|
||||
}
|
||||
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
|
||||
if (primitiveStack[i].source !== hookStack[i].source) {
|
||||
// If the next two frames are functions called `useX` then we assume that they're part of the
|
||||
// wrappers that the React packager or other packages adds around the dispatcher.
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.primitive)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
if (
|
||||
i < hookStack.length - 1 &&
|
||||
isReactWrapper(hookStack[i].functionName, hook.primitive)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseTrimmedStack(rootStack, hook) {
|
||||
// Get the stack trace between the primitive hook function and
|
||||
// the root function call. I.e. the stack frames of custom hooks.
|
||||
let hookStack = ErrorStackParser.parse(hook.stackError);
|
||||
let rootIndex = findCommonAncestorIndex(rootStack, hookStack);
|
||||
let primitiveIndex = findPrimitiveIndex(hookStack, hook);
|
||||
if (
|
||||
rootIndex === -1 ||
|
||||
primitiveIndex === -1 ||
|
||||
rootIndex - primitiveIndex < 2
|
||||
) {
|
||||
// Something went wrong. Give up.
|
||||
return null;
|
||||
}
|
||||
return hookStack.slice(primitiveIndex, rootIndex - 1);
|
||||
}
|
||||
|
||||
function parseCustomHookName(functionName: void | string): string {
|
||||
if (!functionName) {
|
||||
return '';
|
||||
}
|
||||
let startIndex = functionName.lastIndexOf('.');
|
||||
if (startIndex === -1) {
|
||||
startIndex = 0;
|
||||
}
|
||||
if (functionName.substr(startIndex, 3) === 'use') {
|
||||
startIndex += 3;
|
||||
}
|
||||
return functionName.substr(startIndex);
|
||||
}
|
||||
|
||||
function buildTree(rootStack, readHookLog): HooksTree {
|
||||
let rootChildren = [];
|
||||
let prevStack = null;
|
||||
let levelChildren = rootChildren;
|
||||
let stackOfChildren = [];
|
||||
for (let i = 0; i < readHookLog.length; i++) {
|
||||
let hook = readHookLog[i];
|
||||
let stack = parseTrimmedStack(rootStack, hook);
|
||||
if (stack !== null) {
|
||||
// Note: The indices 0 <= n < length-1 will contain the names.
|
||||
// The indices 1 <= n < length will contain the source locations.
|
||||
// That's why we get the name from n - 1 and don't check the source
|
||||
// of index 0.
|
||||
let commonSteps = 0;
|
||||
if (prevStack !== null) {
|
||||
// Compare the current level's stack to the new stack.
|
||||
while (commonSteps < stack.length && commonSteps < prevStack.length) {
|
||||
let stackSource = stack[stack.length - commonSteps - 1].source;
|
||||
let prevSource = prevStack[prevStack.length - commonSteps - 1].source;
|
||||
if (stackSource !== prevSource) {
|
||||
break;
|
||||
}
|
||||
commonSteps++;
|
||||
}
|
||||
// Pop back the stack as many steps as were not common.
|
||||
for (let j = prevStack.length - 1; j > commonSteps; j--) {
|
||||
levelChildren = stackOfChildren.pop();
|
||||
}
|
||||
}
|
||||
// The remaining part of the new stack are custom hooks. Push them
|
||||
// to the tree.
|
||||
for (let j = stack.length - commonSteps - 1; j >= 1; j--) {
|
||||
let children = [];
|
||||
levelChildren.push({
|
||||
name: parseCustomHookName(stack[j - 1].functionName),
|
||||
value: undefined,
|
||||
subHooks: children,
|
||||
});
|
||||
stackOfChildren.push(levelChildren);
|
||||
levelChildren = children;
|
||||
}
|
||||
prevStack = stack;
|
||||
}
|
||||
levelChildren.push({
|
||||
name: hook.primitive,
|
||||
value: hook.value,
|
||||
subHooks: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Associate custom hook values (useDebugValue() hook entries) with the correct hooks.
|
||||
processDebugValues(rootChildren, null);
|
||||
|
||||
return rootChildren;
|
||||
}
|
||||
|
||||
// Custom hooks support user-configurable labels (via the special useDebugValue() hook).
|
||||
// That hook adds user-provided values to the hooks tree,
|
||||
// but these values aren't intended to appear alongside of the other hooks.
|
||||
// Instead they should be attributed to their parent custom hook.
|
||||
// This method walks the tree and assigns debug values to their custom hook owners.
|
||||
function processDebugValues(
|
||||
hooksTree: HooksTree,
|
||||
parentHooksNode: HooksNode | null,
|
||||
): void {
|
||||
let debugValueHooksNodes: Array<HooksNode> = [];
|
||||
|
||||
for (let i = 0; i < hooksTree.length; i++) {
|
||||
const hooksNode = hooksTree[i];
|
||||
if (hooksNode.name === 'DebugValue' && hooksNode.subHooks.length === 0) {
|
||||
hooksTree.splice(i, 1);
|
||||
i--;
|
||||
debugValueHooksNodes.push(hooksNode);
|
||||
} else {
|
||||
processDebugValues(hooksNode.subHooks, hooksNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble debug value labels to their custom hook owner.
|
||||
// If there is no parent hook, just ignore them for now.
|
||||
// (We may warn about this in the future.)
|
||||
if (parentHooksNode !== null) {
|
||||
if (debugValueHooksNodes.length === 1) {
|
||||
parentHooksNode.value = debugValueHooksNodes[0].value;
|
||||
} else if (debugValueHooksNodes.length > 1) {
|
||||
parentHooksNode.value = debugValueHooksNodes.map(({value}) => value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function inspectHooks<Props>(
|
||||
renderFunction: Props => React$Node,
|
||||
props: Props,
|
||||
currentDispatcher: ?CurrentDispatcherRef,
|
||||
): HooksTree {
|
||||
// DevTools will pass the current renderer's injected dispatcher.
|
||||
// Other apps might compile debug hooks as part of their app though.
|
||||
if (currentDispatcher == null) {
|
||||
currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
||||
}
|
||||
|
||||
let previousDispatcher = currentDispatcher.current;
|
||||
let readHookLog;
|
||||
currentDispatcher.current = Dispatcher;
|
||||
let ancestorStackError;
|
||||
try {
|
||||
ancestorStackError = new Error();
|
||||
renderFunction(props);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
currentDispatcher.current = previousDispatcher;
|
||||
}
|
||||
let rootStack = ErrorStackParser.parse(ancestorStackError);
|
||||
return buildTree(rootStack, readHookLog);
|
||||
}
|
||||
|
||||
function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
|
||||
let current = fiber;
|
||||
while (current) {
|
||||
if (current.tag === ContextProvider) {
|
||||
const providerType: ReactProviderType<any> = current.type;
|
||||
const context: ReactContext<any> = providerType._context;
|
||||
if (!contextMap.has(context)) {
|
||||
// Store the current value that we're going to restore later.
|
||||
contextMap.set(context, context._currentValue);
|
||||
// Set the inner most provider value on the context.
|
||||
context._currentValue = current.memoizedProps.value;
|
||||
}
|
||||
}
|
||||
current = current.return;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreContexts(contextMap: Map<ReactContext<any>, any>) {
|
||||
contextMap.forEach((value, context) => (context._currentValue = value));
|
||||
}
|
||||
|
||||
function inspectHooksOfForwardRef<Props, Ref>(
|
||||
renderFunction: (Props, Ref) => React$Node,
|
||||
props: Props,
|
||||
ref: Ref,
|
||||
currentDispatcher: CurrentDispatcherRef,
|
||||
): HooksTree {
|
||||
let previousDispatcher = currentDispatcher.current;
|
||||
let readHookLog;
|
||||
currentDispatcher.current = Dispatcher;
|
||||
let ancestorStackError;
|
||||
try {
|
||||
ancestorStackError = new Error();
|
||||
renderFunction(props, ref);
|
||||
} finally {
|
||||
readHookLog = hookLog;
|
||||
hookLog = [];
|
||||
currentDispatcher.current = previousDispatcher;
|
||||
}
|
||||
let rootStack = ErrorStackParser.parse(ancestorStackError);
|
||||
return buildTree(rootStack, readHookLog);
|
||||
}
|
||||
|
||||
function resolveDefaultProps(Component, baseProps) {
|
||||
if (Component && Component.defaultProps) {
|
||||
// Resolve default props. Taken from ReactElement
|
||||
const props = Object.assign({}, baseProps);
|
||||
const defaultProps = Component.defaultProps;
|
||||
for (let propName in defaultProps) {
|
||||
if (props[propName] === undefined) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
return baseProps;
|
||||
}
|
||||
|
||||
export function inspectHooksOfFiber(
|
||||
fiber: Fiber,
|
||||
currentDispatcher: ?CurrentDispatcherRef,
|
||||
) {
|
||||
// DevTools will pass the current renderer's injected dispatcher.
|
||||
// Other apps might compile debug hooks as part of their app though.
|
||||
if (currentDispatcher == null) {
|
||||
currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
||||
}
|
||||
|
||||
if (
|
||||
fiber.tag !== FunctionComponent &&
|
||||
fiber.tag !== SimpleMemoComponent &&
|
||||
fiber.tag !== ForwardRef
|
||||
) {
|
||||
throw new Error(
|
||||
'Unknown Fiber. Needs to be a function component to inspect hooks.',
|
||||
);
|
||||
}
|
||||
// Warm up the cache so that it doesn't consume the currentHook.
|
||||
getPrimitiveStackCache();
|
||||
let type = fiber.type;
|
||||
let props = fiber.memoizedProps;
|
||||
if (type !== fiber.elementType) {
|
||||
props = resolveDefaultProps(type, props);
|
||||
}
|
||||
// Set up the current hook so that we can step through and read the
|
||||
// current state from them.
|
||||
currentHook = (fiber.memoizedState: Hook);
|
||||
let contextMap = new Map();
|
||||
try {
|
||||
setupContexts(contextMap, fiber);
|
||||
if (fiber.tag === ForwardRef) {
|
||||
return inspectHooksOfForwardRef(
|
||||
type.render,
|
||||
props,
|
||||
fiber.ref,
|
||||
currentDispatcher,
|
||||
);
|
||||
}
|
||||
return inspectHooks(type, props, currentDispatcher);
|
||||
} finally {
|
||||
currentHook = null;
|
||||
restoreContexts(contextMap);
|
||||
}
|
||||
}
|
||||
12
packages/react-debug-tools/src/ReactDebugTools.js
vendored
Normal file
12
packages/react-debug-tools/src/ReactDebugTools.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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 {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks';
|
||||
|
||||
export {inspectHooks, inspectHooksOfFiber};
|
||||
285
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
vendored
Normal file
285
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDebugTools;
|
||||
|
||||
describe('ReactHooksInspection', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
it('should inspect a simple useState hook', () => {
|
||||
function Foo(props) {
|
||||
let [state] = React.useState('hello world');
|
||||
return <div>{state}</div>;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'State',
|
||||
value: 'hello world',
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect a simple custom hook', () => {
|
||||
function useCustom(value) {
|
||||
let [state] = React.useState(value);
|
||||
React.useDebugValue('custom hook label');
|
||||
return state;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value = useCustom('hello world');
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: __DEV__ ? 'custom hook label' : undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'State',
|
||||
value: 'hello world',
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect a tree of multiple hooks', () => {
|
||||
function effect() {}
|
||||
function useCustom(value) {
|
||||
let [state] = React.useState(value);
|
||||
React.useEffect(effect);
|
||||
return state;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value1 = useCustom('hello');
|
||||
let value2 = useCustom('world');
|
||||
return (
|
||||
<div>
|
||||
{value1} {value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'State',
|
||||
subHooks: [],
|
||||
value: 'hello',
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
subHooks: [],
|
||||
value: effect,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'State',
|
||||
value: 'world',
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect a tree of multiple levels of hooks', () => {
|
||||
function effect() {}
|
||||
function useCustom(value) {
|
||||
let [state] = React.useReducer((s, a) => s, value);
|
||||
React.useEffect(effect);
|
||||
return state;
|
||||
}
|
||||
function useBar(value) {
|
||||
let result = useCustom(value);
|
||||
React.useLayoutEffect(effect);
|
||||
return result;
|
||||
}
|
||||
function useBaz(value) {
|
||||
React.useLayoutEffect(effect);
|
||||
let result = useCustom(value);
|
||||
return result;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value1 = useBar('hello');
|
||||
let value2 = useBaz('world');
|
||||
return (
|
||||
<div>
|
||||
{value1} {value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Bar',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Reducer',
|
||||
value: 'hello',
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'LayoutEffect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Baz',
|
||||
value: undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'LayoutEffect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Custom',
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Reducer',
|
||||
subHooks: [],
|
||||
value: 'world',
|
||||
},
|
||||
{
|
||||
name: 'Effect',
|
||||
subHooks: [],
|
||||
value: effect,
|
||||
},
|
||||
],
|
||||
value: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect the default value using the useContext hook', () => {
|
||||
let MyContext = React.createContext('default');
|
||||
function Foo(props) {
|
||||
let value = React.useContext(MyContext);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Context',
|
||||
value: 'default',
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support an injected dispatcher', () => {
|
||||
function Foo(props) {
|
||||
let [state] = React.useState('hello world');
|
||||
return <div>{state}</div>;
|
||||
}
|
||||
|
||||
let initial = {};
|
||||
let current = initial;
|
||||
let getterCalls = 0;
|
||||
let setterCalls = [];
|
||||
let FakeDispatcherRef = {
|
||||
get current() {
|
||||
getterCalls++;
|
||||
return current;
|
||||
},
|
||||
set current(value) {
|
||||
setterCalls.push(value);
|
||||
current = value;
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
ReactDebugTools.inspectHooks(Foo, {}, FakeDispatcherRef);
|
||||
}).toThrow(
|
||||
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
|
||||
' one of the following reasons:\n' +
|
||||
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
|
||||
'2. You might be breaking the Rules of Hooks\n' +
|
||||
'3. You might have more than one copy of React in the same app\n' +
|
||||
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
);
|
||||
|
||||
expect(getterCalls).toBe(1);
|
||||
expect(setterCalls).toHaveLength(2);
|
||||
expect(setterCalls[0]).not.toBe(initial);
|
||||
expect(setterCalls[1]).toBe(initial);
|
||||
});
|
||||
|
||||
describe('useDebugValue', () => {
|
||||
it('should be ignored when called outside of a custom hook', () => {
|
||||
function Foo(props) {
|
||||
React.useDebugValue('this is invalid');
|
||||
return null;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should support an optional formatter function param', () => {
|
||||
function useCustom() {
|
||||
React.useDebugValue({bar: 123}, object => `bar:${object.bar}`);
|
||||
React.useState(0);
|
||||
}
|
||||
function Foo(props) {
|
||||
useCustom();
|
||||
return null;
|
||||
}
|
||||
let tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: __DEV__ ? 'bar:123' : undefined,
|
||||
subHooks: [{name: 'State', subHooks: [], value: 0}],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
473
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
vendored
Normal file
473
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
vendored
Normal file
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactTestRenderer;
|
||||
let ReactDebugTools;
|
||||
let act;
|
||||
|
||||
describe('ReactHooksInspectionIntegration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
act = ReactTestRenderer.act;
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
it('should inspect the current state of useState hooks', () => {
|
||||
let useState = React.useState;
|
||||
function Foo(props) {
|
||||
let [state1, setState1] = useState('hello');
|
||||
let [state2, setState2] = useState('world');
|
||||
return (
|
||||
<div onMouseDown={setState1} onMouseUp={setState2}>
|
||||
{state1} {state2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
|
||||
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'hello', subHooks: []},
|
||||
{name: 'State', value: 'world', subHooks: []},
|
||||
]);
|
||||
|
||||
let {
|
||||
onMouseDown: setStateA,
|
||||
onMouseUp: setStateB,
|
||||
} = renderer.root.findByType('div').props;
|
||||
|
||||
act(() => setStateA('Hi'));
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'Hi', subHooks: []},
|
||||
{name: 'State', value: 'world', subHooks: []},
|
||||
]);
|
||||
|
||||
act(() => setStateB('world!'));
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'Hi', subHooks: []},
|
||||
{name: 'State', value: 'world!', subHooks: []},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect the current state of all stateful hooks', () => {
|
||||
let outsideRef = React.createRef();
|
||||
function effect() {}
|
||||
function Foo(props) {
|
||||
let [state1, setState] = React.useState('a');
|
||||
let [state2, dispatch] = React.useReducer((s, a) => a.value, 'b');
|
||||
let ref = React.useRef('c');
|
||||
|
||||
React.useLayoutEffect(effect);
|
||||
React.useEffect(effect);
|
||||
|
||||
React.useImperativeHandle(
|
||||
outsideRef,
|
||||
() => {
|
||||
// Return a function so that jest treats them as non-equal.
|
||||
return function Instance() {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
React.useMemo(() => state1 + state2, [state1]);
|
||||
|
||||
function update() {
|
||||
act(() => {
|
||||
setState('A');
|
||||
});
|
||||
act(() => {
|
||||
dispatch({value: 'B'});
|
||||
});
|
||||
ref.current = 'C';
|
||||
}
|
||||
let memoizedUpdate = React.useCallback(update, []);
|
||||
return (
|
||||
<div onClick={memoizedUpdate}>
|
||||
{state1} {state2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
|
||||
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
|
||||
let {onClick: updateStates} = renderer.root.findByType('div').props;
|
||||
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'a', subHooks: []},
|
||||
{name: 'Reducer', value: 'b', subHooks: []},
|
||||
{name: 'Ref', value: 'c', subHooks: []},
|
||||
{name: 'LayoutEffect', value: effect, subHooks: []},
|
||||
{name: 'Effect', value: effect, subHooks: []},
|
||||
{name: 'ImperativeHandle', value: outsideRef.current, subHooks: []},
|
||||
{name: 'Memo', value: 'ab', subHooks: []},
|
||||
{name: 'Callback', value: updateStates, subHooks: []},
|
||||
]);
|
||||
|
||||
updateStates();
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
|
||||
expect(tree).toEqual([
|
||||
{name: 'State', value: 'A', subHooks: []},
|
||||
{name: 'Reducer', value: 'B', subHooks: []},
|
||||
{name: 'Ref', value: 'C', subHooks: []},
|
||||
{name: 'LayoutEffect', value: effect, subHooks: []},
|
||||
{name: 'Effect', value: effect, subHooks: []},
|
||||
{name: 'ImperativeHandle', value: outsideRef.current, subHooks: []},
|
||||
{name: 'Memo', value: 'Ab', subHooks: []},
|
||||
{name: 'Callback', value: updateStates, subHooks: []},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect the value of the current provider in useContext', () => {
|
||||
let MyContext = React.createContext('default');
|
||||
function Foo(props) {
|
||||
let value = React.useContext(MyContext);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(
|
||||
<MyContext.Provider value="contextual">
|
||||
<Foo prop="prop" />
|
||||
</MyContext.Provider>,
|
||||
);
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Context',
|
||||
value: 'contextual',
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect forwardRef', () => {
|
||||
let obj = function() {};
|
||||
let Foo = React.forwardRef(function(props, ref) {
|
||||
React.useImperativeHandle(ref, () => obj);
|
||||
return <div />;
|
||||
});
|
||||
let ref = React.createRef();
|
||||
let renderer = ReactTestRenderer.create(<Foo ref={ref} />);
|
||||
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'ImperativeHandle', value: obj, subHooks: []},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inspect memo', () => {
|
||||
function InnerFoo(props) {
|
||||
let [value] = React.useState('hello');
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let Foo = React.memo(InnerFoo);
|
||||
let renderer = ReactTestRenderer.create(<Foo />);
|
||||
// TODO: Test renderer findByType is broken for memo. Have to search for the inner.
|
||||
let childFiber = renderer.root.findByType(InnerFoo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([{name: 'State', value: 'hello', subHooks: []}]);
|
||||
});
|
||||
|
||||
it('should inspect custom hooks', () => {
|
||||
function useCustom() {
|
||||
let [value] = React.useState('hello');
|
||||
return value;
|
||||
}
|
||||
function Foo(props) {
|
||||
let value = useCustom();
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Foo />);
|
||||
let childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: undefined,
|
||||
subHooks: [{name: 'State', value: 'hello', subHooks: []}],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('useDebugValue', () => {
|
||||
it('should support inspectable values for multiple custom hooks', () => {
|
||||
function useLabeledValue(label) {
|
||||
let [value] = React.useState(label);
|
||||
React.useDebugValue(`custom label ${label}`);
|
||||
return value;
|
||||
}
|
||||
function useAnonymous(label) {
|
||||
let [value] = React.useState(label);
|
||||
return value;
|
||||
}
|
||||
function Example() {
|
||||
useLabeledValue('a');
|
||||
React.useState('b');
|
||||
useAnonymous('c');
|
||||
useLabeledValue('d');
|
||||
return null;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Example />);
|
||||
let childFiber = renderer.root.findByType(Example)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'LabeledValue',
|
||||
value: __DEV__ ? 'custom label a' : undefined,
|
||||
subHooks: [{name: 'State', value: 'a', subHooks: []}],
|
||||
},
|
||||
{
|
||||
name: 'State',
|
||||
value: 'b',
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
value: undefined,
|
||||
subHooks: [{name: 'State', value: 'c', subHooks: []}],
|
||||
},
|
||||
{
|
||||
name: 'LabeledValue',
|
||||
value: __DEV__ ? 'custom label d' : undefined,
|
||||
subHooks: [{name: 'State', value: 'd', subHooks: []}],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support inspectable values for nested custom hooks', () => {
|
||||
function useInner() {
|
||||
React.useDebugValue('inner');
|
||||
React.useState(0);
|
||||
}
|
||||
function useOuter() {
|
||||
React.useDebugValue('outer');
|
||||
useInner();
|
||||
}
|
||||
function Example() {
|
||||
useOuter();
|
||||
return null;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Example />);
|
||||
let childFiber = renderer.root.findByType(Example)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Outer',
|
||||
value: __DEV__ ? 'outer' : undefined,
|
||||
subHooks: [
|
||||
{
|
||||
name: 'Inner',
|
||||
value: __DEV__ ? 'inner' : undefined,
|
||||
subHooks: [{name: 'State', value: 0, subHooks: []}],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support multiple inspectable values per custom hooks', () => {
|
||||
function useMultiLabelCustom() {
|
||||
React.useDebugValue('one');
|
||||
React.useDebugValue('two');
|
||||
React.useDebugValue('three');
|
||||
React.useState(0);
|
||||
}
|
||||
function useSingleLabelCustom(value) {
|
||||
React.useDebugValue(`single ${value}`);
|
||||
React.useState(0);
|
||||
}
|
||||
function Example() {
|
||||
useSingleLabelCustom('one');
|
||||
useMultiLabelCustom();
|
||||
useSingleLabelCustom('two');
|
||||
return null;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Example />);
|
||||
let childFiber = renderer.root.findByType(Example)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'SingleLabelCustom',
|
||||
value: __DEV__ ? 'single one' : undefined,
|
||||
subHooks: [{name: 'State', value: 0, subHooks: []}],
|
||||
},
|
||||
{
|
||||
name: 'MultiLabelCustom',
|
||||
value: __DEV__ ? ['one', 'two', 'three'] : undefined,
|
||||
subHooks: [{name: 'State', value: 0, subHooks: []}],
|
||||
},
|
||||
{
|
||||
name: 'SingleLabelCustom',
|
||||
value: __DEV__ ? 'single two' : undefined,
|
||||
subHooks: [{name: 'State', value: 0, subHooks: []}],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore useDebugValue() made outside of a custom hook', () => {
|
||||
function Example() {
|
||||
React.useDebugValue('this is invalid');
|
||||
return null;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Example />);
|
||||
let childFiber = renderer.root.findByType(Example)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should support an optional formatter function param', () => {
|
||||
function useCustom() {
|
||||
React.useDebugValue({bar: 123}, object => `bar:${object.bar}`);
|
||||
React.useState(0);
|
||||
}
|
||||
function Example() {
|
||||
useCustom();
|
||||
return null;
|
||||
}
|
||||
let renderer = ReactTestRenderer.create(<Example />);
|
||||
let childFiber = renderer.root.findByType(Example)._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
name: 'Custom',
|
||||
value: __DEV__ ? 'bar:123' : undefined,
|
||||
subHooks: [{name: 'State', subHooks: [], value: 0}],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support defaultProps and lazy', async () => {
|
||||
let Suspense = React.Suspense;
|
||||
|
||||
function Foo(props) {
|
||||
let [value] = React.useState(props.defaultValue.substr(0, 3));
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
Foo.defaultProps = {
|
||||
defaultValue: 'default',
|
||||
};
|
||||
|
||||
async function fakeImport(result) {
|
||||
return {default: result};
|
||||
}
|
||||
|
||||
let LazyFoo = React.lazy(() => fakeImport(Foo));
|
||||
|
||||
let renderer = ReactTestRenderer.create(
|
||||
<Suspense fallback="Loading...">
|
||||
<LazyFoo />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
await LazyFoo;
|
||||
|
||||
let childFiber = renderer.root._currentFiber();
|
||||
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([{name: 'State', value: 'def', subHooks: []}]);
|
||||
});
|
||||
|
||||
it('should support an injected dispatcher', () => {
|
||||
function Foo(props) {
|
||||
let [state] = React.useState('hello world');
|
||||
return <div>{state}</div>;
|
||||
}
|
||||
|
||||
let initial = {};
|
||||
let current = initial;
|
||||
let getterCalls = 0;
|
||||
let setterCalls = [];
|
||||
let FakeDispatcherRef = {
|
||||
get current() {
|
||||
getterCalls++;
|
||||
return current;
|
||||
},
|
||||
set current(value) {
|
||||
setterCalls.push(value);
|
||||
current = value;
|
||||
},
|
||||
};
|
||||
|
||||
let renderer = ReactTestRenderer.create(<Foo />);
|
||||
let childFiber = renderer.root._currentFiber();
|
||||
expect(() => {
|
||||
ReactDebugTools.inspectHooksOfFiber(childFiber, FakeDispatcherRef);
|
||||
}).toThrow(
|
||||
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
|
||||
' one of the following reasons:\n' +
|
||||
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
|
||||
'2. You might be breaking the Rules of Hooks\n' +
|
||||
'3. You might have more than one copy of React in the same app\n' +
|
||||
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
);
|
||||
|
||||
expect(getterCalls).toBe(1);
|
||||
expect(setterCalls).toHaveLength(2);
|
||||
expect(setterCalls[0]).not.toBe(initial);
|
||||
expect(setterCalls[1]).toBe(initial);
|
||||
});
|
||||
|
||||
// This test case is based on an open source bug report:
|
||||
// facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
|
||||
it('should properly advance the current hook for useContext', () => {
|
||||
const MyContext = React.createContext(1);
|
||||
|
||||
let incrementCount;
|
||||
|
||||
function Foo(props) {
|
||||
const context = React.useContext(MyContext);
|
||||
const [data, setData] = React.useState({count: context});
|
||||
|
||||
incrementCount = () => setData(({count}) => ({count: count + 1}));
|
||||
|
||||
return <div>count: {data.count}</div>;
|
||||
}
|
||||
|
||||
const renderer = ReactTestRenderer.create(<Foo />);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
props: {},
|
||||
children: ['count: ', '1'],
|
||||
});
|
||||
|
||||
act(incrementCount);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
props: {},
|
||||
children: ['count: ', '2'],
|
||||
});
|
||||
|
||||
const childFiber = renderer.root._currentFiber();
|
||||
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
expect(tree).toEqual([
|
||||
{name: 'Context', value: 1, subHooks: []},
|
||||
{name: 'State', value: {count: 2}, subHooks: []},
|
||||
]);
|
||||
});
|
||||
});
|
||||
7
packages/react-dom/npm/unstable-fizz.browser.js
Normal file
7
packages/react-dom/npm/unstable-fizz.browser.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./cjs/react-dom-unstable-fizz.browser.production.min.js');
|
||||
} else {
|
||||
module.exports = require('./cjs/react-dom-unstable-fizz.browser.development.js');
|
||||
}
|
||||
3
packages/react-dom/npm/unstable-fizz.js
vendored
Normal file
3
packages/react-dom/npm/unstable-fizz.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./unstable-fizz.node');
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user