@aurelienbbn/oxlint-plugin-xstate

中文文档
0.2.0发布于 2026/10/14MITTypeScriptES Module构建来源已验证近 30 天下载 2417

Custom oxlint rules for XState machines and actors.

原始使用文档
文档目录5 节
  1. 13 solid, 2 MAYBE, 2 deprecated
  2. What fires
  3. require-event-satisfies matters where types are erased
  4. Registered contract inventory
  5. Credited concepts

@aurelienbbn/oxlint-plugin-xstate

npm downloads CI license node

17 oxlint rules for XState v5 and v6 alpha machines (with @xstate/effect), built with setup().createMachine(), the shape eslint-plugin-xstate can't see.

v6 support is checked against [email protected], @xstate/[email protected] and @xstate/[email protected]; alpha APIs can move between releases.

eslint-plugin-xstatethis plugin
setup().createMachine() machines❌ bare createMachine callee only✅ setup-aware
XState v6 alpha + @xstate/effect❌✅ v5 and v6 spellings
maintained❌ last published 2023-12, ESLint 8 peer✅ oxlint >=1.82.0 <2.0.0
pnpm add -D @aurelienbbn/oxlint-plugin-xstate oxlint
{
  "jsPlugins": ["@aurelienbbn/oxlint-plugin-xstate"],
  "rules": {
    "xstate/require-setup-create-machine": "error",
    "xstate/no-context-mutation": "error",
    "xstate/require-effect-actor": "error",
    "xstate/no-deferred-enqueue": "error"
  }
}

No preset, no autofix: enable each rule by name.

13 solid, 2 MAYBE, 2 deprecated

solid       █████████████  13   error
MAYBE       ██              2   off or warn: no-unreachable-transition · promise-actor-abort-signal
deprecated  ██              2   v5 only: no-imperative-action-creator · prefer-send-to

no-imperative-action-creator and prefer-send-to are deprecated since 0.2.0. XState v6 removes the action creators and sendParent (importing one is a type error); both rules still work on v5 until you migrate.

no-direct-named-action contradicts the upstream v6 docs, which show direct actions.x(...) calls. It reports them anyway: in alpha.64 a direct call ran twice for one send, and also ran during pure transition() and snapshot.can().

What fires

setup({ types: {} }).createMachine({
  entry: ({ context }) => {
    assign({ count: context.count + 1 });
  }, // ❌ no-imperative-action-creator: builds an action, runs nothing
});
setup({ types: {} }).createMachine({
  entry: assign({ count: ({ context }) => context.count + 1 }), // ✅
});

actorRef.send({ type: "ADD_ITEM", id: "x" }); // ❌ require-event-satisfies
actorRef.send({ type: "ADD_ITEM" } satisfies CartEvent); // ✅

setup({}).createMachine({ id: "app.cart" });
// ❌ require-setup-create-machine: neither `types` (v5) nor `schemas` (v6)

// v6 alpha / @xstate/effect
createActor(setupEffect({ actors }).createMachine(config)); // ❌ require-effect-actor: status 'error', use createEffectActor
on: { SAVE: (_, enq) => { setTimeout(() => enq.raise({ type: "SAVED" }), 100); } } // ❌ no-deferred-enqueue
on: { SAVE: (_, enq) => { enq.raise({ type: "SAVED" }, { delay: 100 }); } } // ✅
after: { "5m": { target: "expired" } } // ❌ valid-duration: write "PT5M" or 300000

require-event-satisfies matters where types are erased

setup({ types }) machine  ──▶  TypeScript already checks the payload  → rule redundant
sendTo / sendParent targets,
AnyActorRef, untyped refs ──▶  event type erased                      → rule is the only check

Fully typed codebase? Narrow sendCalleeNames to the erased senders. satisfies unknown / any isn't evidence; the rule enforces syntax, not the intended event union.

