mattpocock / xstate-codegen

A codegen tool for 100% TS type-safety in XState
MIT License
245 stars 12 forks source link

Having multiple machines codegenerated breaks `send` autocompletion? #76

Open github0013 opened 3 years ago

github0013 commented 3 years ago

When I only have one machine ts file like below,

// switch.machine.ts
import { Machine } from "@xstate/compiled"

interface Context {}

type Event = { type: "toggle" }

export default Machine<Context, Event, "basicSwitch">({
  context: {},
  initial: "active",
  states: {
    active: {
      on: {
        toggle: {
          target: "inactive",
        },
      },
    },
    inactive: {
      on: {
        toggle: {
          target: "active",
        },
      },
    },
  },
})

using send on react side works as expected. It shows the mismatching event name, and it autocompletes. Screen Shot 2021-04-02 at 15 34 29

However, if I create another machine ts file, then it generates for the new one fine, but then send error message and autocompletion get disappeared. Screen Shot 2021-04-02 at 15 36 40

send signature when works

const send: (event: SingleOrArray<Event<Event>> | SCXML.Event<Event>, payload?: EventData) => State<Context, Event, unknown, {
    ...;
}>

send signature when doesn't

const send: (event: SCXML.Event<EventObject> | SingleOrArray<Event<EventObject>>, payload?: EventData) => State<Context, EventObject, unknown, {
    ...;
}>
mattpocock commented 3 years ago

@github0013 Are each of the id's unique on your machine?

github0013 commented 3 years ago

@mattpocock yes

basicSwitch and basicSwitch2

{
  "scripts": {
    "dev": "next",
    "codegen": "xstate-codegen \"src/**/**.machine.ts\" --outDir=\"src\""
  },
  "dependencies": {
    "@xstate/inspect": "^0.4.1",
    "@xstate/react": "^1.3.1",
    "next": "^10.1.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "xstate": "^4.17.1",
    "xstate-codegen": "^0.3.0"
  },
  "devDependencies": {
    "@types/react": "^17.0.3",
    "typescript": "^4.2.3"
  }
}
import { Machine } from "@xstate/compiled"

interface Context {}

type Event = { type: "toggle" }

export default Machine<Context, Event, "basicSwitch">({
  context: {},
  initial: "active",
  states: {
    active: {
      on: {
        toggle: {
          target: "inactive",
        },
      },
    },
    inactive: {
      on: {
        toggle: {
          target: "active",
        },
      },
    },
  },
})
import { Machine } from "@xstate/compiled"

interface Context {}

type Event = { type: "toggle" }

export default Machine<Context, Event, "basicSwitch2">({
  context: {},
  initial: "active",
  states: {
    active: {
      on: {
        toggle: {
          target: "inactive",
        },
      },
    },
    inactive: {
      on: {
        toggle: {
          target: "active",
        },
      },
    },
  },
})

pages/test.tsx

import { useMachine } from "@xstate/compiled/react"
import React from "react"
import machine from "src/machines/switch.machine"

interface Props {}

const BasicSwitch: React.FC<Props> = (props) => {
  const [state, send] = useMachine(machine, {})

  return (
    <>
      <h1>{state.toStrings().join(" > ")}</h1>
      <pre>{JSON.stringify(state.context, null, 2)}</pre>
      <button
        onClick={() => {
          send("toggle")
        }}
      >
        toggle
      </button>
    </>
  )
}
export default BasicSwitch

index.d.ts

import {
  EventObject,
  SingleOrArray,
  InvokeConfig,
  StateMachine,
  Actions,
  DoneEventObject,
  DelayedTransitions,
  DelayConfig,
  Activity,
  Mapper,
  PropertyMapper,
  Condition,
  StateValue,
  ActionObject,
  ActionFunction,
  ActivityConfig,
  DoneInvokeEvent,
  ErrorPlatformEvent,
  InvokeCreator,
  assign,
  send,
  Expr,
  InterpreterOptions,
} from 'xstate';
import {
  StateWithMatches,
  InterpreterWithMatches,
  RegisteredMachine,
} from '@xstate/compiled';
import { Interpreter } from 'xstate/lib/interpreter';
import { State } from 'xstate/lib/State';
import { StateNode } from 'xstate/lib/StateNode';

declare module '@xstate/compiled' {
  type TwoLevelPartial<T extends object> = { [K in keyof T]?: Partial<T[K]>};

