diff --git a/config/scope.js b/config/scope.js index 85902a8..05ba3a1 100644 --- a/config/scope.js +++ b/config/scope.js @@ -1,5 +1,6 @@ module.exports = { rules: { "effector/strict-effect-handlers": "error", + "effector/require-pickup-in-persist": "error", }, }; diff --git a/docs/rules/require-pickup-in-persist.md b/docs/rules/require-pickup-in-persist.md new file mode 100644 index 0000000..6bcfeb0 --- /dev/null +++ b/docs/rules/require-pickup-in-persist.md @@ -0,0 +1,19 @@ +# effector/require-pickup-in-persist + +Requires every `persist` call of the [`effector-storage`](https://github.com/yumauri/effector-storage) library to include a `pickup` event when using [`Scope`s](https://effector.dev/api/effector/scope/). This ensures the correct initial value is loaded into the store for each `Scope`. + +```ts +import { persist } from "effector-storage/query"; + +const $store = createStore("example"); + +// 👎 no pickup, does not work with Scope +persist({ store: $store }); +``` + +```ts +const pickup = createEvent(); + +// 👍 pickup is specified +persist({ store: $store, pickup }); +``` diff --git a/index.js b/index.js index ea36aa9..66b7952 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ module.exports = { "no-guard": require("./rules/no-guard/no-guard"), "mandatory-scope-binding": require("./rules/mandatory-scope-binding/mandatory-scope-binding"), "prefer-useUnit": require("./rules/prefer-useUnit/prefer-useUnit"), + "require-pickup-in-persist": require("./rules/require-pickup-in-persist/require-pickup-in-persist"), "no-patronum-debug": require("./rules/no-patronum-debug/no-patronum-debug"), }, configs: { diff --git a/rules/require-pickup-in-persist/examples/correct-complete-config.js b/rules/require-pickup-in-persist/examples/correct-complete-config.js new file mode 100644 index 0000000..47637c8 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/correct-complete-config.js @@ -0,0 +1,18 @@ +import { createStore, createEvent } from "effector"; + +import { persist } from "effector-storage/local"; + +const $store = createStore("example"); +const updated = createEvent(); + +const appStarted = createEvent(); + +persist({ + source: $store.updates, + target: updated, + + pickup: appStarted, + + key: "store", + keyPrefix: "local", +}); diff --git a/rules/require-pickup-in-persist/examples/correct-core-package.js b/rules/require-pickup-in-persist/examples/correct-core-package.js new file mode 100644 index 0000000..44656a9 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/correct-core-package.js @@ -0,0 +1,9 @@ +import { createStore, createEvent } from "effector"; + +import { persist } from "effector-storage"; +import { local as localAdapter } from "effector-storage/local"; + +const $store = createStore("example"); +const pickup = createEvent(); + +persist({ store: $store, pickup, adapter: localAdapter }); diff --git a/rules/require-pickup-in-persist/examples/correct-other-packages.js b/rules/require-pickup-in-persist/examples/correct-other-packages.js new file mode 100644 index 0000000..f41be6d --- /dev/null +++ b/rules/require-pickup-in-persist/examples/correct-other-packages.js @@ -0,0 +1,9 @@ +import { createStore } from "effector"; + +import { persist } from "other-persist"; +import { persist as persistNested } from "other-persist/nested"; + +const $store = createStore("example"); + +persist({ store: $store }); +persistNested({ store: $store }); diff --git a/rules/require-pickup-in-persist/examples/correct-query-package.js b/rules/require-pickup-in-persist/examples/correct-query-package.js new file mode 100644 index 0000000..736efbd --- /dev/null +++ b/rules/require-pickup-in-persist/examples/correct-query-package.js @@ -0,0 +1,8 @@ +import { createStore, createEvent } from "effector"; + +import { persist as persistQuery } from "effector-storage/query"; + +const $store = createStore("example"); +const pickup = createEvent(); + +persistQuery({ store: $store, pickup }); diff --git a/rules/require-pickup-in-persist/examples/correct-scoped-package.js b/rules/require-pickup-in-persist/examples/correct-scoped-package.js new file mode 100644 index 0000000..2a28683 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/correct-scoped-package.js @@ -0,0 +1,8 @@ +import { createStore, createEvent } from "effector"; + +import { persist as persistAsync } from "@effector-storage/react-native-async-storage"; + +const $store = createStore("example"); +const pickup = createEvent(); + +persistAsync({ store: $store, pickup }); diff --git a/rules/require-pickup-in-persist/examples/correct-skip-misconfigured.js b/rules/require-pickup-in-persist/examples/correct-skip-misconfigured.js new file mode 100644 index 0000000..2c6fe14 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/correct-skip-misconfigured.js @@ -0,0 +1,9 @@ +import { createStore } from "effector"; + +import { persist } from "effector-storage"; + +const randomCall = () => ({ store: createStore() }); + +persist(); +persist("invalid"); +persist(randomCall()); diff --git a/rules/require-pickup-in-persist/examples/incorrect-complete-config.js b/rules/require-pickup-in-persist/examples/incorrect-complete-config.js new file mode 100644 index 0000000..9166764 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/incorrect-complete-config.js @@ -0,0 +1,14 @@ +import { createStore, createEvent } from "effector"; + +import { persist } from "effector-storage/local"; + +const $store = createStore("example"); +const updated = createEvent(); + +persist({ + source: $store, + target: updated, + + key: "store", + keyPrefix: "local", +}); diff --git a/rules/require-pickup-in-persist/examples/incorrect-core-package.js b/rules/require-pickup-in-persist/examples/incorrect-core-package.js new file mode 100644 index 0000000..710b99f --- /dev/null +++ b/rules/require-pickup-in-persist/examples/incorrect-core-package.js @@ -0,0 +1,7 @@ +import { createStore } from "effector"; + +import { persist } from "effector-storage"; + +const $store = createStore("example"); + +persist({ store: $store, adapter: localAdapter }); diff --git a/rules/require-pickup-in-persist/examples/incorrect-scoped-package.js b/rules/require-pickup-in-persist/examples/incorrect-scoped-package.js new file mode 100644 index 0000000..e952844 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/incorrect-scoped-package.js @@ -0,0 +1,7 @@ +import { createStore, createEvent } from "effector"; + +import { persist as persistAsync } from "@effector-storage/react-native-async-storage"; + +const $store = createStore("example"); + +persistAsync({ store: $store }); diff --git a/rules/require-pickup-in-persist/examples/incorrect-unrelated-pickup.js b/rules/require-pickup-in-persist/examples/incorrect-unrelated-pickup.js new file mode 100644 index 0000000..7701420 --- /dev/null +++ b/rules/require-pickup-in-persist/examples/incorrect-unrelated-pickup.js @@ -0,0 +1,8 @@ +import { combine } from "effector"; + +import { persist } from "effector-storage/local"; + +persist({ + store: combine({ pickup: true }), + param: { pickup: "yes" }, +}); diff --git a/rules/require-pickup-in-persist/require-pickup-in-persist.js b/rules/require-pickup-in-persist/require-pickup-in-persist.js new file mode 100644 index 0000000..97b395f --- /dev/null +++ b/rules/require-pickup-in-persist/require-pickup-in-persist.js @@ -0,0 +1,47 @@ +const { createLinkToRule } = require("../../utils/create-link-to-rule"); + +module.exports = { + meta: { + type: "problem", + docs: { + category: "Quality", + url: createLinkToRule("require-pickup-in-persist"), + }, + messages: { + pickupMissing: + "This `persist` call does not specify a `pickup` event that is required for scoped usage of `effector-storage`.", + }, + schema: [], + }, + create(context) { + const pickupImports = new Set(); + + /** + * Finds `effector-storage` packages, scoped and unscoped, including + * contents of these packages. See examples for a full list. + */ + const PACKAGE_NAME = /^@?effector-storage(\u002F[\w-]+)*$/; + + const declarationSelector = `ImportDeclaration[source.value=${PACKAGE_NAME}]`; + const persistImportSelector = `ImportSpecifier[imported.name="persist"]`; + + const configSelector = `[arguments.length=1][arguments.0.type="ObjectExpression"]`; + const callSelector = `[callee.type="Identifier"]`; + + return { + [`${declarationSelector} > ${persistImportSelector}`](node) { + pickupImports.add(node.local.name); + }, + [`CallExpression${configSelector}${callSelector}`](node) { + if (!pickupImports.has(node.callee.name)) return; + + const config = node.arguments[0]; + + if (config.properties.some((prop) => prop.key?.name === "pickup")) + return; + + context.report({ node, messageId: "pickupMissing" }); + }, + }; + }, +}; diff --git a/rules/require-pickup-in-persist/require-pickup-in-persist.md b/rules/require-pickup-in-persist/require-pickup-in-persist.md new file mode 100644 index 0000000..0dfa7b3 --- /dev/null +++ b/rules/require-pickup-in-persist/require-pickup-in-persist.md @@ -0,0 +1 @@ +https://eslint.effector.dev/rules/require-pickup-in-persist.html diff --git a/rules/require-pickup-in-persist/require-pickup-in-persist.test.js b/rules/require-pickup-in-persist/require-pickup-in-persist.test.js new file mode 100644 index 0000000..223485a --- /dev/null +++ b/rules/require-pickup-in-persist/require-pickup-in-persist.test.js @@ -0,0 +1,40 @@ +const { RuleTester } = require("eslint"); +const { join } = require("path"); + +const { + readExample, + getCorrectExamples, + getIncorrectExamples, +} = require("../../utils/read-example"); + +const rule = require("./require-pickup-in-persist"); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}); + +const readExampleForTheRule = (name) => ({ + code: readExample(__dirname, name), + filename: join(__dirname, "examples", name), +}); + +ruleTester.run("effector/require-pickup-in-persist.js.test", rule, { + valid: getCorrectExamples(__dirname).map(readExampleForTheRule), + + invalid: + // Errors + getIncorrectExamples(__dirname) + .map(readExampleForTheRule) + .map((result) => ({ + ...result, + errors: [ + { + messageId: "pickupMissing", + type: "CallExpression", + }, + ], + })), +});