Options and defaults
RuleOptionDefault
machine-namingpattern^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$ (generic dotted kebab-case namespace, not oio-specific)
named-actor-srcv5 files: invoke.src or first argument of spawnChild / enqueue.spawnChild that is a call, a function, or an identifier not bound to a string constant; v6: first argument of enq.spawn that is a call, new, or a functionfalse; true also checks spawn(...) from an assign argument or a v6 context factory. ⏭️ v6 files (no v5-only import, no types): invoke.src, identifiers
guardsfalse; true also reports inline function guard values
no-context-mutationallowCollectionMethodsfalse; true exempts Map/Set set / add / delete / clear; v6 createLogic / createAsyncLogic / <setup>.createInvoke / createStateConfig(path, config), setupEffect (@xstate/effect), .provide / .extend
require-event-satisfiessendCalleeNames["send", "raise", "sendTo", "sendParent", "emit", "sendBack"]; v6 enq.raise / sendTo / emit / sendBack on the handler's second parameter; @xstate/effect send(actor, ev) / send(ev); .send on <system>.createActor and yield* createEffectActor refs
Exact triggers and scope per rule
Rule❌ Fires onMatched / ⏭️ skipped
no-imperative-action-creator⚠️ deprecated (v5 only). assign / raise / sendTo / sendParent / forwardTo / emit / cancel / stopChild / stop / spawnChild / log / enqueueActions as a discarded statement (also void / awaited), or returned by a concise arrow in entry / exit / actions or setup({ actions })creators imported from xstate, destructured from or called on a setup() result
no-context-mutationassignment, ++ / --, delete, or a mutating method on a path rooted at context or snapshot.context / getSnapshot().contextcontext destructured by an implementation function inside setup / createMachine / createStateConfig / createAction / assign / enqueueActions. Methods: push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin, set, add, delete, clear. ⏭️ Map/Set with allowCollectionMethods: true
no-machine-in-rendercreateMachine, <setup>.createMachine (incl. setupEffect, system.setup), createActor, <system>.createActor, interpret, createActorAtoms directly in a component or hook bodynearest enclosing function is a component (/^[A-Z]/) or hook (/^use[A-Z]/), memo / forwardRef included, in a file importing react or @xstate/react. ⏭️ nested callbacks (useMemo, useEffect, handlers), machine.provide(...)
named-actor-srcinvoke.src or first argument of spawnChild / enqueue.spawnChild that is a call, a function, or an identifier not bound to a string constantinvoke.src in createMachine / <setup>.createMachine / <setup>.createStateConfig config. ⏭️ member expressions
stable-selector-resultinline useSelector or actor.select selector returning a fresh object/array, no comparator argumentuseSelector from @xstate/react, @xstate/store-react, legacy @xstate/store/react, <ActorContext>.useSelector. Fresh = object/array literal, .map / .filter / .flatMap / .toSorted, Object.keys / values / entries; .select on createActor / <system>.createActor / useActorRef refs. ⏭️ atoms .select
require-setup-create-machinecreateMachine, setup, setupEffect or <system>.setup whose literal config has neither types nor schemas, unless every .createMachine / .extend on that setup in the file declares themnamed and namespace imports, aliases, lexical shadows. ⏭️ setup(...) arguments not statically readable (identifiers, spreads)
require-event-satisfiesan object-literal event sent without satisfiessend, raise, sendTo, sendParent, emit from xstate; enqueue.raise / sendTo / sendParent / emit on enqueueActions' enqueue; .send on refs from createActor, useActorRef, <ActorContext>.useActorRef(), and the useMachine / useActor tuple (send and actor-ref slots). ⏭️ unrelated .send methods
machine-naminga present static machine id failing pattern⏭️ absent ids; createStateConfig (state-node ids, not machine ids)
no-unreachable-transitionan unguarded branch not last in always, on.<EVENT>, after.<DELAY>, onDone, onError arrays (v5); a targetless unconditional always with actions or context, or a v6 always function returning a targetless { context } patch on every path (infinite loop)⏭️ branches with spreads or computed keys
prefer-send-to⚠️ deprecated (v5 only). sendParent imported from xstate, and <object>.sendParent(...) such as enqueue.sendParent
promise-actor-abort-signalinline fromPromise, createAsyncLogic({ run }) or <setup>.createInvoke({ src }) logic calling global fetch for a read (no init, or literal GET / HEAD) never referencing signal⏭️ wrapped HTTP clients are invisible to it
require-effect-actorcreateActor / useMachine / useActor / useActorRef / createActorContext given fromEffect* logic or a setupEffect machine (inline, through a const, .provide, .extend)⏭️ createEffectActor, createActorAtoms
no-discarded-effectenq(() => Effect.…), or an Effect returned from entry / exit / a transitionEffect imported from effect. ⏭️ Effect.run* / Effect.is*, enq(actions.x, params)
no-deferred-enqueueenq / enq.* inside a timer, promise, listener or enqueued-effect callback, or after await, in a v6 handler⏭️ synchronous callbacks (forEach), createAsyncLogic run
no-direct-named-actionactions.x(...) called directly in a v6 handler body{ actions } / { actions: a } / args.actions. ⏭️ inside enq(() => …), action implementations, context factory
valid-durationduration-like after key, timeout, or createAsyncLogic timeout string XState can't parse and no delays entry namesaccepts ms numbers, <n>ms, <n>s, ISO 8601 (PT5M). Report only
no-persist-live-snapshotJSON.stringify of getSnapshot(), a useMachine snapshot, or a subscribe listener snapshot⏭️ getPersistedSnapshot(), getSnapshot().context, receivers not provably actors