  /** Generated Types */
  export class BasicSwitchStateMachine<
    TContext,
    TEvent extends EventObject,
    Id extends 'basicSwitch'
  > extends StateNodeWithGeneratedTypes<TContext, any, TEvent> {
    id: Id;
    states: StateNode<TContext, any, TEvent>['states'];
    _matches:
      | 'active'
      | 'inactive'
    _options: {
      context?: Partial<TContext>;
      guards?: {
      };
      actions?: {  
      };
      services?: {
      };
      activities?: {
      };
      delays?: {
      };
      devTools?: boolean;
    };
    _subState: {
    targets: '.active' | 'active' | '.inactive' | 'inactive';
    sources: never;
    states: {
      active: {
    targets: 'active' | 'inactive';
    sources: 'toggle';
    states: {

    };
  }
inactive: {
    targets: 'active' | 'inactive';
    sources: 'toggle';
    states: {

    };
  }
    };
  };
    withConfig(
      options: TwoLevelPartial<BasicSwitchStateMachine<
        TContext,
        TEvent,
        'basicSwitch'
      >['_options']>
    ): this;
  }
  export class BasicSwitch2StateMachine<
    TContext,
    TEvent extends EventObject,
    Id extends 'basicSwitch2'
  > extends StateNodeWithGeneratedTypes<TContext, any, TEvent> {
    id: Id;
    states: StateNode<TContext, any, TEvent>['states'];
    _matches:
      | 'active'
      | 'inactive'
    _options: {
      context?: Partial<TContext>;
      guards?: {
      };
      actions?: {  
      };
      services?: {
      };
      activities?: {
      };
      delays?: {
      };
      devTools?: boolean;
    };
    _subState: {
    targets: '.active' | 'active' | '.inactive' | 'inactive';
    sources: never;
    states: {
      active: {
    targets: 'active' | 'inactive';
    sources: 'toggle';
    states: {

    };
  }
inactive: {
    targets: 'active' | 'inactive';
    sources: 'toggle';
    states: {

    };
  }
    };
  };
    withConfig(
      options: TwoLevelPartial<BasicSwitch2StateMachine<
        TContext,
        TEvent,
        'basicSwitch2'
      >['_options']>
    ): this;
  }

  export interface RegisteredMachinesMap<TContext, TEvent extends EventObject> {
    basicSwitch: BasicSwitchStateMachine<TContext, TEvent, 'basicSwitch'>
    basicSwitch2: BasicSwitch2StateMachine<TContext, TEvent, 'basicSwitch2'>
  }

  /** Utility types */

  export type InvokeConfig<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > = {
    /**
     * The unique identifier for the invoked machine. If not specified, this
     * will be the machine's own `id`, or the URL (from `src`).
     */
    id?: string;
    /**
     * The source of the machine to be invoked, or the machine itself.
     */
    src:
      | string
      | StateMachine<any, any, any>
      | InvokeCreator<TContext, TEvent, any>;
    /**
     * If `true`, events sent to the parent service will be forwarded to the invoked service.
     *
     * Default: `false`
     */
    autoForward?: boolean;
    /**
     * @deprecated
     *
     *  Use `autoForward` property instead of `forward`. Support for `forward` will get removed in the future.
     */
    forward?: boolean;
    /**
     * Data from the parent machine's context to set as the (partial or full) context
     * for the invoked child machine.
     *
     * Data should be mapped to match the child machine's context shape.
     */
    data?:
      | Mapper<TContext, TEvent, any>
      | PropertyMapper<TContext, TEvent, any>;
    /**
     * The transition to take upon the invoked child machine reaching its final top-level state.
     */
    onDone?:
      | TSubState['targets']
      | SingleOrArray<TransitionConfig<TContext, DoneEventObject, TSubState>>;
    /**
     * The transition to take upon the invoked child machine sending an error event.
     */
    onError?:
      | TSubState['targets']
      | SingleOrArray<TransitionConfig<TContext, ErrorPlatformEvent, TSubState>>;
  };

  export type RegisteredMachine<
    TContext,
    TEvent extends EventObject
  > = RegisteredMachinesMap<TContext, TEvent>[keyof RegisteredMachinesMap<
    TContext,
    TEvent
  >];

  export class StateNodeWithGeneratedTypes<
    TContext,
    TSchema,
    TEvent extends EventObject
  > extends StateNode<TContext, TSchema, TEvent> {}

  export type DelayedTransitions<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > =
    | Record<
        string | number,
        | TSubState['targets']
        | SingleOrArray<TransitionConfig<TContext, TEvent, TSubState>>
      >
    | Array<
        TransitionConfig<TContext, TEvent, TSubState> & {
          delay: number | string | Expr<TContext, TEvent, number>;
        }
      >;

  export type InterpreterWithMatches<
    TContext,
    TSchema,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  > = Omit<Interpreter<TContext, TSchema, TEvent>, 'state'> & {
    state: StateWithMatches<
      TContext,
      TEvent,
      Id
    >;
  };

