statelyai / xstate-tools

Public monorepo for XState tooling
183 stars 40 forks source link

create `eslint` config and plugin for v5 #216

Open with-heart opened 2 years ago

with-heart commented 2 years ago

I've been thinking about how cool it would be to provide tooling to assist with (eventual) user migration to v5. One of the things that could be quite helpful would be an eslint plugin and config defining rules that help the user work through all of the breaking changes or utilize new functionality.

Here's what I think those rules might look like, using the xstate@5.0.0-alpha.0 release notes as a starting point.

rule description
avoid-context-spread Since machine.withContext now permits partial context, no need to spread machine.context
no-atomic-internal internal property no longer has an effect on atomic state nodes
no-cond cond has been renamed to guard
no-deprecated-config-properties onEntry, onExit, parallel, and forward have been removed
no-eventless-on-transition Eventless transitions must now be specified using always instead of on[''].
no-emitted-from EmittedFrom has been renamed to SnapshotFrom
no-factory-context-arg The 3rd arg (context) has been removed from and createMachine()
no-generic-state-schema StateSchema has been removed from all generics
no-guard-in The in property for transitions has been removed and replaced with guards. Prefer stateIn() or not(StateIn())
no-machine-factory Machine factory has been removed; use createMachine instead
no-machine-transition-context-arg 3rd arg (context) to machine.transition has been removed. Can use State.from('state', {}) as 1st argument instead.
no-service-batch service.batch(events) has been removed
no-service-children children can only be accessed from state.children
no-service-execute service.execute has been removed
no-service-onChange service.onChange has been removed in favor of service.onTransition or service.subscribe
no-service-onEvent service.onEvent has been removed in favor of service.onTransition or service.subscribe
no-service-onSend service.onSend has been removed in favor of service.onTransition or service.subscribe
no-service-send-type-payload service.send(type, payload) is no longer supported. use service.send({ type, …payload }) instead
no-spawn-import spawn is now available in 3rd arg to assign
no-state-activities state.activites has been removed
no-state-children-direct-reference state.children is now a mapping of invoked actor IDs to their ActorRef and should never be referenced directly. Not really sure about the name or what this rule would even do, but wanted to make a note of it
no-state-events state.events has been removed
no-state-history state.history has been removed
no-state-historyValue state.historyValue is considered internal
no-state-node-isTransient stateNode.isTransient has been removed
no-state-node-version stateNode.version has been removed. version is only available on root machine node
prefer-spawn-implementation-name Prefer referencing actors defined in config.services by name (spawn('promiseActor') instead of spawn(promiseActor))
prefer-wildcard-event-descriptors Detects multiple namespaced actions with the same config object that could be combined (?)
rename-machine-withConfig-to-provide machine.withConfig(…) -> machine.provide(…)
require-compound-state-initial-key Compound states now require an initial key (did they not already?)
require-object-context machine.context is now required to be an object
require-parameterized-actions-params Parameterized actions now require a params property ({ message: 'Hello' } -> { params: { message: 'Hello' }})
require-parameterized-guards-params Guard parameters should now be placed in object at params key ({ minQueryLength: 3 } -> { params: { minQueryLength: 3 }})
davidkpiano commented 2 years ago

This is an excellent list, thanks for making it! I think it's a good idea, and an eslint plugin for XState is generally a good idea as well.

with-heart commented 2 years ago

Thanks for the encouragement @davidkpiano! Already have a few rules working 😁

with-heart commented 2 years ago

I need to think about how this would behave but after talking to @Andarist earlier, we likely need a rule to encourage static configs. Bidirectional editing between code and Studio requires semi-static configs, so the more we can encourage static configs, the better things will be.

Basically we want users to avoid evaluating/computing things in their machine definitions:

const IDLE = 'idle'
const a = 'action'
const b = 1

const machine = createMachine(
  {
    // no vars as values
    initial: IDLE,
    // no `TemplateLiteral`s that require evaluation
    entry: `${a}${b}`,
    states: {
      // no computed properties
      [IDLE]: {}
    }
  },
  {
    actions: {
      action1: () => {},
    }
  }
)
davidkpiano commented 2 years ago

I need to think about how this would behave but after talking to @Andarist earlier, we likely need a rule to encourage static configs. Bidirectional editing between code and Studio requires semi-static configs, so the more we can encourage static configs, the better things will be.

Basically we want users to avoid evaluating/computing things in their machine definitions:

const IDLE = 'idle'
const a = 'action'
const b = 1

const machine = createMachine(
  {
    // no vars as values
    initial: IDLE,
    // no `TemplateLiteral`s that require evaluation
    entry: `${a}${b}`,
    states: {
      // no computed properties
      [IDLE]: {}
    }
  },
  {
    actions: {
      action1: () => {},
    }
  }
)

In the future, I feel like it should be possible to even have some of these work with bidirectional editing. If we treat the AST like a directed graph, then it's a matter of taking one extra "step" to find the source of the value, and modifying it there (as long as it's not shared anywhere non-machine-related).

Computed properties are not uncommon, so I'm wondering if we can't at least support that.

with-heart commented 2 years ago

as long as it's not shared anywhere non-machine-related

Can you clarify what that means?

Andarist commented 2 years ago

If we treat the AST like a directed graph, then it's a matter of taking one extra "step" to find the source of the value,

I agree that it's possible to resolve (and even modify) a lot of pattern.

(as long as it's not shared anywhere non-machine-related).

This is a major blocker though - and how do you make this intuitive so people would know where the line between supported patterns and unsupported ones is?