eslint-plugin-effector-units-order 0.0.2 → 0.0.3

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
@@ -90,23 +90,63 @@ export default [
90
90
 
91
91
  ## ⚙️ Options
92
92
 
93
- | Option | Type | Default | Description |
94
- | ------------------------- | ---------- | ------------------------------------------------------- | ------------------------------------------------ |
95
- | `order` | `string[]` | `['domain', 'events', 'effects', 'stores', 'computed']` | Defines the order of unit groups |
96
- | `emptyLinesBetweenGroups` | `number` | `1` | Number of empty lines between groups (0-3) |
97
- | `groups` | `object` | See below | Custom group definitions and factory mappings |
98
- | `customFactories` | `object` | `{}` | Maps custom factory functions to specific groups |
99
- | `packages` | `string[]` | `['effector', 'patronum']` | Packages to detect imports from |
93
+ | Option | Type | Default | Description |
94
+ | ------------------------- | ---------- | ------------------------------------------------------- | ----------------------------------------------------------- |
95
+ | `order` | `string[]` | `['domain', 'events', 'effects', 'stores', 'computed']` | Defines the order of unit groups |
96
+ | `emptyLinesBetweenGroups` | `number` | `1` | Number of empty lines between groups (0-3) |
97
+ | `groups` | `object` | See below | Custom group definitions with factories and derived methods |
98
+ | `customFactories` | `object` | `{}` | Maps custom factory functions to specific groups |
99
+ | `packages` | `string[]` | `['effector', 'patronum']` | Packages to detect imports from |
100
+
101
+ **Group Options:**
102
+
103
+ Each group in `groups` can have:
104
+
105
+ - `factories: string[]` — Factory functions that create units of this type (e.g., `['createStore']`)
106
+ - `derivedMethods?: string[]` — Methods and properties that create derived units (e.g., `['stores.map', 'effects.pending']`)
100
107
 
101
108
  ### Default Groups
102
109
 
103
110
  ```javascript
104
111
  {
105
- domain: { factories: ['createDomain'] },
106
- events: { factories: ['createEvent'] },
107
- effects: { factories: ['createEffect'] },
108
- stores: { factories: ['createStore'] },
109
- computed: { factories: ['combine', 'or'] },
112
+ domain: {
113
+ factories: ['createDomain']
114
+ },
115
+ events: {
116
+ factories: ['createEvent'],
117
+ derivedMethods: [
118
+ 'events.prepend',
119
+ 'events.map',
120
+ 'events.filter',
121
+ 'events.filterMap',
122
+ 'stores.updates',
123
+ 'stores.reinit',
124
+ 'effects.done',
125
+ 'effects.doneData',
126
+ 'effects.fail',
127
+ 'effects.failData',
128
+ 'effects.finally'
129
+ ]
130
+ },
131
+ effects: {
132
+ factories: ['createEffect'],
133
+ derivedMethods: [
134
+ 'effects.map',
135
+ 'effects.prepend',
136
+ 'effects.filterMap'
137
+ ]
138
+ },
139
+ stores: {
140
+ factories: ['createStore']
141
+ },
142
+ computed: {
143
+ factories: ['or', 'combine', 'merge', 'attach', 'forward'],
144
+ derivedMethods: [
145
+ 'stores.map',
146
+ 'effects.pending',
147
+ 'effects.inFlight'
148
+ ]
149
+ }
110
150
  }
111
151
  ```
112
152
 
@@ -153,10 +193,10 @@ export const $user = userDomain.createStore(null);
153
193
 
154
194
  ### Computed Stores
155
195
 
156
- Computed stores using `combine()` and `or()` from patronum are properly recognized:
196
+ Computed stores using `combine()`, `merge()`, `attach()`, `forward()`, and `or()` from patronum are properly recognized:
157
197
 
158
198
  ```typescript
159
- import { createStore, combine } from "effector";
199
+ import { createStore, combine, merge, attach, forward } from "effector";
160
200
  import { or } from "patronum";
161
201
 
162
202
  export const $isLoading = createStore(false);
@@ -169,6 +209,65 @@ export const $state = combine({
169
209
  });
170
210
  ```
171
211
 
212
+ ### Derived Units
213
+
214
+ The plugin automatically recognizes derived stores and events created from existing units:
215
+
216
+ #### Derived Stores
217
+
218
+ ```typescript
219
+ import { createStore, createEffect } from "effector";
220
+
221
+ export const $products = createStore([]);
222
+ export const fetchProductsFx = createEffect();
223
+
224
+ export const $enrichedProducts = $products.map((products) =>
225
+ products.map((p) => ({ ...p, priceWithTax: p.price * 1.2 }))
226
+ );
227
+ export const $loading = fetchProductsFx.pending;
228
+ export const $isLoading = $loading.map((loading) => loading);
229
+ ```
230
+
231
+ #### Derived Events
232
+
233
+ ```typescript
234
+ import { createEvent, createEffect, createStore } from "effector";
235
+
236
+ export const userLogin = createEvent();
237
+ export const fetchUserFx = createEffect();
238
+ export const $user = createStore(null);
239
+
240
+ export const loginWithEmail = userLogin.map((email) => ({ email })); /
241
+ export const fetchSucceeded = fetchUserFx.done;
242
+ export const fetchFailed = fetchUserFx.fail;
243
+ export const userUpdated = $user.updates;
244
+ export const resetUser = $user.reinit;
245
+ ```
246
+
247
+ #### ❌ Incorrect Order (Auto-fixed)
248
+
249
+ ```typescript
250
+ import { createStore, createEffect } from "effector";
251
+
252
+ export const $loading = fetchProductsFx.pending;
253
+ export const fetchProductsFx = createEffect();
254
+ export const $products = createStore([]);
255
+ export const $enriched = $products.map((p) => p);
256
+ ```
257
+
258
+ #### ✅ Correct Order (After auto-fix)
259
+
260
+ ```typescript
261
+ import { createStore, createEffect } from "effector";
262
+
263
+ export const fetchProductsFx = createEffect();
264
+
265
+ export const $products = createStore([]);
266
+
267
+ export const $loading = fetchProductsFx.pending;
268
+ export const $enriched = $products.map((p) => p);
269
+ ```
270
+
172
271
  ### Custom Factories
173
272
 
174
273
  You can map custom factory functions to specific groups:
@@ -219,6 +318,59 @@ export default [
219
318
  ];
220
319
  ```
221
320
 
321
+ ### Custom Derived Methods
322
+
323
+ You can configure which methods and properties create derived units:
324
+
325
+ ```javascript
326
+ // eslint.config.js
327
+ export default [
328
+ {
329
+ plugins: { "effector-order": effectorOrder },
330
+ rules: {
331
+ "effector-order/keep-units-order": [
332
+ "error",
333
+ {
334
+ order: ["domain", "events", "effects", "stores", "computed"],
335
+ groups: {
336
+ events: {
337
+ factories: ["createEvent"],
338
+ // Configure which methods create derived events
339
+ derivedMethods: [
340
+ "events.map",
341
+ "events.prepend",
342
+ "effects.done",
343
+ "stores.updates",
344
+ ],
345
+ },
346
+ effects: {
347
+ factories: ["createEffect"],
348
+ derivedMethods: ["effects.map", "effects.prepend"],
349
+ },
350
+ computed: {
351
+ factories: ["combine", "merge", "attach", "forward"],
352
+ // Configure which methods create derived stores
353
+ derivedMethods: [
354
+ "stores.map",
355
+ "effects.pending",
356
+ "effects.inFlight",
357
+ ],
358
+ },
359
+ },
360
+ },
361
+ ],
362
+ },
363
+ },
364
+ ];
365
+ ```
366
+
367
+ **Format for `derivedMethods`:**
368
+
369
+ - `"unitType.methodName"` — for methods like `$store.map()`, `event.prepend()`
370
+ - `"unitType.property"` — for properties like `effect.pending`, `store.updates`
371
+
372
+ Where `unitType` is the type of the parent unit (`stores`, `events`, `effects`, `computed`), and the result will be categorized into the group that contains this `derivedMethod` in its configuration.
373
+
222
374
  ### No Empty Lines Between Groups
223
375
 
224
376
  You can disable empty lines between groups by setting `emptyLinesBetweenGroups` to `0`:
@@ -1,3 +1,3 @@
1
1
  const name = "eslint-plugin-effector-units-order";
2
- const version = "0.0.1";
2
+ const version = "0.0.2";
3
3
  export { name, version };
@@ -17,19 +17,62 @@ const keepUnitsOrder = createESLintRule({
17
17
  return {
18
18
  "Program:exit": () => {
19
19
  let units = [];
20
+ let unitsMap = /* @__PURE__ */ new Map();
21
+ let processedNames = /* @__PURE__ */ new Set();
22
+ let isBaseUnitType = (type) =>
23
+ ["effects", "domain", "events", "stores"].includes(type);
20
24
  for (let potentialUnit of potentialUnits) {
25
+ if (!potentialUnit.name || processedNames.has(potentialUnit.name)) {
26
+ continue;
27
+ }
28
+ if (potentialUnit.node.init?.type !== "CallExpression") {
29
+ continue;
30
+ }
31
+ let unitType = detectUnitType({
32
+ importMap: importedFromEffector,
33
+ node: potentialUnit.node.init,
34
+ unitsMap: /* @__PURE__ */ new Map(),
35
+ // Empty for base units detection
36
+ domainMap,
37
+ config,
38
+ });
39
+ if (!unitType || !isBaseUnitType(unitType)) {
40
+ continue;
41
+ }
42
+ unitsMap.set(potentialUnit.name, unitType);
43
+ processedNames.add(potentialUnit.name);
44
+ units.push({
45
+ statement: potentialUnit.statement,
46
+ node: potentialUnit.node,
47
+ line: potentialUnit.line,
48
+ name: potentialUnit.name,
49
+ type: unitType,
50
+ });
51
+ }
52
+ for (let potentialUnit of potentialUnits) {
53
+ if (!potentialUnit.name || processedNames.has(potentialUnit.name)) {
54
+ continue;
55
+ }
21
56
  if (potentialUnit.node.init?.type !== "CallExpression") {
22
57
  continue;
23
58
  }
59
+ let { callee } = potentialUnit.node.init;
60
+ if (callee.type !== "Identifier") {
61
+ continue;
62
+ }
24
63
  let unitType = detectUnitType({
25
64
  importMap: importedFromEffector,
26
65
  node: potentialUnit.node.init,
27
66
  domainMap,
67
+ unitsMap,
68
+ // Now we have base units
28
69
  config,
29
70
  });
30
- if (!unitType) {
71
+ if (!unitType || unitType !== "computed") {
31
72
  continue;
32
73
  }
74
+ unitsMap.set(potentialUnit.name, unitType);
75
+ processedNames.add(potentialUnit.name);
33
76
  units.push({
34
77
  statement: potentialUnit.statement,
35
78
  node: potentialUnit.node,
@@ -38,6 +81,52 @@ const keepUnitsOrder = createESLintRule({
38
81
  type: unitType,
39
82
  });
40
83
  }
84
+ for (let potentialUnit of potentialUnits) {
85
+ if (!potentialUnit.name || processedNames.has(potentialUnit.name)) {
86
+ continue;
87
+ }
88
+ if (potentialUnit.node.init?.type === "CallExpression") {
89
+ let unitType = detectUnitType({
90
+ importMap: importedFromEffector,
91
+ node: potentialUnit.node.init,
92
+ domainMap,
93
+ unitsMap,
94
+ config,
95
+ });
96
+ if (unitType) {
97
+ unitsMap.set(potentialUnit.name, unitType);
98
+ processedNames.add(potentialUnit.name);
99
+ units.push({
100
+ statement: potentialUnit.statement,
101
+ node: potentialUnit.node,
102
+ line: potentialUnit.line,
103
+ name: potentialUnit.name,
104
+ type: unitType,
105
+ });
106
+ continue;
107
+ }
108
+ }
109
+ if (potentialUnit.node.init?.type === "MemberExpression") {
110
+ let unitType = detectUnitType({
111
+ importMap: importedFromEffector,
112
+ node: potentialUnit.node.init,
113
+ domainMap,
114
+ unitsMap,
115
+ config,
116
+ });
117
+ if (unitType) {
118
+ unitsMap.set(potentialUnit.name, unitType);
119
+ processedNames.add(potentialUnit.name);
120
+ units.push({
121
+ statement: potentialUnit.statement,
122
+ node: potentialUnit.node,
123
+ line: potentialUnit.line,
124
+ name: potentialUnit.name,
125
+ type: unitType,
126
+ });
127
+ }
128
+ }
129
+ }
41
130
  let usedDomains = /* @__PURE__ */ new Set();
42
131
  for (let unit of units) {
43
132
  if (
@@ -55,6 +144,7 @@ const keepUnitsOrder = createESLintRule({
55
144
  unit.isUsedByOthers = usedDomains.has(unit.name);
56
145
  }
57
146
  }
147
+ units.sort((a, b) => a.line - b.line);
58
148
  let violations = validateOrder({ config, units });
59
149
  if (violations.length === 0) {
60
150
  return;
@@ -80,10 +170,16 @@ const keepUnitsOrder = createESLintRule({
80
170
  },
81
171
  VariableDeclaration: (declarationNode) => {
82
172
  for (let node of declarationNode.declarations) {
83
- if (node.init?.type !== "CallExpression") {
173
+ if (
174
+ node.init?.type !== "CallExpression" &&
175
+ node.init?.type !== "MemberExpression"
176
+ ) {
84
177
  continue;
85
178
  }
86
- if (node.init.callee.type === "Identifier") {
179
+ if (
180
+ node.init.type === "CallExpression" &&
181
+ node.init.callee.type === "Identifier"
182
+ ) {
87
183
  let functionName = node.init.callee.name;
88
184
  if (importedFromEffector.has(functionName)) {
89
185
  for (let [groupName, groupOptions] of Object.entries(
@@ -17,7 +17,6 @@ export interface EffectorOrderOptions {
17
17
  /**
18
18
  * Number of empty lines between groups.
19
19
  * @default 1
20
- * @minimum 0
21
20
  */
22
21
  emptyLinesBetweenGroups?: number;
23
22
  /**
@@ -36,6 +35,12 @@ export interface EffectorOrderOptions {
36
35
  * Options for a unit group.
37
36
  */
38
37
  export interface GroupOptions {
38
+ /**
39
+ * Methods and properties that create units of this type.
40
+ * Format: "unitType.methodOrProperty".
41
+ * @example ['stores.map', 'effects.pending', 'computed.restore']
42
+ */
43
+ derivedMethods?: string[];
39
44
  /**
40
45
  * List of factories that belong to this group.
41
46
  * @example ['authFactory', 'apiFactory']
@@ -12,19 +12,26 @@ interface Parameters {
12
12
  * Map of imported names to package names.
13
13
  */
14
14
  importMap: Map<string, string>;
15
- node: TSESTree.CallExpression;
15
+ node: TSESTree.CallExpression | TSESTree.MemberExpression;
16
16
  /**
17
17
  * Normalized configuration with groups and factories.
18
18
  */
19
19
  config: NormalizedConfig;
20
+ /**
21
+ * Map of unit names to their types
22
+ * Key: unit variable name (e.g., '$store', 'event', 'effectFx')
23
+ * Value: unit type (e.g., 'stores', 'events', 'effects')
24
+ */
25
+ unitsMap: Map<string, UnitType>;
20
26
  }
21
27
  /**
22
- * Detects the unit type from a CallExpression node.
28
+ * Detects the unit type from a CallExpression or MemberExpression node.
23
29
  *
24
30
  * Handles:
25
31
  * - Direct calls: `createEvent()`, `createStore()`, etc.
26
32
  * - Domain calls: `domain.createEvent()`, `domain.createStore()`, etc.
27
- * - Computed: `or()`, `combine()`.
33
+ * - Computed: `or()`, `combine()`, `merge()`, `attach()`, `forward()`, `restore()`.
34
+ * - Derived units: `$store.map()`, `effect.pending`, `event.map()`, etc.
28
35
  *
29
36
  * @param params - Parameters for detection.
30
37
  * @returns UnitType if detected, null otherwise.
@@ -34,5 +41,6 @@ export declare function detectUnitType({
34
41
  domainMap,
35
42
  config,
36
43
  node,
44
+ unitsMap,
37
45
  }: Parameters): UnitType | null;
38
46
  export {};
@@ -1,32 +1,66 @@
1
- function detectUnitType({ importMap, domainMap, config, node }) {
2
- let { callee } = node;
3
- if (callee.type === "Identifier") {
4
- let functionName = callee.name;
5
- if (!importMap.has(functionName)) {
1
+ function detectUnitType({ importMap, domainMap, config, node, unitsMap }) {
2
+ if (node.type === "CallExpression") {
3
+ let { callee } = node;
4
+ if (callee.type === "Identifier") {
5
+ let functionName = callee.name;
6
+ if (!importMap.has(functionName)) {
7
+ return null;
8
+ }
9
+ for (let [groupName, groupOptions] of Object.entries(config.groups)) {
10
+ if (groupOptions.factories.includes(functionName)) {
11
+ return groupName;
12
+ }
13
+ }
6
14
  return null;
7
15
  }
8
- for (let [groupName, groupOptions] of Object.entries(config.groups)) {
9
- if (groupOptions.factories.includes(functionName)) {
10
- return groupName;
16
+ if (callee.type === "MemberExpression") {
17
+ let { property, object } = callee;
18
+ if (object.type !== "Identifier") {
19
+ return null;
11
20
  }
21
+ if (property.type !== "Identifier") {
22
+ return null;
23
+ }
24
+ let objectName = object.name;
25
+ let methodName = property.name;
26
+ if (domainMap.get(objectName)) {
27
+ for (let [groupName, groupOptions] of Object.entries(config.groups)) {
28
+ if (groupOptions.factories.includes(methodName)) {
29
+ return groupName;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ let unitType = unitsMap.get(objectName);
35
+ if (unitType) {
36
+ let derivedMethodKey = `${unitType}.${methodName}`;
37
+ for (let [groupName, groupOptions] of Object.entries(config.groups)) {
38
+ if (groupOptions.derivedMethods?.includes(derivedMethodKey)) {
39
+ return groupName;
40
+ }
41
+ }
42
+ }
43
+ return null;
12
44
  }
13
45
  return null;
14
46
  }
15
- if (callee.type === "MemberExpression") {
16
- let { property, object } = callee;
47
+ if (node.type === "MemberExpression") {
48
+ let { property, object } = node;
17
49
  if (object.type !== "Identifier") {
18
50
  return null;
19
51
  }
20
52
  if (property.type !== "Identifier") {
21
53
  return null;
22
54
  }
23
- let domainName = object.name;
24
- let factoryName = property.name;
25
- if (!domainMap.get(domainName)) {
55
+ let unitName = object.name;
56
+ let propertyName = property.name;
57
+ let unitType = unitsMap.get(unitName);
58
+ if (!unitType) {
26
59
  return null;
27
60
  }
61
+ let derivedMethodKey = `${unitType}.${propertyName}`;
28
62
  for (let [groupName, groupOptions] of Object.entries(config.groups)) {
29
- if (groupOptions.factories.includes(factoryName)) {
63
+ if (groupOptions.derivedMethods?.includes(derivedMethodKey)) {
30
64
  return groupName;
31
65
  }
32
66
  }
@@ -1,15 +1,62 @@
1
1
  const DEFAULT_GROUPS = {
2
2
  computed: {
3
- factories: ["or", "combine"],
3
+ factories: ["or", "combine", "merge", "attach", "forward"],
4
+ derivedMethods: [
5
+ // Derived stores (идут после базовых stores)
6
+ "stores.map",
7
+ // $store.map() → computed store
8
+ "effects.pending",
9
+ // effect.pending → computed store
10
+ "effects.inFlight",
11
+ // effect.inFlight → computed store
12
+ // Derived events на computed тоже идут в computed
13
+ "computed.map",
14
+ // merge([...]).map() → computed
15
+ ],
4
16
  },
5
17
  effects: {
6
18
  factories: ["createEffect"],
19
+ derivedMethods: [
20
+ "effects.map",
21
+ // effect.map() → effect
22
+ "effects.prepend",
23
+ // effect.prepend() → effect
24
+ "effects.filterMap",
25
+ // effect.filterMap() → effect
26
+ ],
7
27
  },
8
28
  domain: {
9
29
  factories: ["createDomain"],
10
30
  },
11
31
  events: {
12
32
  factories: ["createEvent"],
33
+ derivedMethods: [
34
+ // Event Methods, которые создают derived events
35
+ "events.prepend",
36
+ // event.prepend() → event
37
+ "events.map",
38
+ // event.map() → event
39
+ "events.filter",
40
+ // event.filter() → event
41
+ "events.filterMap",
42
+ // event.filterMap() → event
43
+ // Store Properties, которые возвращают events
44
+ "stores.updates",
45
+ // store.updates → event
46
+ "stores.reinit",
47
+ // store.reinit → event
48
+ // Effect Properties, которые возвращают events
49
+ "effects.done",
50
+ // effect.done → event
51
+ "effects.doneData",
52
+ // effect.doneData → event
53
+ "effects.fail",
54
+ // effect.fail → event
55
+ "effects.failData",
56
+ // effect.failData → event
57
+ "effects.finally",
58
+ // effect.finally → event
59
+ ],
13
60
  },
14
61
  stores: {
15
62
  factories: ["createStore"],
@@ -33,6 +80,10 @@ function normalizeConfig(options = {}) {
33
80
  ...(groups[groupName]?.factories ?? []),
34
81
  ...groupOptions.factories,
35
82
  ],
83
+ derivedMethods: [
84
+ ...(groups[groupName]?.derivedMethods ?? []),
85
+ ...(groupOptions.derivedMethods ?? []),
86
+ ],
36
87
  };
37
88
  }
38
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-effector-units-order",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "ESLint plugin for enforcing consistent ordering of Effector units",
5
5
  "keywords": [
6
6
  "eslint",