diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..4d17014 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + env: { + browser: true, + es6: true, + }, + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + plugins: [ + 'react', + '@typescript-eslint', + 'react-hooks', + '@react-hook-utilities', + ], + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + '@react-hook-utilities/exhaustive-deps': 'warn', + }, +}; diff --git a/.gitignore b/.gitignore index a5107aa..242f43b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ dist/ *.log coverage/ -docs/ \ No newline at end of file +docs/ +pageDocs/ \ No newline at end of file diff --git a/README.md b/README.md index 70ef41d..a7a0da7 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,21 @@ A state that only resolves after setting truthy values. # Typescript -react-hook-utilities sees Typescript is a first-class citizen. The library is built for and around Typescript and you'll get bonus points for using it. Nonetheless, pure JavaScript files are also available if you're _that_ guy. +**react-hook-utilities** sees Typescript is a first-class citizen. The library is built for and around Typescript and you'll get bonus points for using it. Nonetheless, pure JavaScript files are also available if you're _that_ guy. + +# ESLint + +If you're using ESLint and don't want to lose your errors and warnings regarding dependencies, **react-hook-utilities** comes packaged with an [ESLint plugin](eslint-plugin/README.md) to lint it's own hooks. It is recommended to install the plugin as a local dependency: + +```sh +$ yarn add -D ./node_modules/react-hook-utilities/eslint-plugin +``` + +We recommend you read the [full documentation](eslint-plugin/README.md) on how to use the ESLint plugin + +# Documentation + +The documentation is available at: https://fjcaetano.github.com/react-hook-utilities # [Full Documentation](https://fjcaetano.github.com/react-hook-utilities) diff --git a/eslint-plugin/README.md b/eslint-plugin/README.md new file mode 100644 index 0000000..a1cea19 --- /dev/null +++ b/eslint-plugin/README.md @@ -0,0 +1,51 @@ +# @react-hook-utilities/eslint-plugin + +ESLint rules for hook react-hook-utilities + +## Installation + +You'll first need to install [ESLint](http://eslint.org) and [react-hook-utilities](https://www.npmjs.com/package/react-hook-utilities) + +``` +$ npm i eslint react-hook-utilities --save-dev +``` + +Next, install `@react-hook-utilities/eslint-plugin` directly from the host dependency: + +``` +$ npm install ./node_modules/react-hook-utilities/eslint-plugin --save-dev +``` + +**Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `react-hook-utilities` globally. + +## Usage + +Add `@react-hook-utilities` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: + +```json +{ + "plugins": [ + "@react-hook-utilities" + ] +} +``` + + +Then configure the rules you want to use under the rules section. + +```json +{ + "rules": { + "@react-hook-utilities/exhaustive-deps": 2 + } +} +``` + +## Supported Rules + +* exhaustive-deps: Ensures all hooks external references have been declared as dependencies. + + + + + diff --git a/eslint-plugin/lib/__tests__/ESLintRuleExhaustiveDeps-test.js b/eslint-plugin/lib/__tests__/ESLintRuleExhaustiveDeps-test.js new file mode 100644 index 0000000..be2e969 --- /dev/null +++ b/eslint-plugin/lib/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -0,0 +1,5067 @@ +/** + * 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. + * + * Original Source: https://github.com/facebook/react/blob/9e64bf18e11828d6b4c0363bff5ed2eca1ccd838/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js + */ +'use strict'; + +const ESLintTester = require('eslint').RuleTester; + +const ReactHooksESLintPlugin = require('..'); + +const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps']; +ESLintTester.setDefaultConfig({ + parser: require.resolve('babel-eslint'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}); + +// *************************************************** +// For easier local testing, you can add to any case: +// { +// skip: true, +// --or-- +// only: true, +// ... +// } +// *************************************************** + +const tests = { + valid: [ + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useDidMount(() => { + console.log(local); + }); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useDidMount(() => { + return history.listen(); + }); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useDidMount(async () => { + console.log(local); + }); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useDidUnmount(() => { + console.log(local); + }); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useDidUnmount(async () => { + console.log(local); + }); + } + `, + }, + { + code: ` + function MyComponent() { + useAsyncEffect(async () => { + const local = {}; + console.log(local); + }, []); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + }, + { + // OK because `props` wasn't defined. + // We don't technically know if `props` is supposed + // to be an import that hasn't been added yet, or + // a component-level variable. Ignore it until it + // gets defined (a different rule would flag it anyway). + code: ` + function MyComponent() { + useAsyncEffect(async () => { + console.log(props.foo); + }, []); + } + `, + }, + { + code: ` + function MyComponent() { + const local1 = {}; + { + const local2 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + }); + } + } + `, + }, + { + code: ` + function MyComponent() { + const local1 = {}; + { + const local2 = {}; + useWorker(async () => { + console.log(local1); + console.log(local2); + }, [local1, local2]); + } + } + `, + }, + { + code: ` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useWorker(async () => { + console.log(local1); + console.log(local2); + }, [local2]); + } + } + `, + }, + { + code: ` + function MyComponent() { + const local1 = {}; + { + const local2 = {}; + useWorkerState(async () => { + console.log(local1); + console.log(local2); + }, [local1, local2]); + } + } + `, + }, + { + code: ` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useWorkerState(async () => { + console.log(local1); + console.log(local2); + }, [local2]); + } + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + console.log(local); + }, [local]); + } + `, + }, + { + code: ` + function MyComponent() { + useAsyncEffect(async () => { + console.log(unresolved); + }, []); + } + `, + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [,,,local,,,]); + } + `, + }, + { + // Regression test + code: ` + function MyComponent({ foo }) { + useAsyncEffect(async () => { + console.log(foo.length); + }, [foo]); + } + `, + }, + { + // Regression test + code: ` + function MyComponent({ foo }) { + useAsyncEffect(async () => { + console.log(foo.length); + console.log(foo.slice(0)); + }, [foo]); + } + `, + }, + { + // Regression test + code: ` + function MyComponent({ history }) { + useAsyncEffect(async () => { + return history.listen(); + }, [history]); + } + `, + }, + { + // Valid because they have meaning without deps. + code: ` + function MyComponent(props) { + useAsyncEffect(async () => {}); + useAsyncLayoutEffect(async () => {}); + useConditionalEffect(props.innerRef, () => {}); + } + `, + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, [props.foo]); + } + `, + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props.bar, props.foo]); + } + `, + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props.foo, props.bar]); + } + `, + }, + { + code: ` + function MyComponent(props) { + const local = {}; + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, [props.foo, props.bar, local]); + } + `, + }, + { + // [props, props.foo] is technically unnecessary ('props' covers 'props.foo'). + // However, it's valid for effects to over-specify their deps. + // So we don't warn about this. We *would* warn about useEffectUpdate/useWorker. + code: ` + function MyComponent(props) { + const local = {}; + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props, props.foo]); + + let color = {} + useAsyncEffect(async () => { + console.log(props.foo.bar.baz); + console.log(color); + }, [props.foo, props.foo.bar.baz, color]); + } + `, + }, + { + // Valid because we don't care about hooks outside of components. + code: ` + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, []); + `, + }, + { + // Valid because we don't care about hooks outside of components. + code: ` + const local1 = {}; + { + const local2 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + }, []); + } + `, + }, + { + code: ` + function MyComponent() { + const ref = useRef(); + useAsyncEffect(async () => { + console.log(ref.current); + }, [ref]); + } + `, + }, + { + code: ` + function MyComponent() { + const ref = useRef(); + useAsyncEffect(async () => { + console.log(ref.current); + }, []); + } + `, + }, + { + code: ` + function MyComponent({ maybeRef2, foo }) { + const definitelyRef1 = useRef(); + const definitelyRef2 = useRef(); + const maybeRef1 = useSomeOtherRefyThing(); + const [state1, setState1] = useState(); + const [state2, setState2] = React.useState(); + const [state3, dispatch1] = useReducer(); + const [state4, dispatch2] = React.useReducer(); + const [state5, maybeSetState] = useFunnyState(); + const [state6, maybeDispatch] = useFunnyReducer(); + const mySetState = useWorker(async () => {}, []); + let myDispatch = useWorker(async () => {}, []); + + useAsyncEffect(async () => { + // Known to be static + console.log(definitelyRef1.current); + console.log(definitelyRef2.current); + console.log(maybeRef1.current); + console.log(maybeRef2.current); + setState1(); + setState2(); + dispatch1(); + dispatch2(); + + // Dynamic + console.log(state1); + console.log(state2); + console.log(state3); + console.log(state4); + console.log(state5); + console.log(state6); + mySetState(); + myDispatch(); + + // Not sure; assume dynamic + maybeSetState(); + maybeDispatch(); + }, [ + // Dynamic + state1, state2, state3, state4, state5, state6, + maybeRef1, maybeRef2, + + // Not sure; assume dynamic + mySetState, myDispatch, + maybeSetState, maybeDispatch + + // In this test, we don't specify static deps. + // That should be okay. + ]); + } + `, + }, + { + code: ` + function MyComponent({ maybeRef2 }) { + const definitelyRef1 = useRef(); + const definitelyRef2 = useRef(); + const maybeRef1 = useSomeOtherRefyThing(); + + const [state1, setState1] = useState(); + const [state2, setState2] = React.useState(); + const [state3, dispatch1] = useReducer(); + const [state4, dispatch2] = React.useReducer(); + + const [state5, maybeSetState] = useFunnyState(); + const [state6, maybeDispatch] = useFunnyReducer(); + + const mySetState = useWorker(async () => {}, []); + let myDispatch = useWorker(async () => {}, []); + + useAsyncEffect(async () => { + // Known to be static + console.log(definitelyRef1.current); + console.log(definitelyRef2.current); + console.log(maybeRef1.current); + console.log(maybeRef2.current); + setState1(); + setState2(); + dispatch1(); + dispatch2(); + + // Dynamic + console.log(state1); + console.log(state2); + console.log(state3); + console.log(state4); + console.log(state5); + console.log(state6); + mySetState(); + myDispatch(); + + // Not sure; assume dynamic + maybeSetState(); + maybeDispatch(); + }, [ + // Dynamic + state1, state2, state3, state4, state5, state6, + maybeRef1, maybeRef2, + + // Not sure; assume dynamic + mySetState, myDispatch, + maybeSetState, maybeDispatch, + + // In this test, we specify static deps. + // That should be okay too! + definitelyRef1, definitelyRef2, setState1, setState2, dispatch1, dispatch2 + ]); + } + `, + }, + { + code: ` + function MyComponent({ maybeRef2, foo }) { + const definitelyRef1 = useRef(); + const definitelyRef2 = useRef(); + const maybeRef1 = useSomeOtherRefyThing(); + const [state1, setState1] = useState(); + const [state2, setState2] = React.useState(); + const [state3, dispatch1] = useReducer(); + const [state4, dispatch2] = React.useReducer(); + const [state5, maybeSetState] = useFunnyState(); + const [state6, maybeDispatch] = useFunnyReducer(); + const mySetState = useWorkerState(async () => {}, []); + let myDispatch = useWorkerState(async () => {}, []); + + useAsyncEffect(async () => { + // Known to be static + console.log(definitelyRef1.current); + console.log(definitelyRef2.current); + console.log(maybeRef1.current); + console.log(maybeRef2.current); + setState1(); + setState2(); + dispatch1(); + dispatch2(); + + // Dynamic + console.log(state1); + console.log(state2); + console.log(state3); + console.log(state4); + console.log(state5); + console.log(state6); + mySetState(); + myDispatch(); + + // Not sure; assume dynamic + maybeSetState(); + maybeDispatch(); + }, [ + // Dynamic + state1, state2, state3, state4, state5, state6, + maybeRef1, maybeRef2, + + // Not sure; assume dynamic + mySetState, myDispatch, + maybeSetState, maybeDispatch + + // In this test, we don't specify static deps. + // That should be okay. + ]); + } + `, + }, + { + code: ` + function MyComponent({ maybeRef2 }) { + const definitelyRef1 = useRef(); + const definitelyRef2 = useRef(); + const maybeRef1 = useSomeOtherRefyThing(); + + const [state1, setState1] = useState(); + const [state2, setState2] = React.useState(); + const [state3, dispatch1] = useReducer(); + const [state4, dispatch2] = React.useReducer(); + + const [state5, maybeSetState] = useFunnyState(); + const [state6, maybeDispatch] = useFunnyReducer(); + + const mySetState = useWorkerState(async () => {}, []); + let myDispatch = useWorkerState(async () => {}, []); + + useAsyncEffect(async () => { + // Known to be static + console.log(definitelyRef1.current); + console.log(definitelyRef2.current); + console.log(maybeRef1.current); + console.log(maybeRef2.current); + setState1(); + setState2(); + dispatch1(); + dispatch2(); + + // Dynamic + console.log(state1); + console.log(state2); + console.log(state3); + console.log(state4); + console.log(state5); + console.log(state6); + mySetState(); + myDispatch(); + + // Not sure; assume dynamic + maybeSetState(); + maybeDispatch(); + }, [ + // Dynamic + state1, state2, state3, state4, state5, state6, + maybeRef1, maybeRef2, + + // Not sure; assume dynamic + mySetState, myDispatch, + maybeSetState, maybeDispatch, + + // In this test, we specify static deps. + // That should be okay too! + definitelyRef1, definitelyRef2, setState1, setState2, dispatch1, dispatch2 + ]); + } + `, + }, + { + code: ` + const MyComponent = forwardRef((props, ref) => { + useConditionalEffect(ref, () => ({ + focus() { + alert(props.hello); + } + })) + }); + `, + }, + { + code: ` + const MyComponent = forwardRef((props, ref) => { + useConditionalEffect(ref, () => ({ + focus() { + alert(props.hello); + } + }), [props.hello]) + }); + `, + }, + { + // This is not ideal but warning would likely create + // too many false positives. We do, however, prevent + // direct assignments. + code: ` + function MyComponent(props) { + let obj = {}; + useAsyncEffect(async () => { + obj.foo = true; + }, [obj]); + } + `, + }, + { + // Valid because we assign ref.current + // ourselves. Therefore it's likely not + // a ref managed by React. + code: ` + function MyComponent() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current = {}; + return () => { + console.log(myRef.current.toString()) + }; + }, []); + return
; + } + `, + }, + { + // Valid because we assign ref.current + // ourselves. Therefore it's likely not + // a ref managed by React. + code: ` + function useMyThing(myRef) { + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current = {}; + return () => { + console.log(myRef.current.toString()) + }; + }, [myRef]); + } + `, + }, + { + // Valid because the ref is captured. + code: ` + function MyComponent() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => {}; + const node = myRef.current; + node.addEventListener('mousemove', handleMove); + return () => node.removeEventListener('mousemove', handleMove); + }, []); + return
; + } + `, + }, + { + // Valid because the ref is captured. + code: ` + function useMyThing(myRef) { + useAsyncEffect(async () => { + const handleMove = () => {}; + const node = myRef.current; + node.addEventListener('mousemove', handleMove); + return () => node.removeEventListener('mousemove', handleMove); + }, [myRef]); + return
; + } + `, + }, + { + // Valid because it's not an effect. + code: ` + function useMyThing(myRef) { + useWorker(async () => { + const handleMouse = () => {}; + myRef.current.addEventListener('mousemove', handleMouse); + myRef.current.addEventListener('mousein', handleMouse); + return function() { + setTimeout(() => { + myRef.current.removeEventListener('mousemove', handleMouse); + myRef.current.removeEventListener('mousein', handleMouse); + }); + } + }, [myRef]); + } + `, + }, + { + // Valid because it's not an effect. + code: ` + function useMyThing(myRef) { + useWorkerState(async () => { + const handleMouse = () => {}; + myRef.current.addEventListener('mousemove', handleMouse); + myRef.current.addEventListener('mousein', handleMouse); + return function() { + setTimeout(() => { + myRef.current.removeEventListener('mousemove', handleMouse); + myRef.current.removeEventListener('mousein', handleMouse); + }); + } + }, [myRef]); + } + `, + }, + { + // Valid because we read ref.current in a function that isn't cleanup. + code: ` + function useMyThing() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => { + console.log(myRef.current) + }; + window.addEventListener('mousemove', handleMove); + return () => window.removeEventListener('mousemove', handleMove); + }, []); + return
; + } + `, + }, + { + // Valid because we read ref.current in a function that isn't cleanup. + code: ` + function useMyThing() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => { + return () => window.removeEventListener('mousemove', handleMove); + }; + window.addEventListener('mousemove', handleMove); + return () => {}; + }, []); + return
; + } + `, + }, + { + // Valid because it's a primitive constant. + code: ` + function MyComponent() { + const local1 = 42; + const local2 = '42'; + const local3 = null; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + console.log(local3); + }, []); + } + `, + }, + { + // It's not a mistake to specify constant values though. + code: ` + function MyComponent() { + const local1 = 42; + const local2 = '42'; + const local3 = null; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + console.log(local3); + }, [local1, local2, local3]); + } + `, + }, + { + // It is valid for effects to over-specify their deps. + code: ` + function MyComponent(props) { + const local = props.local; + useAsyncEffect(async () => {}, [local]); + } + `, + }, + { + // Valid even though activeTab is "unused". + // We allow over-specifying deps for effects, but not callbacks or memo. + code: ` + function Foo({ activeTab }) { + useAsyncEffect(async () => { + window.scrollTo(0, 0); + }, [activeTab]); + } + `, + }, + { + // It is valid to specify broader effect deps than strictly necessary. + // Don't warn for this. + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo.bar.baz); + }, [props]); + useAsyncEffect(async () => { + console.log(props.foo.bar.baz); + }, [props.foo]); + useAsyncEffect(async () => { + console.log(props.foo.bar.baz); + }, [props.foo.bar]); + useAsyncEffect(async () => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz]); + } + `, + }, + { + // It is *also* valid to specify broader memo/callback deps than strictly necessary. + // Don't warn for this either. + code: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + }, [props]); + const fn2 = useWorker(async () => { + console.log(props.foo.bar.baz); + }, [props.foo]); + const fn3 = useEffectUpdate(() => { + console.log(props.foo.bar.baz); + }, [props.foo.bar]); + const fn4 = useEffectUpdate(() => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz]); + } + `, + }, + { + // It is *also* valid to specify broader memo/callback deps than strictly necessary. + // Don't warn for this either. + code: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + }, [props]); + const fn2 = useWorkerState(async () => { + console.log(props.foo.bar.baz); + }, [props.foo]); + const fn3 = useEffectUpdate(() => { + console.log(props.foo.bar.baz); + }, [props.foo.bar]); + const fn4 = useEffectUpdate(() => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz]); + } + `, + }, + { + // Declaring handleNext is optional because + // it doesn't use anything in the function scope. + code: ` + function MyComponent(props) { + function handleNext1() { + console.log('hello'); + } + const handleNext2 = () => { + console.log('hello'); + }; + let handleNext3 = function() { + console.log('hello'); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, []); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, []); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + { + // Declaring handleNext is optional because + // it doesn't use anything in the function scope. + code: ` + function MyComponent(props) { + function handleNext() { + console.log('hello'); + } + useAsyncEffect(async () => { + return Store.subscribe(handleNext); + }, []); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext); + }, []); + useEffectUpdate(() => { + return Store.subscribe(handleNext); + }, []); + } + `, + }, + { + // Declaring handleNext is optional because + // everything they use is fully static. + code: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + + function handleNext1(value) { + let value2 = value * 100; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(foo(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(value); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, []); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, []); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + { + code: ` + function useInterval(callback, delay) { + const savedCallback = useRef(); + useAsyncEffect(async () => { + savedCallback.current = callback; + }); + useAsyncEffect(async () => { + function tick() { + savedCallback.current(); + } + if (delay !== null) { + let id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, [delay]); + } + `, + }, + { + code: ` + function Counter() { + const [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(c => c + 1); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + code: ` + function Counter() { + const [count, setCount] = useState(0); + + function tick() { + setCount(c => c + 1); + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + tick(); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + code: ` + function Counter() { + const [count, dispatch] = useReducer((state, action) => { + if (action === 'inc') { + return state + 1; + } + }, 0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + dispatch('inc'); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + code: ` + function Counter() { + const [count, dispatch] = useReducer((state, action) => { + if (action === 'inc') { + return state + 1; + } + }, 0); + + const tick = () => { + dispatch('inc'); + }; + + useAsyncEffect(async () => { + let id = setInterval(tick, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + // Regression test for a crash + code: ` + function Podcasts() { + useAsyncEffect(async () => { + setPodcasts([]); + }, []); + let [podcasts, setPodcasts] = useState(null); + } + `, + }, + { + code: ` + function withFetch(fetchPodcasts) { + return function Podcasts({ id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + fetchPodcasts(id).then(setPodcasts); + }, [id]); + } + } + `, + }, + { + code: ` + function Podcasts({ id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + function doFetch({ fetchPodcasts }) { + fetchPodcasts(id).then(setPodcasts); + } + doFetch({ fetchPodcasts: API.fetchPodcasts }); + }, [id]); + } + `, + }, + { + code: ` + function Counter() { + let [count, setCount] = useState(0); + + function increment(x) { + return x + 1; + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + code: ` + function Counter() { + let [count, setCount] = useState(0); + + function increment(x) { + return x + 1; + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => increment(count)); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + code: ` + import increment from './increment'; + function Counter() { + let [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + }, + { + code: ` + function withStuff(increment) { + return function Counter() { + let [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + } + `, + }, + { + code: ` + function App() { + const [query, setQuery] = useState('react'); + const [state, setState] = useState(null); + useAsyncEffect(async () => { + let ignore = false; + fetchSomething(); + async function fetchSomething() { + const result = await (await fetch('http://hn.algolia.com/api/v1/search?query=' + query)).json(); + if (!ignore) setState(result); + } + return () => { ignore = true; }; + }, [query]); + return ( + <> + setQuery(e.target.value)} /> + {JSON.stringify(state)} + + ); + } + `, + }, + { + code: ` + function Example() { + const foo = useWorker(async () => { + foo(); + }, []); + } + `, + }, + { + code: ` + function Example({ prop }) { + const foo = useWorker(async () => { + if (prop) { + foo(); + } + }, [prop]); + } + `, + }, + { + code: ` + function Example() { + const foo = useWorkerState(async () => { + foo(); + }, []); + } + `, + }, + { + code: ` + function Example({ prop }) { + const foo = useWorkerState(async () => { + if (prop) { + foo(); + } + }, [prop]); + } + `, + }, + { + code: ` + function Hello() { + const [state, setState] = useState(0); + useAsyncEffect(async () => { + const handleResize = () => setState(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }); + } + `, + }, // Ignore Generic Type Variables for arrow functions + { + code: ` + function Example({ prop }) { + const bar = useAsyncEffect((a: T): Hello => { + prop(); + }, [prop]); + } + `, + }, // Ignore arguments keyword for arrow functions. + { + code: ` + function Example() { + useAsyncEffect(async () => { + arguments + }, []) + } + `, + }, + { + code: ` + function Example() { + useAsyncEffect(async () => { + const bar = () => { + arguments; + }; + bar(); + }, []) + } + `, + }, + ], + invalid: [ + { + code: ` + function MyComponent() { + const ref = useLazyRef(); + const [state, setState] = useState(); + useAsyncEffect(async () => { + ref.current = {}; + setState(state + 1); + }, []); + } + `, + output: ` + function MyComponent() { + const ref = useLazyRef(); + const [state, setState] = useState(); + useAsyncEffect(async () => { + ref.current = {}; + setState(state + 1); + }, [state]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + ], + }, + { + code: ` + function MyComponent() { + const ref = useLazyRef(); + const [state, setState] = useState(); + useAsyncEffect(() => { + ref.current = {}; + setState(state + 1); + }, [ref]); + } + `, + // We don't ask to remove static deps but don't add them either. + // Don't suggest removing "ref" (it's fine either way) + // but *do* add "state". *Don't* add "setState" ourselves. + output: ` + function MyComponent() { + const ref = useLazyRef(); + const [state, setState] = useState(); + useAsyncEffect(() => { + ref.current = {}; + setState(state + 1); + }, [ref, state]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + ], + }, + { + code: ` + function MyComponent(props) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + useAsyncEffect(() => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, []); + } + `, + output: ` + function MyComponent(props) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + useAsyncEffect(() => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [props.color, props.someOtherRefs]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + useAsyncEffect(() => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [ref1.current, ref2.current, props.someOtherRefs, props.color]); + } + `, + output: ` + function MyComponent(props) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + useAsyncEffect(() => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [props.someOtherRefs, props.color]); + } + `, + errors: [ + "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const ref = useLazyRef(); + useAsyncEffect(() => { + console.log(ref.current); + }, [ref.current]); + } + `, + output: ` + function MyComponent() { + const ref = useLazyRef(); + useAsyncEffect(() => { + console.log(ref.current); + }, []); + } + `, + errors: [ + "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " + + 'Either exclude it or remove the dependency array. ' + + "Mutable values like 'ref.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent({ activeTab }) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + useAsyncEffect(() => { + ref1.current.scrollTop = 0; + ref2.current.scrollTop = 0; + }, [ref1.current, ref2.current, activeTab]); + } + `, + output: ` + function MyComponent({ activeTab }) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + useAsyncEffect(() => { + ref1.current.scrollTop = 0; + ref2.current.scrollTop = 0; + }, [activeTab]); + } + `, + errors: [ + "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent({ activeTab, initY }) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + const fn = useWorker(() => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [ref1.current, ref2.current, activeTab, initY]); + } + `, + output: ` + function MyComponent({ activeTab, initY }) { + const ref1 = useLazyRef(); + const ref2 = useLazyRef(); + const fn = useWorker(() => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [initY]); + } + `, + errors: [ + "React Hook useWorker has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const ref = useLazyRef(); + useAsyncEffect(() => { + console.log(ref.current); + }, [ref.current, ref]); + } + `, + output: ` + function MyComponent() { + const ref = useLazyRef(); + useAsyncEffect(() => { + console.log(ref.current); + }, [ref]); + } + `, + errors: [ + "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " + + 'Either exclude it or remove the dependency array. ' + + "Mutable values like 'ref.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, []); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Note: we *could* detect it's a primitive and never assigned + // even though it's not a constant -- but we currently don't. + // So this is an error. + code: ` + function MyComponent() { + let local = 42; + useAsyncEffect(async () => { + console.log(local); + }, []); + } + `, + output: ` + function MyComponent() { + let local = 42; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regexes are literals but potentially stateful. + code: ` + function MyComponent() { + const local = /foo/; + useAsyncEffect(async () => { + console.log(local); + }, []); + } + `, + output: ` + function MyComponent() { + const local = /foo/; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regression test + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + if (true) { + console.log(local); + } + }, []); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + if (true) { + console.log(local); + } + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regression test + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + try { + console.log(local); + } finally {} + }, []); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + try { + console.log(local); + } finally {} + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regression test + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + function inner() { + console.log(local); + } + inner(); + }, []); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + function inner() { + console.log(local); + } + inner(); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + { + const local2 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + }, []); + } + } + `, + output: ` + function MyComponent() { + const local1 = {}; + { + const local2 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + }, [local1, local2]); + } + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'local1' and 'local2'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + const local2 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + }, [local1]); + } + `, + output: ` + function MyComponent() { + const local1 = {}; + const local2 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + }, [local1, local2]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local2'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useWorker(async () => { + console.log(local1); + console.log(local2); + }, [local1]); + } + } + `, + output: ` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useWorker(async () => { + console.log(local1); + console.log(local2); + }, [local2]); + } + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'local2'. " + + 'Either include it or remove the dependency array. ' + + "Outer scope values like 'local1' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useWorkerState(async () => { + console.log(local1); + console.log(local2); + }, [local1]); + } + } + `, + output: ` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useWorkerState(async () => { + console.log(local1); + console.log(local2); + }, [local2]); + } + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'local2'. " + + 'Either include it or remove the dependency array. ' + + "Outer scope values like 'local1' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + console.log(local); + }, []); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + console.log(local); + }, [local, local]); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a duplicate dependency: 'local'. " + + 'Either omit it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + useWorker(async () => {}, [window]); + } + `, + output: ` + function MyComponent() { + useWorker(async () => {}, []); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'window'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'window' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + useWorkerState(async () => {}, [window]); + } + `, + output: ` + function MyComponent() { + useWorkerState(async () => {}, []); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'window'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'window' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + // It is not valid for useWorker to specify extraneous deps + // because it doesn't serve as a side effect trigger unlike useAsyncEffect. + code: ` + function MyComponent(props) { + let local = props.foo; + useWorker(async () => {}, [local]); + } + `, + output: ` + function MyComponent(props) { + let local = props.foo; + useWorker(async () => {}, []); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'local'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + // It is not valid for useWorkerState to specify extraneous deps + // because it doesn't serve as a side effect trigger unlike useAsyncEffect. + code: ` + function MyComponent(props) { + let local = props.foo; + useWorkerState(async () => {}, [local]); + } + `, + output: ` + function MyComponent(props) { + let local = props.foo; + useWorkerState(async () => {}, []); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'local'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent({ history }) { + useAsyncEffect(async () => { + return history.listen(); + }, []); + } + `, + output: ` + function MyComponent({ history }) { + useAsyncEffect(async () => { + return history.listen(); + }, [history]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'history'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent({ history }) { + useAsyncEffect(async () => { + return [ + history.foo.bar[2].dobedo.listen(), + history.foo.bar().dobedo.listen[2] + ]; + }, []); + } + `, + output: ` + function MyComponent({ history }) { + useAsyncEffect(async () => { + return [ + history.foo.bar[2].dobedo.listen(), + history.foo.bar().dobedo.listen[2] + ]; + }, [history.foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'history.foo'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + useAsyncEffect(async () => {}, ['foo']); + } + `, + // TODO: we could autofix this. + output: ` + function MyComponent() { + useAsyncEffect(async () => {}, ['foo']); + } + `, + errors: [ + // Don't assume user meant `foo` because it's not used in the effect. + "The 'foo' literal is not a valid dependency because it never changes. " + + 'You can safely remove it.', + ], + }, + { + code: ` + function MyComponent({ foo, bar, baz }) { + useAsyncEffect(async () => { + console.log(foo, bar, baz); + }, ['foo', 'bar']); + } + `, + output: ` + function MyComponent({ foo, bar, baz }) { + useAsyncEffect(async () => { + console.log(foo, bar, baz); + }, [bar, baz, foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + + 'Either include them or remove the dependency array.', + "The 'foo' literal is not a valid dependency because it never changes. " + + 'Did you mean to include foo in the array instead?', + "The 'bar' literal is not a valid dependency because it never changes. " + + 'Did you mean to include bar in the array instead?', + ], + }, + { + code: ` + function MyComponent({ foo, bar, baz }) { + useAsyncEffect(async () => { + console.log(foo, bar, baz); + }, [42, false, null]); + } + `, + output: ` + function MyComponent({ foo, bar, baz }) { + useAsyncEffect(async () => { + console.log(foo, bar, baz); + }, [bar, baz, foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + + 'Either include them or remove the dependency array.', + 'The 42 literal is not a valid dependency because it never changes. You can safely remove it.', + 'The false literal is not a valid dependency because it never changes. You can safely remove it.', + 'The null literal is not a valid dependency because it never changes. You can safely remove it.', + ], + }, + { + code: ` + function MyComponent() { + const dependencies = []; + useAsyncEffect(async () => {}, dependencies); + } + `, + output: ` + function MyComponent() { + const dependencies = []; + useAsyncEffect(async () => {}, dependencies); + } + `, + errors: [ + 'React Hook useAsyncEffect was passed a dependency list that is not an ' + + "array literal. This means we can't statically verify whether you've " + + 'passed the correct dependencies.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + const dependencies = [local]; + useAsyncEffect(async () => { + console.log(local); + }, dependencies); + } + `, + // TODO: should this autofix or bail out? + output: ` + function MyComponent() { + const local = {}; + const dependencies = [local]; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + 'React Hook useAsyncEffect was passed a dependency list that is not an ' + + "array literal. This means we can't statically verify whether you've " + + 'passed the correct dependencies.', + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + const dependencies = [local]; + useAsyncEffect(async () => { + console.log(local); + }, [...dependencies]); + } + `, + // TODO: should this autofix or bail out? + output: ` + function MyComponent() { + const local = {}; + const dependencies = [local]; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + 'React Hook useAsyncEffect has a spread element in its dependency array. ' + + "This means we can't statically verify whether you've passed the " + + 'correct dependencies.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local, ...dependencies]); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local, ...dependencies]); + } + `, + errors: [ + 'React Hook useAsyncEffect has a spread element in its dependency array. ' + + "This means we can't statically verify whether you've passed the " + + 'correct dependencies.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [computeCacheKey(local)]); + } + `, + // TODO: I'm not sure this is a good idea. + // Maybe bail out? + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.items[0]); + }, [props.items[0]]); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.items[0]); + }, [props.items]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props.items'. " + + 'Either include it or remove the dependency array.', + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.items[0]); + }, [props.items, props.items[0]]); + } + `, + // TODO: ideally autofix would remove the bad expression? + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.items[0]); + }, [props.items, props.items[0]]); + } + `, + errors: [ + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + code: ` + function MyComponent({ items }) { + useAsyncEffect(async () => { + console.log(items[0]); + }, [items[0]]); + } + `, + output: ` + function MyComponent({ items }) { + useAsyncEffect(async () => { + console.log(items[0]); + }, [items]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'items'. " + + 'Either include it or remove the dependency array.', + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + code: ` + function MyComponent({ items }) { + useAsyncEffect(async () => { + console.log(items[0]); + }, [items, items[0]]); + } + `, + // TODO: ideally autofix would remove the bad expression? + output: ` + function MyComponent({ items }) { + useAsyncEffect(async () => { + console.log(items[0]); + }, [items, items[0]]); + } + `, + errors: [ + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + // It is not valid for useWorker to specify extraneous deps + // because it doesn't serve as a side effect trigger unlike useAsyncEffect. + // However, we generally allow specifying *broader* deps as escape hatch. + // So while [props, props.foo] is unnecessary, 'props' wins here as the + // broader one, and this is why 'props.foo' is reported as unnecessary. + code: ` + function MyComponent(props) { + const local = {}; + useWorker(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props, props.foo]); + } + `, + output: ` + function MyComponent(props) { + const local = {}; + useWorker(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props]); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'props.foo'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + // Since we don't have 'props' in the list, we'll suggest narrow dependencies. + code: ` + function MyComponent(props) { + const local = {}; + useWorker(async () => { + console.log(props.foo); + console.log(props.bar); + }, []); + } + `, + output: ` + function MyComponent(props) { + const local = {}; + useWorker(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props.bar, props.foo]); + } + `, + errors: [ + "React Hook useWorker has missing dependencies: 'props.bar' and 'props.foo'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + // It is not valid for useWorkerState to specify extraneous deps + // because it doesn't serve as a side effect trigger unlike useAsyncEffect. + // However, we generally allow specifying *broader* deps as escape hatch. + // So while [props, props.foo] is unnecessary, 'props' wins here as the + // broader one, and this is why 'props.foo' is reported as unnecessary. + code: ` + function MyComponent(props) { + const local = {}; + useWorkerState(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props, props.foo]); + } + `, + output: ` + function MyComponent(props) { + const local = {}; + useWorkerState(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props]); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'props.foo'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + // Since we don't have 'props' in the list, we'll suggest narrow dependencies. + code: ` + function MyComponent(props) { + const local = {}; + useWorkerState(async () => { + console.log(props.foo); + console.log(props.bar); + }, []); + } + `, + output: ` + function MyComponent(props) { + const local = {}; + useWorkerState(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props.bar, props.foo]); + } + `, + errors: [ + "React Hook useWorkerState has missing dependencies: 'props.bar' and 'props.foo'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + // Effects are allowed to over-specify deps. We'll complain about missing + // 'local', but we won't remove the already-specified 'local.id' from your list. + code: ` + function MyComponent() { + const local = {id: 42}; + useAsyncEffect(async () => { + console.log(local); + }, [local.id]); + } + `, + output: ` + function MyComponent() { + const local = {id: 42}; + useAsyncEffect(async () => { + console.log(local); + }, [local, local.id]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Callbacks are not allowed to over-specify deps. So we'll complain about missing + // 'local' and we will also *remove* 'local.id' from your list. + code: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorker(async () => { + console.log(local); + }, [local.id]); + } + `, + output: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorker(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Callbacks are not allowed to over-specify deps. So we'll complain about + // the unnecessary 'local.id'. + code: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorker(async () => { + console.log(local); + }, [local.id, local]); + } + `, + output: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorker(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'local.id'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + }, []); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz]); + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'props.foo.bar.baz'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + let color = {} + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + console.log(color); + }, [props.foo, props.foo.bar.baz]); + } + `, + output: ` + function MyComponent(props) { + let color = {} + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + console.log(color); + }, [color, props.foo.bar.baz]); + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'color'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Callbacks are not allowed to over-specify deps. So one of these is extra. + // However, it *is* allowed to specify broader deps then strictly necessary. + // So in this case we ask you to remove 'props.foo.bar.baz' because 'props.foo' + // already covers it, and having both is unnecessary. + // TODO: maybe consider suggesting a narrower one by default in these cases. + code: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz, props.foo]); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + }, [props.foo]); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'props.foo.bar.baz'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + console.log(props.foo.fizz.bizz); + }, []); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar.baz); + console.log(props.foo.fizz.bizz); + }, [props.foo.bar.baz, props.foo.fizz.bizz]); + } + `, + errors: [ + "React Hook useWorker has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + // Normally we allow specifying deps too broadly. + // So we'd be okay if 'props.foo.bar' was there rather than 'props.foo.bar.baz'. + // However, 'props.foo.bar.baz' is missing. So we know there is a mistake. + // When we're sure there is a mistake, for callbacks we will rebuild the list + // from scratch. This will set the user on a better path by default. + // This is why we end up with just 'props.foo.bar', and not them both. + code: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar); + }, [props.foo.bar.baz]); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props.foo.bar); + }, [props.foo.bar]); + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'props.foo.bar'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props); + console.log(props.hello); + }, [props.foo.bar.baz]); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorker(async () => { + console.log(props); + console.log(props.hello); + }, [props]); + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Callbacks are not allowed to over-specify deps. So we'll complain about missing + // 'local' and we will also *remove* 'local.id' from your list. + code: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorkerState(async () => { + console.log(local); + }, [local.id]); + } + `, + output: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorkerState(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Callbacks are not allowed to over-specify deps. So we'll complain about + // the unnecessary 'local.id'. + code: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorkerState(async () => { + console.log(local); + }, [local.id, local]); + } + `, + output: ` + function MyComponent() { + const local = {id: 42}; + const fn = useWorkerState(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'local.id'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + }, []); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz]); + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'props.foo.bar.baz'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + let color = {} + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + console.log(color); + }, [props.foo, props.foo.bar.baz]); + } + `, + output: ` + function MyComponent(props) { + let color = {} + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + console.log(color); + }, [color, props.foo.bar.baz]); + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'color'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Callbacks are not allowed to over-specify deps. So one of these is extra. + // However, it *is* allowed to specify broader deps then strictly necessary. + // So in this case we ask you to remove 'props.foo.bar.baz' because 'props.foo' + // already covers it, and having both is unnecessary. + // TODO: maybe consider suggesting a narrower one by default in these cases. + code: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz, props.foo]); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + }, [props.foo]); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'props.foo.bar.baz'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + console.log(props.foo.fizz.bizz); + }, []); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar.baz); + console.log(props.foo.fizz.bizz); + }, [props.foo.bar.baz, props.foo.fizz.bizz]); + } + `, + errors: [ + "React Hook useWorkerState has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + // Normally we allow specifying deps too broadly. + // So we'd be okay if 'props.foo.bar' was there rather than 'props.foo.bar.baz'. + // However, 'props.foo.bar.baz' is missing. So we know there is a mistake. + // When we're sure there is a mistake, for callbacks we will rebuild the list + // from scratch. This will set the user on a better path by default. + // This is why we end up with just 'props.foo.bar', and not them both. + code: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar); + }, [props.foo.bar.baz]); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props.foo.bar); + }, [props.foo.bar]); + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'props.foo.bar'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props); + console.log(props.hello); + }, [props.foo.bar.baz]); + } + `, + output: ` + function MyComponent(props) { + const fn = useWorkerState(async () => { + console.log(props); + console.log(props.hello); + }, [props]); + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local, local]); + } + `, + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a duplicate dependency: 'local'. " + + 'Either omit it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + useWorker(async () => { + const local1 = {}; + console.log(local1); + }, [local1]); + } + `, + output: ` + function MyComponent() { + const local1 = {}; + useWorker(async () => { + const local1 = {}; + console.log(local1); + }, []); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'local1'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + useWorker(async () => {}, [local1]); + } + `, + output: ` + function MyComponent() { + const local1 = {}; + useWorker(async () => {}, []); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'local1'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + useWorkerState(async () => { + const local1 = {}; + console.log(local1); + }, [local1]); + } + `, + output: ` + function MyComponent() { + const local1 = {}; + useWorkerState(async () => { + const local1 = {}; + console.log(local1); + }, []); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'local1'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local1 = {}; + useWorkerState(async () => {}, [local1]); + } + `, + output: ` + function MyComponent() { + const local1 = {}; + useWorkerState(async () => {}, []); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'local1'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, [props.foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + }, [props.bar, props.foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'props.bar' and 'props.foo'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useAsyncEffect(async () => { + console.log(b, e, d, c, a, g, f); + }, [c, a, g]); + } + `, + // Don't alphabetize if it wasn't alphabetized in the first place. + output: ` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useAsyncEffect(async () => { + console.log(b, e, d, c, a, g, f); + }, [c, a, g, b, e, d, f]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useAsyncEffect(async () => { + console.log(b, e, d, c, a, g, f); + }, [a, c, g]); + } + `, + // Alphabetize if it was alphabetized. + output: ` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useAsyncEffect(async () => { + console.log(b, e, d, c, a, g, f); + }, [a, b, c, d, e, f, g]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useAsyncEffect(async () => { + console.log(b, e, d, c, a, g, f); + }, []); + } + `, + // Alphabetize if it was empty. + output: ` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useAsyncEffect(async () => { + console.log(b, e, d, c, a, g, f); + }, [a, b, c, d, e, f, g]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'a', 'b', 'c', 'd', 'e', 'f', and 'g'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const local = {}; + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, []); + } + `, + output: ` + function MyComponent(props) { + const local = {}; + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, [local, props.bar, props.foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'local', 'props.bar', and 'props.foo'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const local = {}; + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, [props]); + } + `, + output: ` + function MyComponent(props) { + const local = {}; + useAsyncEffect(async () => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, [local, props]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, []); + useWorker(async () => { + console.log(props.foo); + }, []); + useEffectUpdate(() => { + console.log(props.foo); + }, []); + React.useAsyncEffect(async () => { + console.log(props.foo); + }, []); + React.useWorker(async () => { + console.log(props.foo); + }, []); + React.useEffectUpdate(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, [props.foo]); + useWorker(async () => { + console.log(props.foo); + }, [props.foo]); + useEffectUpdate(() => { + console.log(props.foo); + }, [props.foo]); + React.useAsyncEffect(async () => { + console.log(props.foo); + }, [props.foo]); + React.useWorker(async () => { + console.log(props.foo); + }, [props.foo]); + React.useEffectUpdate(() => { + console.log(props.foo); + }, [props.foo]); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook useWorker has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook useEffectUpdate has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook React.useAsyncEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook React.useWorker has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook React.useEffectUpdate has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, []); + useWorkerState(async () => { + console.log(props.foo); + }, []); + useEffectUpdate(() => { + console.log(props.foo); + }, []); + React.useAsyncEffect(async () => { + console.log(props.foo); + }, []); + React.useWorkerState(async () => { + console.log(props.foo); + }, []); + React.useEffectUpdate(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + console.log(props.foo); + }, [props.foo]); + useWorkerState(async () => { + console.log(props.foo); + }, [props.foo]); + useEffectUpdate(() => { + console.log(props.foo); + }, [props.foo]); + React.useAsyncEffect(async () => { + console.log(props.foo); + }, [props.foo]); + React.useWorkerState(async () => { + console.log(props.foo); + }, [props.foo]); + React.useEffectUpdate(() => { + console.log(props.foo); + }, [props.foo]); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook useWorkerState has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook useEffectUpdate has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook React.useAsyncEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook React.useWorkerState has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + "React Hook React.useEffectUpdate has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [a ? local : b]); + } + `, + // TODO: should we bail out instead? + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + code: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [a && local]); + } + `, + // TODO: should we bail out instead? + output: ` + function MyComponent() { + const local = {}; + useAsyncEffect(async () => { + console.log(local); + }, [local]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + 'React Hook useAsyncEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + ], + }, + { + code: ` + function MyComponent() { + const ref = useRef(); + const [state, setState] = useState(); + useAsyncEffect(async () => { + ref.current = {}; + setState(state + 1); + }, []); + } + `, + output: ` + function MyComponent() { + const ref = useRef(); + const [state, setState] = useState(); + useAsyncEffect(async () => { + ref.current = {}; + setState(state + 1); + }, [state]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + ], + }, + { + code: ` + function MyComponent() { + const ref = useRef(); + const [state, setState] = useState(); + useAsyncEffect(async () => { + ref.current = {}; + setState(state + 1); + }, [ref]); + } + `, + // We don't ask to remove static deps but don't add them either. + // Don't suggest removing "ref" (it's fine either way) + // but *do* add "state". *Don't* add "setState" ourselves. + output: ` + function MyComponent() { + const ref = useRef(); + const [state, setState] = useState(); + useAsyncEffect(async () => { + ref.current = {}; + setState(state + 1); + }, [ref, state]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + ], + }, + { + code: ` + function MyComponent(props) { + const ref1 = useRef(); + const ref2 = useRef(); + useAsyncEffect(async () => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, []); + } + `, + output: ` + function MyComponent(props) { + const ref1 = useRef(); + const ref2 = useRef(); + useAsyncEffect(async () => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [props.color, props.someOtherRefs]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " + + 'Either include them or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + const ref1 = useRef(); + const ref2 = useRef(); + useAsyncEffect(async () => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [ref1.current, ref2.current, props.someOtherRefs, props.color]); + } + `, + output: ` + function MyComponent(props) { + const ref1 = useRef(); + const ref2 = useRef(); + useAsyncEffect(async () => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [props.someOtherRefs, props.color]); + } + `, + errors: [ + "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const ref = useRef(); + useAsyncEffect(async () => { + console.log(ref.current); + }, [ref.current]); + } + `, + output: ` + function MyComponent() { + const ref = useRef(); + useAsyncEffect(async () => { + console.log(ref.current); + }, []); + } + `, + errors: [ + "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " + + 'Either exclude it or remove the dependency array. ' + + "Mutable values like 'ref.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent({ activeTab }) { + const ref1 = useRef(); + const ref2 = useRef(); + useAsyncEffect(async () => { + ref1.current.scrollTop = 0; + ref2.current.scrollTop = 0; + }, [ref1.current, ref2.current, activeTab]); + } + `, + output: ` + function MyComponent({ activeTab }) { + const ref1 = useRef(); + const ref2 = useRef(); + useAsyncEffect(async () => { + ref1.current.scrollTop = 0; + ref2.current.scrollTop = 0; + }, [activeTab]); + } + `, + errors: [ + "React Hook useAsyncEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent({ activeTab, initY }) { + const ref1 = useRef(); + const ref2 = useRef(); + const fn = useWorker(async () => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [ref1.current, ref2.current, activeTab, initY]); + } + `, + output: ` + function MyComponent({ activeTab, initY }) { + const ref1 = useRef(); + const ref2 = useRef(); + const fn = useWorker(async () => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [initY]); + } + `, + errors: [ + "React Hook useWorker has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent({ activeTab, initY }) { + const ref1 = useRef(); + const ref2 = useRef(); + const fn = useWorkerState(async () => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [ref1.current, ref2.current, activeTab, initY]); + } + `, + output: ` + function MyComponent({ activeTab, initY }) { + const ref1 = useRef(); + const ref2 = useRef(); + const fn = useWorkerState(async () => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [initY]); + } + `, + errors: [ + "React Hook useWorkerState has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + function MyComponent() { + const ref = useRef(); + useAsyncEffect(async () => { + console.log(ref.current); + }, [ref.current, ref]); + } + `, + output: ` + function MyComponent() { + const ref = useRef(); + useAsyncEffect(async () => { + console.log(ref.current); + }, [ref]); + } + `, + errors: [ + "React Hook useAsyncEffect has an unnecessary dependency: 'ref.current'. " + + 'Either exclude it or remove the dependency array. ' + + "Mutable values like 'ref.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + const MyComponent = forwardRef((props, ref) => { + useConditionalEffect(ref, () => ({ + focus() { + alert(props.hello); + } + }), []) + }); + `, + output: ` + const MyComponent = forwardRef((props, ref) => { + useConditionalEffect(ref, () => ({ + focus() { + alert(props.hello); + } + }), [props.hello]) + }); + `, + errors: [ + "React Hook useConditionalEffect has a missing dependency: 'props.hello'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + if (props.onChange) { + props.onChange(); + } + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + if (props.onChange) { + props.onChange(); + } + }, [props]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useAsyncEffect call and refer to those specific ` + + `props inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + function play() { + props.onPlay(); + } + function pause() { + props.onPause(); + } + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + function play() { + props.onPlay(); + } + function pause() { + props.onPause(); + } + }, [props]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useAsyncEffect call and refer to those specific ` + + `props inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + if (props.foo.onChange) { + props.foo.onChange(); + } + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + if (props.foo.onChange) { + props.foo.onChange(); + } + }, [props.foo]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + props.onChange(); + if (props.foo.onChange) { + props.foo.onChange(); + } + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + props.onChange(); + if (props.foo.onChange) { + props.foo.onChange(); + } + }, [props]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useAsyncEffect call and refer to those specific ` + + `props inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent(props) { + const [skillsCount] = useState(); + useAsyncEffect(async () => { + if (skillsCount === 0 && !props.isEditMode) { + props.toggleEditMode(); + } + }, [skillsCount, props.isEditMode, props.toggleEditMode]); + } + `, + output: ` + function MyComponent(props) { + const [skillsCount] = useState(); + useAsyncEffect(async () => { + if (skillsCount === 0 && !props.isEditMode) { + props.toggleEditMode(); + } + }, [skillsCount, props.isEditMode, props.toggleEditMode, props]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useAsyncEffect call and refer to those specific ` + + `props inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent(props) { + const [skillsCount] = useState(); + useAsyncEffect(async () => { + if (skillsCount === 0 && !props.isEditMode) { + props.toggleEditMode(); + } + }, []); + } + `, + output: ` + function MyComponent(props) { + const [skillsCount] = useState(); + useAsyncEffect(async () => { + if (skillsCount === 0 && !props.isEditMode) { + props.toggleEditMode(); + } + }, [props, skillsCount]); + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'props' and 'skillsCount'. " + + 'Either include them or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useAsyncEffect call and refer to those specific ` + + `props inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + externalCall(props); + props.onChange(); + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + externalCall(props); + props.onChange(); + }, [props]); + } + `, + // Don't suggest to destructure props here since you can't. + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + useAsyncEffect(async () => { + props.onChange(); + externalCall(props); + }, []); + } + `, + output: ` + function MyComponent(props) { + useAsyncEffect(async () => { + props.onChange(); + externalCall(props); + }, [props]); + } + `, + // Don't suggest to destructure props here since you can't. + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent(props) { + let value; + let value2; + let value3; + let value4; + let asyncValue; + useAsyncEffect(async () => { + if (value4) { + value = {}; + } + value2 = 100; + value = 43; + value4 = true; + console.log(value2); + console.log(value3); + setTimeout(() => { + asyncValue = 100; + }); + }, []); + } + `, + // This is a separate warning unrelated to others. + // We could've made a separate rule for it but it's rare enough to name it. + // No autofix suggestion because the intent isn't clear. + output: ` + function MyComponent(props) { + let value; + let value2; + let value3; + let value4; + let asyncValue; + useAsyncEffect(async () => { + if (value4) { + value = {}; + } + value2 = 100; + value = 43; + value4 = true; + console.log(value2); + console.log(value3); + setTimeout(() => { + asyncValue = 100; + }); + }, []); + } + `, + errors: [ + // value2 + `Assignments to the 'value2' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, // value + `Assignments to the 'value' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, // value4 + `Assignments to the 'value4' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, // asyncValue + `Assignments to the 'asyncValue' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent(props) { + let value; + let value2; + let value3; + let asyncValue; + useAsyncEffect(async () => { + value = {}; + value2 = 100; + value = 43; + console.log(value2); + console.log(value3); + setTimeout(() => { + asyncValue = 100; + }); + }, [value, value2, value3]); + } + `, + // This is a separate warning unrelated to others. + // We could've made a separate rule for it but it's rare enough to name it. + // No autofix suggestion because the intent isn't clear. + output: ` + function MyComponent(props) { + let value; + let value2; + let value3; + let asyncValue; + useAsyncEffect(async () => { + value = {}; + value2 = 100; + value = 43; + console.log(value2); + console.log(value3); + setTimeout(() => { + asyncValue = 100; + }); + }, [value, value2, value3]); + } + `, + errors: [ + // value + `Assignments to the 'value' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, // value2 + `Assignments to the 'value2' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, // asyncValue + `Assignments to the 'asyncValue' variable from inside React Hook useAsyncEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef or useLazyRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useAsyncEffect.`, + ], + }, + { + code: ` + function MyComponent() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }, []); + return
; + } + `, + output: ` + function MyComponent() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }, []); + return
; + } + `, + errors: [ + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + ], + }, + { + code: ` + function MyComponent() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }); + return
; + } + `, + output: ` + function MyComponent() { + const myRef = useRef(); + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }); + return
; + } + `, + errors: [ + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + ], + }, + { + code: ` + function useMyThing(myRef) { + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }, [myRef]); + } + `, + output: ` + function useMyThing(myRef) { + useAsyncEffect(async () => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }, [myRef]); + } + `, + errors: [ + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + ], + }, + { + code: ` + function useMyThing(myRef) { + useAsyncEffect(async () => { + const handleMouse = () => {}; + myRef.current.addEventListener('mousemove', handleMouse); + myRef.current.addEventListener('mousein', handleMouse); + return function() { + setTimeout(() => { + myRef.current.removeEventListener('mousemove', handleMouse); + myRef.current.removeEventListener('mousein', handleMouse); + }); + } + }, [myRef]); + } + `, + output: ` + function useMyThing(myRef) { + useAsyncEffect(async () => { + const handleMouse = () => {}; + myRef.current.addEventListener('mousemove', handleMouse); + myRef.current.addEventListener('mousein', handleMouse); + return function() { + setTimeout(() => { + myRef.current.removeEventListener('mousemove', handleMouse); + myRef.current.removeEventListener('mousein', handleMouse); + }); + } + }, [myRef]); + } + `, + errors: [ + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + ], + }, + { + code: ` + function useMyThing(myRef, active) { + useAsyncEffect(async () => { + const handleMove = () => {}; + if (active) { + myRef.current.addEventListener('mousemove', handleMove); + return function() { + setTimeout(() => { + myRef.current.removeEventListener('mousemove', handleMove); + }); + } + } + }, [myRef, active]); + } + `, + output: ` + function useMyThing(myRef, active) { + useAsyncEffect(async () => { + const handleMove = () => {}; + if (active) { + myRef.current.addEventListener('mousemove', handleMove); + return function() { + setTimeout(() => { + myRef.current.removeEventListener('mousemove', handleMove); + }); + } + } + }, [myRef, active]); + } + `, + errors: [ + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + ], + }, + { + // Autofix ignores constant primitives (leaving the ones that are there). + code: ` + function MyComponent() { + const local1 = 42; + const local2 = '42'; + const local3 = null; + const local4 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + console.log(local3); + console.log(local4); + }, [local1, local3]); + } + `, + output: ` + function MyComponent() { + const local1 = 42; + const local2 = '42'; + const local3 = null; + const local4 = {}; + useAsyncEffect(async () => { + console.log(local1); + console.log(local2); + console.log(local3); + console.log(local4); + }, [local1, local3, local4]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'local4'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function MyComponent() { + useAsyncEffect(async () => { + window.scrollTo(0, 0); + }, [window]); + } + `, + errors: [ + "React Hook useAsyncEffect has an unnecessary dependency: 'window'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'window' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + import MutableStore from 'store'; + + function MyComponent() { + useAsyncEffect(async () => { + console.log(MutableStore.hello); + }, [MutableStore.hello]); + } + `, + output: ` + import MutableStore from 'store'; + + function MyComponent() { + useAsyncEffect(async () => { + console.log(MutableStore.hello); + }, []); + } + `, + errors: [ + "React Hook useAsyncEffect has an unnecessary dependency: 'MutableStore.hello'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + useAsyncEffect(async () => { + console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff); + }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); + } + } + `, + output: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + useAsyncEffect(async () => { + console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff); + }, [props.foo, x, y]); + } + } + `, + errors: [ + 'React Hook useAsyncEffect has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + useAsyncEffect(async () => { + // nothing + }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); + } + } + `, + // The output should contain the ones that are inside a component + // since there are legit reasons to over-specify them for effects. + output: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + useAsyncEffect(async () => { + // nothing + }, [props.foo, x, y]); + } + } + `, + errors: [ + 'React Hook useAsyncEffect has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + const fn = useWorker(async () => { + // nothing + }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); + } + } + `, + output: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + const fn = useWorker(async () => { + // nothing + }, []); + } + } + `, + errors: [ + 'React Hook useWorker has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + code: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + const fn = useWorkerState(async () => { + // nothing + }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); + } + } + `, + output: ` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + const fn = useWorkerState(async () => { + // nothing + }, []); + } + } + `, + errors: [ + 'React Hook useWorkerState has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + ], + }, + { + // Every almost-static function is tainted by a dynamic value. + code: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + setTimeout(() => console.log(taint)); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, []); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, []); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + output: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + setTimeout(() => console.log(taint)); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, [handleNext1]); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, [handleNext2]); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, [handleNext3]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'handleNext1'. " + + 'Either include it or remove the dependency array.', + "React Hook useAsyncLayoutEffect has a missing dependency: 'handleNext2'. " + + 'Either include it or remove the dependency array.', + "React Hook useEffectUpdate has a missing dependency: 'handleNext3'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regression test + code: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + function handleChange() {} + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, []); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, []); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + output: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + function handleChange() {} + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, [handleNext1]); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, [handleNext2]); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, [handleNext3]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'handleNext1'. " + + 'Either include it or remove the dependency array.', + "React Hook useAsyncLayoutEffect has a missing dependency: 'handleNext2'. " + + 'Either include it or remove the dependency array.', + "React Hook useEffectUpdate has a missing dependency: 'handleNext3'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regression test + code: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + const handleChange = () => {}; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, []); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, []); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + output: ` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + const handleChange = () => {}; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useAsyncEffect(async () => { + return Store.subscribe(handleNext1); + }, [handleNext1]); + useAsyncLayoutEffect(async () => { + return Store.subscribe(handleNext2); + }, [handleNext2]); + useEffectUpdate(() => { + return Store.subscribe(handleNext3); + }, [handleNext3]); + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'handleNext1'. " + + 'Either include it or remove the dependency array.', + "React Hook useAsyncLayoutEffect has a missing dependency: 'handleNext2'. " + + 'Either include it or remove the dependency array.', + "React Hook useEffectUpdate has a missing dependency: 'handleNext3'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function Counter() { + let [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count + 1); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter() { + let [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count + 1); + }, 1000); + return () => clearInterval(id); + }, [count]); + + return

{count}

; + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'count'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setCount(c => ...)' if you ` + + `only need 'count' in the 'setCount' call.`, + ], + }, + { + code: ` + function Counter() { + let [count, setCount] = useState(0); + let [increment, setIncrement] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count + increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter() { + let [count, setCount] = useState(0); + let [increment, setIncrement] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count + increment); + }, 1000); + return () => clearInterval(id); + }, [count, increment]); + + return

{count}

; + } + `, + errors: [ + "React Hook useAsyncEffect has missing dependencies: 'count' and 'increment'. " + + 'Either include them or remove the dependency array. ' + + `You can also do a functional update 'setCount(c => ...)' if you ` + + `only need 'count' in the 'setCount' call.`, + ], + }, + { + code: ` + function Counter() { + let [count, setCount] = useState(0); + let [increment, setIncrement] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter() { + let [count, setCount] = useState(0); + let [increment, setIncrement] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, [increment]); + + return

{count}

; + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array. ' + + `You can also replace multiple useState variables with useReducer ` + + `if 'setCount' needs the current value of 'increment'.`, + ], + }, + { + code: ` + function Counter() { + let [count, setCount] = useState(0); + let increment = useCustomHook(); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter() { + let [count, setCount] = useState(0); + let increment = useCustomHook(); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, [increment]); + + return

{count}

; + } + `, + // This intentionally doesn't show the reducer message + // because we don't know if it's safe for it to close over a value. + // We only show it for state variables (and possibly props). + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function Counter({ step }) { + let [count, setCount] = useState(0); + + function increment(x) { + return x + step; + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => increment(count)); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter({ step }) { + let [count, setCount] = useState(0); + + function increment(x) { + return x + step; + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => increment(count)); + }, 1000); + return () => clearInterval(id); + }, [increment]); + + return

{count}

; + } + `, + // This intentionally doesn't show the reducer message + // because we don't know if it's safe for it to close over a value. + // We only show it for state variables (and possibly props). + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function Counter({ increment }) { + let [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter({ increment }) { + let [count, setCount] = useState(0); + + useAsyncEffect(async () => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, [increment]); + + return

{count}

; + } + `, + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array. ' + + `If 'setCount' needs the current value of 'increment', ` + + `you can also switch to useReducer instead of useState and read 'increment' in the reducer.`, + ], + }, + { + code: ` + function Counter() { + const [count, setCount] = useState(0); + + function tick() { + setCount(count + 1); + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + tick(); + }, 1000); + return () => clearInterval(id); + }, []); + + return

