mattpocock / xstate-codegen

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

Rewrite to ts morph #29

Closed Andarist closed 3 years ago

changeset-bot[bot] commented 4 years ago

🦋 Changeset detected

Latest commit: 0305fc17117802187a97a86f8e0971e386e7b025

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package | Name | Type | | -------------- | ----- | | xstate-codegen | Minor |

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

mattpocock commented 4 years ago

Very exciting. I've added a complex machine test case into master which tests parallel states, nested states etc. When I tried it on this branch, the tests failed. Could you pull that into this PR and try to make it pass?

mattpocock commented 3 years ago

Found a machine in my work repo that fails on the branch. I've added it to master - pull it in to see the changes. Copying here as well in case merging is a PITA:

import { Machine, send, assign } from '@xstate/compiled';

type Attendee = {
  name: string;
  email: string;
  id: string;
};

interface Context {
  initialAttendees: Attendee[];
  attendeesToCreate: Attendee[];
  attendeesInList: Attendee[];
  attendeeIdsToDelete: Set<string>;
}

type Event =
  | { type: 'ADD_ATTENDEE'; name: string; email: string }
  | { type: 'EDIT_ATTENDEE'; id: string; name: string; email: string }
  | { type: 'REMOVE_ATTENDEE'; id: string }
  | {
      type: 'GO_BACK';
    }
  | {
      type: 'SUBMIT';
    }
  | {
      type: 'REPORT_ERROR';
    }
  | {
      type: 'done.invoke.createViewing';
      data: string;
    };

const assignAttendee = assign<
  Context,
  Extract<Event, { type: 'ADD_ATTENDEE' }>
>((context, event) => {
  const newAttendee = {
    id: '1',
    email: event.email,
    name: event.name,
  };
  return {
    attendeesToCreate: [...context.attendeesToCreate, newAttendee],
    attendeesInList: [...context.attendeesInList, newAttendee],
  };
});

export const addViewingAttendeesMachine = Machine<
  Context,
  Event,
  'addViewingAttendees'
>({
  id: 'addViewingAttendees',
  context: {
    attendeesToCreate: [],
    attendeeIdsToDelete: new Set(),
    initialAttendees: [],
    attendeesInList: [],
  },
  initial: 'idle',
  states: {
    idle: {
      on: {
        GO_BACK: [
          {
            cond: 'inCreateMode',
            actions: 'goToPrevPage',
          },
          {
            cond: 'inEditMode',
            actions: 'goBackToEditOverview',
          },
        ],
        ADD_ATTENDEE: [
          {
            actions: [assignAttendee],
          },
        ],
        REMOVE_ATTENDEE: {
          actions: [
            assign((context, event) => {
              let attendeeIdsToDelete = new Set(context.attendeeIdsToDelete);
              attendeeIdsToDelete.add(event.id);

              return {
                attendeeIdsToDelete,
                attendeesInList: context.attendeesInList.filter(
                  (attendee) => attendee.id !== event.id,
                ),
                attendeesToCreate: context.attendeesToCreate.filter(
                  (attendee) => attendee.id !== event.id,
                ),
              };
            }),
          ],
        },
        EDIT_ATTENDEE: [
          {
            actions: [
              assign((context, event) => {
                const attendeeWasOnInitialList = context.initialAttendees.some(
                  (attendee) => {
                    return attendee.id === event.id;
                  },
                );

                let attendeeIdsToDelete = new Set(context.attendeeIdsToDelete);
                if (attendeeWasOnInitialList) {
                  attendeeIdsToDelete.add(event.id);
                }

                return {
                  attendeeIdsToDelete,
                  attendeesToCreate: [
                    ...context.attendeesToCreate.filter(
                      (attendee) => attendee.id !== event.id,
                    ),
                    { email: event.email, id: event.id, name: event.name },
                  ],
                  attendeesInList: context.attendeesInList.map((attendee) => {
                    if (attendee.id === event.id) {
                      return {
                        id: event.id,
                        name: event.name,
                        email: event.email,
                      };
                    }
                    return attendee;
                  }),
                };
              }),
            ],
          },
        ],
      },
      initial: 'initial',
      states: {
        initial: {
          on: {
            SUBMIT: [
              {
                cond: 'hasNotAddedAnyInvitees',
                target: 'isWarningThatUserIsNotInvitingAnyone',
              },
              {
                cond: 'currentFormStateIsValidAndInCreateMode',
                target: '#creating',
              },
              {
                cond: 'currentFormStateIsValidAndInUpdateMode',
                target: '#updating',
              },
            ],
          },
        },
        isWarningThatUserIsNotInvitingAnyone: {
          on: {
            ADD_ATTENDEE: {
              actions: [assignAttendee],
              target: 'initial',
            },
            SUBMIT: [
              {
                cond: 'currentFormStateIsValidAndInCreateMode',
                target: '#creating',
              },
              {
                cond: 'currentFormStateIsValidAndInUpdateMode',
                target: '#updating',
              },
            ],
          },
        },
        errored: {},
      },
    },
    creating: {
      id: 'creating',
      invoke: {
        src: 'createViewing',
        onDone: {
          target: 'idle',
          actions: 'goToSuccessPage',
        },
        onError: {
          target: 'idle.errored',
        },
      },
    },
    updating: {
      id: 'updating',
      initial: 'checkingAttendees',
      states: {
        checkingAttendees: {
          always: [
            {
              cond: (context) =>
                Array.from(context.attendeeIdsToDelete).length > 0,
              target: 'deletingExcessGuests',
            },
            {
              cond: (context) => context.attendeesToCreate.length > 0,
              target: 'creatingNewAndEditedGuests',
            },
            {
              target: 'complete',
            },
          ],
        },
        deletingExcessGuests: {
          invoke: {
            src: 'deleteExcessGuests',
            onDone: [
              {
                cond: (context) => context.attendeesToCreate.length > 0,
                target: 'creatingNewAndEditedGuests',
              },
              {
                target: 'complete',
              },
            ],
            onError: 'errored',
          },
        },
        creatingNewAndEditedGuests: {
          invoke: {
            src: 'createNewGuests',
            onDone: [
              {
                target: 'complete',
              },
            ],
            onError: 'errored',
          },
        },
        errored: {
          entry: send({
            type: 'REPORT_ERROR',
          }),
        },
        complete: {
          type: 'final',
          entry: ['goBackToEditOverview', 'showToastWithChangesSaved'],
        },
      },
    },
  },
});
Andarist commented 3 years ago