Config-walking rules treat the argument of <setup>.createStateConfig(...) (v6: the second argument of createStateConfig(path, config)) as machine config. v6 rules recognise handlers as the functions in entry / exit / always / on / after / onDone / onError / onSnapshot / onTimeout / to slots, with enq as their second parameter.

Not shipped: no-deprecated-xstate-api. Type-aware typescript/no-deprecated already reports interpret, stop, getNextSnapshot and the other @deprecated exports.

Credit: no-imperative-action-creator and no-unreachable-transition re-implement eslint-plugin-xstate's no-imperative-action and no-infinite-loop ideas (Richard Laffers); no-discarded-effect re-implements the idea of statelyai/xstate's no-inline-effect (MIT). No code copied.

Registered contract inventory

Generated from package exports by pnpm catalog. Rule-specific options and limitations are described above and in the source tests.

Rule/checkTrigger or review scope
machine-namingRequire XState machine ids to follow the configured naming convention.
named-actor-srcRequire invoke.src and spawnChild() (XState v5) and enq.spawn() (XState v6) to reference actors declared in setup({ actors }) by key instead of inline actor logic.
no-context-mutationForbid in-place mutation of XState context (assignments, delete, and mutating array/Map/Set methods on context or snapshot.context).
no-deferred-enqueueForbid calling a v6 handler's enq from a timer, promise, listener, or enqueued-effect callback, or after an await.
no-direct-named-actionForbid calling a named action directly in a v6 handler body instead of enq(actions.name, params).
no-discarded-effectForbid Effects built inside an inline enq(() => ...) effect or returned from a machine handler, where XState discards them unrun.
no-imperative-action-creatorForbid XState action creators (assign, raise, sendTo, ...) called as discarded statements or returned from inline action functions, where they are silent no-ops.
no-machine-in-renderForbid createMachine, setup().createMachine, createActor, system.createActor, interpret and createActorAtoms calls directly in a React component or hook body.
no-persist-live-snapshotForbid JSON.stringify of an actor's live snapshot (getSnapshot(), a useMachine snapshot, a subscribe listener's snapshot) instead of getPersistedSnapshot().
no-unreachable-transitionForbid an unguarded transition placed before other branches of the same transition array (XState v5), and targetless always transitions that re-run their actions or context update forever.
prefer-send-toPrefer sendTo() with an actor ref passed through input over sendParent() and enqueue.sendParent().
promise-actor-abort-signalRequire inline async actor logic (fromPromise, createAsyncLogic run, createInvoke src) that calls fetch for a read to forward the actor's abort signal.
require-effect-actorRequire @xstate/effect logic (fromEffect*, setupEffect machines) to be started with createEffectActor instead of createActor or @xstate/react hooks.
require-event-satisfiesRequire object-literal events sent to machines to be checked with satisfies.
require-setup-create-machineRequire XState machines to declare types (XState v5) or schemas (XState v6) on createMachine(), setup(), setupEffect() or system.setup().
stable-selector-resultForbid inline useSelector and actor.select selectors that return a fresh object or array when no comparator argument is passed.
valid-durationRequire after keys, state timeouts, and createAsyncLogic timeouts written as duration strings to use a format XState parses, unless they name a declared delay.

Credited concepts

  • ai-automation by Sandro Maglione (inspiration, independently re-implemented)
  • eslint-plugin-xstate no-imperative-action by Richard Laffers (concept)
  • eslint-plugin-xstate no-infinite-loop by Richard Laffers (concept)
  • statelyai/xstate no-inline-effect (MIT; concept, independently implemented)

版本与发布记录

@aurelienbbn/oxlint-plugin-xstate 共发布 3 个版本,首次发布于 2026/09/25;过去 12 个月发布了 3 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.0latest2026/10/1424 天前
0.1.12026/09/251 个月前
0.1.02026/09/251 个月前

关于 @aurelienbbn/oxlint-plugin-xstate 的常见问题

@aurelienbbn/oxlint-plugin-xstate 是什么?

Custom oxlint rules for XState machines and actors.

如何安装 @aurelienbbn/oxlint-plugin-xstate?

在项目目录运行 npm install @aurelienbbn/oxlint-plugin-xstate,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:oxlint(>=1.82.0 <2.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@aurelienbbn/oxlint-plugin-xstate 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@aurelienbbn/oxlint-plugin-xstate 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@aurelienbbn/oxlint-plugin-xstate 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js ^22.19.0 || ^24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@aurelienbbn/oxlint-plugin-xstate 可以商用吗?

@aurelienbbn/oxlint-plugin-xstate 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@aurelienbbn/oxlint-plugin-xstate 的下载量有多少?

根据 npm 官方下载统计,@aurelienbbn/oxlint-plugin-xstate 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,417 次,日均约 81 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aurelienbbn/oxlint-plugin-xstate 还在维护吗?

最新版本 0.2.0 发布于 2026/10/14(24 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/09/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。