diff --git a/new-packages/ts-project/__tests__/actors.test.ts b/new-packages/ts-project/__tests__/actors.test.ts index f2aa5f40..2c3e1886 100644 --- a/new-packages/ts-project/__tests__/actors.test.ts +++ b/new-packages/ts-project/__tests__/actors.test.ts @@ -75,6 +75,7 @@ test('should extract an actor with string src (direct)', async () => { ] `); }); + test('should extract multiple actors with different string sources (direct)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -208,6 +209,7 @@ test('should extract multiple actors with different string sources (direct)', as ] `); }); + test('should extract multiple actors with the same string source (direct)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -336,6 +338,7 @@ test('should extract multiple actors with the same string source (direct)', asyn ] `); }); + test('should extract multiple actors with string source (array)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -452,6 +455,7 @@ test('should extract multiple actors with string source (array)', async () => { ] `); }); + test('should extract actor with inline source (direct)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -585,6 +589,7 @@ test('should extract actor with inline source (direct)', async () => { ] `); }); + test('should raise error if actor is missing src property', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -663,6 +668,7 @@ test('should raise error if actor is missing src property', async () => { ] `); }); + test('should extract actor id if it is present with a string value', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), diff --git a/new-packages/ts-project/__tests__/guards.test.ts b/new-packages/ts-project/__tests__/guards.test.ts index 244cd694..e8516f6d 100644 --- a/new-packages/ts-project/__tests__/guards.test.ts +++ b/new-packages/ts-project/__tests__/guards.test.ts @@ -134,6 +134,7 @@ test('should extract guard from transition (string)', async () => { ] `); }); + test('should extract guard (inline)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -267,6 +268,7 @@ test('should extract guard (inline)', async () => { ] `); }); + test('should extract parameterized guards', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -402,6 +404,7 @@ test('should extract parameterized guards', async () => { ] `); }); + test('should extract higher order guards as inline', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -535,6 +538,7 @@ test('should extract higher order guards as inline', async () => { ] `); }); + test('should extract multiple guards', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -767,6 +771,7 @@ test('should extract multiple guards', async () => { ] `); }); + test('should support XState v4 guard', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -900,6 +905,7 @@ test('should support XState v4 guard', async () => { ] `); }); + test('should raise error if both guard and cond are provided, pick guard over cond and extract transition (cond before guard)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -1298,6 +1304,7 @@ test('should raise error for parameterized guard is missing type property', asyn ] `); }); + test('should raise error for parameterized guard with invalid type property', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), diff --git a/new-packages/ts-project/__tests__/state.test.ts b/new-packages/ts-project/__tests__/state.test.ts index a6dc582c..f78b0f46 100644 --- a/new-packages/ts-project/__tests__/state.test.ts +++ b/new-packages/ts-project/__tests__/state.test.ts @@ -275,6 +275,7 @@ test('should extract state.initial with string value', async () => { ] `); }); + test('should extract state.initial with template literal value', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -804,6 +805,7 @@ test('should extract state.history with value "shallow"', async () => { ] `); }); + test('should extract state.history with value "deep"', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -1455,6 +1457,7 @@ test("should extract state.meta when it's a javascript object", async () => { ] `); }); + test("should extract state.meta when it's a javascript object containing nested array items", async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -1550,6 +1553,7 @@ test("should extract state.meta when it's a javascript object containing nested ] `); }); + test("should extract state.meta when it's a javascript object and contains multi level object value", async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -1732,7 +1736,7 @@ test('should raise error when state.meta contains any value other than a plain j initial: 'foo' states: { foo: { - meta: 'some string meta' + meta: 'some string meta' } }, }); diff --git a/new-packages/ts-project/__tests__/transitions.test.ts b/new-packages/ts-project/__tests__/transitions.test.ts index 7353d04f..0a371182 100644 --- a/new-packages/ts-project/__tests__/transitions.test.ts +++ b/new-packages/ts-project/__tests__/transitions.test.ts @@ -3061,6 +3061,7 @@ test('should extract transition description (multi-line)', async () => { ] `); }); + test('should extract state.onDone transition (direct string)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -3178,26 +3179,26 @@ test("should extract transition.meta when it's a javascript object", async () => 'index.ts': ts` import { createMachine } from "xstate"; createMachine({ - initial: 'foo' + initial: "foo", states: { foo: { on: { EV: { - target: 'bar', + target: "bar", meta: { - str: 'some string', + str: "some string", num: 123, bool: true, arr: [1, 2, 3], obj: { - foo: 'bar' + foo: "bar", }, - null: null - } - } - } + null: null, + }, + }, + }, }, - bar: {} + bar: {}, }, }); `, @@ -3328,25 +3329,26 @@ test("should extract transition.meta when it's a javascript object", async () => ] `); }); + test("should extract transition.meta when it's a javascript object containing nested array items", async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), 'index.ts': ts` import { createMachine } from "xstate"; createMachine({ - initial: 'foo' + initial: "foo", states: { foo: { on: { EV: { - target: 'bar', + target: "bar", meta: { - arr: ['str', 123, true, [1, 2, 3], { foo: 'bar' }, null] - } - } - } + arr: ["str", 123, true, [1, 2, 3], { foo: "bar" }, null], + }, + }, + }, }, - bar: {} + bar: {}, }, }); `, @@ -3464,31 +3466,32 @@ test("should extract transition.meta when it's a javascript object containing ne ] `); }); + test("should extract transition.meta when it's a javascript object and contains multi level object value", async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), 'index.ts': ts` import { createMachine } from "xstate"; createMachine({ - initial: 'foo' + initial: "foo", states: { foo: { on: { EV: { - target: 'bar', + target: "bar", meta: { obj: { x: { y: { - z: 'some string' - } - } - } - } - } - } + z: "some string", + }, + }, + }, + }, + }, + }, }, - bar: {} + bar: {}, }, }); `, @@ -3606,17 +3609,17 @@ test('should not raise error for transition.meta with undefined value', async () 'index.ts': ts` import { createMachine } from "xstate"; createMachine({ - initial: 'foo' + initial: "foo", states: { foo: { on: { EV: { - target: 'bar', - meta: undefined - } - } + target: "bar", + meta: undefined, + }, + }, }, - bar: {} + bar: {}, }, }); `, @@ -3724,17 +3727,17 @@ test('should raise error when transition.meta contains any value other than a pl 'index.ts': ts` import { createMachine } from "xstate"; createMachine({ - initial: 'foo' + initial: "foo", states: { foo: { on: { EV: { - target: 'bar', - meta: 'some string meta' - } - } + target: "bar", + meta: "some string meta", + }, + }, }, - bar: {} + bar: {}, }, }); `, @@ -3838,6 +3841,7 @@ test('should raise error when transition.meta contains any value other than a pl ] `); }); + test('should extract after transition (number delay)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -3861,95 +3865,97 @@ test('should extract after transition (number delay)', async () => { const project = await createTestProject(tmpPath); expect(replaceUniqueIds(project.extractMachines('index.ts'))) .toMatchInlineSnapshot(` - [ [ - { - "blocks": {}, - "data": { - "context": {}, - }, - "edges": { - "edge-0": { - "data": { - "actions": [], - "description": undefined, - "eventTypeData": { - "delay": "100", - "type": "after", + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "delay": "100", + "type": "after", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], }, - "guard": undefined, - "internal": true, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", }, - "source": "state-1", - "targets": [ - "state-2", - ], - "type": "edge", - "uniqueId": "edge-0", }, - }, - "implementations": { - "actions": {}, - "actors": {}, - "guards": {}, - }, - "nodes": { - "state-0": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": "foo", - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", - }, - "parentId": undefined, - "type": "node", - "uniqueId": "state-0", + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, }, - "state-1": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-1", - }, - "state-2": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-2", }, + "root": "state-0", }, - "root": "state-0", - }, - [], - ], - ] - `); + [], + ], + ] + `); }); + test('should extract delayed transition (identifier delay)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -3973,95 +3979,97 @@ test('should extract delayed transition (identifier delay)', async () => { const project = await createTestProject(tmpPath); expect(replaceUniqueIds(project.extractMachines('index.ts'))) .toMatchInlineSnapshot(` - [ [ - { - "blocks": {}, - "data": { - "context": {}, - }, - "edges": { - "edge-0": { - "data": { - "actions": [], - "description": undefined, - "eventTypeData": { - "delay": "myDelay", - "type": "after", + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "delay": "myDelay", + "type": "after", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], }, - "guard": undefined, - "internal": true, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", }, - "source": "state-1", - "targets": [ - "state-2", - ], - "type": "edge", - "uniqueId": "edge-0", }, - }, - "implementations": { - "actions": {}, - "actors": {}, - "guards": {}, - }, - "nodes": { - "state-0": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": "foo", - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", - }, - "parentId": undefined, - "type": "node", - "uniqueId": "state-0", + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, }, - "state-1": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-1", - }, - "state-2": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-2", }, + "root": "state-0", }, - "root": "state-0", - }, - [], - ], - ] - `); + [], + ], + ] + `); }); + test('should extract after transition (string with whitespace delay)', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -4085,95 +4093,97 @@ test('should extract after transition (string with whitespace delay)', async () const project = await createTestProject(tmpPath); expect(replaceUniqueIds(project.extractMachines('index.ts'))) .toMatchInlineSnapshot(` - [ [ - { - "blocks": {}, - "data": { - "context": {}, - }, - "edges": { - "edge-0": { - "data": { - "actions": [], - "description": undefined, - "eventTypeData": { - "delay": "named delay", - "type": "after", + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "delay": "my delay", + "type": "after", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], }, - "guard": undefined, - "internal": true, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", }, - "source": "state-1", - "targets": [ - "state-2", - ], - "type": "edge", - "uniqueId": "edge-0", }, - }, - "implementations": { - "actions": {}, - "actors": {}, - "guards": {}, - }, - "nodes": { - "state-0": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": "foo", - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", - }, - "parentId": undefined, - "type": "node", - "uniqueId": "state-0", + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, }, - "state-1": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-1", - }, - "state-2": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-2", }, + "root": "state-0", }, - "root": "state-0", - }, - [], - ], - ] - `); + [], + ], + ] + `); }); + test('should extract multiple delayed transitions', async () => { const tmpPath = await testdir({ 'tsconfig.json': JSON.stringify({}), @@ -4199,143 +4209,1915 @@ test('should extract multiple delayed transitions', async () => { const project = await createTestProject(tmpPath); expect(replaceUniqueIds(project.extractMachines('index.ts'))) .toMatchInlineSnapshot(` - [ [ - { - "blocks": { - "block-0": { - "blockType": "guard", - "parentId": "edge-1", - "properties": { - "params": {}, - "type": "condition", + [ + { + "blocks": { + "block-0": { + "blockType": "guard", + "parentId": "edge-1", + "properties": { + "params": {}, + "type": "condition", + }, + "sourceId": "condition", + "uniqueId": "block-0", }, - "sourceId": "condition", - "uniqueId": "block-0", }, - }, - "data": { - "context": {}, - }, - "edges": { - "edge-0": { - "data": { - "actions": [], - "description": undefined, - "eventTypeData": { - "delay": "200", - "type": "after", + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "delay": "200", + "type": "after", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-3", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + "edge-1": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "delay": "100", + "type": "after", + }, + "guard": "block-0", + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-1", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": { + "condition": { + "id": "condition", + "name": "condition", + "type": "guard", + }, + }, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + "state-3": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-3", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a sibling transition as internal by default (direct string)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: "bar", + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a descendant transition as internal by default (direct string)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: ".bar", + }, + states: { + bar: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a sibling transition as internal by default (direct object with target)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { target: "bar" }, + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a descendant transition as internal by default (direct object with target)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { + target: ".bar", + }, + }, + states: { + bar: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract an explicit reentering transition as not internal', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { target: "bar", reenter: true }, + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": false, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract an explicit non-reentering transition as internal', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { target: "bar", reenter: false }, + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a sibling transition as not internal by default (direct string, v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: "bar", + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": false, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a descendant transition as internal by default (direct string, v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: ".bar", + }, + states: { + bar: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a sibling transition as internal by default (direct object with target, v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { target: "bar" }, + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": false, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract a descendant transition as internal by default (direct object with target, v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { + target: ".bar", + }, + }, + states: { + bar: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract an explicit internal transition as internal (v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { target: "bar", internal: true }, + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract an explicit non-internal transition as not internal (v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + FOO: { target: "bar", internal: false }, + }, + }, + bar: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "FOO", + "type": "named", + }, + "guard": undefined, + "internal": false, + "metaEntries": [], }, - "guard": undefined, - "internal": true, + "source": "state-1", + "targets": [ + "state-2", + ], + "type": "edge", + "uniqueId": "edge-0", }, - "source": "state-1", - "targets": [ - "state-3", - ], - "type": "edge", - "uniqueId": "edge-0", }, - "edge-1": { - "data": { - "actions": [], - "description": undefined, - "eventTypeData": { - "delay": "100", - "type": "after", + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "foo", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-2", + }, + }, + "root": "state-0", + }, + [], + ], + ] + `); +}); + +test('should extract transition with multiple targets as internal when any of its targets is defined using a descendant target by default (array of strings, v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + on: { + MULTIPLE: { + target: [".a.a2", "#two"], + }, + }, + type: "parallel", + states: { + a: { + initial: "a1", + states: { + a1: {}, + a2: {}, + }, + }, + b: { + initial: "b1", + states: { + b1: {}, + b2: { + id: "two", + }, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, + }, + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "MULTIPLE", + "type": "named", + }, + "guard": undefined, + "internal": true, + "metaEntries": [], + }, + "source": "state-0", + "targets": [ + "state-3", + "state-6", + ], + "type": "edge", + "uniqueId": "edge-0", + }, + }, + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "parallel", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "a1", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-2", + }, + "state-3": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-3", + }, + "state-4": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "b1", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-4", + }, + "state-5": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-4", + "type": "node", + "uniqueId": "state-5", + }, + "state-6": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", }, - "guard": "block-0", - "internal": true, + "parentId": "state-4", + "type": "node", + "uniqueId": "state-6", }, - "source": "state-1", - "targets": [ - "state-2", - ], - "type": "edge", - "uniqueId": "edge-1", }, + "root": "state-0", }, - "implementations": { - "actions": {}, - "actors": {}, - "guards": { - "condition": { - "id": "condition", - "name": "condition", - "type": "guard", + [], + ], + ] + `); +}); + +test('should extract transition with multiple targets as not internal when none of its targets is defined using a descendant target by default (array of strings, v4)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + on: { + MULTIPLE: { + target: ["#one", "#two"], + }, + }, + type: "parallel", + states: { + a: { + initial: "a1", + states: { + a1: {}, + a2: { + id: "one", }, }, }, - "nodes": { - "state-0": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": "foo", - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + b: { + initial: "b1", + states: { + b1: {}, + b2: { + id: "two", }, - "parentId": undefined, - "type": "node", - "uniqueId": "state-0", }, - "state-1": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", - }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-1", + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath, { xstateVersion: '4' }); + expect(replaceUniqueIds(project.extractMachines('index.ts'))) + .toMatchInlineSnapshot(` + [ + [ + { + "blocks": {}, + "data": { + "context": {}, }, - "state-2": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "edges": { + "edge-0": { + "data": { + "actions": [], + "description": undefined, + "eventTypeData": { + "eventType": "MULTIPLE", + "type": "named", + }, + "guard": undefined, + "internal": false, + "metaEntries": [], + }, + "source": "state-0", + "targets": [ + "state-3", + "state-6", + ], + "type": "edge", + "uniqueId": "edge-0", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-2", }, - "state-3": { - "data": { - "description": undefined, - "entry": [], - "exit": [], - "history": undefined, - "initial": undefined, - "invoke": [], - "metaEntries": [], - "tags": [], - "type": "normal", + "implementations": { + "actions": {}, + "actors": {}, + "guards": {}, + }, + "nodes": { + "state-0": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "parallel", + }, + "parentId": undefined, + "type": "node", + "uniqueId": "state-0", + }, + "state-1": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "a1", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-1", + }, + "state-2": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-2", + }, + "state-3": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-1", + "type": "node", + "uniqueId": "state-3", + }, + "state-4": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": "b1", + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-0", + "type": "node", + "uniqueId": "state-4", + }, + "state-5": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-4", + "type": "node", + "uniqueId": "state-5", + }, + "state-6": { + "data": { + "description": undefined, + "entry": [], + "exit": [], + "history": undefined, + "initial": undefined, + "invoke": [], + "metaEntries": [], + "tags": [], + "type": "normal", + }, + "parentId": "state-4", + "type": "node", + "uniqueId": "state-6", }, - "parentId": "state-0", - "type": "node", - "uniqueId": "state-3", }, + "root": "state-0", }, - "root": "state-0", - }, - [], - ], - ] - `); + [], + ], + ] + `); }); diff --git a/new-packages/ts-project/__tests__/utils.ts b/new-packages/ts-project/__tests__/utils.ts index 74de880a..9f7bf07c 100644 --- a/new-packages/ts-project/__tests__/utils.ts +++ b/new-packages/ts-project/__tests__/utils.ts @@ -5,7 +5,7 @@ import path from 'path'; import { onExit } from 'signal-exit'; import { temporaryDirectory } from 'tempy'; import typescript from 'typescript'; -import { XStateProject, createProject } from '../src/index'; +import { TSProjectOptions, XStateProject, createProject } from '../src/index'; import { ActorBlock } from '../src/types'; export const js = outdent; @@ -105,13 +105,17 @@ async function createTestProgram( export async function createTestProject( cwd: string, - { ts = typescript, ...options }: Partial<TypeScriptTestProgramOptions> = {}, + { + ts = typescript, + xstateVersion: version, + ...options + }: Partial<TypeScriptTestProgramOptions & TSProjectOptions> = {}, ) { const program = await createTestProgram(cwd, { ts: typescript, ...options, }); - return createProject(ts, program); + return createProject(ts, program, { xstateVersion: version }); } function replaceUniqueIdsRecursively( diff --git a/new-packages/ts-project/src/index.ts b/new-packages/ts-project/src/index.ts index d527a79e..67b826df 100644 --- a/new-packages/ts-project/src/index.ts +++ b/new-packages/ts-project/src/index.ts @@ -5,6 +5,7 @@ import { ExtractionError, ExtractorDigraphDef, TreeNode, + XStateVersion, } from './types'; function findCreateMachineCalls( @@ -104,31 +105,10 @@ function resolveTargets(ctx: ExtractionContext) { } function extractMachineConfig( + ctx: ExtractionContext, ts: typeof import('typescript'), createMachineCall: CallExpression, - sourceFile: SourceFile, ): readonly [ExtractorDigraphDef | undefined, ExtractionError[]] { - const ctx: ExtractionContext = { - sourceFile, - errors: [], - digraph: { - nodes: {}, - edges: {}, - blocks: {}, - implementations: { - actions: {}, - actors: {}, - guards: {}, - }, - data: { - context: {}, - }, - }, - treeNodes: {}, - idMap: {}, - originalTargets: {}, - }; - const rootState = createMachineCall.arguments[0]; const rootNode = extractState(ctx, ts, rootState, undefined); @@ -151,9 +131,14 @@ function extractMachineConfig( ]; } +export interface TSProjectOptions { + xstateVersion?: XStateVersion | undefined; +} + export function createProject( ts: typeof import('typescript'), tsProgram: Program, + { xstateVersion = '5' }: TSProjectOptions = {}, ) { return { extractMachines(fileName: string) { @@ -162,7 +147,28 @@ export function createProject( return []; } return findCreateMachineCalls(ts, sourceFile).map((call) => { - return extractMachineConfig(ts, call, sourceFile); + const ctx: ExtractionContext = { + sourceFile, + xstateVersion, + errors: [], + digraph: { + nodes: {}, + edges: {}, + blocks: {}, + implementations: { + actions: {}, + actors: {}, + guards: {}, + }, + data: { + context: {}, + }, + }, + treeNodes: {}, + idMap: {}, + originalTargets: {}, + }; + return extractMachineConfig(ctx, ts, call); }); }, }; diff --git a/new-packages/ts-project/src/state.ts b/new-packages/ts-project/src/state.ts index d98a9cfc..92465a62 100644 --- a/new-packages/ts-project/src/state.ts +++ b/new-packages/ts-project/src/state.ts @@ -89,9 +89,11 @@ const createGuardBlock = ({ function createEdge({ sourceId, eventTypeData, + internal = true, }: { sourceId: string; eventTypeData: Edge['data']['eventTypeData']; + internal?: boolean; }): Edge { return { type: 'edge', @@ -104,8 +106,7 @@ function createEdge({ guard: undefined, description: undefined, metaEntries: [], - // TODO: to compute this correctly we need to know if we are extracting v4 or v5 - internal: true, + internal, }, }; } @@ -221,6 +222,19 @@ function registerActionBlocks( } } +/** + * returns the auto-assigned internal value when no explicit configuration for this property is found + */ +function getImpliedInternalValue( + ctx: ExtractionContext, + targets: string[] | undefined, +) { + if (ctx.xstateVersion !== '4') { + return true; + } + return targets ? targets.some((t) => t.startsWith('.')) : true; +} + function extractEdgeGroup( ctx: ExtractionContext, ts: typeof import('typescript'), @@ -251,6 +265,7 @@ function extractEdgeGroup( createEdge({ sourceId, eventTypeData, + internal: getImpliedInternalValue(ctx, [element.text]), }), [element.text], ]; @@ -269,6 +284,7 @@ function extractEdgeGroup( }); let seenGuardProp = false; + let seenInternalProp = false; forEachStaticProperty(ctx, ts, element, (prop, key) => { switch (key) { @@ -348,6 +364,49 @@ function extractEdgeGroup( registerActionBlocks(ctx, blocks, edge.data.actions); return; } + case 'internal': { + if (seenInternalProp) { + // `reenter` was already seen + ctx.errors.push({ + type: 'property_mixed', + }); + return; + } + seenInternalProp = true; + + if (findProperty(ctx, ts, element, 'reenter')) { + return; + } + + if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) { + edge.data.internal = true; + return; + } + if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) { + edge.data.internal = false; + return; + } + + return; + } + case 'reenter': + if (seenInternalProp) { + ctx.errors.push({ + type: 'property_mixed', + }); + } + seenInternalProp = true; + + if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) { + edge.data.internal = false; + return; + } + if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) { + edge.data.internal = true; + return; + } + + return; case 'description': { edge.data.description = ts.isStringLiteralLike(prop.initializer) ? prop.initializer.text @@ -361,6 +420,10 @@ function extractEdgeGroup( } }); + if (!seenInternalProp) { + edge.data.internal = getImpliedInternalValue(ctx, targets); + } + return [edge, targets]; } }, diff --git a/new-packages/ts-project/src/types.ts b/new-packages/ts-project/src/types.ts index 3b4cdabe..0dc7fdb3 100644 --- a/new-packages/ts-project/src/types.ts +++ b/new-packages/ts-project/src/types.ts @@ -1,5 +1,9 @@ import type { SourceFile } from 'typescript'; +// it's acting as a threshold - atm there is no need to know the exact version +// strings are used to allow for future minor versions +export type XStateVersion = '4' | '5'; + export interface TreeNode { uniqueId: string; parentId: string | undefined; @@ -8,6 +12,7 @@ export interface TreeNode { export interface ExtractionContext { sourceFile: SourceFile; + xstateVersion: XStateVersion; errors: ExtractionError[]; digraph: Pick< ExtractorDigraphDef,