Ah, i knew this has to happen - was just hoping that a little bit later :P i think i need to bring back the schema-based approach, just need to make the logic more tolerant to matching any state-likes (previously ive tried to be too strict about compound/atomic/etc). Will work on that today/tomorrow - hopefully wont take too long. That approach will allow me to just skip over extracting context which is problematic here

danielkcz commented 3 years ago

@Andarist Do you think you will find time to continue on this? I am not sure if this PR solves it specifically, but missing support for path aliases from tsconfig.json pains me. Or if you can give some pointers on the next steps, I can attempt to finish it by myself.

Andarist commented 3 years ago

@Andarist Do you think you will find time to continue on this?

I have actually got back to working on this already earlier this week.

I am not sure if this PR solves it specifically, but missing support for path aliases from tsconfig.json pains me.

It should because file resolution is delegated to TS and it's tsconfig.json-aware. Unless you would have some very specific setup for which the current logic fails to locate the tsconfig.json correctly - that would be solvable with an additiona tsconfigPath option or smth but I don't expect that this will be required.

Or if you can give some pointers on the next steps, I can attempt to finish it by myself.

We could definitely use some help if you'd like to collaborate on this. I have very limited time to work on all of this (2 kids at home 😉) and there are some things to figure out in relation to IDE, linting etc. We have some additional plans to restructure this tool plus provide VSCode extension - gonna be posting more about this soon but the implementation definitely won't happen over night.

Andarist commented 3 years ago

I've pushed out my recent work on this - it works for the majority of cases, existing tests pass. However, if I fail to extract things the DX ain't great because no helpful errors are printed to the user. It's very important to get this right as without that this is not really that usable.

I've also dropped temporarily support for choose since based on type-info alone I could not access used guards/actions within it. I've experimented with some kinda crazy types that could replace XState's choose and which would enable me to extract the required information at the type-level. Here it is: TS playground

This relies on type inference so explicit type params for TContext and TEvent are not allowed with this. I need to think about the implications of that.

VanTanev commented 3 years ago

Was this closed accidentally?

Andarist commented 3 years ago

@VanTanev no, this branch was created on my fork and I've pushed it out to the origin and created this PR: https://github.com/mattpocock/xstate-codegen/pull/78 , in the hope to fix the problem with snapshot releases that I've tried to add to this. Unfortunately, that didn't fix the problem with those releases 😬