Skip to content

zeroqs/eslint-plugin-effector-order

Repository files navigation

eslint-plugin-effector-units-order

An ESLint plugin that enforces a consistent order for Effector units (domains, events, effects, stores, and computed values) in your codebase. Keep your Effector code organized and maintainable with automatic code reordering.

Alt text

✨ Features

  • 🔧 Auto-fix — Automatically reorders your code to match the configured order
  • 📦 Grouping — Organize units into logical groups for better code structure
  • 📏 Empty lines — Configurable spacing between groups (0-3 lines)
  • 🏭 Custom factories — Support for custom unit factories and factory methods
  • 🌐 Domain support — Works with domain.createEvent(), domain.createEffect(), and other domain methods
  • 🧮 Computed stores — Supports combine() and or() from patronum
  • ⚙️ Flexible configuration — Configure via rule options or ESLint shared settings
  • 📋 ESLint 9+ — Supports both flat config and legacy .eslintrc format

📦 Installation

# npm
npm install -D eslint-plugin-effector-units-order

# pnpm
pnpm add -D eslint-plugin-effector-units-order

# yarn
yarn add -D eslint-plugin-effector-units-order

🚀 Quick Start

ESLint Flat Config (ESLint 9+)

// eslint.config.js
import effectorOrder from "eslint-plugin-effector-units-order";

export default [
  // Use recommended config
  effectorOrder.configs.recommended,
];

Legacy Config (.eslintrc)

{
  "extends": ["plugin:effector-order/recommended-legacy"]
}

📖 Usage

Recommended Configuration

The recommended configuration enforces the following order:

  1. domaincreateDomain()
  2. eventscreateEvent(), domain.createEvent()
  3. effectscreateEffect(), domain.createEffect()
  4. storescreateStore(), domain.createStore()
  5. computedcombine(), or()

With 1 empty line between groups for better readability.

Custom Configuration

// eslint.config.js
import effectorOrder from "eslint-plugin-effector-units-order";

export default [
  {
    plugins: {
      "effector-order": effectorOrder,
    },
    rules: {
      "effector-order/keep-units-order": [
        "error",
        {
          order: ["domain", "events", "effects", "stores", "computed"],
          emptyLinesBetweenGroups: 1,
        },
      ],
    },
  },
];

⚙️ Options

Option Type Default Description
order string[] ['domain', 'events', 'effects', 'stores', 'computed'] Defines the order of unit groups
emptyLinesBetweenGroups number 1 Number of empty lines between groups (0-3)
groups object See below Custom group definitions with factories and derived methods
customFactories object {} Maps custom factory functions to specific groups
packages string[] ['effector', 'patronum'] Packages to detect imports from

Group Options:

Each group in groups can have:

  • factories: string[] — Factory functions that create units of this type (e.g., ['createStore'])
  • derivedMethods?: string[] — Methods and properties that create derived units (e.g., ['stores.map', 'effects.pending'])

Default Groups

{
  domain: {
    factories: ['createDomain']
  },
  events: {
    factories: ['createEvent'],
    derivedMethods: [
      'events.prepend',
      'events.map',
      'events.filter',
      'events.filterMap',
      'stores.updates',
      'stores.reinit',
      'effects.done',
      'effects.doneData',
      'effects.fail',
      'effects.failData',
      'effects.finally'
    ]
  },
  effects: {
    factories: ['createEffect'],
    derivedMethods: [
      'effects.map',
      'effects.prepend',
      'effects.filterMap'
    ]
  },
  stores: {
    factories: ['createStore']
  },
  computed: {
    factories: ['or', 'combine', 'merge', 'attach', 'forward'],
    derivedMethods: [
      'stores.map',
      'effects.pending',
      'effects.inFlight'
    ]
  }
}

📋 Examples

Basic Usage

❌ Incorrect Order

import { createStore, createEvent, createDomain } from "effector";

export const $user = createStore(null); // ❌ Store before event
export const userClicked = createEvent(); // ❌ Event before domain
export const userDomain = createDomain();

✅ Correct Order

import { createStore, createEvent, createDomain } from "effector";

export const userDomain = createDomain();

export const userClicked = createEvent();

export const $user = createStore(null);

Domain Methods

The plugin also recognizes units created via domain methods:

import { createDomain } from "effector";

export const userDomain = createDomain();

export const userClicked = userDomain.createEvent();
export const fetchUserFx = userDomain.createEffect();

export const $user = userDomain.createStore(null);

Computed Stores

Computed stores using combine(), merge(), attach(), forward(), and or() from patronum are properly recognized:

import { createStore, combine, merge, attach, forward } from "effector";
import { or } from "patronum";

export const $isLoading = createStore(false);
export const $error = createStore(null);

