diff --git a/package-lock.json b/package-lock.json index cef5231..bbeed6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -703,6 +703,30 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", + "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", @@ -1004,9 +1028,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, "license": "MIT", "dependencies": { @@ -2128,6 +2152,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2406,6 +2440,10 @@ "tslib": "^2.0.3" } }, + "node_modules/node-test-floating-promises": { + "resolved": "packages/node-test-floating-promises", + "link": true + }, "node_modules/optionator": { "version": "0.9.3", "license": "MIT", @@ -2618,6 +2656,25 @@ "dev": true, "license": "MIT" }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/redux-toolkit-floating-promises": { + "resolved": "packages/redux-toolkit-floating-promises", + "link": true + }, "node_modules/require-from-string": { "version": "2.0.2", "dev": true, @@ -2626,6 +2683,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3664,6 +3727,29 @@ "typescript-eslint": "^8.18.0" } }, + "packages/node-test-floating-promises": { + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.10.7", + "eslint": "^9.16.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" + } + }, + "packages/redux-toolkit-floating-promises": { + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "2.5.0" + }, + "devDependencies": { + "@types/node": "^22.10.7", + "eslint": "^9.16.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" + } + }, "packages/typed-rule-via-linter": { "version": "0.0.0", "license": "MIT", diff --git a/packages/redux-toolkit-floating-promises/README.md b/packages/redux-toolkit-floating-promises/README.md new file mode 100644 index 0000000..8b89e63 --- /dev/null +++ b/packages/redux-toolkit-floating-promises/README.md @@ -0,0 +1,25 @@ +# Example: `redux-toolkit` and Floating Promise Detection + +An example of using [`redux-toolkit`](https://redux-toolkit.js.org) along with the [`@typescript-eslint/no-floating-promises` rule](https://typescript-eslint.io/rules/no-floating-promises) enabled. +It uses the [`allowForKnownSafePromises` rule option](https://typescript-eslint.io/rules/no-floating-promises/#allowforknownsafepromises) to not report on RTK APIs that create a `SafePromise`, such as `createAsyncThunk`. + +## Setup + +```shell +npm i +``` + +## Usage + +```shell +npm run lint +``` + +There should be no lint reports. + +If you remove the `allowForKnownSafePromises` option from `eslint.config.js`, there will be: + +```plaintext +.../index.test.ts + 4:1 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises +``` \ No newline at end of file diff --git a/packages/redux-toolkit-floating-promises/eslint.config.js b/packages/redux-toolkit-floating-promises/eslint.config.js new file mode 100644 index 0000000..b418e95 --- /dev/null +++ b/packages/redux-toolkit-floating-promises/eslint.config.js @@ -0,0 +1,29 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.config.*'] + }, + }, + }, + rules: { + "@typescript-eslint/no-floating-promises": [ + "error", { + "allowForKnownSafePromises": [ + { from: "package", name: "SafePromise", package: "@reduxjs/toolkit" }, + { from: 'package', name: 'SafePromise', package: '@reduxjs/toolkit/query' }, + { from: 'package', name: 'SafePromise', package: '@reduxjs/toolkit/query/react' }, + ] + } + ] + } + } +); diff --git a/packages/redux-toolkit-floating-promises/index.ts b/packages/redux-toolkit-floating-promises/index.ts new file mode 100644 index 0000000..81eba26 --- /dev/null +++ b/packages/redux-toolkit-floating-promises/index.ts @@ -0,0 +1,16 @@ +import { configureStore, createAsyncThunk } from '@reduxjs/toolkit'; + +function reducer() { + // ... +} + +const store = configureStore({ reducer }); + +const exampleThunk = createAsyncThunk( + 'example', + async () => { + // ... + }, +); + +store.dispatch(exampleThunk()); diff --git a/packages/redux-toolkit-floating-promises/package.json b/packages/redux-toolkit-floating-promises/package.json new file mode 100644 index 0000000..fc0a59d --- /dev/null +++ b/packages/redux-toolkit-floating-promises/package.json @@ -0,0 +1,26 @@ +{ + "name": "redux-toolkit-floating-promises", + "version": "0.0.0", + "description": "Example of using `redux-toolkit` with `@typescript-eslint/no-floating-promises`'s `allowForKnownSafePromises` option.", + "main": "index.ts", + "repository": { + "directory": "packages/redux-toolkit-floating-promises", + "type": "git", + "url": "https://github.com/typescript-eslint/typescript-eslint-examples" + }, + "license": "MIT", + "devDependencies": { + "@types/node": "^22.10.7", + "eslint": "^9.16.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" + }, + "dependencies": { + "@reduxjs/toolkit": "2.5.0" + }, + "scripts": { + "lint": "eslint .", + "tsc": "tsc" + }, + "type": "module" +} diff --git a/packages/redux-toolkit-floating-promises/tsconfig.json b/packages/redux-toolkit-floating-promises/tsconfig.json new file mode 100644 index 0000000..5283fbc --- /dev/null +++ b/packages/redux-toolkit-floating-promises/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true + }, +}