@krymskyimaksym/eslint-plugin-react-api-client 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @krymskyimaksym/eslint-plugin-react-api-client
2
2
 
3
- ESLint-правила для [`@krymskyimaksym/react-api-client`](../..).
3
+ ESLint-правила для [`@krymskyimaksym/react-api-client`](https://www.npmjs.com/package/@krymskyimaksym/react-api-client) (требуется `^2.0.0`).
4
4
 
5
5
  ## Установка
6
6
 
@@ -29,7 +29,15 @@ module.exports = {
29
29
  }
30
30
  ```
31
31
 
32
- ## Правила
32
+ ## Правила (recommended)
33
+
34
+ | Rule | Severity | Autofix |
35
+ |---|---|---|
36
+ | `no-await-mutate` | error | ✓ |
37
+ | `no-non-serializable-params` | error | — |
38
+ | `require-query-key-when-endpoint-is-fn` | warn | — |
39
+
40
+ ## Описание
33
41
 
34
42
  ### `no-await-mutate` (error, autofix)
35
43
 
@@ -65,6 +73,20 @@ userApi.useFetch({ id: 1 }, { queryKey: ['user', 1] });
65
73
  возвращает одну и ту же строку для одних и тех же params. Явный
66
74
  `queryKey` снимает риск.
67
75
 
76
+ ### `no-non-serializable-params` (error)
77
+
78
+ Запрещает функции и `Symbol` в объекте `params`. Они сломают
79
+ `hashQueryKey` в рантайме (throw при первом mount):
80
+
81
+ ```ts
82
+ // ❌
83
+ api.useFetch({ id: 1, cb: () => 1 });
84
+ api.useFetch({ tag: Symbol('x') });
85
+
86
+ // ✅
87
+ api.useFetch({ id: 1 });
88
+ ```
89
+
68
90
  ## License
69
91
 
70
92
  MIT
package/dist/index.d.mts CHANGED
@@ -2,6 +2,7 @@ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts
2
2
 
3
3
  declare const rules: {
4
4
  'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
5
+ 'no-non-serializable-params': _typescript_eslint_utils_ts_eslint.RuleModule<"noFunction" | "noSymbol", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
5
6
  'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
6
7
  };
7
8
  declare const configs: {
@@ -9,6 +10,7 @@ declare const configs: {
9
10
  plugins: string[];
10
11
  rules: {
11
12
  '@krymskyimaksym/react-api-client/no-await-mutate': string;
13
+ '@krymskyimaksym/react-api-client/no-non-serializable-params': string;
12
14
  '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
13
15
  };
14
16
  };
@@ -16,6 +18,7 @@ declare const configs: {
16
18
  declare const _default: {
17
19
  rules: {
18
20
  'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
21
+ 'no-non-serializable-params': _typescript_eslint_utils_ts_eslint.RuleModule<"noFunction" | "noSymbol", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
19
22
  'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
20
23
  };
21
24
  configs: {
@@ -23,6 +26,7 @@ declare const _default: {
23
26
  plugins: string[];
24
27
  rules: {
25
28
  '@krymskyimaksym/react-api-client/no-await-mutate': string;
29
+ '@krymskyimaksym/react-api-client/no-non-serializable-params': string;
26
30
  '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
27
31
  };
28
32
  };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts
2
2
 
3
3
  declare const rules: {
4
4
  'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
5
+ 'no-non-serializable-params': _typescript_eslint_utils_ts_eslint.RuleModule<"noFunction" | "noSymbol", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
5
6
  'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
6
7
  };
7
8
  declare const configs: {
@@ -9,6 +10,7 @@ declare const configs: {
9
10
  plugins: string[];
10
11
  rules: {
11
12
  '@krymskyimaksym/react-api-client/no-await-mutate': string;
13
+ '@krymskyimaksym/react-api-client/no-non-serializable-params': string;
12
14
  '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
13
15
  };
14
16
  };
@@ -16,6 +18,7 @@ declare const configs: {
16
18
  declare const _default: {
17
19
  rules: {
18
20
  'no-await-mutate': _typescript_eslint_utils_ts_eslint.RuleModule<"avoid", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
21
+ 'no-non-serializable-params': _typescript_eslint_utils_ts_eslint.RuleModule<"noFunction" | "noSymbol", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
19
22
  'require-query-key-when-endpoint-is-fn': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
20
23
  };
21
24
  configs: {
@@ -23,6 +26,7 @@ declare const _default: {
23
26
  plugins: string[];
24
27
  rules: {
25
28
  '@krymskyimaksym/react-api-client/no-await-mutate': string;
29
+ '@krymskyimaksym/react-api-client/no-non-serializable-params': string;
26
30
  '@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn': string;
27
31
  };
28
32
  };
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var noAwaitMutate = createRule({
14
14
  type: "problem",
15
15
  docs: {
16
16
  description: "mutate() returns void; await it has no effect. Use mutateAsync for awaitable mutations.",
17
- recommended: "error"
17
+ recommended: "recommended"
18
18
  },
19
19
  fixable: "code",
20
20
  schema: [],
@@ -46,13 +46,64 @@ var noAwaitMutate = createRule({
46
46
  var createRule2 = utils.ESLintUtils.RuleCreator(
47
47
  (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
48
48
  );
49
- var requireQueryKeyWhenEndpointIsFn = createRule2({
49
+ var noNonSerializableParams = createRule2({
50
+ name: "no-non-serializable-params",
51
+ meta: {
52
+ type: "problem",
53
+ docs: {
54
+ description: "Params for useFetch/usePaginate/useQuery must be serializable. Functions and Symbol break hashQueryKey at runtime.",
55
+ recommended: "recommended"
56
+ },
57
+ schema: [],
58
+ messages: {
59
+ noFunction: "\u041D\u0435\u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0443\u0435\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0432 params (\u0444\u0443\u043D\u043A\u0446\u0438\u044F): \u0441\u043B\u043E\u043C\u0430\u0435\u0442 hashQueryKey \u0432 \u0440\u0430\u043D\u0442\u0430\u0439\u043C\u0435. \u0412\u044B\u043D\u0435\u0441\u0438 callback \u043D\u0430\u0440\u0443\u0436\u0443 \u0445\u0443\u043A\u0430.",
60
+ noSymbol: "\u041D\u0435\u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0443\u0435\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0432 params (Symbol): \u0441\u043B\u043E\u043C\u0430\u0435\u0442 hashQueryKey \u0432 \u0440\u0430\u043D\u0442\u0430\u0439\u043C\u0435."
61
+ }
62
+ },
63
+ defaultOptions: [],
64
+ create(context) {
65
+ const HOOK_NAMES = /* @__PURE__ */ new Set(["useFetch", "usePaginate", "useQuery"]);
66
+ function checkObjectExpression(obj) {
67
+ for (const prop of obj.properties) {
68
+ if (prop.type !== "Property") continue;
69
+ const v = prop.value;
70
+ if (v.type === "ArrowFunctionExpression" || v.type === "FunctionExpression") {
71
+ context.report({ node: v, messageId: "noFunction" });
72
+ continue;
73
+ }
74
+ if (v.type === "CallExpression" && v.callee.type === "Identifier" && v.callee.name === "Symbol") {
75
+ context.report({ node: v, messageId: "noSymbol" });
76
+ }
77
+ }
78
+ }
79
+ return {
80
+ CallExpression(node) {
81
+ if (node.callee.type !== "MemberExpression") {
82
+ if (node.callee.type === "Identifier" && node.callee.name === "useQuery") {
83
+ return;
84
+ }
85
+ return;
86
+ }
87
+ const prop = node.callee.property;
88
+ if (prop.type !== "Identifier" || !HOOK_NAMES.has(prop.name)) return;
89
+ const paramsArg = node.arguments[0];
90
+ if (!paramsArg) return;
91
+ if (paramsArg.type !== "ObjectExpression") return;
92
+ checkObjectExpression(paramsArg);
93
+ }
94
+ };
95
+ }
96
+ });
97
+ var createRule3 = utils.ESLintUtils.RuleCreator(
98
+ (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
99
+ );
100
+ var requireQueryKeyWhenEndpointIsFn = createRule3({
50
101
  name: "require-query-key-when-endpoint-is-fn",
51
102
  meta: {
52
103
  type: "suggestion",
53
104
  docs: {
54
105
  description: "When apiClient is created from an endpoint function, useFetch should specify explicit queryKey.",
55
- recommended: "warn"
106
+ recommended: "recommended"
56
107
  },
57
108
  schema: [],
58
109
  messages: {
@@ -105,6 +156,7 @@ var requireQueryKeyWhenEndpointIsFn = createRule2({
105
156
  // src/index.ts
106
157
  var rules = {
107
158
  "no-await-mutate": noAwaitMutate,
159
+ "no-non-serializable-params": noNonSerializableParams,
108
160
  "require-query-key-when-endpoint-is-fn": requireQueryKeyWhenEndpointIsFn
109
161
  };
110
162
  var configs = {
@@ -112,6 +164,7 @@ var configs = {
112
164
  plugins: ["@krymskyimaksym/react-api-client"],
113
165
  rules: {
114
166
  "@krymskyimaksym/react-api-client/no-await-mutate": "error",
167
+ "@krymskyimaksym/react-api-client/no-non-serializable-params": "error",
115
168
  "@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn": "warn"
116
169
  }
117
170
  }
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/rules/no-await-mutate.ts","../src/rules/no-non-serializable-params.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: 'recommended',\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 * Запрещает несериализуемые значения в `params` для `useFetch` /\n * `usePaginate` / `useQuery`. Функции и Symbol сломают `hashQueryKey`\n * (он `throw`-ает на них), а это приведёт к рантайм-ошибке при mount'е.\n *\n * Ловим статически:\n * - стрелочные/функциональные выражения как значения свойств;\n * - вызовы `Symbol(...)` как значения свойств.\n *\n * Эвристика — без типов; ловит самые частые случаи.\n */\nexport const noNonSerializableParams = createRule({\n name: 'no-non-serializable-params',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Params for useFetch/usePaginate/useQuery must be serializable. Functions and Symbol break hashQueryKey at runtime.',\n recommended: 'recommended',\n },\n schema: [],\n messages: {\n noFunction:\n 'Несериализуемое значение в params (функция): сломает hashQueryKey в рантайме. Вынеси callback наружу хука.',\n noSymbol:\n 'Несериализуемое значение в params (Symbol): сломает hashQueryKey в рантайме.',\n },\n },\n defaultOptions: [],\n create(context) {\n const HOOK_NAMES = new Set(['useFetch', 'usePaginate', 'useQuery']);\n\n function checkObjectExpression(obj: TSESTree.ObjectExpression) {\n for (const prop of obj.properties) {\n if (prop.type !== 'Property') continue;\n const v = prop.value;\n if (\n v.type === 'ArrowFunctionExpression' ||\n v.type === 'FunctionExpression'\n ) {\n context.report({ node: v, messageId: 'noFunction' });\n continue;\n }\n if (\n v.type === 'CallExpression' &&\n v.callee.type === 'Identifier' &&\n v.callee.name === 'Symbol'\n ) {\n context.report({ node: v, messageId: 'noSymbol' });\n }\n }\n }\n\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'MemberExpression') {\n // useQuery(key, fn, opts) — первый аргумент это key,\n // не params; пропускаем.\n if (\n node.callee.type === 'Identifier' &&\n node.callee.name === 'useQuery'\n ) {\n return;\n }\n return;\n }\n const prop = node.callee.property;\n if (prop.type !== 'Identifier' || !HOOK_NAMES.has(prop.name)) return;\n const paramsArg = node.arguments[0];\n if (!paramsArg) return;\n if (paramsArg.type !== 'ObjectExpression') return;\n checkObjectExpression(paramsArg);\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: 'recommended',\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 { noNonSerializableParams } from './rules/no-non-serializable-params';\nimport { requireQueryKeyWhenEndpointIsFn } from './rules/require-query-key-when-endpoint-is-fn';\n\nexport const rules = {\n 'no-await-mutate': noAwaitMutate,\n 'no-non-serializable-params': noNonSerializableParams,\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/no-non-serializable-params': '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 CHANGED
@@ -10,7 +10,7 @@ var noAwaitMutate = createRule({
10
10
  type: "problem",
11
11
  docs: {
12
12
  description: "mutate() returns void; await it has no effect. Use mutateAsync for awaitable mutations.",
13
- recommended: "error"
13
+ recommended: "recommended"
14
14
  },
15
15
  fixable: "code",
16
16
  schema: [],
@@ -42,13 +42,64 @@ var noAwaitMutate = createRule({
42
42
  var createRule2 = ESLintUtils.RuleCreator(
43
43
  (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
44
44
  );
45
- var requireQueryKeyWhenEndpointIsFn = createRule2({
45
+ var noNonSerializableParams = createRule2({
46
+ name: "no-non-serializable-params",
47
+ meta: {
48
+ type: "problem",
49
+ docs: {
50
+ description: "Params for useFetch/usePaginate/useQuery must be serializable. Functions and Symbol break hashQueryKey at runtime.",
51
+ recommended: "recommended"
52
+ },
53
+ schema: [],
54
+ messages: {
55
+ noFunction: "\u041D\u0435\u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0443\u0435\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0432 params (\u0444\u0443\u043D\u043A\u0446\u0438\u044F): \u0441\u043B\u043E\u043C\u0430\u0435\u0442 hashQueryKey \u0432 \u0440\u0430\u043D\u0442\u0430\u0439\u043C\u0435. \u0412\u044B\u043D\u0435\u0441\u0438 callback \u043D\u0430\u0440\u0443\u0436\u0443 \u0445\u0443\u043A\u0430.",
56
+ noSymbol: "\u041D\u0435\u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0443\u0435\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0432 params (Symbol): \u0441\u043B\u043E\u043C\u0430\u0435\u0442 hashQueryKey \u0432 \u0440\u0430\u043D\u0442\u0430\u0439\u043C\u0435."
57
+ }
58
+ },
59
+ defaultOptions: [],
60
+ create(context) {
61
+ const HOOK_NAMES = /* @__PURE__ */ new Set(["useFetch", "usePaginate", "useQuery"]);
62
+ function checkObjectExpression(obj) {
63
+ for (const prop of obj.properties) {
64
+ if (prop.type !== "Property") continue;
65
+ const v = prop.value;
66
+ if (v.type === "ArrowFunctionExpression" || v.type === "FunctionExpression") {
67
+ context.report({ node: v, messageId: "noFunction" });
68
+ continue;
69
+ }
70
+ if (v.type === "CallExpression" && v.callee.type === "Identifier" && v.callee.name === "Symbol") {
71
+ context.report({ node: v, messageId: "noSymbol" });
72
+ }
73
+ }
74
+ }
75
+ return {
76
+ CallExpression(node) {
77
+ if (node.callee.type !== "MemberExpression") {
78
+ if (node.callee.type === "Identifier" && node.callee.name === "useQuery") {
79
+ return;
80
+ }
81
+ return;
82
+ }
83
+ const prop = node.callee.property;
84
+ if (prop.type !== "Identifier" || !HOOK_NAMES.has(prop.name)) return;
85
+ const paramsArg = node.arguments[0];
86
+ if (!paramsArg) return;
87
+ if (paramsArg.type !== "ObjectExpression") return;
88
+ checkObjectExpression(paramsArg);
89
+ }
90
+ };
91
+ }
92
+ });
93
+ var createRule3 = ESLintUtils.RuleCreator(
94
+ (name) => `https://github.com/krymskyimaksym/react-api-client/tree/main/packages/eslint-plugin/docs/${name}.md`
95
+ );
96
+ var requireQueryKeyWhenEndpointIsFn = createRule3({
46
97
  name: "require-query-key-when-endpoint-is-fn",
47
98
  meta: {
48
99
  type: "suggestion",
49
100
  docs: {
50
101
  description: "When apiClient is created from an endpoint function, useFetch should specify explicit queryKey.",
51
- recommended: "warn"
102
+ recommended: "recommended"
52
103
  },
53
104
  schema: [],
54
105
  messages: {
@@ -101,6 +152,7 @@ var requireQueryKeyWhenEndpointIsFn = createRule2({
101
152
  // src/index.ts
102
153
  var rules = {
103
154
  "no-await-mutate": noAwaitMutate,
155
+ "no-non-serializable-params": noNonSerializableParams,
104
156
  "require-query-key-when-endpoint-is-fn": requireQueryKeyWhenEndpointIsFn
105
157
  };
106
158
  var configs = {
@@ -108,6 +160,7 @@ var configs = {
108
160
  plugins: ["@krymskyimaksym/react-api-client"],
109
161
  rules: {
110
162
  "@krymskyimaksym/react-api-client/no-await-mutate": "error",
163
+ "@krymskyimaksym/react-api-client/no-non-serializable-params": "error",
111
164
  "@krymskyimaksym/react-api-client/require-query-key-when-endpoint-is-fn": "warn"
112
165
  }
113
166
  }
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/rules/no-await-mutate.ts","../src/rules/no-non-serializable-params.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: 'recommended',\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 * Запрещает несериализуемые значения в `params` для `useFetch` /\n * `usePaginate` / `useQuery`. Функции и Symbol сломают `hashQueryKey`\n * (он `throw`-ает на них), а это приведёт к рантайм-ошибке при mount'е.\n *\n * Ловим статически:\n * - стрелочные/функциональные выражения как значения свойств;\n * - вызовы `Symbol(...)` как значения свойств.\n *\n * Эвристика — без типов; ловит самые частые случаи.\n */\nexport const noNonSerializableParams = createRule({\n name: 'no-non-serializable-params',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Params for useFetch/usePaginate/useQuery must be serializable. Functions and Symbol break hashQueryKey at runtime.',\n recommended: 'recommended',\n },\n schema: [],\n messages: {\n noFunction:\n 'Несериализуемое значение в params (функция): сломает hashQueryKey в рантайме. Вынеси callback наружу хука.',\n noSymbol:\n 'Несериализуемое значение в params (Symbol): сломает hashQueryKey в рантайме.',\n },\n },\n defaultOptions: [],\n create(context) {\n const HOOK_NAMES = new Set(['useFetch', 'usePaginate', 'useQuery']);\n\n function checkObjectExpression(obj: TSESTree.ObjectExpression) {\n for (const prop of obj.properties) {\n if (prop.type !== 'Property') continue;\n const v = prop.value;\n if (\n v.type === 'ArrowFunctionExpression' ||\n v.type === 'FunctionExpression'\n ) {\n context.report({ node: v, messageId: 'noFunction' });\n continue;\n }\n if (\n v.type === 'CallExpression' &&\n v.callee.type === 'Identifier' &&\n v.callee.name === 'Symbol'\n ) {\n context.report({ node: v, messageId: 'noSymbol' });\n }\n }\n }\n\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'MemberExpression') {\n // useQuery(key, fn, opts) — первый аргумент это key,\n // не params; пропускаем.\n if (\n node.callee.type === 'Identifier' &&\n node.callee.name === 'useQuery'\n ) {\n return;\n }\n return;\n }\n const prop = node.callee.property;\n if (prop.type !== 'Identifier' || !HOOK_NAMES.has(prop.name)) return;\n const paramsArg = node.arguments[0];\n if (!paramsArg) return;\n if (paramsArg.type !== 'ObjectExpression') return;\n checkObjectExpression(paramsArg);\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: 'recommended',\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 { noNonSerializableParams } from './rules/no-non-serializable-params';\nimport { requireQueryKeyWhenEndpointIsFn } from './rules/require-query-key-when-endpoint-is-fn';\n\nexport const rules = {\n 'no-await-mutate': noAwaitMutate,\n 'no-non-serializable-params': noNonSerializableParams,\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/no-non-serializable-params': '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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krymskyimaksym/eslint-plugin-react-api-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "ESLint rules for @krymskyimaksym/react-api-client",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",