diff --git a/new-packages/ts-project/__tests__/source-edits/mark-transition-as-reentering.test.ts b/new-packages/ts-project/__tests__/source-edits/mark-transition-as-reentering.test.ts new file mode 100644 index 00000000..761fcdaa --- /dev/null +++ b/new-packages/ts-project/__tests__/source-edits/mark-transition-as-reentering.test.ts @@ -0,0 +1,541 @@ +import { expect, test } from 'vitest'; +import { createTestProject, testdir, ts } from '../utils'; + +test(`should be possible to mark a transition as reentering (just target)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: ".a1", + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: true, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: true + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to mark a transition as reentering (object, no reenter property)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: true, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: true, + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to mark a transition as reentering (object, reenter false)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: false, + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: true, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: true, + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to mark a transition as non-reentering (just target)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: ".a1", + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: false, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: false + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to mark a transition as non-reentering (object, no reenter property)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: false, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: false, + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to mark a transition as non-reentering (object, reenter true)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: true, + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: false, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: false, + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to remove reenter property (reenter true)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: true, + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: undefined, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to remove reenter property (reenter false)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: false, + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: undefined, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); + +test(`should be possible to remove reenter property (multiple properties)`, async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + reenter: false, + reenter: true, + }, + }, + states: { + a1: {}, + }, + }, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'mark_transition_as_reentering', + sourcePath: ['a'], + transitionPath: ['on', 'NEXT', 0], + reenter: undefined, + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + states: { + a: { + on: { + NEXT: { + target: ".a1", + }, + }, + states: { + a1: {}, + }, + }, + }, + });", + } + `); +}); diff --git a/new-packages/ts-project/__tests__/utils.ts b/new-packages/ts-project/__tests__/utils.ts index 8bc2d0d0..91ec81ae 100644 --- a/new-packages/ts-project/__tests__/utils.ts +++ b/new-packages/ts-project/__tests__/utils.ts @@ -216,10 +216,10 @@ type MachineEdit = newTransitionPath: TransitionPath; } | { - type: 'mark_transition_as_external'; + type: 'mark_transition_as_reentering'; sourcePath: string[]; transitionPath: TransitionPath; - external: boolean; + reenter?: boolean | undefined; } | { type: 'add_action'; @@ -459,8 +459,19 @@ function produceNewDigraphUsingEdit( case 'remove_transition': case 'reanchor_transition': case 'change_transition_path': - case 'mark_transition_as_external': throw new Error(`Not implemented`); + case 'mark_transition_as_reentering': { + const eventTypeData = getEventTypeData(digraphDraft, edit); + const edge = + digraphDraft.edges[ + getEdgeGroup(digraphDraft, eventTypeData)[ + last(edit.transitionPath) as number + ] + ]; + edge.data.internal = + typeof edit.reenter === 'boolean' ? !edit.reenter : undefined; + break; + } case 'add_action': { const node = findNodeByStatePath(digraphDraft, edit.path); if (edit.actionPath[0] === 'entry' || edit.actionPath[0] === 'exit') { diff --git a/new-packages/ts-project/src/index.ts b/new-packages/ts-project/src/index.ts index e5fe520b..4cd05f59 100644 --- a/new-packages/ts-project/src/index.ts +++ b/new-packages/ts-project/src/index.ts @@ -652,13 +652,10 @@ function createProjectMachine({ ); if (initialProp) { - codeChanges.replaceRange(sourceFile, { - range: { - start: initialProp.initializer.getStart(), - end: initialProp.initializer.getEnd(), - }, - element: c.string(patch.value), - }); + codeChanges.replaceWith( + initialProp.initializer, + c.string(patch.value), + ); break; } @@ -698,13 +695,10 @@ function createProjectMachine({ ); if (typeProp) { - codeChanges.replaceRange(sourceFile, { - range: { - start: typeProp.initializer.getStart(), - end: typeProp.initializer.getEnd(), - }, - element: c.string(patch.value), - }); + codeChanges.replaceWith( + typeProp.initializer, + c.string(patch.value), + ); break; } @@ -729,13 +723,10 @@ function createProjectMachine({ ); if (historyProp) { - codeChanges.replaceRange(sourceFile, { - range: { - start: historyProp.initializer.getStart(), - end: historyProp.initializer.getEnd(), - }, - element: c.string(patch.value), - }); + codeChanges.replaceWith( + historyProp.initializer, + c.string(patch.value), + ); break; } @@ -768,13 +759,10 @@ function createProjectMachine({ }); if (descriptionProp) { - codeChanges.replaceRange(sourceFile, { - range: { - start: descriptionProp.initializer.getStart(), - end: descriptionProp.initializer.getEnd(), - }, + codeChanges.replaceWith( + descriptionProp.initializer, element, - }); + ); break; } @@ -814,6 +802,51 @@ function createProjectMachine({ ); break; } + if (patch.path[2] === 'data' && patch.path[3] === 'internal') { + const internal: unknown = patch.value; + assert( + typeof internal === 'boolean' || internal === undefined, + ); + const reenter = + typeof internal === 'boolean' ? !internal : undefined; + + if (typeof reenter === 'boolean') { + if (!host.ts.isObjectLiteralExpression(transitionNode)) { + codeChanges.wrapIntoObject(transitionNode, { + reuseAs: 'target', + newProperties: [ + c.property('reenter', c.boolean(reenter)), + ], + }); + break; + } + const reenterProp = findProperty( + undefined, + host.ts, + transitionNode, + 'reenter', + ); + if (reenterProp) { + codeChanges.replaceWith( + reenterProp.initializer, + c.boolean(reenter), + ); + break; + } + codeChanges.insertPropertyIntoObject( + transitionNode, + 'reenter', + c.boolean(reenter), + ); + break; + } + + if (host.ts.isObjectLiteralExpression(transitionNode)) { + codeChanges.removeObjectProperty(transitionNode, 'reenter'); + break; + } + break; + } break; } case 'blocks': {