  export type StateWithMatches<
    TContext,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  > = Omit<State<TContext, TEvent>, 'matches'> & {
    matches: (matches: RegisteredMachinesMap<TContext, TEvent>[Id]['_matches']) => boolean;
  };

  export function interpret<
    TContext,
    TSchema,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  >(
    machine: Extract<RegisteredMachine<TContext, TEvent>, { id: Id }>,
    options?: Partial<InterpreterOptions>
  ): InterpreterWithMatches<TContext, TSchema, TEvent, Id>;

  export function Machine<
    TContext,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  >(
    config: MachineConfig<
      TContext,
      TEvent,
      RegisteredMachinesMap<TContext, TEvent>[Id]['_subState']
    >,
    options?: TwoLevelPartial<
      Extract<
        RegisteredMachine<TContext, TEvent>,
        { id: Id }
      >['_options']
    >,
  ): RegisteredMachinesMap<TContext, TEvent>[Id];

  export function createMachine<
    TContext,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  >(
    config: MachineConfig<
      TContext,
      TEvent,
      RegisteredMachinesMap<TContext, TEvent>[Id]['_subState']
    >,
    options?: TwoLevelPartial<
      Extract<
        RegisteredMachine<TContext, TEvent>,
        { id: Id }
      >['_options']
    >,
  ): RegisteredMachinesMap<TContext, TEvent>[Id];

  export interface MachineConfig<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > extends StateNodeConfig<TContext, TEvent, TSubState> {
    /**
     * The initial context (extended state)
     */
    context?: TContext | (() => TContext);
    /**
     * The machine's own version.
     */
    version?: string;
  }

  export interface SubState {
    targets: string;
    sources: string;
    states: Record<string, SubState>;
  }

  export type TransitionConfigTarget<TSubState extends SubState> =
    | TSubState['targets']
    | undefined;

  export type TransitionTarget<TSubState extends SubState> = SingleOrArray<
    TSubState['targets']
  >;

  export interface TransitionConfig<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > {
    cond?: Condition<TContext, TEvent>;
    actions?: Actions<TContext, TEvent>;
    in?: StateValue;
    internal?: boolean;
    target?: TransitionTarget<TSubState>;
    meta?: Record<string, any>;
  }

  export type TransitionConfigOrTarget<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > = SingleOrArray<
    | TransitionConfigTarget<TSubState>
    | TransitionConfig<TContext, TEvent, TSubState>
  >;

  export type TransitionsConfigMap<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > = {
    [K in TEvent['type']]?: TransitionConfigOrTarget<
      TContext,
      TEvent extends {
        type: K;
      }
        ? TEvent
        : never,
      TSubState
    >;
  } & {
    ''?: TransitionConfigOrTarget<TContext, TEvent, TSubState>;
  } & {
    '*'?: TransitionConfigOrTarget<TContext, TEvent, TSubState>;
  };

  export type TransitionsConfigArray<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > = Array<TransitionsConfigMap<TContext, TEvent, TSubState>>;

  export type TransitionsConfig<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > =
    | TransitionsConfigMap<TContext, TEvent, TSubState>
    | TransitionsConfigArray<TContext, TEvent, TSubState>;