{count}

; + } + `, + output: ` + function Counter() { + const [count, setCount] = useState(0); + + function tick() { + setCount(count + 1); + } + + useAsyncEffect(async () => { + let id = setInterval(() => { + tick(); + }, 1000); + return () => clearInterval(id); + }, [tick]); + + return

{count}

; + } + `, + // TODO: ideally this should suggest useState updater form + // since this code doesn't actually work. The autofix could + // at least avoid suggesting 'tick' since it's obviously + // always different, and thus useless. + errors: [ + "React Hook useAsyncEffect has a missing dependency: 'tick'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + // Regression test for a crash + code: ` + function Podcasts() { + useAsyncEffect(async () => { + alert(podcasts); + }, []); + let [podcasts, setPodcasts] = useState(null); + } + `, + // Note: this autofix is shady because + // the variable is used before declaration. + // TODO: Maybe we can catch those fixes and not autofix. + output: ` + function Podcasts() { + useAsyncEffect(async () => { + alert(podcasts); + }, [podcasts]); + let [podcasts, setPodcasts] = useState(null); + } + `, + errors: [ + `React Hook useAsyncEffect has a missing dependency: 'podcasts'. ` + + `Either include it or remove the dependency array.`, + ], + }, + { + code: ` + function Podcasts({ fetchPodcasts, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + fetchPodcasts(id).then(setPodcasts); + }, [id]); + } + `, + output: ` + function Podcasts({ fetchPodcasts, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + fetchPodcasts(id).then(setPodcasts); + }, [fetchPodcasts, id]); + } + `, + errors: [ + `React Hook useAsyncEffect has a missing dependency: 'fetchPodcasts'. ` + + `Either include it or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useWorker.`, + ], + }, + { + code: ` + function Podcasts({ api: { fetchPodcasts }, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + fetchPodcasts(id).then(setPodcasts); + }, [id]); + } + `, + output: ` + function Podcasts({ api: { fetchPodcasts }, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + fetchPodcasts(id).then(setPodcasts); + }, [fetchPodcasts, id]); + } + `, + errors: [ + `React Hook useAsyncEffect has a missing dependency: 'fetchPodcasts'. ` + + `Either include it or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useWorker.`, + ], + }, + { + code: ` + function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + setTimeout(() => { + console.log(id); + fetchPodcasts(id).then(setPodcasts); + fetchPodcasts2(id).then(setPodcasts); + }); + }, [id]); + } + `, + output: ` + function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + setTimeout(() => { + console.log(id); + fetchPodcasts(id).then(setPodcasts); + fetchPodcasts2(id).then(setPodcasts); + }); + }, [fetchPodcasts, fetchPodcasts2, id]); + } + `, + errors: [ + `React Hook useAsyncEffect has missing dependencies: 'fetchPodcasts' and 'fetchPodcasts2'. ` + + `Either include them or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useWorker.`, + ], + }, + { + code: ` + function Podcasts({ fetchPodcasts, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + console.log(fetchPodcasts); + fetchPodcasts(id).then(setPodcasts); + }, [id]); + } + `, + output: ` + function Podcasts({ fetchPodcasts, id }) { + let [podcasts, setPodcasts] = useState(null); + useAsyncEffect(async () => { + console.log(fetchPodcasts); + fetchPodcasts(id).then(setPodcasts); + }, [fetchPodcasts, id]); + } + `, + errors: [ + `React Hook useAsyncEffect has a missing dependency: 'fetchPodcasts'. ` + + `Either include it or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useWorker.`, + ], + }, + { + // The mistake here is that it was moved inside the effect + // so it can't be referenced in the deps array. + code: ` + function Thing() { + useAsyncEffect(async () => { + const fetchData = async () => {}; + fetchData(); + }, [fetchData]); + } + `, + output: ` + function Thing() { + useAsyncEffect(async () => { + const fetchData = async () => {}; + fetchData(); + }, []); + } + `, + errors: [ + `React Hook useAsyncEffect has an unnecessary dependency: 'fetchData'. ` + + `Either exclude it or remove the dependency array.`, + ], + }, + { + code: ` + function Hello() { + const [state, setState] = useState(0); + useAsyncEffect(async () => { + setState({}); + }); + } + `, + output: ` + function Hello() { + const [state, setState] = useState(0); + useAsyncEffect(async () => { + setState({}); + }, []); + } + `, + errors: [ + `React Hook useAsyncEffect contains a call to 'setState'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [] as a second argument to the useAsyncEffect Hook.`, + ], + }, + { + code: ` + function Hello() { + const [data, setData] = useState(0); + useAsyncEffect(async () => { + fetchData.then(setData); + }); + } + `, + output: ` + function Hello() { + const [data, setData] = useState(0); + useAsyncEffect(async () => { + fetchData.then(setData); + }, []); + } + `, + errors: [ + `React Hook useAsyncEffect contains a call to 'setData'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [] as a second argument to the useAsyncEffect Hook.`, + ], + }, + { + code: ` + function Hello({ country }) { + const [data, setData] = useState(0); + useAsyncEffect(async () => { + fetchData(country).then(setData); + }); + } + `, + output: ` + function Hello({ country }) { + const [data, setData] = useState(0); + useAsyncEffect(async () => { + fetchData(country).then(setData); + }, [country]); + } + `, + errors: [ + `React Hook useAsyncEffect contains a call to 'setData'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [country] as a second argument to the useAsyncEffect Hook.`, + ], + }, + { + code: ` + function Hello({ prop1, prop2 }) { + const [state, setState] = useState(0); + useAsyncEffect(async () => { + if (prop1) { + setState(prop2); + } + }); + } + `, + output: ` + function Hello({ prop1, prop2 }) { + const [state, setState] = useState(0); + useAsyncEffect(async () => { + if (prop1) { + setState(prop2); + } + }, [prop1, prop2]); + } + `, + errors: [ + `React Hook useAsyncEffect contains a call to 'setState'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [prop1, prop2] as a second argument to the useAsyncEffect Hook.`, + ], + }, + { + code: ` + function Example() { + const foo = useWorker(async () => { + foo(); + }, [foo]); + } + `, + output: ` + function Example() { + const foo = useWorker(async () => { + foo(); + }, []); + } + `, + errors: [ + "React Hook useWorker has an unnecessary dependency: 'foo'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function Example({ prop }) { + const foo = useWorker(async () => { + prop.hello(foo); + }, [foo]); + const bar = useWorker(async () => { + foo(); + }, [foo]); + } + `, + output: ` + function Example({ prop }) { + const foo = useWorker(async () => { + prop.hello(foo); + }, [prop]); + const bar = useWorker(async () => { + foo(); + }, [foo]); + } + `, + errors: [ + "React Hook useWorker has a missing dependency: 'prop'. " + + 'Either include it or remove the dependency array.', + ], + }, + { + code: ` + function Example() { + const foo = useWorkerState(async () => { + foo(); + }, [foo]); + } + `, + output: ` + function Example() { + const foo = useWorkerState(async () => { + foo(); + }, []); + } + `, + errors: [ + "React Hook useWorkerState has an unnecessary dependency: 'foo'. " + + 'Either exclude it or remove the dependency array.', + ], + }, + { + code: ` + function Example({ prop }) { + const foo = useWorkerState(async () => { + prop.hello(foo); + }, [foo]); + const bar = useWorkerState(async () => { + foo(); + }, [foo]); + } + `, + output: ` + function Example({ prop }) { + const foo = useWorkerState(async () => { + prop.hello(foo); + }, [prop]); + const bar = useWorkerState(async () => { + foo(); + }, [foo]); + } + `, + errors: [ + "React Hook useWorkerState has a missing dependency: 'prop'. " + + 'Either include it or remove the dependency array.', + ], + }, + ], +}; // For easier local testing + +if (!process.env.CI) { + let only = []; + let skipped = []; + [...tests.valid, ...tests.invalid].forEach(t => { + if (t.skip) { + delete t.skip; + skipped.push(t); + } + + if (t.only) { + delete t.only; + only.push(t); + } // if (!t.options) { + // t.options = [{ additionalHooks: 'useAsyncLayoutEffect' }]; + // } + }); + + const predicate = t => { + if (only.length > 0) { + return only.indexOf(t) !== -1; + } + + if (skipped.length > 0) { + return skipped.indexOf(t) === -1; + } + + return true; + }; + + tests.valid = tests.valid.filter(predicate); + tests.invalid = tests.invalid.filter(predicate); +} + +const eslintTester = new ESLintTester(); +eslintTester.run( + '@react-hook-utilities/eslint-plugin', + ReactHooksESLintRule, + tests, +); diff --git a/eslint-plugin/lib/index.js b/eslint-plugin/lib/index.js new file mode 100644 index 0000000..196d98c --- /dev/null +++ b/eslint-plugin/lib/index.js @@ -0,0 +1,10 @@ +/** + * @fileoverview ESLint rules for hook react-hook-utilities + * @author Flávio Caetano + */ +'use strict'; + +const ExhaustiveDeps = require('./rules/ExaustiveDeps'); +exports.rules = { + 'exhaustive-deps': ExhaustiveDeps, +}; diff --git a/eslint-plugin/lib/rules/ExaustiveDeps.js b/eslint-plugin/lib/rules/ExaustiveDeps.js new file mode 100644 index 0000000..97036db --- /dev/null +++ b/eslint-plugin/lib/rules/ExaustiveDeps.js @@ -0,0 +1,1327 @@ +/** + * 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. + * + * Original source: https://github.com/facebook/react/blob/9e64bf18e11828d6b4c0363bff5ed2eca1ccd838/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js + */ + +'use strict'; + +module.exports = { + meta: { + fixable: 'code', + schema: [ + { + type: 'object', + additionalProperties: false, + }, + ], + }, + + create(context) { + // Should be shared between visitors. + let setStateCallSites = new WeakMap(); + let stateVariables = new WeakSet(); + let staticKnownValueCache = new WeakMap(); + let functionWithoutCapturedValueCache = new WeakMap(); + + function memoizeWithWeakMap(fn, map) { + return function(arg) { + if (map.has(arg)) { + // to verify cache hits: + // console.log(arg.name) + return map.get(arg); + } + + const result = fn(arg); + map.set(arg, result); + return result; + }; + } + + return { + FunctionExpression: visitFunctionExpression, + ArrowFunctionExpression: visitFunctionExpression, + }; + /** + * Visitor for both function expressions and arrow function expressions. + */ + + function visitFunctionExpression(node) { + // We only want to lint nodes which are reactive hook callbacks. + if ( + (node.type !== 'FunctionExpression' && + node.type !== 'ArrowFunctionExpression') || + node.parent.type !== 'CallExpression' + ) { + return; + } + + const callbackIndex = getReactiveHookCallbackIndex(node.parent.callee); + + if (node.parent.arguments[callbackIndex] !== node) { + return; + } // Get the reactive hook node. + + const reactiveHook = node.parent.callee; + const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name; + const isEffect = reactiveHookName.endsWith('Effect'); // Get the declared dependencies for this reactive hook. If there is no + // second argument then the reactive callback will re-run on every render. + // So no need to check for dependency inclusion. + + const depsIndex = callbackIndex + 1; + const declaredDependenciesNode = node.parent.arguments[depsIndex]; // Get the current scope. + + const scope = context.getScope(); // Find all our "pure scopes". On every re-render of a component these + // pure scopes may have changes to the variables declared within. So all + // variables used in our reactive hook callback but declared in a pure + // scope need to be listed as dependencies of our reactive hook callback. + // + // According to the rules of React you can't read a mutable value in pure + // scope. We can't enforce this in a lint so we trust that all variables + // declared outside of pure scope are indeed frozen. + + const pureScopes = new Set(); + let componentScope = null; + { + let currentScope = scope.upper; + + while (currentScope) { + pureScopes.add(currentScope); + + if (currentScope.type === 'function') { + break; + } + + currentScope = currentScope.upper; + } // If there is no parent function scope then there are no pure scopes. + // The ones we've collected so far are incorrect. So don't continue with + // the lint. + + if (!currentScope) { + return; + } + + componentScope = currentScope; + } // Next we'll define a few helpers that helps us + // tell if some values don't have to be declared as deps. + // Some are known to be static based on Hook calls. + // const [state, setState] = useState() / React.useState() + // ^^^ true for this reference + // const [state, dispatch] = useReducer() / React.useReducer() + // ^^^ true for this reference + // const ref = useRef() + // ^^^ true for this reference + // False for everything else. + + function isStaticKnownHookValue(resolved) { + if (!Array.isArray(resolved.defs)) { + return false; + } + + const def = resolved.defs[0]; + + if (def == null) { + return false; + } // Look for `let stuff = ...` + + if (def.node.type !== 'VariableDeclarator') { + return false; + } + + const init = def.node.init; + + if (init == null) { + return false; + } // Detect primitive constants + // const foo = 42 + + let declaration = def.node.parent; + + if (declaration == null) { + // This might happen if variable is declared after the callback. + // In that case ESLint won't set up .parent refs. + // So we'll set them up manually. + fastFindReferenceWithParent(componentScope.block, def.node.id); + declaration = def.node.parent; + + if (declaration == null) { + return false; + } + } + + if ( + declaration.kind === 'const' && + init.type === 'Literal' && + (typeof init.value === 'string' || + typeof init.value === 'number' || + init.value === null) + ) { + // Definitely static + return true; + } // Detect known Hook calls + // const [_, setState] = useState() + + if (init.type !== 'CallExpression') { + return false; + } + + let callee = init.callee; // Step into `= React.something` initializer. + + if ( + callee.type === 'MemberExpression' && + callee.object.name === 'React' && + callee.property != null && + !callee.computed + ) { + callee = callee.property; + } + + if (callee.type !== 'Identifier') { + return false; + } + + const id = def.node.id; + const { name } = callee; + + if ( + (name === 'useRef' || name === 'useLazyRef') && + id.type === 'Identifier' + ) { + // useRef() return value is static. + return true; + } else if (name === 'useState' || name === 'useReducer') { + // Only consider second value in initializing tuple static. + if ( + id.type === 'ArrayPattern' && + id.elements.length === 2 && + Array.isArray(resolved.identifiers) + ) { + // Is second tuple value the same reference we're checking? + if (id.elements[1] === resolved.identifiers[0]) { + if (name === 'useState') { + const references = resolved.references; + + for (let i = 0; i < references.length; i++) { + setStateCallSites.set( + references[i].identifier, + id.elements[0], + ); + } + } // Setter is static. + + return true; + } else if (id.elements[0] === resolved.identifiers[0]) { + if (name === 'useState') { + const references = resolved.references; + + for (let i = 0; i < references.length; i++) { + stateVariables.add(references[i].identifier); + } + } // State variable itself is dynamic. + + return false; + } + } + } // By default assume it's dynamic. + + return false; + } // Some are just functions that don't reference anything dynamic. + + function isFunctionWithoutCapturedValues(resolved) { + if (!Array.isArray(resolved.defs)) { + return false; + } + + const def = resolved.defs[0]; + + if (def == null) { + return false; + } + + if (def.node == null || def.node.id == null) { + return false; + } // Search the direct component subscopes for + // top-level function definitions matching this reference. + + const fnNode = def.node; + let childScopes = componentScope.childScopes; + let fnScope = null; + let i; + + for (i = 0; i < childScopes.length; i++) { + let childScope = childScopes[i]; + let childScopeBlock = childScope.block; + + if ( + // function handleChange() {} + (fnNode.type === 'FunctionDeclaration' && + childScopeBlock === fnNode) || // const handleChange = () => {} + // const handleChange = function() {} + (fnNode.type === 'VariableDeclarator' && + childScopeBlock.parent === fnNode) + ) { + // Found it! + fnScope = childScope; + break; + } + } + + if (fnScope == null) { + return false; + } // Does this function capture any values + // that are in pure scopes (aka render)? + + for (i = 0; i < fnScope.through.length; i++) { + const ref = fnScope.through[i]; + + if (ref.resolved == null) { + continue; + } + + if ( + pureScopes.has(ref.resolved.scope) && // Static values are fine though, + // although we won't check functions deeper. + !memoizedIsStaticKnownHookValue(ref.resolved) + ) { + return false; + } + } // If we got here, this function doesn't capture anything + // from render--or everything it captures is known static. + + return true; + } // Remember such values. Avoid re-running extra checks on them. + + const memoizedIsStaticKnownHookValue = memoizeWithWeakMap( + isStaticKnownHookValue, + staticKnownValueCache, + ); + const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap( + isFunctionWithoutCapturedValues, + functionWithoutCapturedValueCache, + ); // These are usually mistaken. Collect them. + + const currentRefsInEffectCleanup = new Map(); // Is this reference inside a cleanup function for this effect node? + // We can check by traversing scopes upwards from the reference, and checking + // if the last "return () => " we encounter is located directly inside the effect. + + function isInsideEffectCleanup(reference) { + let curScope = reference.from; + let isInReturnedFunction = false; + + while (curScope.block !== node) { + if (curScope.type === 'function') { + isInReturnedFunction = + curScope.block.parent != null && + curScope.block.parent.type === 'ReturnStatement'; + } + + curScope = curScope.upper; + } + + return isInReturnedFunction; + } // Get dependencies from all our resolved references in pure scopes. + // Key is dependency string, value is whether it's static. + + const dependencies = new Map(); + gatherDependenciesRecursively(scope); + + function gatherDependenciesRecursively(currentScope) { + for (const reference of currentScope.references) { + // If this reference is not resolved or it is not declared in a pure + // scope then we don't care about this reference. + if (!reference.resolved) { + continue; + } + + if (!pureScopes.has(reference.resolved.scope)) { + continue; + } // Narrow the scope of a dependency if it is, say, a member expression. + // Then normalize the narrowed dependency. + + const referenceNode = fastFindReferenceWithParent( + node, + reference.identifier, + ); + const dependencyNode = getDependency(referenceNode); + const dependency = toPropertyAccessString(dependencyNode); // Accessing ref.current inside effect cleanup is bad. + + if ( + // We're in an effect... + isEffect && // ... and this look like accessing .current... + dependencyNode.type === 'Identifier' && + dependencyNode.parent.type === 'MemberExpression' && + !dependencyNode.parent.computed && + dependencyNode.parent.property.type === 'Identifier' && + dependencyNode.parent.property.name === 'current' && // ...in a cleanup function or below... + isInsideEffectCleanup(reference) + ) { + currentRefsInEffectCleanup.set(dependency, { + reference, + dependencyNode, + }); + } + + const def = reference.resolved.defs[0]; + + if (def == null) { + continue; + } // Ignore references to the function itself as it's not defined yet. + + if (def.node != null && def.node.init === node.parent) { + continue; + } // Ignore Flow type parameters + + if (def.type === 'TypeParameter') { + continue; + } // Add the dependency to a map so we can make sure it is referenced + // again in our dependencies array. Remember whether it's static. + + if (!dependencies.has(dependency)) { + const resolved = reference.resolved; + const isStatic = + memoizedIsStaticKnownHookValue(resolved) || + memoizedIsFunctionWithoutCapturedValues(resolved); + dependencies.set(dependency, { + isStatic, + references: [reference], + }); + } else { + dependencies.get(dependency).references.push(reference); + } + } + + for (const childScope of currentScope.childScopes) { + gatherDependenciesRecursively(childScope); + } + } // Warn about accessing .current in cleanup effects. + + currentRefsInEffectCleanup.forEach( + ({ reference, dependencyNode }, dependency) => { + const references = reference.resolved.references; // Is React managing this ref or us? + // Let's see if we can find a .current assignment. + + let foundCurrentAssignment = false; + + for (let i = 0; i < references.length; i++) { + const { identifier } = references[i]; + const { parent } = identifier; + + if ( + parent != null && // ref.current + parent.type === 'MemberExpression' && + !parent.computed && + parent.property.type === 'Identifier' && + parent.property.name === 'current' && // ref.current = + parent.parent.type === 'AssignmentExpression' && + parent.parent.left === parent + ) { + foundCurrentAssignment = true; + break; + } + } // We only want to warn about React-managed refs. + + if (foundCurrentAssignment) { + return; + } + + context.report({ + node: dependencyNode.parent.property, + message: + `The ref value '${dependency}.current' will likely have ` + + `changed by the time this effect cleanup function runs. If ` + + `this ref points to a node rendered by React, copy ` + + `'${dependency}.current' to a variable inside the effect, and ` + + `use that variable in the cleanup function.`, + }); + }, + ); // Warn about assigning to variables in the outer scope. + // Those are usually bugs. + + let staleAssignments = new Set(); + + function reportStaleAssignment(writeExpr, key) { + if (staleAssignments.has(key)) { + return; + } + + staleAssignments.add(key); + context.report({ + node: writeExpr, + message: + `Assignments to the '${key}' variable from inside React Hook ` + + `${context.getSource(reactiveHook)} will be lost after each ` + + `render. To preserve the value over time, store it in a useRef or useLazyRef ` + + `Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside ` + + `${context.getSource(reactiveHook)}.`, + }); + } // Remember which deps are optional and report bad usage first. + + const optionalDependencies = new Set(); + dependencies.forEach(({ isStatic, references }, key) => { + if (isStatic) { + optionalDependencies.add(key); + } + + references.forEach(reference => { + if (reference.writeExpr) { + reportStaleAssignment(reference.writeExpr, key); + } + }); + }); + + if (staleAssignments.size > 0) { + // The intent isn't clear so we'll wait until you fix those first. + return; + } + + if (!declaredDependenciesNode) { + // Check if there are any top-level setState() calls. + // Those tend to lead to infinite loops. + let setStateInsideEffectWithoutDeps = null; + dependencies.forEach(({ isStatic, references }, key) => { + if (setStateInsideEffectWithoutDeps) { + return; + } + + references.forEach(reference => { + if (setStateInsideEffectWithoutDeps) { + return; + } + + const id = reference.identifier; + const isSetState = setStateCallSites.has(id); + + if (!isSetState) { + return; + } + + let fnScope = reference.from; + + while (fnScope.type !== 'function') { + fnScope = fnScope.upper; + } + + const isDirectlyInsideEffect = fnScope.block === node; + + if (isDirectlyInsideEffect) { + // TODO: we could potentially ignore early returns. + setStateInsideEffectWithoutDeps = key; + } + }); + }); + + if (setStateInsideEffectWithoutDeps) { + let { suggestedDependencies } = collectRecommendations({ + dependencies, + declaredDependencies: [], + optionalDependencies, + externalDependencies: new Set(), + isEffect: true, + }); + context.report({ + node: node.parent.callee, + message: + `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [` + + suggestedDependencies.join(', ') + + `] as a second argument to the ${reactiveHookName} Hook.`, + + fix(fixer) { + return fixer.insertTextAfter( + node, + `, [${suggestedDependencies.join(', ')}]`, + ); + }, + }); + } + + return; + } + + const declaredDependencies = []; + const externalDependencies = new Set(); + + if (declaredDependenciesNode.type !== 'ArrayExpression') { + // If the declared dependencies are not an array expression then we + // can't verify that the user provided the correct dependencies. Tell + // the user this in an error. + context.report({ + node: declaredDependenciesNode, + message: + `React Hook ${context.getSource(reactiveHook)} was passed a ` + + 'dependency list that is not an array literal. This means we ' + + "can't statically verify whether you've passed the correct " + + 'dependencies.', + }); + } else { + declaredDependenciesNode.elements.forEach(declaredDependencyNode => { + // Skip elided elements. + if (declaredDependencyNode === null) { + return; + } // If we see a spread element then add a special warning. + + if (declaredDependencyNode.type === 'SpreadElement') { + context.report({ + node: declaredDependencyNode, + message: + `React Hook ${context.getSource(reactiveHook)} has a spread ` + + "element in its dependency array. This means we can't " + + "statically verify whether you've passed the " + + 'correct dependencies.', + }); + return; + } // Try to normalize the declared dependency. If we can't then an error + // will be thrown. We will catch that error and report an error. + + let declaredDependency; + + try { + declaredDependency = toPropertyAccessString(declaredDependencyNode); + } catch (error) { + if (/Unsupported node type/.test(error.message)) { + if (declaredDependencyNode.type === 'Literal') { + if (dependencies.has(declaredDependencyNode.value)) { + context.report({ + node: declaredDependencyNode, + message: + `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + + `because it never changes. ` + + `Did you mean to include ${declaredDependencyNode.value} in the array instead?`, + }); + } else { + context.report({ + node: declaredDependencyNode, + message: + `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + + 'because it never changes. You can safely remove it.', + }); + } + } else { + context.report({ + node: declaredDependencyNode, + message: + `React Hook ${context.getSource(reactiveHook)} has a ` + + `complex expression in the dependency array. ` + + 'Extract it to a separate variable so it can be statically checked.', + }); + } + + return; + } else { + throw error; + } + } + + let maybeID = declaredDependencyNode; + + while (maybeID.type === 'MemberExpression') { + maybeID = maybeID.object; + } + + const isDeclaredInComponent = !componentScope.through.some( + ref => ref.identifier === maybeID, + ); // Add the dependency to our declared dependency map. + + declaredDependencies.push({ + key: declaredDependency, + node: declaredDependencyNode, + }); + + if (!isDeclaredInComponent) { + externalDependencies.add(declaredDependency); + } + }); + } + + let { + suggestedDependencies, + unnecessaryDependencies, + missingDependencies, + duplicateDependencies, + } = collectRecommendations({ + dependencies, + declaredDependencies, + optionalDependencies, + externalDependencies, + isEffect, + }); + const problemCount = + duplicateDependencies.size + + missingDependencies.size + + unnecessaryDependencies.size; // If we're going to report a missing dependency, + // we might as well recalculate the list ignoring + // the currently specified deps. This can result + // in some extra deduplication. We can't do this + // for effects though because those have legit + // use cases for over-specifying deps. + + if (!isEffect && missingDependencies.size > 0) { + suggestedDependencies = collectRecommendations({ + dependencies, + declaredDependencies: [], + // Pretend we don't know + optionalDependencies, + externalDependencies, + isEffect, + }).suggestedDependencies; + } // Alphabetize the suggestions, but only if deps were already alphabetized. + + function areDeclaredDepsAlphabetized() { + if (declaredDependencies.length === 0) { + return true; + } + + const declaredDepKeys = declaredDependencies.map(dep => dep.key); + const sortedDeclaredDepKeys = declaredDepKeys.slice().sort(); + return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(','); + } + + if (areDeclaredDepsAlphabetized()) { + suggestedDependencies.sort(); + } + + function getWarningMessage(deps, singlePrefix, label, fixVerb) { + if (deps.size === 0) { + return null; + } + + return ( + (deps.size > 1 ? '' : singlePrefix + ' ') + + label + + ' ' + + (deps.size > 1 ? 'dependencies' : 'dependency') + + ': ' + + joinEnglish( + Array.from(deps) + .sort() + .map(name => "'" + name + "'"), + ) + + `. Either ${fixVerb} ${ + deps.size > 1 ? 'them' : 'it' + } or remove the dependency array.` + ); + } + + let extraWarning = ''; + + if (unnecessaryDependencies.size > 0) { + let badRef = null; + Array.from(unnecessaryDependencies.keys()).forEach(key => { + if (badRef !== null) { + return; + } + + if (key.endsWith('.current')) { + badRef = key; + } + }); + + if (badRef !== null) { + extraWarning = + ` Mutable values like '${badRef}' aren't valid dependencies ` + + "because mutating them doesn't re-render the component."; + } else if (externalDependencies.size > 0) { + const dep = Array.from(externalDependencies)[0]; // Don't show this warning for things that likely just got moved *inside* the callback + // because in that case they're clearly not referring to globals. + + if (!scope.set.has(dep)) { + extraWarning = + ` Outer scope values like '${dep}' aren't valid dependencies ` + + `because mutating them doesn't re-render the component.`; + } + } + } // `props.foo()` marks `props` as a dependency because it has + // a `this` value. This warning can be confusing. + // So if we're going to show it, append a clarification. + + if (!extraWarning && missingDependencies.has('props')) { + let propDep = dependencies.get('props'); + + if (propDep == null) { + return; + } + + const refs = propDep.references; + + if (!Array.isArray(refs)) { + return; + } + + let isPropsOnlyUsedInMembers = true; + + for (let i = 0; i < refs.length; i++) { + const ref = refs[i]; + const id = fastFindReferenceWithParent( + componentScope.block, + ref.identifier, + ); + + if (!id) { + isPropsOnlyUsedInMembers = false; + break; + } + + const parent = id.parent; + + if (parent == null) { + isPropsOnlyUsedInMembers = false; + break; + } + + if (parent.type !== 'MemberExpression') { + isPropsOnlyUsedInMembers = false; + break; + } + } + + if (isPropsOnlyUsedInMembers) { + extraWarning = + ` However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside of ` + + `the ${reactiveHookName} call and refer to those specific props ` + + `inside ${context.getSource(reactiveHook)}.`; + } + } + + if (!extraWarning && missingDependencies.size > 0) { + // See if the user is trying to avoid specifying a callable prop. + // This usually means they're unaware of useWorker. + let missingCallbackDep = null; + missingDependencies.forEach(missingDep => { + if (missingCallbackDep) { + return; + } // Is this a variable from top scope? + + const topScopeRef = componentScope.set.get(missingDep); + const usedDep = dependencies.get(missingDep); + + if (usedDep.references[0].resolved !== topScopeRef) { + return; + } // Is this a destructured prop? + + const def = topScopeRef.defs[0]; + + if (def == null || def.name == null || def.type !== 'Parameter') { + return; + } // Was it called in at least one case? Then it's a function. + + let isFunctionCall = false; + let id; + + for (let i = 0; i < usedDep.references.length; i++) { + id = usedDep.references[i].identifier; + + if ( + id != null && + id.parent != null && + id.parent.type === 'CallExpression' && + id.parent.callee === id + ) { + isFunctionCall = true; + break; + } + } + + if (!isFunctionCall) { + return; + } // If it's missing (i.e. in component scope) *and* it's a parameter + // then it is definitely coming from props destructuring. + // (It could also be props itself but we wouldn't be calling it then.) + + missingCallbackDep = missingDep; + }); + + if (missingCallbackDep !== null) { + extraWarning = + ` If '${missingCallbackDep}' changes too often, ` + + `find the parent component that defines it ` + + `and wrap that definition in useWorker.`; + } + } + + if (!extraWarning && missingDependencies.size > 0) { + let setStateRecommendation = null; + missingDependencies.forEach(missingDep => { + if (setStateRecommendation !== null) { + return; + } + + const usedDep = dependencies.get(missingDep); + const references = usedDep.references; + let id; + let maybeCall; + + for (let i = 0; i < references.length; i++) { + id = references[i].identifier; + maybeCall = id.parent; // Try to see if we have setState(someExpr(missingDep)). + + while (maybeCall != null && maybeCall !== componentScope.block) { + if (maybeCall.type === 'CallExpression') { + const correspondingStateVariable = setStateCallSites.get( + maybeCall.callee, + ); + + if (correspondingStateVariable != null) { + if (correspondingStateVariable.name === missingDep) { + // setCount(count + 1) + setStateRecommendation = { + missingDep, + setter: maybeCall.callee.name, + form: 'updater', + }; + } else if (stateVariables.has(id)) { + // setCount(count + increment) + setStateRecommendation = { + missingDep, + setter: maybeCall.callee.name, + form: 'reducer', + }; + } else { + const resolved = references[i].resolved; + + if (resolved != null) { + // If it's a parameter *and* a missing dep, + // it must be a prop or something inside a prop. + // Therefore, recommend an inline reducer. + const def = resolved.defs[0]; + + if (def != null && def.type === 'Parameter') { + setStateRecommendation = { + missingDep, + setter: maybeCall.callee.name, + form: 'inlineReducer', + }; + } + } + } + + break; + } + } + + maybeCall = maybeCall.parent; + } + + if (setStateRecommendation !== null) { + break; + } + } + }); + + if (setStateRecommendation !== null) { + switch (setStateRecommendation.form) { + case 'reducer': + extraWarning = + ` You can also replace multiple useState variables with useReducer ` + + `if '${setStateRecommendation.setter}' needs the ` + + `current value of '${setStateRecommendation.missingDep}'.`; + break; + + case 'inlineReducer': + extraWarning = + ` If '${setStateRecommendation.setter}' needs the ` + + `current value of '${setStateRecommendation.missingDep}', ` + + `you can also switch to useReducer instead of useState and ` + + `read '${setStateRecommendation.missingDep}' in the reducer.`; + break; + + case 'updater': + extraWarning = + ` You can also do a functional update '${ + setStateRecommendation.setter + }(${setStateRecommendation.missingDep.substring( + 0, + 1, + )} => ...)' if you only need '${ + setStateRecommendation.missingDep + }'` + ` in the '${setStateRecommendation.setter}' call.`; + break; + + default: + throw new Error('Unknown case.'); + } + } + } + + if (problemCount !== 0) { + context.report({ + node: declaredDependenciesNode, + message: + `React Hook ${context.getSource(reactiveHook)} has ` + // To avoid a long message, show the next actionable item. + (getWarningMessage( + missingDependencies, + 'a', + 'missing', + 'include', + ) || + getWarningMessage( + unnecessaryDependencies, + 'an', + 'unnecessary', + 'exclude', + ) || + getWarningMessage( + duplicateDependencies, + 'a', + 'duplicate', + 'omit', + )) + + extraWarning, + + fix(fixer) { + // TODO: consider preserving the comments or formatting? + return fixer.replaceText( + declaredDependenciesNode, + `[${suggestedDependencies.join(', ')}]`, + ); + }, + }); + } + } + }, +}; // The meat of the logic. + +function collectRecommendations({ + dependencies, + declaredDependencies, + optionalDependencies, + externalDependencies, + isEffect, +}) { + // Our primary data structure. + // It is a logical representation of property chains: + // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz` + // -> `props.lol` + // -> `props.huh` -> `props.huh.okay` + // -> `props.wow` + // We'll use it to mark nodes that are *used* by the programmer, + // and the nodes that were *declared* as deps. Then we will + // traverse it to learn which deps are missing or unnecessary. + const depTree = createDepTree(); + + function createDepTree() { + return { + isRequired: false, + // True if used in code + isSatisfiedRecursively: false, + // True if specified in deps + hasRequiredNodesBelow: false, + // True if something deeper is used by code + children: new Map(), // Nodes for properties + }; + } // Mark all required nodes first. + // Imagine exclamation marks next to each used deep property. + + dependencies.forEach((_, key) => { + const node = getOrCreateNodeByPath(depTree, key); + node.isRequired = true; + markAllParentsByPath(depTree, key, parent => { + parent.hasRequiredNodesBelow = true; + }); + }); // Mark all satisfied nodes. + // Imagine checkmarks next to each declared dependency. + + declaredDependencies.forEach(({ key }) => { + const node = getOrCreateNodeByPath(depTree, key); + node.isSatisfiedRecursively = true; + }); + optionalDependencies.forEach(key => { + const node = getOrCreateNodeByPath(depTree, key); + node.isSatisfiedRecursively = true; + }); // Tree manipulation helpers. + + function getOrCreateNodeByPath(rootNode, path) { + let keys = path.split('.'); + let node = rootNode; + + for (let key of keys) { + let child = node.children.get(key); + + if (!child) { + child = createDepTree(); + node.children.set(key, child); + } + + node = child; + } + + return node; + } + + function markAllParentsByPath(rootNode, path, fn) { + let keys = path.split('.'); + let node = rootNode; + + for (let key of keys) { + let child = node.children.get(key); + + if (!child) { + return; + } + + fn(child); + node = child; + } + } // Now we can learn which dependencies are missing or necessary. + + let missingDependencies = new Set(); + let satisfyingDependencies = new Set(); + scanTreeRecursively( + depTree, + missingDependencies, + satisfyingDependencies, + key => key, + ); + + function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) { + node.children.forEach((child, key) => { + const path = keyToPath(key); + + if (child.isSatisfiedRecursively) { + if (child.hasRequiredNodesBelow) { + // Remember this dep actually satisfied something. + satisfyingPaths.add(path); + } // It doesn't matter if there's something deeper. + // It would be transitively satisfied since we assume immutability. + // `props.foo` is enough if you read `props.foo.id`. + + return; + } + + if (child.isRequired) { + // Remember that no declared deps satisfied this node. + missingPaths.add(path); // If we got here, nothing in its subtree was satisfied. + // No need to search further. + + return; + } + + scanTreeRecursively( + child, + missingPaths, + satisfyingPaths, + childKey => path + '.' + childKey, + ); + }); + } // Collect suggestions in the order they were originally specified. + + let suggestedDependencies = []; + let unnecessaryDependencies = new Set(); + let duplicateDependencies = new Set(); + declaredDependencies.forEach(({ key }) => { + // Does this declared dep satisfy a real need? + if (satisfyingDependencies.has(key)) { + if (suggestedDependencies.indexOf(key) === -1) { + // Good one. + suggestedDependencies.push(key); + } else { + // Duplicate. + duplicateDependencies.add(key); + } + } else { + if ( + isEffect && + !key.endsWith('.current') && + !externalDependencies.has(key) + ) { + // Effects are allowed extra "unnecessary" deps. + // Such as resetting scroll when ID changes. + // Consider them legit. + // The exception is ref.current which is always wrong. + if (suggestedDependencies.indexOf(key) === -1) { + suggestedDependencies.push(key); + } + } else { + // It's definitely not needed. + unnecessaryDependencies.add(key); + } + } + }); // Then add the missing ones at the end. + + missingDependencies.forEach(key => { + suggestedDependencies.push(key); + }); + return { + suggestedDependencies, + unnecessaryDependencies, + duplicateDependencies, + missingDependencies, + }; +} +/** + * Assuming () means the passed/returned node: + * (props) => (props) + * props.(foo) => (props.foo) + * props.foo.(bar) => (props).foo.bar + * props.foo.bar.(baz) => (props).foo.bar.baz + */ + +function getDependency(node) { + if ( + node.parent.type === 'MemberExpression' && + node.parent.object === node && + node.parent.property.name !== 'current' && + !node.parent.computed && + !( + node.parent.parent != null && + node.parent.parent.type === 'CallExpression' && + node.parent.parent.callee === node.parent + ) + ) { + return getDependency(node.parent); + } else { + return node; + } +} +/** + * Assuming () means the passed node. + * (foo) -> 'foo' + * foo.(bar) -> 'foo.bar' + * foo.bar.(baz) -> 'foo.bar.baz' + * Otherwise throw. + */ + +function toPropertyAccessString(node) { + if (node.type === 'Identifier') { + return node.name; + } else if (node.type === 'MemberExpression' && !node.computed) { + const object = toPropertyAccessString(node.object); + const property = toPropertyAccessString(node.property); + return `${object}.${property}`; + } else { + throw new Error(`Unsupported node type: ${node.type}`); + } +} + +function getNodeWithoutReactNamespace(node) { + if ( + node.type === 'MemberExpression' && + node.object.type === 'Identifier' && + node.object.name === 'React' && + node.property.type === 'Identifier' && + !node.computed + ) { + return node.property; + } + + return node; +} // What's the index of callback that needs to be analyzed for a given Hook? +// -1 if it's not a Hook we care about (e.g. useState). +// 0 for useEffect/useMemo/useCallback(fn). +// 1 for useImperativeHandle(ref, fn). +// For additionally configured Hooks, assume that they're like useEffect (0). + +function getReactiveHookCallbackIndex(calleeNode) { + let node = getNodeWithoutReactNamespace(calleeNode); + + if (node.type !== 'Identifier') { + return null; + } + + switch (node.name) { + case 'useAsyncEffect': + case 'useAsyncLayoutEffect': + case 'useWorker': + case 'useWorkerState': + case 'useEffectUpdate': + return 0; + + case 'useConditionalEffect': + return 1; + + default: + return -1; + } +} +/** + * ESLint won't assign node.parent to references from context.getScope() + * + * So instead we search for the node from an ancestor assigning node.parent + * as we go. This mutates the AST. + * + * This traversal is: + * - optimized by only searching nodes with a range surrounding our target node + * - agnostic to AST node types, it looks for `{ type: string, ... }` + */ + +function fastFindReferenceWithParent(start, target) { + let queue = [start]; + let item = null; + + while (queue.length) { + item = queue.shift(); + + if (isSameIdentifier(item, target)) { + return item; + } + + if (!isAncestorNodeOf(item, target)) { + continue; + } + + for (let [key, value] of Object.entries(item)) { + if (key === 'parent') { + continue; + } + + if (isNodeLike(value)) { + value.parent = item; + queue.push(value); + } else if (Array.isArray(value)) { + value.forEach(val => { + if (isNodeLike(val)) { + val.parent = item; + queue.push(val); + } + }); + } + } + } + + return null; +} + +function joinEnglish(arr) { + let s = ''; + + for (let i = 0; i < arr.length; i++) { + s += arr[i]; + + if (i === 0 && arr.length === 2) { + s += ' and '; + } else if (i === arr.length - 2 && arr.length > 2) { + s += ', and '; + } else if (i < arr.length - 1) { + s += ', '; + } + } + + return s; +} + +function isNodeLike(val) { + return ( + typeof val === 'object' && + val !== null && + !Array.isArray(val) && + typeof val.type === 'string' + ); +} + +function isSameIdentifier(a, b) { + return ( + a.type === 'Identifier' && + a.name === b.name && + a.range[0] === b.range[0] && + a.range[1] === b.range[1] + ); +} + +function isAncestorNodeOf(a, b) { + return a.range[0] <= b.range[0] && a.range[1] >= b.range[1]; +} diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json new file mode 100644 index 0000000..e48bbf4 --- /dev/null +++ b/eslint-plugin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@react-hook-utilities/eslint-plugin", + "version": "0.1.0", + "description": "ESLint rules for hook react-hook-utilities", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin" + ], + "author": "Flávio Caetano", + "main": "lib/index.js", + "scripts": { + "test": "mocha tests --recursive src/__tests__/**/*", + "babel": "rm -rf lib && babel src/ -d ./lib" + }, + "devDependencies": { + "mocha": "^3.1.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "license": "ISC" +} diff --git a/eslint-plugin/yarn.lock b/eslint-plugin/yarn.lock new file mode 100644 index 0000000..cdcf854 --- /dev/null +++ b/eslint-plugin/yarn.lock @@ -0,0 +1,227 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +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" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8= + +commander@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= + dependencies: + graceful-readlink ">= 1.0.0" + +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= + +debug@2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + integrity sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw= + dependencies: + ms "2.0.0" + +diff@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k= + +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= + +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= + +glob@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg= + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= + +growl@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + +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: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +json3@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4= + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= + +lodash._basecreate@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" + integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw= + +lodash.create@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" + integrity sha1-1/KEnw29p+BGgruM1yqwIkYd6+c= + dependencies: + lodash._baseassign "^3.0.0" + lodash._basecreate "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo= + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +minimatch@^3.0.2: + 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" + +mocha@^3.1.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" + integrity sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg== + dependencies: + browser-stdout "1.3.0" + commander "2.9.0" + debug "2.6.8" + diff "3.2.0" + escape-string-regexp "1.0.5" + glob "7.1.1" + growl "1.9.2" + he "1.1.1" + json3 "3.3.2" + lodash.create "3.1.1" + mkdirp "0.5.1" + supports-color "3.1.2" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +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" + +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= + +supports-color@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + integrity sha1-cqJiiU2dQIuVbKBf83su2KbiotU= + dependencies: + has-flag "^1.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/jest.config.js b/jest.config.js index 749c55a..2fbe9b0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,6 @@ module.exports = { - roots: [ - "/src" - ], + roots: ['/src', '/eslint-plugin/lib'], transform: { - "^.+\\.tsx?$": "ts-jest" - } -} \ No newline at end of file + '^.+\\.tsx?$': 'ts-jest', + }, +}; diff --git a/package.json b/package.json index 441345e..2fc0f89 100644 --- a/package.json +++ b/package.json @@ -21,31 +21,40 @@ "scripts": { "build": "tsc && prettier --check 'dist/**/*' --write", "test": "jest --coverage", - "lint": "prettier --check 'src/**/*'", - "docs": "typedoc --out docs/$npm_package_version && scripts/gen-docs-index.sh $npm_package_version", + "lint": "eslint --ext=.ts,.tsx --max-warnings=0 src/ && prettier --check 'src/**/*'", + "docs": "scripts/gen-docs.sh $npm_package_version", "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" }, "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist/index.js", - "dist/index.d.ts" + "dist/index.d.ts", + "eslint-plugin/!(*/__tests__|yarn*)" ], "dependencies": { "react": "^16" }, "devDependencies": { + "@react-hook-utilities/eslint-plugin": "./eslint-plugin", "@testing-library/react-hooks": "^2.0.1", "@types/jest": "^24.0.18", "@types/react": "^16.9.2", + "@typescript-eslint/eslint-plugin": "^2.3.1", + "@typescript-eslint/parser": "^2.3.1", + "babel-eslint": "^10.0.3", "commitlint": "^8.2.0", "conventional-changelog-cli": "^2.0.25", + "eslint": "^6.5.0", + "eslint-plugin-react": "^7.14.3", + "eslint-plugin-react-hooks": "^2.1.1", "husky": "^3.0.5", "jest": "^24.9.0", "prettier": "^1.19.1", "react-test-renderer": "^16.9.0", "ts-jest": "^24.1.0", "typedoc": "^0.16.9", - "typescript": "^3.7" + "typescript": "^3.7", + "typedoc-plugin-markdown-pages": "^0.3.0" } } diff --git a/scripts/gen-docs-index.sh b/scripts/gen-docs-index.sh deleted file mode 100755 index 6c643e2..0000000 --- a/scripts/gen-docs-index.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -PKG_VERSION=$1 - -echo ";" > docs/index.html \ No newline at end of file diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh new file mode 100755 index 0000000..7786187 --- /dev/null +++ b/scripts/gen-docs.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +PKG_VERSION=$1 + +mkdir pageDocs && ln -sf ../eslint-plugin/README.md pageDocs/EslintPlugin.md + +yarn run typedoc --out "docs/${PKG_VERSION}" + +echo ";" > docs/index.html + +rm -rf pageDocs \ No newline at end of file diff --git a/src/__tests__/useDidUnmount.ts b/src/__tests__/useDidUnmount.ts index e96ae7c..65b8634 100644 --- a/src/__tests__/useDidUnmount.ts +++ b/src/__tests__/useDidUnmount.ts @@ -3,23 +3,23 @@ import { renderHook } from '@testing-library/react-hooks'; import { useDidUnmount } from '..'; let cleanup: jest.Mock; -let hook: any; +let useHook: any; beforeEach(() => { cleanup = jest.fn(); - hook = ({ func = cleanup, deps }: any = {}) => { + useHook = ({ func = cleanup, deps }: any = {}) => { useDidUnmount(func, deps); }; }); it('does not call the effect if the component is mounted', () => { - renderHook(hook); + renderHook(useHook); expect(cleanup).not.toHaveBeenCalled(); }); it('calls the effect when the component gets unmounted', () => { - const { unmount } = renderHook(hook); + const { unmount } = renderHook(useHook); expect(cleanup).not.toHaveBeenCalled(); unmount(); @@ -28,7 +28,7 @@ it('calls the effect when the component gets unmounted', () => { }); it('calls the effect only once', () => { - const { unmount } = renderHook(hook); + const { unmount } = renderHook(useHook); expect(cleanup).not.toHaveBeenCalled(); unmount(); @@ -50,7 +50,7 @@ it('runs an async funciton', async () => { finished = true; }); - const { unmount } = renderHook(hook); + const { unmount } = renderHook(useHook); unmount(); @@ -64,7 +64,7 @@ it('runs an async funciton', async () => { it('calls the effect with updated dependencies', () => { const secondCleanup = jest.fn(); - const { unmount, rerender } = renderHook(hook, { + const { unmount, rerender } = renderHook(useHook, { initialProps: { func: cleanup, deps: [cleanup] }, }); @@ -78,7 +78,7 @@ it('calls the effect with updated dependencies', () => { it('does not update the effect when the dependencies have not been updated', () => { const secondCleanup = jest.fn(); - const { unmount, rerender } = renderHook(hook, { + const { unmount, rerender } = renderHook(useHook, { initialProps: { func: cleanup, deps: [cleanup] }, }); diff --git a/src/__tests__/useEffectUpdate.ts b/src/__tests__/useEffectUpdate.ts index c00df4a..6304dc5 100644 --- a/src/__tests__/useEffectUpdate.ts +++ b/src/__tests__/useEffectUpdate.ts @@ -4,12 +4,15 @@ import { useEffectUpdate } from '..'; const deps = [1, 2, 3]; -const hook = async ({ dependencies, cleanup }: any) => +const hook = async ({ dependencies = [], cleanup }: any) => new Promise(resolve => { - useEffectUpdate(oldState => { - resolve(oldState); - return cleanup; - }, dependencies); + useEffectUpdate( + oldState => { + resolve(oldState); + return cleanup; + }, + dependencies, // eslint-disable-line @react-hook-utilities/exhaustive-deps + ); }); it('receives empty array as old state', () => { diff --git a/src/__tests__/useLazyRef.ts b/src/__tests__/useLazyRef.ts index d793201..b30eea4 100644 --- a/src/__tests__/useLazyRef.ts +++ b/src/__tests__/useLazyRef.ts @@ -3,18 +3,18 @@ import { renderHook } from '@testing-library/react-hooks'; import { useLazyRef } from '..'; const callbackFn = jest.fn(); -const mockHook = () => useLazyRef(callbackFn); +const useMockHook = () => useLazyRef(callbackFn); beforeEach(callbackFn.mockRestore); it('calls factory on first call', () => { - renderHook(mockHook); + renderHook(useMockHook); expect(callbackFn.mock.calls.length).toEqual(1); }); it('does not call factory when value is unset', () => { - const { result, rerender } = renderHook(mockHook); + const { result, rerender } = renderHook(useMockHook); expect(callbackFn.mock.calls.length).toEqual(1); // act @@ -28,7 +28,7 @@ describe('change in current value', () => { const assertChangeCurrentValue = () => { // initial value callbackFn.mockReturnValue(0); - const { result } = renderHook(mockHook); + const { result } = renderHook(useMockHook); expect(result.current.current).toEqual(0); diff --git a/src/__tests__/usePromisedState.ts b/src/__tests__/usePromisedState.ts index eaf7abf..1dca33a 100644 --- a/src/__tests__/usePromisedState.ts +++ b/src/__tests__/usePromisedState.ts @@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { usePromisedState } from '..'; describe('non-nullable, non-undefined state type', () => { - const hookHelper = () => usePromisedState(); + const useHookHelper = () => usePromisedState(); it("does not resolve promise if set isn't called", () => { let resolved = false; @@ -11,7 +11,7 @@ describe('non-nullable, non-undefined state type', () => { result: { current: [promise], }, - } = renderHook(hookHelper); + } = renderHook(useHookHelper); promise.then(() => { resolved = true; @@ -27,7 +27,7 @@ describe('non-nullable, non-undefined state type', () => { result: { current: [promise, set], }, - } = renderHook(hookHelper); + } = renderHook(useHookHelper); promise.then(() => { resolved = true; @@ -48,7 +48,7 @@ describe('non-nullable, non-undefined state type', () => { it('resolves a new promise when the state is set', async () => { let resolved = false; - const { result } = renderHook(hookHelper); + const { result } = renderHook(useHookHelper); result.current[0].then(() => { resolved = true; @@ -69,7 +69,7 @@ describe('non-nullable, non-undefined state type', () => { }); describe('nullable state type', () => { - const hookHelper = () => usePromisedState(); + const useHookHelper = () => usePromisedState(); it("does not resolve promise if set isn't called", () => { let resolved = false; @@ -77,7 +77,7 @@ describe('nullable state type', () => { result: { current: [promise], }, - } = renderHook(hookHelper); + } = renderHook(useHookHelper); promise.then(() => { resolved = true; @@ -93,7 +93,7 @@ describe('nullable state type', () => { result: { current: [promise, set], }, - } = renderHook(hookHelper); + } = renderHook(useHookHelper); promise.then(() => { resolved = true; @@ -108,7 +108,7 @@ describe('nullable state type', () => { }); it('waits on a new state after setting undefined', async () => { - const { result } = renderHook(hookHelper); + const { result } = renderHook(useHookHelper); act(() => { result.current[1](0); @@ -126,7 +126,7 @@ describe('nullable state type', () => { it('resolves a new promise when the state is set', async () => { let resolved = false; - const { result } = renderHook(hookHelper); + const { result } = renderHook(useHookHelper); result.current[0].then(() => { resolved = true; diff --git a/src/__tests__/useWorkerState.ts b/src/__tests__/useWorkerState.ts index 946d493..f3ede35 100644 --- a/src/__tests__/useWorkerState.ts +++ b/src/__tests__/useWorkerState.ts @@ -210,7 +210,7 @@ describe('callback', () => { }); it('updates the scope of the callback', async () => { - const hook = () => { + const useHook = () => { const [state, setState] = useState(0); const worker = useWorkerState(async () => { callbackFn(state); @@ -218,7 +218,7 @@ describe('callback', () => { return { state, setState, ...worker }; }; - const { result, waitForNextUpdate } = renderHook(hook); + const { result, waitForNextUpdate } = renderHook(useHook); expect(callbackFn.mock.calls[0][0]).toBe(0); diff --git a/src/index.tsx b/src/index.tsx index da9ea48..e807b53 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,7 @@ import { useCallback, useState, useRef, + useMemo, MutableRefObject, } from 'react'; @@ -55,11 +56,11 @@ export interface ValuablePromise extends Promise { */ export const useAsyncEffect = ( effect: () => Promise, - dependencies?: readonly any[], + dependencies: readonly any[] = [], ) => { useEffect(() => { effect(); - }, dependencies); + }, [...dependencies, effect]); // eslint-disable-line react-hooks/exhaustive-deps }; /** @@ -73,11 +74,11 @@ export const useAsyncEffect = ( */ export const useAsyncLayoutEffect = ( effect: () => Promise, - dependencies?: readonly any[], + dependencies: readonly any[] = [], ) => { useLayoutEffect(() => { effect(); - }, dependencies); + }, [...dependencies, effect]); // eslint-disable-line react-hooks/exhaustive-deps }; /** @@ -114,19 +115,20 @@ export const useWorker = ( setState(s => ({ ...s, isLoading })); }, []); - const callback = useCallback(async (...args: TArgs): Promise< - TRet | undefined - > => { - try { - setIsLoading(true); - const result = await worker(...args); - setState({ isLoading: false, error: undefined }); + const callback = useCallback( + async (...args: TArgs): Promise => { + try { + setIsLoading(true); + const result = await worker(...args); + setState({ isLoading: false, error: undefined }); - return result; - } catch (error) { - setState({ isLoading: false, error }); - } - }, dependencies); + return result; + } catch (error) { + setState({ isLoading: false, error }); + } + }, + [...dependencies, worker], // eslint-disable-line react-hooks/exhaustive-deps + ); return { callback, error, isLoading, setError, setIsLoading }; }; @@ -280,7 +282,7 @@ export function useEffectUpdate( } finally { oldState.current = dependencies; } - }, dependencies); + }, [...dependencies, effect]); // eslint-disable-line react-hooks/exhaustive-deps } /** @@ -448,7 +450,7 @@ export function useConditionalEffect( */ export function useConditionalEffect( evalCondition: (oldState: Dependencies) => boolean, - effect: () => (() => void) | void, + effect: () => void | (() => void), dependencies: Dependencies, ): void; export function useConditionalEffect( @@ -458,7 +460,7 @@ export function useConditionalEffect( ): void { useEffectUpdate( oldState => (evalCondition(oldState) ? effect() : undefined), - dependencies, + dependencies, // eslint-disable-line @react-hook-utilities/exhaustive-deps ); } @@ -476,7 +478,7 @@ export const useDidMount = ( if (typeof result === 'function') { return result; } - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps }; /** @@ -508,7 +510,7 @@ export const useDidUnmount = ( effect(); } }, - dependencies || [], + dependencies || [], // eslint-disable-line react-hooks/exhaustive-deps ); }; @@ -574,7 +576,7 @@ export function useWorkerState( async () => { setData(await worker()); }, - dependencies, + dependencies, // eslint-disable-line @react-hook-utilities/exhaustive-deps ); // start loading immediately @@ -612,28 +614,34 @@ export const usePromisedState = (): [ ValuablePromise, (_: T) => void, ] => { - let resolve: (v: T) => void; - const createPromise = (value?: T) => { - const result: ValuablePromise = new Promise(r => { - resolve = r; - if (value) { - r(value); - } - }); - result.value = value; - return result; - }; + let resolve = useRef<(v: T) => void>(); + const createPromise = useMemo( + () => (value?: T) => { + const result: ValuablePromise = new Promise(r => { + resolve.current = r; + if (value) { + r(value); + } + }); + result.value = value; + return result; + }, + [], + ); const [state, setState] = useState(createPromise); return [ state, - useCallback((newValue: T) => { - if (!!newValue) { - resolve(newValue); - state.value = newValue; - } - - setState(createPromise(newValue)); - }, []), + useCallback( + (newValue: T) => { + if (!!newValue) { + resolve.current!(newValue); + state.value = newValue; + } + + setState(createPromise(newValue)); + }, + [createPromise, state.value], + ), // eslint-disable-line @react-hook-utilities/exhaustive-deps ]; }; diff --git a/tsconfig.json b/tsconfig.json index cdbdbeb..fce9ac4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,8 @@ }, "typedocOptions": { "exclude": "src/__tests__/**/*", - "out": "docs" + "out": "docs", + "mdPagesSourcePath": "pageDocs", + "theme": "markdown-pages" } } diff --git a/yarn.lock b/yarn.lock index 3ce89dd..6a26720 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,16 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.6.2": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369" + integrity sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg== + dependencies: + "@babel/types" "^7.7.4" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-function-name@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" @@ -86,7 +96,12 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0": +"@babel/parser@^7.0.0", "@babel/parser@^7.6.0", "@babel/parser@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1" + integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg== + +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== @@ -114,7 +129,22 @@ "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.0": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c" + integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.2" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.6.2" + "@babel/types" "^7.6.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/traverse@^7.4.3", "@babel/traverse@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.0.tgz#389391d510f79be7ce2ddd6717be66d3fed4b516" integrity sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ== @@ -138,6 +168,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193" + integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -428,6 +467,9 @@ mkdirp "^0.5.1" rimraf "^2.5.2" +"@react-hook-utilities/eslint-plugin@./eslint-plugin": + version "0.1.0" + "@testing-library/react-hooks@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-2.0.1.tgz#1c3ec40882d0830df3078ddae0056fdf7366c81d" @@ -470,6 +512,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -502,6 +549,11 @@ dependencies: "@types/jest-diff" "*" +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + "@types/minimatch@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -559,6 +611,49 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^2.3.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz#c4cb103275e555e8a7e9b3d14c5951eb6d431e70" + integrity sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw== + dependencies: + "@typescript-eslint/experimental-utils" "2.10.0" + eslint-utils "^1.4.3" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz#8db1656cdfd3d9dcbdbf360b8274dea76f0b2c2c" + integrity sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.10.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/parser@^2.3.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.10.0.tgz#24b2e48384ab6d5a6121e4c4faf8892c79657ad3" + integrity sha512-wQNiBokcP5ZsTuB+i4BlmVWq6o+oAhd8en2eSm/EE9m7BgZUIfEeYFd6z3S+T7bgNuloeiHA1/cevvbBDLr98g== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.10.0" + "@typescript-eslint/typescript-estree" "2.10.0" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz#89cdabd5e8c774e9d590588cb42fb9afd14dcbd9" + integrity sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -585,6 +680,11 @@ acorn-globals@^4.1.0: acorn "^6.0.1" acorn-walk "^6.0.1" +acorn-jsx@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" + integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== + acorn-walk@^6.0.1: version "6.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" @@ -600,12 +700,17 @@ acorn@^6.0.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== +acorn@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= -ajv@^6.5.5: +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== @@ -620,6 +725,13 @@ ansi-escapes@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" + integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg== + dependencies: + type-fest "^0.8.1" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -635,6 +747,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -700,6 +817,14 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -757,6 +882,18 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +babel-eslint@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" + integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.0.0" + "@babel/traverse" "^7.0.0" + "@babel/types" "^7.0.0" + eslint-visitor-keys "^1.0.0" + resolve "^1.12.0" + babel-jest@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" @@ -982,7 +1119,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -991,6 +1128,11 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + chownr@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" @@ -1011,6 +1153,18 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.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= + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -1303,7 +1457,7 @@ cosmiconfig@^5.2.0, cosmiconfig@^5.2.1: js-yaml "^3.13.1" parse-json "^4.0.0" -cross-spawn@^6.0.0: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -1380,7 +1534,7 @@ debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -1469,6 +1623,20 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +doctrine@^2.1.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" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -1496,6 +1664,11 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -1510,6 +1683,22 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.12.0, es-abstract@^1.15.0, es-abstract@^1.7.0: + version "1.16.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.2.tgz#4e874331645e9925edef141e74fc4bd144669d34" + integrity sha512-jYo/J8XU2emLXl3OLwfwtuFfuF2w6DYPs+xy9ZfVyPkDcrauu6LYrw/q2TyCtrbc/KUdCiC5e9UajRhgNkVopA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.7.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + es-abstract@^1.5.1: version "1.14.2" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.14.2.tgz#7ce108fad83068c8783c3cdf62e504e084d8c497" @@ -1535,6 +1724,15 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^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" @@ -1552,6 +1750,104 @@ escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" +eslint-plugin-eslint-plugin@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz#a7a00f15a886957d855feacaafee264f039e62d5" + integrity sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg== + +eslint-plugin-react-hooks@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a" + integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw== + +eslint-plugin-react@^7.14.3: + version "7.17.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz#a31b3e134b76046abe3cd278e7482bd35a1d12d7" + integrity sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A== + dependencies: + array-includes "^3.0.3" + doctrine "^2.1.0" + eslint-plugin-eslint-plugin "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.2.3" + object.entries "^1.1.0" + object.fromentries "^2.0.1" + object.values "^1.1.0" + prop-types "^15.7.2" + resolve "^1.13.1" + +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@^6.5.0: + version "6.7.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1" + integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" + integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== + dependencies: + acorn "^7.1.0" + acorn-jsx "^5.1.0" + eslint-visitor-keys "^1.1.0" + esprima@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -1562,7 +1858,21 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -estraverse@^4.2.0: +esquery@^1.0.1: + 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.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -1640,6 +1950,15 @@ extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -1674,7 +1993,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^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: +fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -1686,6 +2005,20 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +figures@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" + integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -1726,6 +2059,20 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -1786,6 +2133,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -1890,6 +2242,13 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +glob-parent@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -1902,6 +2261,18 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + 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" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -1914,6 +2285,13 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" + integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== + dependencies: + type-fest "^0.8.1" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" @@ -1980,6 +2358,11 @@ has-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -2066,7 +2449,7 @@ husky@^3.0.5: run-node "^1.0.0" slash "^3.0.0" -iconv-lite@0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -2080,6 +2463,11 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -2139,6 +2527,25 @@ ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +inquirer@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" + integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^2.4.2" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^4.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + interpret@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" @@ -2241,6 +2648,11 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -2260,11 +2672,23 @@ is-fullwidth-code-point@^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-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -2289,6 +2713,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +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-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -2843,6 +3272,11 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -2877,6 +3311,14 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jsx-ast-utils@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f" + integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA== + dependencies: + array-includes "^3.0.3" + object.assign "^4.1.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -2916,7 +3358,7 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levn@~0.3.0: +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= @@ -3008,12 +3450,17 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash@4.17.14: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@^4.14.14, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.2.1: +lodash@^4.14.14, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -3167,6 +3614,11 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.40.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -3242,6 +3694,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -3414,7 +3871,12 @@ object-inspect@^1.6.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== -object-keys@^1.0.12, object-keys@^1.1.1: +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -3426,6 +3888,36 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.entries@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" + integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +object.fromentries@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" + integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.15.0" + function-bind "^1.1.1" + has "^1.0.3" + object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -3441,6 +3933,16 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +object.values@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" + integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3448,6 +3950,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + opencollective-postinstall@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" @@ -3473,12 +3982,24 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-tmpdir@^1.0.0: +os-tmpdir@^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= @@ -3743,7 +4264,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@^2.0.3: +progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -3756,7 +4277,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@^15.6.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -3969,6 +4490,16 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +regexpp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" + integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -4089,18 +4620,40 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2: +resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== dependencies: path-parse "^1.0.6" +resolve@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16" + integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w== + dependencies: + path-parse "^1.0.6" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +rimraf@2.6.3: + 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" + rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -4113,11 +4666,25 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +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" + run-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A== +rxjs@^6.4.0: + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" @@ -4183,7 +4750,7 @@ semver@6.2.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== -semver@^6.0.0, semver@^6.2.0: +semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -4249,6 +4816,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -4432,7 +5008,16 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string.prototype.trimleft@^2.0.0: +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.trimleft@^2.0.0, string.prototype.trimleft@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== @@ -4440,7 +5025,7 @@ string.prototype.trimleft@^2.0.0: define-properties "^1.1.3" function-bind "^1.1.1" -string.prototype.trimright@^2.0.0: +string.prototype.trimright@^2.0.0, string.prototype.trimright@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== @@ -4483,6 +5068,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -4512,6 +5104,11 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= +strip-json-comments@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + 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" @@ -4536,6 +5133,16 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + tar@^4: version "4.4.11" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.11.tgz#7ac09801445a3cf74445ed27499136b5240ffb73" @@ -4582,6 +5189,11 @@ text-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.0.0.tgz#43eabd1b495482fae4a2bf65e5f56c29f69220f6" integrity sha512-F91ZqLgvi1E0PdvmxMgp+gcf6q8fMH7mhdwWfzXnl1k+GbpQDmi8l7DzLC5JTASKbwpY3TfxajAUzAXcv2NmsQ== +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= + throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" @@ -4602,11 +5214,18 @@ through2@^3.0.0: dependencies: readable-stream "2 || 3" -through@2, "through@>=2.2.7 <3": +through@2, "through@>=2.2.7 <3", 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" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -4701,6 +5320,18 @@ ts-jest@^24.1.0: semver "^5.5" yargs-parser "10.x" +tslib@^1.8.1, tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -4725,6 +5356,11 @@ type-fest@^0.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + typedoc-default-themes@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.7.2.tgz#1e9896f920b58e6da0bba9d7e643738d02405a5a" @@ -4735,6 +5371,11 @@ typedoc-default-themes@^0.7.2: lunr "^2.3.8" underscore "^1.9.1" +typedoc-plugin-markdown-pages@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown-pages/-/typedoc-plugin-markdown-pages-0.3.0.tgz#17f0f9ff3aa312e9ed142eb15ea216ea95d5fb5f" + integrity sha512-QVWs6eL0BEOiRgJm6wRRgE8kESFv85HlefLv14ZbMM2VQ1HqErVeT9ukbhkYMznsD/juAsX3Xc5M+Ju7/ZDq4Q== + typedoc@^0.16.9: version "0.16.9" resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.16.9.tgz#d6f46f4dea7d3362029927a92981efdf896f435b" @@ -4833,6 +5474,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -4918,6 +5564,11 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" @@ -4951,6 +5602,13 @@ write-file-atomic@2.4.1: imurmurhash "^0.1.4" signal-exit "^3.0.2" +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + ws@^5.2.0: version "5.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"