@krymskyimaksym/eslint-plugin-react-api-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @krymskyimaksym/eslint-plugin-react-api-client
2
+
3
+ ESLint-правила для [`@krymskyimaksym/react-api-client`](../..).
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ npm i -D @krymskyimaksym/eslint-plugin-react-api-client
9
+ ```
10
+
11
+ ## Подключение
12
+
13
+ ```js
14
+ // .eslintrc.js
15
+ module.exports = {
16
+ plugins: ['@krymskyimaksym/react-api-client'],
17
+ extends: ['plugin:@krymskyimaksym/react-api-client/recommended'],
18
+ };
19
+ ```
20
+
21
+ Или вручную:
22
+
23
+ ```js
24
+ {
25
+ rules: {
26
+ '@krymskyimaksym/react-api-client/no-await-mutate': 'error',
27
+ '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': 'warn',
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Правила
33
+
34
+ ### `no-await-mutate` (error, autofix)
35
+
36
+ `mutate(...)` возвращает `void`. `await api.mutate(...)` отдаст
37
+ `undefined` и не поймает ошибки в `try/catch`. Для последовательной
38
+ логики или try/catch используй `mutateAsync`.
39
+
40
+ ```ts
41
+ // ❌
42
+ await api.mutate({ id: 1 });
43
+
44
+ // ✅
45
+ await api.mutateAsync({ id: 1 });
46
+ ```
47
+
48
+ Autofix меняет `.mutate` на `.mutateAsync`.
49
+
50
+ ### `require-query-key-when-endpoint-is-fn` (warn)
51
+
52
+ Если `apiClient`/`apiPaginate` создан с endpoint-функцией, и хук
53
+ вызывается без явного `queryKey` в опциях — предупреждение.
54
+
55
+ ```ts
56
+ // ❌
57
+ const userApi = apiClient((p) => `/users/${p.id}`);
58
+ userApi.useFetch({ id: 1 });
59
+
60
+ // ✅
61
+ userApi.useFetch({ id: 1 }, { queryKey: ['user', 1] });
62
+ ```
63
+
64
+ При endpoint-функции стабильность ключа зависит от того, что функция
65
+ возвращает одну и ту же строку для одних и тех же params. Явный
66
+ `queryKey` снимает риск.
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,32 @@
1
+ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
2
+
3
+ declare const rules: {
4
+ 'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
5
+ 'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
6
+ };
7
+ declare const configs: {
8
+ recommended: {
9
+ plugins: string[];
10
+ rules: {
11
+ '@krymskyimaksym/react-api-client/no-await-mutate': string;
12
+ '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
13
+ };
14
+ };
15
+ };
16
+ declare const _default: {
17
+ rules: {
18
+ 'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
19
+ 'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
20
+ };
21
+ configs: {
22
+ recommended: {
23
+ plugins: string[];
24
+ rules: {
25
+ '@krymskyimaksym/react-api-client/no-await-mutate': string;
26
+ '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
27
+ };
28
+ };
29
+ };
30
+ };
31
+
32
+ export { configs, _default as default, rules };
@@ -0,0 +1,32 @@
1
+ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
2
+
3
+ declare const rules: {
4
+ 'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
5
+ 'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
6
+ };
7
+ declare const configs: {
8
+ recommended: {
9
+ plugins: string[];
10
+ rules: {
11
+ '@krymskyimaksym/react-api-client/no-await-mutate': string;
12
+ '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
13
+ };
14
+ };
15
+ };
16
+ declare const _default: {
17
+ rules: {
18
+ 'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
19
+ 'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
20
+ };
21
+ configs: {
22
+ recommended: {
23
+ plugins: string[];
24
+ rules: {
25
+ '@krymskyimaksym/react-api-client/no-await-mutate': string;
26
+ '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
27
+ };
28
+ };
29
+ };
30
+ };
31
+
32
+ export { configs, _default as default, rules };
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var utils = require('@typescript-eslint/utils');
6
+
7
+ // src/rules/no-await-mutate.ts
8
+ var createRule = utils.ESLintUtils.RuleCreator(
9
+ (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
10
+ );
11
+ var noAwaitMutate = createRule({
12
+ name: "no-await-mutate",
13
+ meta: {
14
+ type: "problem",
15
+ docs: {
16
+ description: "mutate() returns void; await it has no effect. Use mutateAsync for awaitable mutations.",
17
+ recommended: "error"
18
+ },
19
+ fixable: "code",
20
+ schema: [],
21
+ messages: {
22
+ avoid: "`mutate` \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 void \u2014 await \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 `mutateAsync` \u0434\u043B\u044F await/try-catch."
23
+ }
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ return {
28
+ AwaitExpression(node) {
29
+ const arg = node.argument;
30
+ if (arg.type !== "CallExpression") return;
31
+ const callee = arg.callee;
32
+ if (callee.type !== "MemberExpression") return;
33
+ const prop = callee.property;
34
+ if (prop.type !== "Identifier" || prop.name !== "mutate") return;
35
+ context.report({
36
+ node,
37
+ messageId: "avoid",
38
+ fix(fixer) {
39
+ return fixer.replaceText(prop, "mutateAsync");
40
+ }
41
+ });
42
+ }
43
+ };
44
+ }
45
+ });
46
+ var createRule2 = utils.ESLintUtils.RuleCreator(
47
+ (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
48
+ );
49
+ var requireQueryKeyWhenEndpointIsFn = createRule2({
50
+ name: "require-query-key-when-endpoint-is-fn",
51
+ meta: {
52
+ type: "suggestion",
53
+ docs: {
54
+ description: "When apiClient is created from an endpoint function, useFetch should specify explicit queryKey.",
55
+ recommended: "warn"
56
+ },
57
+ schema: [],
58
+ messages: {
59
+ missing: "apiClient \u0441 endpoint-\u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439: \u0443\u043A\u0430\u0436\u0438 `queryKey` \u044F\u0432\u043D\u043E, \u0447\u0442\u043E\u0431\u044B \u043A\u043B\u044E\u0447 \u043A\u044D\u0448\u0430 \u0431\u044B\u043B \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u044B\u043C."
60
+ }
61
+ },
62
+ defaultOptions: [],
63
+ create(context) {
64
+ const apisWithFnEndpoint = /* @__PURE__ */ new Set();
65
+ return {
66
+ // const xApi = apiClient(fn, ...)
67
+ VariableDeclarator(node) {
68
+ if (node.id.type !== "Identifier") return;
69
+ if (!node.init || node.init.type !== "CallExpression") return;
70
+ const call = node.init;
71
+ if (call.callee.type !== "Identifier" || call.callee.name !== "apiClient" && call.callee.name !== "apiPaginate")
72
+ return;
73
+ const firstArg = call.arguments[0];
74
+ if (!firstArg) return;
75
+ if (firstArg.type === "ArrowFunctionExpression" || firstArg.type === "FunctionExpression") {
76
+ apisWithFnEndpoint.add(node.id.name);
77
+ }
78
+ },
79
+ // xApi.useFetch(params, options?)
80
+ CallExpression(node) {
81
+ if (node.callee.type !== "MemberExpression") return;
82
+ const obj = node.callee.object;
83
+ const prop = node.callee.property;
84
+ if (obj.type !== "Identifier") return;
85
+ if (!apisWithFnEndpoint.has(obj.name)) return;
86
+ if (prop.type !== "Identifier") return;
87
+ if (prop.name !== "useFetch" && prop.name !== "usePaginate") return;
88
+ const optionsArg = node.arguments[1];
89
+ if (!optionsArg) {
90
+ context.report({ node, messageId: "missing" });
91
+ return;
92
+ }
93
+ if (optionsArg.type !== "ObjectExpression") return;
94
+ const hasQueryKey = optionsArg.properties.some(
95
+ (p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === "queryKey"
96
+ );
97
+ if (!hasQueryKey) {
98
+ context.report({ node, messageId: "missing" });
99
+ }
100
+ }
101
+ };
102
+ }
103
+ });
104
+
105
+ // src/index.ts
106
+ var rules = {
107
+ "no-await-mutate": noAwaitMutate,
108
+ "require-query-key-when-endpoint-is-fn": requireQueryKeyWhenEndpointIsFn
109
+ };
110
+ var configs = {
111
+ recommended: {
112
+ plugins: ["@krymskyimaksym/react-api-client"],
113
+ rules: {
114
+ "@krymskyimaksym/react-api-client/no-await-mutate": "error",
115
+ "@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn": "warn"
116
+ }
117
+ }
118
+ };
119
+ var index_default = { rules, configs };
120
+
121
+ exports.configs = configs;
122
+ exports.default = index_default;
123
+ exports.rules = rules;
124
+ //# sourceMappingURL=index.js.map
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/no-await-mutate.ts","../src/rules/require-query-key-when-endpoint-is-fn.ts","../src/index.ts"],"names":["ESLintUtils","createRule"],"mappings":"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","file":"index.js","sourcesContent":["import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';\n\nconst createRule = ESLintUtils.RuleCreator(\n name =>\n `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`,\n);\n\n/**\n * Запрещает `await x.mutate(...)` — `mutate` возвращает `void`, а не\n * `Promise`. Для последовательной логики или try/catch используй\n * `mutateAsync`.\n *\n * Эвристика: `await <expr>.mutate(...)`.\n */\nexport const noAwaitMutate = createRule({\n name: 'no-await-mutate',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'mutate() returns void; await it has no effect. Use mutateAsync for awaitable mutations.',\n recommended: 'error',\n },\n fixable: 'code',\n schema: [],\n messages: {\n avoid:\n '`mutate` возвращает void — await не сработает. Используй `mutateAsync` для await/try-catch.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n AwaitExpression(node: TSESTree.AwaitExpression) {\n const arg = node.argument;\n if (arg.type !== 'CallExpression') return;\n const callee = arg.callee;\n if (callee.type !== 'MemberExpression') return;\n const prop = callee.property;\n if (prop.type !== 'Identifier' || prop.name !== 'mutate') return;\n\n context.report({\n node,\n messageId: 'avoid',\n fix(fixer) {\n return fixer.replaceText(prop, 'mutateAsync');\n },\n });\n },\n };\n },\n});\n","import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';\n\nconst createRule = ESLintUtils.RuleCreator(\n name =>\n `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`,\n);\n\n/**\n * Если `apiClient(fn, ...)` создан с endpoint-функцией, и где-то\n * вызывается `<api>.useFetch(params)` без `queryKey` в options —\n * предупредить. По умолчанию ключ генерится из endpoint+params, но при\n * endpoint-функции стабильность ключа зависит от того, что функция\n * возвращает одну и ту же строку для одних и тех же params. Явный\n * `queryKey` снимает риск.\n *\n * Эвристика статическая: ищем `const xxxApi = apiClient(SomethingFn, ...)`,\n * запоминаем имя, потом ругаемся на `xxxApi.useFetch(...)` без queryKey\n * в options-объекте.\n */\nexport const requireQueryKeyWhenEndpointIsFn = createRule({\n name: 'require-query-key-when-endpoint-is-fn',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'When apiClient is created from an endpoint function, useFetch should specify explicit queryKey.',\n recommended: 'warn',\n },\n schema: [],\n messages: {\n missing:\n 'apiClient с endpoint-функцией: укажи `queryKey` явно, чтобы ключ кэша был стабильным.',\n },\n },\n defaultOptions: [],\n create(context) {\n const apisWithFnEndpoint = new Set<string>();\n\n return {\n // const xApi = apiClient(fn, ...)\n VariableDeclarator(node: TSESTree.VariableDeclarator) {\n if (node.id.type !== 'Identifier') return;\n if (!node.init || node.init.type !== 'CallExpression') return;\n const call = node.init;\n if (\n call.callee.type !== 'Identifier' ||\n (call.callee.name !== 'apiClient' && call.callee.name !== 'apiPaginate')\n )\n return;\n const firstArg = call.arguments[0];\n if (!firstArg) return;\n if (\n firstArg.type === 'ArrowFunctionExpression' ||\n firstArg.type === 'FunctionExpression'\n ) {\n apisWithFnEndpoint.add(node.id.name);\n }\n },\n\n // xApi.useFetch(params, options?)\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'MemberExpression') return;\n const obj = node.callee.object;\n const prop = node.callee.property;\n if (obj.type !== 'Identifier') return;\n if (!apisWithFnEndpoint.has(obj.name)) return;\n if (prop.type !== 'Identifier') return;\n if (prop.name !== 'useFetch' && prop.name !== 'usePaginate') return;\n\n const optionsArg = node.arguments[1];\n if (!optionsArg) {\n context.report({ node, messageId: 'missing' });\n return;\n }\n if (optionsArg.type !== 'ObjectExpression') return; // не статически анализируем\n const hasQueryKey = optionsArg.properties.some(\n p =>\n p.type === 'Property' &&\n p.key.type === 'Identifier' &&\n p.key.name === 'queryKey',\n );\n if (!hasQueryKey) {\n context.report({ node, messageId: 'missing' });\n }\n },\n };\n },\n});\n","import { noAwaitMutate } from './rules/no-await-mutate';\nimport { requireQueryKeyWhenEndpointIsFn } from './rules/require-query-key-when-endpoint-is-fn';\n\nexport const rules = {\n 'no-await-mutate': noAwaitMutate,\n 'require-query-key-when-endpoint-is-fn': requireQueryKeyWhenEndpointIsFn,\n};\n\nexport const configs = {\n recommended: {\n plugins: ['@krymskyimaksym/react-api-client'],\n rules: {\n '@krymskyimaksym/react-api-client/no-await-mutate': 'error',\n '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn':\n 'warn',\n },\n },\n};\n\nexport default { rules, configs };\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,119 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+
3
+ // src/rules/no-await-mutate.ts
4
+ var createRule = ESLintUtils.RuleCreator(
5
+ (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
6
+ );
7
+ var noAwaitMutate = createRule({
8
+ name: "no-await-mutate",
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description: "mutate() returns void; await it has no effect. Use mutateAsync for awaitable mutations.",
13
+ recommended: "error"
14
+ },
15
+ fixable: "code",
16
+ schema: [],
17
+ messages: {
18
+ avoid: "`mutate` \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 void \u2014 await \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 `mutateAsync` \u0434\u043B\u044F await/try-catch."
19
+ }
20
+ },
21
+ defaultOptions: [],
22
+ create(context) {
23
+ return {
24
+ AwaitExpression(node) {
25
+ const arg = node.argument;
26
+ if (arg.type !== "CallExpression") return;
27
+ const callee = arg.callee;
28
+ if (callee.type !== "MemberExpression") return;
29
+ const prop = callee.property;
30
+ if (prop.type !== "Identifier" || prop.name !== "mutate") return;
31
+ context.report({
32
+ node,
33
+ messageId: "avoid",
34
+ fix(fixer) {
35
+ return fixer.replaceText(prop, "mutateAsync");
36
+ }
37
+ });
38
+ }
39
+ };
40
+ }
41
+ });
42
+ var createRule2 = ESLintUtils.RuleCreator(
43
+ (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
44
+ );
45
+ var requireQueryKeyWhenEndpointIsFn = createRule2({
46
+ name: "require-query-key-when-endpoint-is-fn",
47
+ meta: {
48
+ type: "suggestion",
49
+ docs: {
50
+ description: "When apiClient is created from an endpoint function, useFetch should specify explicit queryKey.",
51
+ recommended: "warn"
52
+ },
53
+ schema: [],
54
+ messages: {
55
+ missing: "apiClient \u0441 endpoint-\u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439: \u0443\u043A\u0430\u0436\u0438 `queryKey` \u044F\u0432\u043D\u043E, \u0447\u0442\u043E\u0431\u044B \u043A\u043B\u044E\u0447 \u043A\u044D\u0448\u0430 \u0431\u044B\u043B \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u044B\u043C."
56
+ }
57
+ },
58
+ defaultOptions: [],
59
+ create(context) {
60
+ const apisWithFnEndpoint = /* @__PURE__ */ new Set();
61
+ return {
62
+ // const xApi = apiClient(fn, ...)
63
+ VariableDeclarator(node) {
64
+ if (node.id.type !== "Identifier") return;
65
+ if (!node.init || node.init.type !== "CallExpression") return;
66
+ const call = node.init;
67
+ if (call.callee.type !== "Identifier" || call.callee.name !== "apiClient" && call.callee.name !== "apiPaginate")
68
+ return;
69
+ const firstArg = call.arguments[0];
70
+ if (!firstArg) return;
71
+ if (firstArg.type === "ArrowFunctionExpression" || firstArg.type === "FunctionExpression") {
72
+ apisWithFnEndpoint.add(node.id.name);
73
+ }
74
+ },
75
+ // xApi.useFetch(params, options?)
76
+ CallExpression(node) {
77
+ if (node.callee.type !== "MemberExpression") return;
78
+ const obj = node.callee.object;
79
+ const prop = node.callee.property;
80
+ if (obj.type !== "Identifier") return;
81
+ if (!apisWithFnEndpoint.has(obj.name)) return;
82
+ if (prop.type !== "Identifier") return;
83
+ if (prop.name !== "useFetch" && prop.name !== "usePaginate") return;
84
+ const optionsArg = node.arguments[1];
85
+ if (!optionsArg) {
86
+ context.report({ node, messageId: "missing" });
87
+ return;
88
+ }
89
+ if (optionsArg.type !== "ObjectExpression") return;
90
+ const hasQueryKey = optionsArg.properties.some(
91
+ (p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === "queryKey"
92
+ );
93
+ if (!hasQueryKey) {
94
+ context.report({ node, messageId: "missing" });
95
+ }
96
+ }
97
+ };
98
+ }
99
+ });
100
+
101
+ // src/index.ts
102
+ var rules = {
103
+ "no-await-mutate": noAwaitMutate,
104
+ "require-query-key-when-endpoint-is-fn": requireQueryKeyWhenEndpointIsFn
105
+ };
106
+ var configs = {
107
+ recommended: {
108
+ plugins: ["@krymskyimaksym/react-api-client"],
109
+ rules: {
110
+ "@krymskyimaksym/react-api-client/no-await-mutate": "error",
111
+ "@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn": "warn"
112
+ }
113
+ }
114
+ };
115
+ var index_default = { rules, configs };
116
+
117
+ export { configs, index_default as default, rules };
118
+ //# sourceMappingURL=index.mjs.map
119
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/no-await-mutate.ts","../src/rules/require-query-key-when-endpoint-is-fn.ts","../src/index.ts"],"names":["createRule","ESLintUtils"],"mappings":"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","file":"index.mjs","sourcesContent":["import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';\n\nconst createRule = ESLintUtils.RuleCreator(\n name =>\n `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`,\n);\n\n/**\n * Запрещает `await x.mutate(...)` — `mutate` возвращает `void`, а не\n * `Promise`. Для последовательной логики или try/catch используй\n * `mutateAsync`.\n *\n * Эвристика: `await <expr>.mutate(...)`.\n */\nexport const noAwaitMutate = createRule({\n name: 'no-await-mutate',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'mutate() returns void; await it has no effect. Use mutateAsync for awaitable mutations.',\n recommended: 'error',\n },\n fixable: 'code',\n schema: [],\n messages: {\n avoid:\n '`mutate` возвращает void — await не сработает. Используй `mutateAsync` для await/try-catch.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n AwaitExpression(node: TSESTree.AwaitExpression) {\n const arg = node.argument;\n if (arg.type !== 'CallExpression') return;\n const callee = arg.callee;\n if (callee.type !== 'MemberExpression') return;\n const prop = callee.property;\n if (prop.type !== 'Identifier' || prop.name !== 'mutate') return;\n\n context.report({\n node,\n messageId: 'avoid',\n fix(fixer) {\n return fixer.replaceText(prop, 'mutateAsync');\n },\n });\n },\n };\n },\n});\n","import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';\n\nconst createRule = ESLintUtils.RuleCreator(\n name =>\n `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`,\n);\n\n/**\n * Если `apiClient(fn, ...)` создан с endpoint-функцией, и где-то\n * вызывается `<api>.useFetch(params)` без `queryKey` в options —\n * предупредить. По умолчанию ключ генерится из endpoint+params, но при\n * endpoint-функции стабильность ключа зависит от того, что функция\n * возвращает одну и ту же строку для одних и тех же params. Явный\n * `queryKey` снимает риск.\n *\n * Эвристика статическая: ищем `const xxxApi = apiClient(SomethingFn, ...)`,\n * запоминаем имя, потом ругаемся на `xxxApi.useFetch(...)` без queryKey\n * в options-объекте.\n */\nexport const requireQueryKeyWhenEndpointIsFn = createRule({\n name: 'require-query-key-when-endpoint-is-fn',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'When apiClient is created from an endpoint function, useFetch should specify explicit queryKey.',\n recommended: 'warn',\n },\n schema: [],\n messages: {\n missing:\n 'apiClient с endpoint-функцией: укажи `queryKey` явно, чтобы ключ кэша был стабильным.',\n },\n },\n defaultOptions: [],\n create(context) {\n const apisWithFnEndpoint = new Set<string>();\n\n return {\n // const xApi = apiClient(fn, ...)\n VariableDeclarator(node: TSESTree.VariableDeclarator) {\n if (node.id.type !== 'Identifier') return;\n if (!node.init || node.init.type !== 'CallExpression') return;\n const call = node.init;\n if (\n call.callee.type !== 'Identifier' ||\n (call.callee.name !== 'apiClient' && call.callee.name !== 'apiPaginate')\n )\n return;\n const firstArg = call.arguments[0];\n if (!firstArg) return;\n if (\n firstArg.type === 'ArrowFunctionExpression' ||\n firstArg.type === 'FunctionExpression'\n ) {\n apisWithFnEndpoint.add(node.id.name);\n }\n },\n\n // xApi.useFetch(params, options?)\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'MemberExpression') return;\n const obj = node.callee.object;\n const prop = node.callee.property;\n if (obj.type !== 'Identifier') return;\n if (!apisWithFnEndpoint.has(obj.name)) return;\n if (prop.type !== 'Identifier') return;\n if (prop.name !== 'useFetch' && prop.name !== 'usePaginate') return;\n\n const optionsArg = node.arguments[1];\n if (!optionsArg) {\n context.report({ node, messageId: 'missing' });\n return;\n }\n if (optionsArg.type !== 'ObjectExpression') return; // не статически анализируем\n const hasQueryKey = optionsArg.properties.some(\n p =>\n p.type === 'Property' &&\n p.key.type === 'Identifier' &&\n p.key.name === 'queryKey',\n );\n if (!hasQueryKey) {\n context.report({ node, messageId: 'missing' });\n }\n },\n };\n },\n});\n","import { noAwaitMutate } from './rules/no-await-mutate';\nimport { requireQueryKeyWhenEndpointIsFn } from './rules/require-query-key-when-endpoint-is-fn';\n\nexport const rules = {\n 'no-await-mutate': noAwaitMutate,\n 'require-query-key-when-endpoint-is-fn': requireQueryKeyWhenEndpointIsFn,\n};\n\nexport const configs = {\n recommended: {\n plugins: ['@krymskyimaksym/react-api-client'],\n rules: {\n '@krymskyimaksym/react-api-client/no-await-mutate': 'error',\n '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn':\n 'warn',\n },\n },\n};\n\nexport default { rules, configs };\n"]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@krymskyimaksym/eslint-plugin-react-api-client",
3
+ "version": "0.1.0",
4
+ "description": "ESLint rules for @krymskyimaksym/react-api-client",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "eslint",
26
+ "eslint-plugin",
27
+ "react-api-client"
28
+ ],
29
+ "author": "Krymskyi Maksym <krimskiymaxim@gmail.com>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/krymskyimaksym/react-api-client.git",
34
+ "directory": "packages/eslint-plugin"
35
+ },
36
+ "peerDependencies": {
37
+ "eslint": ">=8.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/eslint": "^8.56.0",
41
+ "@types/node": "^20.11.0",
42
+ "@typescript-eslint/parser": "^6.21.0",
43
+ "@typescript-eslint/rule-tester": "^6.19.0",
44
+ "@typescript-eslint/utils": "^6.19.0",
45
+ "eslint": "^8.56.0",
46
+ "tsup": "^8.0.1",
47
+ "typescript": "^5.3.3",
48
+ "vitest": "^1.2.0"
49
+ }
50
+ }