  export interface StateNodeConfig<
    TContext,
    TEvent extends EventObject,
    TSubState extends SubState
  > {
    /**
     * The relative key of the state node, which represents its location in the overall state value.
     * This is automatically determined by the configuration shape via the key where it was defined.
     */
    key?: string;
    /**
     * The initial state node key.
     */
    initial?: keyof TSubState['states'];
    /**
     * @deprecated
     */
    parallel?: boolean | undefined;
    /**
     * The type of this state node:
     *
     *  - `'atomic'` - no child state nodes
     *  - `'compound'` - nested child state nodes (XOR)
     *  - `'parallel'` - orthogonal nested child state nodes (AND)
     *  - `'history'` - history state node
     *  - `'final'` - final state node
     */
    type?: 'atomic' | 'compound' | 'parallel' | 'final' | 'history';
    /**
     * The initial context (extended state) of the machine.
     *
     * Can be an object or a function that returns an object.
     */
    context?: TContext | (() => TContext);
    /**
     * Indicates whether the state node is a history state node, and what
     * type of history:
     * shallow, deep, true (shallow), false (none), undefined (none)
     */
    history?: 'shallow' | 'deep' | boolean | undefined;
    /**
     * The mapping of state node keys to their state node configurations (recursive).
     */
    states?: {
      [K in keyof TSubState['states']]: StateNodeConfig<
        TContext,
        TEvent,
        TSubState['states'][K]
      >;
    };
    /**
     * The services to invoke upon entering this state node. These services will be stopped upon exiting this state node.
     */
    invoke?: SingleOrArray<
      | InvokeConfig<
          TContext,
          TEvent,
          TSubState
        >
      | StateMachine<any, any, any>
    >;
    /**
     * The mapping of event types to their potential transition(s).
     */
    on?: TransitionsConfig<TContext, TEvent, TSubState>;
    /**
     * The action(s) to be executed upon entering the state node.
     *
     * @deprecated Use `entry` instead.
     */
    onEntry?: Actions<
      TContext,
      Extract<TEvent, { type: TSubState['sources'] }>
    >;
    /**
     * The action(s) to be executed upon entering the state node.
     */
    entry?: Actions<TContext, Extract<TEvent, { type: TSubState['sources'] }>>;
    /**
     * The action(s) to be executed upon exiting the state node.
     *
     * @deprecated Use `exit` instead.
     */
    onExit?: Actions<TContext, TEvent>;
    /**
     * The action(s) to be executed upon exiting the state node.
     */
    exit?: Actions<TContext, TEvent>;
    /**
     * The potential transition(s) to be taken upon reaching a final child state node.
     *
     * This is equivalent to defining a `[done(id)]` transition on this state node's `on` property.
     */
    onDone?:
      | TSubState['targets']
      | SingleOrArray<TransitionConfig<TContext, DoneEventObject, TSubState>>;
    /**
     * The mapping (or array) of delays (in milliseconds) to their potential transition(s).
     * The delayed transitions are taken after the specified delay in an interpreter.
     */
    after?: DelayedTransitions<TContext, TEvent, TSubState>;
    /**
     * An eventless transition that is always taken when this state node is active.
     * Equivalent to a transition specified as an empty `''`' string in the `on` property.
     */
    always?: TransitionConfigOrTarget<
      TContext,
      Extract<TEvent, { type: TSubState['sources'] }>,
      TSubState
    >;
    /**
     * The activities to be started upon entering the state node,
     * and stopped upon exiting the state node.
     */
    activities?: SingleOrArray<
      Activity<TContext, Extract<TEvent, { type: TSubState['sources'] }>>
    >;
    /**
     * @private
     */
    parent?: StateNode<TContext, any, TEvent>;
    strict?: boolean | undefined;
    /**
     * The meta data associated with this state node, which will be returned in State instances.
     */
    meta?: any;
    /**
     * The data sent with the "done.state._id_" event if this is a final state node.
     *
     * The data will be evaluated with the current `context` and placed on the `.data` property
     * of the event.
     */
    data?:
      | Mapper<TContext, TEvent, any>
      | PropertyMapper<TContext, TEvent, any>;
    /**
     * The unique ID of the state node, which can be referenced as a transition target via the
     * `#id` syntax.
     */
    id?: string | undefined;
    /**
     * The string delimiter for serializing the path to a string. The default is "."
     */
    delimiter?: string;
    /**
     * The order this state node appears. Corresponds to the implicit SCXML document order.
     */
    order?: number;
  }

  // @ts-ignore
  export { mapState, actions, assign, send, sendParent, sendUpdate, forwardTo, matchState, spawn, doneInvoke } from 'xstate';
}

react.d.ts

import {
  EventObject,
} from 'xstate';
import {
  StateWithMatches,
  InterpreterWithMatches,
  RegisteredMachinesMap,
  RegisteredMachine,
} from '@xstate/compiled';

declare module '@xstate/compiled/react' {
  export function useMachine<
    TContext,
    TSchema,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  >(
    machine: Extract<RegisteredMachine<TContext, TEvent>, { id: Id }>,
    options: Extract<
      RegisteredMachine<TContext, TEvent>,
      { id: Id }
    >['_options'],
  ): [
    StateWithMatches<
      TContext,
      TEvent,
      Id
    >,
    InterpreterWithMatches<TContext, TSchema, TEvent, Id>['send'],
    InterpreterWithMatches<TContext, TSchema, TEvent, Id>,
  ];

  export function useService<
    TContext,
    TSchema,
    TEvent extends EventObject,
    Id extends keyof RegisteredMachinesMap<TContext, TEvent>
  >(
    service: InterpreterWithMatches<TContext, TSchema, TEvent, Id>
  ): [
    StateWithMatches<
      TContext,
      TEvent,
      Id
    >,
    InterpreterWithMatches<TContext, TSchema, TEvent, Id>['send'],
    InterpreterWithMatches<TContext, TSchema, TEvent, Id>,
  ];
}
mattpocock commented 3 years ago

Thanks for the detailed repro. Can't see anything you're doing wrong. What happens if you remove outDir?

github0013 commented 3 years ago

Basically same thing.

  1. remove outDir
  2. remove .d.ts files from the local src folder
  3. stop yarn codegen
  4. start yarn codegen
  5. save .machine.ts files
  6. again, send autocomplete fails to show toggle