export const $hasIssues = or($isLoading, $error);
export const $state = combine({
  isLoading: $isLoading,
  error: $error,
});

Derived Units

The plugin automatically recognizes derived stores and events created from existing units:

Derived Stores

import { createStore, createEffect } from "effector";

export const $products = createStore([]);
export const fetchProductsFx = createEffect();

export const $enrichedProducts = $products.map((products) =>
  products.map((p) => ({ ...p, priceWithTax: p.price * 1.2 }))
);
export const $loading = fetchProductsFx.pending;
export const $isLoading = $loading.map((loading) => loading);

Derived Events

import { createEvent, createEffect, createStore } from "effector";

export const userLogin = createEvent();
export const fetchUserFx = createEffect();
export const $user = createStore(null);

export const loginWithEmail = userLogin.map((email) => ({ email })); /
export const fetchSucceeded = fetchUserFx.done;
export const fetchFailed = fetchUserFx.fail;
export const userUpdated = $user.updates;
export const resetUser = $user.reinit;

❌ Incorrect Order (Auto-fixed)

import { createStore, createEffect } from "effector";

export const $loading = fetchProductsFx.pending;
export const fetchProductsFx = createEffect();
export const $products = createStore([]);
export const $enriched = $products.map((p) => p);

✅ Correct Order (After auto-fix)

import { createStore, createEffect } from "effector";

export const fetchProductsFx = createEffect();

export const $products = createStore([]);

export const $loading = fetchProductsFx.pending;
export const $enriched = $products.map((p) => p);

Custom Factories

You can map custom factory functions to specific groups:

// eslint.config.js
export default [
  {
    plugins: { "effector-order": effectorOrder },
    rules: {
      "effector-order/keep-units-order": [
        "error",
        {
          order: ["domain", "events", "effects", "stores", "computed"],
          customFactories: {
            tableStateFactory: "stores",
            permissionSelectionFactory: "stores",
          },
        },
      ],
    },
  },
];

Custom Groups

Define your own groups with custom factory mappings:

// eslint.config.js
export default [
  {
    plugins: { "effector-order": effectorOrder },
    rules: {
      "effector-order/keep-units-order": [
        "error",
        {
          order: ["domain", "actions", "data", "computed"],
          groups: {
            actions: { factories: ["createEvent", "createEffect"] },
            data: { factories: ["createStore"] },
          },
        },
      ],
    },
  },
];

Custom Derived Methods

You can configure which methods and properties create derived units:

// eslint.config.js
export default [
  {
    plugins: { "effector-order": effectorOrder },
    rules: {
      "effector-order/keep-units-order": [
        "error",
        {
          order: ["domain", "events", "effects", "stores", "computed"],
          groups: {
            events: {
              factories: ["createEvent"],
              // Configure which methods create derived events
              derivedMethods: [
                "events.map",
                "events.prepend",
                "effects.done",
                "stores.updates",
              ],
            },
            effects: {
              factories: ["createEffect"],
              derivedMethods: ["effects.map", "effects.prepend"],
            },
            computed: {
              factories: ["combine", "merge", "attach", "forward"],
              // Configure which methods create derived stores
              derivedMethods: [
                "stores.map",
                "effects.pending",
                "effects.inFlight",
              ],
            },
          },
        },
      ],
    },
  },
];

Format for derivedMethods:

  • "unitType.methodName" — for methods like $store.map(), event.prepend()
  • "unitType.property" — for properties like effect.pending, store.updates

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.

No Empty Lines Between Groups

You can disable empty lines between groups by setting emptyLinesBetweenGroups to 0:

// eslint.config.js
export default [
  {
    plugins: { "effector-order": effectorOrder },
    rules: {
      "effector-order/keep-units-order": [
        "error",
        {
          order: ["domain", "events", "effects", "stores"],
          emptyLinesBetweenGroups: 0,
        },
      ],
    },
  },
];

Result:

export const userDomain = createDomain();
export const userClicked = createEvent();
export const fetchUserFx = createEffect();
export const $user = createStore(null);

🔧 Shared Settings

You can also configure the plugin via ESLint shared settings. This is useful when you want to share the same configuration across multiple rules or files:

// eslint.config.js
export default [
  {
    plugins: { "effector-order": effectorOrder },
    settings: {
      "effector-units-order": {
        order: ["domain", "events", "effects", "stores", "computed"],
        emptyLinesBetweenGroups: 1,
      },
    },
    rules: {
      "effector-order/keep-units-order": "error",
    },
  },
];

Note: Rule options take precedence over shared settings. If you specify options directly in the rule configuration, they will override the shared settings.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

📄 License

MIT © Nikita

About

ESLint plugin for enforcing consistent ordering of Effector units

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors