diff --git a/.vscode/settings.json b/.vscode/settings.json index 953fde0f..26d30a5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,5 @@ { "editor.insertSpaces": false, "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": true - } -} + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/package.json b/package.json index d50a4722..81d9c900 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "extractor": "yarn workspace @xstate/machine-extractor", "server": "yarn workspace @xstate/vscode-server", "client": "yarn workspace stately-vscode", + "🍿": "yarn workspace @xstate/recast-experiment", "shared": "yarn workspace @xstate/tools-shared", "prepare": "husky install", "postinstall": "preconstruct dev && manypkg check", diff --git a/packages/machine-extractor/src/actions.ts b/packages/machine-extractor/src/actions.ts index 9a259f33..33a7124a 100644 --- a/packages/machine-extractor/src/actions.ts +++ b/packages/machine-extractor/src/actions.ts @@ -1,4 +1,4 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { Action, ChooseCondition } from "xstate"; import { assign, choose, forwardTo, send } from "xstate/lib/actions"; import { Cond, CondNode } from "./conds"; @@ -33,6 +33,7 @@ import { wrapParserResult } from "./wrapParserResult"; export interface ActionNode { node: t.Node; + path: NodePath; action: Action; name: string; chooseConditions?: ParsedChooseCondition[]; @@ -49,63 +50,67 @@ export interface ParsedChooseCondition { export const ActionAsIdentifier = maybeTsAsExpression( createParser({ babelMatcher: t.isIdentifier, - parseNode: (node, context): ActionNode => { + parsePath: (path, context): ActionNode => { return { - action: node.name, - node, - name: node.name, + action: path.node.name, + node: path.node, + path, + name: path.node.name, declarationType: "identifier", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; }, - }), + }) ); export const ActionAsFunctionExpression = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: isFunctionOrArrowFunctionExpression, - parseNode: (node, context): ActionNode => { + parsePath: (path, context): ActionNode => { const action = function actions() {}; - const id = context.getNodeHash(node); + const id = context.getNodeHash(path.node); action.toJSON = () => id; return { - node, + node: path.node, + path, action, name: "", declarationType: "inline", inlineDeclarationId: id, }; }, - }), - ), + }) + ) ); export const ActionAsString = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: t.isStringLiteral, - parseNode: (node, context): ActionNode => { + parsePath: (path, context): ActionNode => { return { - action: node.value, - node, - name: node.value, + path, + action: path.node.value, + node: path.node, + name: path.node.value, declarationType: "named", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; }, - }), - ), + }) + ) ); export const ActionAsNode = createParser({ babelMatcher: t.isNode, - parseNode: (node, context): ActionNode => { - const id = context.getNodeHash(node); + parsePath: (path, context): ActionNode => { + const id = context.getNodeHash(path.node); return { + path, action: id, - node, + node: path.node, name: "", declarationType: "unknown", inlineDeclarationId: id, @@ -120,12 +125,12 @@ const ChooseFirstArg = arrayOf( // too recursive // TODO - fix actions: maybeArrayOf(ActionAsString), - }), + }) ); export const ChooseAction = wrapParserResult( namedFunctionCall("choose", ChooseFirstArg), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { const conditions: ParsedChooseCondition[] = []; result.argument1Result?.forEach((arg1Result) => { @@ -153,26 +158,29 @@ export const ChooseAction = wrapParserResult( }); return { - node: node, + path, + node: path.node, action: choose(conditions.map((condition) => condition.condition)), chooseConditions: conditions, name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; - }, + } ); interface AssignFirstArg { + path: NodePath; node: t.Node; value: {} | (() => {}); } const AssignFirstArgObject = createParser({ babelMatcher: t.isObjectExpression, - parseNode: (node, context) => { + parsePath: (path, context) => { return { - node, + path, + node: path.node, value: {}, }; }, @@ -180,7 +188,7 @@ const AssignFirstArgObject = createParser({ const AssignFirstArgFunction = createParser({ babelMatcher: isFunctionOrArrowFunctionExpression, - parseNode: (node, context) => { + parsePath: (path, context) => { const value = function anonymous() { return {}; }; @@ -189,7 +197,8 @@ const AssignFirstArgFunction = createParser({ }; return { - node, + path, + node: path.node, value, }; }, @@ -202,7 +211,7 @@ const AssignFirstArg = unionType([ export const AssignAction = wrapParserResult( namedFunctionCall("assign", AssignFirstArg), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { const defaultAction = function anonymous() { return {}; }; @@ -211,13 +220,14 @@ export const AssignAction = wrapParserResult( }; return { + path, node: result.node, action: assign(result.argument1Result?.value || defaultAction), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; - }, + } ); export const SendActionSecondArg = objectTypeWithKnownKeys({ @@ -233,10 +243,11 @@ export const SendAction = wrapParserResult( namedFunctionCall( "send", unionType<{ node: t.Node; value?: string }>([StringLiteral, AnyNode]), - SendActionSecondArg, + SendActionSecondArg ), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, name: "", action: send( @@ -250,12 +261,12 @@ export const SendAction = wrapParserResult( id: result.argument2Result?.id?.value, to: result.argument2Result?.to?.value, delay: result.argument2Result?.delay?.value, - }, + } ), declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; - }, + } ); export const ForwardToActionSecondArg = objectTypeWithKnownKeys({ @@ -264,17 +275,18 @@ export const ForwardToActionSecondArg = objectTypeWithKnownKeys({ export const ForwardToAction = wrapParserResult( namedFunctionCall("forwardTo", StringLiteral, ForwardToActionSecondArg), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { node: result.node, action: forwardTo(result.argument1Result?.value || "", { to: result.argument2Result?.to?.value, }), + path, name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; - }, + } ); const NamedAction = unionType([ @@ -306,5 +318,5 @@ const BasicAction = unionType([ export const ArrayOfBasicActions = maybeArrayOf(BasicAction); export const MaybeArrayOfActions = maybeArrayOf( - unionType([NamedAction, BasicAction]), + unionType([NamedAction, BasicAction]) ); diff --git a/packages/machine-extractor/src/conds.ts b/packages/machine-extractor/src/conds.ts index f318fbfb..250353fc 100644 --- a/packages/machine-extractor/src/conds.ts +++ b/packages/machine-extractor/src/conds.ts @@ -1,4 +1,4 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { Condition } from "xstate"; import { DeclarationType } from "."; import { createParser } from "./createParser"; @@ -7,6 +7,7 @@ import { isFunctionOrArrowFunctionExpression } from "./utils"; export interface CondNode { node: t.Node; + path: NodePath; name: string; cond: Condition; declarationType: DeclarationType; @@ -15,38 +16,41 @@ export interface CondNode { const CondAsFunctionExpression = createParser({ babelMatcher: isFunctionOrArrowFunctionExpression, - parseNode: (node, context): CondNode => { + parsePath: (path, context): CondNode => { return { - node, + path, + node: path.node, name: "", cond: () => { return false; }, declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; }, }); const CondAsStringLiteral = createParser({ babelMatcher: t.isStringLiteral, - parseNode: (node, context): CondNode => { + parsePath: (path, context): CondNode => { return { - node, - name: node.value, - cond: node.value, + path, + node: path.node, + name: path.node.value, + cond: path.node.value, declarationType: "named", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; }, }); const CondAsNode = createParser({ babelMatcher: t.isNode, - parseNode: (node, context): CondNode => { - const id = context.getNodeHash(node); + parsePath: (path, context): CondNode => { + const id = context.getNodeHash(path.node); return { - node, + path, + node: path.node, name: "", cond: id, declarationType: "unknown", @@ -57,10 +61,11 @@ const CondAsNode = createParser({ const CondAsIdentifier = createParser({ babelMatcher: t.isIdentifier, - parseNode: (node, context): CondNode => { - const id = context.getNodeHash(node); + parsePath: (path, context): CondNode => { + const id = context.getNodeHash(path.node); return { - node, + path, + node: path.node, name: "", cond: id, declarationType: "identifier", diff --git a/packages/machine-extractor/src/createParser.ts b/packages/machine-extractor/src/createParser.ts index e0c72667..a6444981 100644 --- a/packages/machine-extractor/src/createParser.ts +++ b/packages/machine-extractor/src/createParser.ts @@ -1,4 +1,4 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { Parser, ParserContext } from "./types"; /** @@ -7,14 +7,14 @@ import { Parser, ParserContext } from "./types"; */ export const createParser = (params: { babelMatcher: (node: any) => node is T; - parseNode: (node: T, context: ParserContext) => Result; + parsePath: (path: NodePath, context: ParserContext) => Result; }): Parser => { - const matches = (node: T) => { + const matches = (node: t.Node) => { return params.babelMatcher(node); }; - const parse = (node: any, context: ParserContext): Result | undefined => { - if (!matches(node)) return undefined; - return params.parseNode(node, context); + const parse = (path: any, context: ParserContext): Result | undefined => { + if (!matches(path?.node)) return undefined; + return params.parsePath(path, context); }; return { parse, diff --git a/packages/machine-extractor/src/history.ts b/packages/machine-extractor/src/history.ts index 0ca5d920..e9bfbb39 100644 --- a/packages/machine-extractor/src/history.ts +++ b/packages/machine-extractor/src/history.ts @@ -1,28 +1,31 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { createParser } from "./createParser"; import { unionType } from "./unionType"; interface HistoryNode { node: t.Node; + path: NodePath; value: "shallow" | "deep" | boolean; } const HistoryAsString = createParser({ babelMatcher: t.isStringLiteral, - parseNode: (node): HistoryNode => { + parsePath: (path): HistoryNode => { return { - node, - value: node.value as HistoryNode["value"], + node: path.node, + path, + value: path.node.value as HistoryNode["value"], }; }, }); const HistoryAsBoolean = createParser({ babelMatcher: t.isBooleanLiteral, - parseNode: (node): HistoryNode => { + parsePath: (path): HistoryNode => { return { - node, - value: node.value, + node: path.node, + path, + value: path.node.value, }; }, }); diff --git a/packages/machine-extractor/src/identifiers.ts b/packages/machine-extractor/src/identifiers.ts index 3f67d6df..ab46088f 100644 --- a/packages/machine-extractor/src/identifiers.ts +++ b/packages/machine-extractor/src/identifiers.ts @@ -1,6 +1,6 @@ -import { types as t, traverse } from "@babel/core"; +import { NodePath, traverse, types as t } from "@babel/core"; import { createParser } from "./createParser"; -import { AnyParser } from "./types"; +import { AnyParser, Parser } from "./types"; import { unionType } from "./unionType"; import { getPropertiesOfObjectExpression, @@ -14,14 +14,14 @@ import { wrapParserResult } from "./wrapParserResult"; */ export const findVariableDeclaratorWithName = ( file: any, - name: string, -): t.VariableDeclarator | null | undefined => { - let declarator: t.VariableDeclarator | null | undefined = null; + name: string +): NodePath | null | undefined => { + let declarator: NodePath | null | undefined = null; traverse(file, { VariableDeclarator(path) { if (t.isIdentifier(path.node.id) && path.node.id.name === name) { - declarator = path.node as any; + declarator = path as any; } }, }); @@ -34,17 +34,17 @@ export const findVariableDeclaratorWithName = ( * which references a variable declaration of a certain type */ export const identifierReferencingVariableDeclaration = ( - parser: AnyParser, + parser: AnyParser ) => { return createParser({ babelMatcher: t.isIdentifier, - parseNode: (node, context) => { + parsePath: (path, context) => { const variableDeclarator = findVariableDeclaratorWithName( context.file, - node.name, + path.node.name ); - return parser.parse(variableDeclarator?.init, context); + return parser.parse(variableDeclarator?.get("init"), context); }, }); }; @@ -55,14 +55,14 @@ export const identifierReferencingVariableDeclaration = ( */ export const findTSEnumDeclarationWithName = ( file: any, - name: string, -): t.TSEnumDeclaration | null | undefined => { - let declarator: t.TSEnumDeclaration | null | undefined = null; + name: string +): NodePath | null | undefined => { + let declarator: NodePath | null | undefined = null; traverse(file, { TSEnumDeclaration(path) { if (t.isIdentifier(path.node.id) && path.node.id.name === name) { - declarator = path.node as any; + declarator = path as any; } }, }); @@ -73,10 +73,11 @@ export const findTSEnumDeclarationWithName = ( interface DeepMemberExpression { child?: DeepMemberExpression; node: t.MemberExpression | t.Identifier; + path: NodePath; } const deepMemberExpressionToPath = ( - memberExpression: DeepMemberExpression, + memberExpression: DeepMemberExpression ): string[] => { let currentLevel: DeepMemberExpression | undefined = memberExpression; const path: string[] = []; @@ -96,145 +97,148 @@ const deepMemberExpressionToPath = ( return path.reverse(); }; -const deepMemberExpression = createParser({ +const deepMemberExpression: Parser< + t.MemberExpression | t.Identifier, + DeepMemberExpression +> = createParser({ babelMatcher(node): node is t.MemberExpression | t.Identifier { return t.isIdentifier(node) || t.isMemberExpression(node); }, - parseNode: ( - node: t.MemberExpression | t.Identifier, - context, - ): DeepMemberExpression => { + parsePath: (path, context) => { + const child = path.get("object") as unknown as NodePath< + t.MemberExpression | t.Identifier + >; return { - node, - child: - "object" in node - ? deepMemberExpression.parse(node.object, context) - : undefined, + path, + node: path.node, + child: child ? deepMemberExpression.parse(child, context) : undefined, }; }, }); export const objectExpressionWithDeepPath = ( - path: string[], - parser: AnyParser, + objPath: string[], + parser: AnyParser ) => createParser({ babelMatcher: t.isObjectExpression, - parseNode: (node, context) => { + parsePath: (path, context) => { let currentIndex = 0; - let currentNode: t.Node | undefined = node; + let currentPath: NodePath | undefined | null = path; - while (path[currentIndex]) { - const pathSection = path[currentIndex]; + while (objPath[currentIndex]) { + const pathSection = objPath[currentIndex]; const objectProperties = getPropertiesOfObjectExpression( - currentNode as any, - context, + currentPath, + context ); - currentNode = ( + currentPath = ( objectProperties.find( (property) => - property.key === pathSection && t.isObjectProperty(property.node), - )?.node as t.ObjectProperty - )?.value; + property.key === pathSection && t.isObjectProperty(property.node) + )?.path as NodePath + ).get("value") as any; currentIndex++; } - return parser.parse(currentNode, context); + return parser.parse(currentPath, context); }, }); const getRootIdentifierOfDeepMemberExpression = ( - deepMemberExpression: DeepMemberExpression | undefined, -): t.Identifier | undefined => { + deepMemberExpression: DeepMemberExpression | undefined +): NodePath | undefined => { if (!deepMemberExpressionToPath) return undefined; - if (t.isIdentifier(deepMemberExpression?.node)) { - return deepMemberExpression?.node; + if (t.isIdentifier(deepMemberExpression?.path.node)) { + return deepMemberExpression?.path as NodePath; } return getRootIdentifierOfDeepMemberExpression(deepMemberExpression?.child); }; export const memberExpressionReferencingObjectExpression = ( - parser: AnyParser, + parser: AnyParser ) => createParser({ babelMatcher: t.isMemberExpression, - parseNode: (node, context) => { - const result = deepMemberExpression.parse(node, context); + parsePath: (path, context) => { + const result = deepMemberExpression.parse(path, context); const rootIdentifier = getRootIdentifierOfDeepMemberExpression(result); if (!result) return undefined; - const path = deepMemberExpressionToPath(result); + const memberPath = deepMemberExpressionToPath(result); return identifierReferencingVariableDeclaration( - objectExpressionWithDeepPath(path.slice(1), parser), + objectExpressionWithDeepPath(memberPath.slice(1), parser) ).parse(rootIdentifier, context); }, }); export const memberExpressionReferencingEnumMember = createParser({ babelMatcher: t.isMemberExpression, - parseNode: (node, context) => { - const result = deepMemberExpression.parse(node, context); + parsePath: (path, context) => { + const result = deepMemberExpression.parse(path, context); const rootIdentifier = getRootIdentifierOfDeepMemberExpression(result); if (!result) return undefined; - const path = deepMemberExpressionToPath(result); + const stringPath = deepMemberExpressionToPath(result); const foundEnum = findTSEnumDeclarationWithName( context.file, - rootIdentifier?.name!, + rootIdentifier?.node.name! ); if (!foundEnum) return undefined; - const targetEnumMember = path[1]; + const targetEnumMember = stringPath[1]; const valueParser = unionType([ wrapParserResult( parserFromBabelMatcher(t.isStringLiteral), - (node) => node.value, + (path) => path.node.value ), wrapParserResult( parserFromBabelMatcher(t.isIdentifier), - (node) => node.name, + (path) => path.node.name ), ]); - const memberIndex = foundEnum.members.findIndex((member) => { - const value = valueParser.parse(member.id, context); + const memberIndex = foundEnum.get("members").findIndex((member) => { + const value = valueParser.parse(member.get("id"), context); return value === targetEnumMember; }); - const member = foundEnum.members[memberIndex]; + const memberPath = foundEnum.get("members")[memberIndex]; - if (!member) { + if (!memberPath) { return undefined; } - if (member?.initializer) { + if (memberPath.get("initializer")?.node) { return { - node: member, + path: memberPath, + node: memberPath.node, value: unionType([ wrapParserResult( parserFromBabelMatcher(t.isStringLiteral), - (node) => node.value, + (path) => path.node.value ), - wrapParserResult(parserFromBabelMatcher(t.isNumericLiteral), (node) => - String(node.value), + wrapParserResult(parserFromBabelMatcher(t.isNumericLiteral), (path) => + String(path.node.value) ), - ]).parse(member.initializer, context) as string, + ]).parse(memberPath.get("initializer"), context) as string, }; } else { return { - node: member, + path: memberPath, + node: memberPath.node, value: String(memberIndex), }; } diff --git a/packages/machine-extractor/src/invoke.ts b/packages/machine-extractor/src/invoke.ts index b6c0cd1b..f08f39d1 100644 --- a/packages/machine-extractor/src/invoke.ts +++ b/packages/machine-extractor/src/invoke.ts @@ -1,5 +1,5 @@ -import { types as t } from "@babel/core"; -import { DeclarationType } from "."; +import { NodePath, types as t } from "@babel/core"; +import { DeclarationType, GetParserResult } from "."; import { createParser } from "./createParser"; import { maybeIdentifierTo } from "./identifiers"; import { BooleanLiteral, StringLiteral } from "./scalars"; @@ -12,7 +12,8 @@ import { objectTypeWithKnownKeys, } from "./utils"; -interface InvokeNode { +export interface InvokeNode { + path: NodePath; node: t.Node; value: string; declarationType: DeclarationType; @@ -23,27 +24,29 @@ const InvokeSrcFunctionExpression = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: isFunctionOrArrowFunctionExpression, - parseNode: (node, context): InvokeNode => { - const id = context.getNodeHash(node); + parsePath: (path, context): InvokeNode => { + const id = context.getNodeHash(path.node); return { value: id, - node, + path, + node: path.node, declarationType: "inline", inlineDeclarationId: id, }; }, - }), - ), + }) + ) ); const InvokeSrcNode = createParser({ babelMatcher: t.isNode, - parseNode: (node, context): InvokeNode => { - const id = context.getNodeHash(node); + parsePath: (path, context): InvokeNode => { + const id = context.getNodeHash(path.node); return { value: id, - node, + path, + node: path.node, declarationType: "unknown", inlineDeclarationId: id, }; @@ -52,23 +55,25 @@ const InvokeSrcNode = createParser({ const InvokeSrcStringLiteral = createParser({ babelMatcher: t.isStringLiteral, - parseNode: (node, context): InvokeNode => ({ - value: node.value, - node, + parsePath: (path, context): InvokeNode => ({ + value: path.node.value, + path, + node: path.node, declarationType: "named", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }), }); const InvokeSrcIdentifier = createParser({ babelMatcher: t.isIdentifier, - parseNode: (node, context): InvokeNode => { - const id = context.getNodeHash(node); + parsePath: (path, context): InvokeNode => { + const id = context.getNodeHash(path.node); return { value: id, - node, + path, + node: path.node, declarationType: "identifier", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(path.node), }; }, }); @@ -89,4 +94,6 @@ const InvokeConfigObject = objectTypeWithKnownKeys({ forward: BooleanLiteral, }); +export type InvokeConfigObjectType = GetParserResult; + export const Invoke = maybeArrayOf(InvokeConfigObject); diff --git a/packages/machine-extractor/src/machineCallExpression.ts b/packages/machine-extractor/src/machineCallExpression.ts index 1a99976c..e774ddbb 100644 --- a/packages/machine-extractor/src/machineCallExpression.ts +++ b/packages/machine-extractor/src/machineCallExpression.ts @@ -1,9 +1,9 @@ import { types as t } from "@babel/core"; -import { StateNode } from "./stateNode"; -import { GetParserResult } from "./utils"; -import { MachineOptions } from "./options"; import { createParser } from "./createParser"; +import { MachineOptions } from "./options"; +import { StateNode } from "./stateNode"; import { AnyTypeParameterList } from "./typeParameters"; +import { GetParserResult } from "./utils"; export type TMachineCallExpression = GetParserResult< typeof MachineCallExpression @@ -17,7 +17,8 @@ export const ALLOWED_CALL_EXPRESSION_NAMES = [ export const MachineCallExpression = createParser({ babelMatcher: t.isCallExpression, - parseNode: (node, context) => { + parsePath: (path, context) => { + const node = path.node; if ( t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property) && @@ -26,11 +27,15 @@ export const MachineCallExpression = createParser({ return { callee: node.callee, calleeName: node.callee.property.name, - definition: StateNode.parse(node.arguments[0], context), - options: MachineOptions.parse(node.arguments[1], context), + definition: StateNode.parse(path.get("arguments")[0], context), + options: MachineOptions.parse(path.get("arguments")[1], context), isMemberExpression: true, - typeArguments: AnyTypeParameterList.parse(node.typeParameters, context), + typeArguments: AnyTypeParameterList.parse( + path.get("typeParameters"), + context + ), node, + path, }; } @@ -41,11 +46,15 @@ export const MachineCallExpression = createParser({ return { callee: node.callee, calleeName: node.callee.name, - definition: StateNode.parse(node.arguments[0], context), - options: MachineOptions.parse(node.arguments[1], context), + definition: StateNode.parse(path.get("arguments")[0], context), + options: MachineOptions.parse(path.get("arguments")[1], context), isMemberExpression: false, - typeArguments: AnyTypeParameterList.parse(node.typeParameters, context), + typeArguments: AnyTypeParameterList.parse( + path.get("typeParameters"), + context + ), node, + path, }; } }, diff --git a/packages/machine-extractor/src/namedActions.ts b/packages/machine-extractor/src/namedActions.ts index 967acca7..38fd4eb6 100644 --- a/packages/machine-extractor/src/namedActions.ts +++ b/packages/machine-extractor/src/namedActions.ts @@ -1,3 +1,4 @@ +import { types as t } from "@babel/core"; import { after, cancel, @@ -14,9 +15,8 @@ import { } from "xstate/lib/actions"; import type { ActionNode } from "./actions"; import { AnyNode, NumericLiteral, StringLiteral } from "./scalars"; -import { namedFunctionCall } from "./utils"; -import { types as t } from "@babel/core"; import { unionType } from "./unionType"; +import { namedFunctionCall } from "./utils"; import { wrapParserResult } from "./wrapParserResult"; export const AfterAction = wrapParserResult( @@ -25,158 +25,170 @@ export const AfterAction = wrapParserResult( unionType<{ node: t.Node; value: number | string }>([ StringLiteral, NumericLiteral, - ]), + ]) ), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: after(result.argument1Result?.value || ""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const CancelAction = wrapParserResult( namedFunctionCall("cancel", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: cancel(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const DoneAction = wrapParserResult( namedFunctionCall("done", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: done(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const EscalateAction = wrapParserResult( namedFunctionCall("escalate", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: escalate(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const LogAction = wrapParserResult( namedFunctionCall("log", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: log(), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const PureAction = wrapParserResult( namedFunctionCall("pure", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: pure(() => []), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const RaiseAction = wrapParserResult( namedFunctionCall("raise", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: raise(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const RespondAction = wrapParserResult( namedFunctionCall("respond", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: respond(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const SendParentAction = wrapParserResult( namedFunctionCall("sendParent", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: sendParent(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const SendUpdateAction = wrapParserResult( namedFunctionCall("sendUpdate", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: sendUpdate(), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const StartAction = wrapParserResult( namedFunctionCall("start", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: start(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); export const StopAction = wrapParserResult( namedFunctionCall("stop", AnyNode), - (result, node, context): ActionNode => { + (result, path, context): ActionNode => { return { + path, node: result.node, action: stop(""), name: "", declarationType: "inline", - inlineDeclarationId: context.getNodeHash(node), + inlineDeclarationId: context.getNodeHash(result.node), }; - }, + } ); diff --git a/packages/machine-extractor/src/parseMachinesFromFile.ts b/packages/machine-extractor/src/parseMachinesFromFile.ts index e1531d66..2b99cc4c 100644 --- a/packages/machine-extractor/src/parseMachinesFromFile.ts +++ b/packages/machine-extractor/src/parseMachinesFromFile.ts @@ -31,13 +31,17 @@ export const parseMachinesFromFile = (fileContents: string): ParseResult => { }, }) as t.File; + return traverseParseResult(parseResult, fileContents); +}; + +export const traverseParseResult = (file: t.File, fileContents: string) => { let result: ParseResult = { machines: [], comments: [], - file: parseResult, + file: file, }; - parseResult.comments?.forEach((comment) => { + file.comments?.forEach((comment) => { if (comment.value.includes("xstate-ignore-next-line")) { result.comments.push({ node: comment, @@ -56,10 +60,10 @@ export const parseMachinesFromFile = (fileContents: string): ParseResult => { return hashedId(fileText); }; - traverse(parseResult as any, { + traverse(file as any, { CallExpression(path) { - const ast = MachineCallExpression.parse(path.node as any, { - file: parseResult, + const ast = MachineCallExpression.parse(path, { + file: file, getNodeHash: getNodeHash, }); if (ast) { diff --git a/packages/machine-extractor/src/scalars.ts b/packages/machine-extractor/src/scalars.ts index 4e01ceec..f3ec268a 100644 --- a/packages/machine-extractor/src/scalars.ts +++ b/packages/machine-extractor/src/scalars.ts @@ -4,30 +4,32 @@ import { maybeIdentifierTo, memberExpressionReferencingEnumMember, } from "./identifiers"; -import { StringLiteralNode } from "./types"; import { maybeTsAsExpression } from "./tsAsExpression"; +import { StringLiteralNode } from "./types"; import { unionType } from "./unionType"; import { wrapParserResult } from "./wrapParserResult"; export const StringLiteral = unionType([ - wrapParserResult(memberExpressionReferencingEnumMember, (node) => { + wrapParserResult(memberExpressionReferencingEnumMember, ({ path, value }) => { return { - node: node.node as t.Node, - value: node.value, + path, + node: path.node as t.Node, + value: value, }; }), maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: t.isStringLiteral, - parseNode: (node): StringLiteralNode => { + parsePath: (path): StringLiteralNode => { return { - value: node.value, - node, + value: path.node.value, + node: path.node, + path, }; }, - }), - ), + }) + ) ), ]); @@ -35,56 +37,59 @@ export const NumericLiteral = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: t.isNumericLiteral, - parseNode: (node) => { + parsePath: (path) => { return { - value: node.value, - node, + value: path.node.value, + node: path.node, + path, }; }, - }), - ), + }) + ) ); export const BooleanLiteral = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: t.isBooleanLiteral, - parseNode: (node) => { + parsePath: (path) => { return { - value: node.value, - node, + value: path.node.value, + node: path.node, + path, }; }, - }), - ), + }) + ) ); export const AnyNode = createParser({ babelMatcher: t.isNode, - parseNode: (node) => ({ node }), + parsePath: (path) => ({ path, node: path.node }), }); export const Identifier = createParser({ babelMatcher: t.isIdentifier, - parseNode: (node) => ({ node }), + parsePath: (path) => ({ path, node: path.node }), }); export const TemplateLiteral = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: t.isTemplateLiteral, - parseNode: (node) => { + parsePath: (path) => { let value = ""; // TODO - this might lead to weird issues if there is actually more than a single quasi there - node.quasis.forEach((quasi) => { + path.node.quasis.forEach((quasi) => { value = `${value}${quasi.value.raw}`; }); return { - node, + node: path.node, + path, value, }; }, - }), - ), + }) + ) ); diff --git a/packages/machine-extractor/src/stateNode.ts b/packages/machine-extractor/src/stateNode.ts index 44282390..7eb83f83 100644 --- a/packages/machine-extractor/src/stateNode.ts +++ b/packages/machine-extractor/src/stateNode.ts @@ -1,4 +1,4 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { MaybeArrayOfActions } from "./actions"; import { Context } from "./context"; import { History } from "./history"; @@ -24,7 +24,10 @@ const After = objectOf(MaybeTransitionArray); const Tags = maybeArrayOf(StringLiteral); type WithValueNodes = { - [K in keyof T]: T[K] & { _valueNode?: t.Node }; + [K in keyof T]: T[K] & { + _valueNode?: t.Node; + _valueNodePath?: NodePath; + }; }; /** @@ -69,7 +72,7 @@ export type StateNodeReturn = WithValueNodes<{ delimiter?: GetParserResult; key?: GetParserResult; }> & - Pick; + Pick; const StateNodeObject: AnyParser = objectTypeWithKnownKeys( () => ({ diff --git a/packages/machine-extractor/src/tsAsExpression.ts b/packages/machine-extractor/src/tsAsExpression.ts index f5b34697..7a42ccdc 100644 --- a/packages/machine-extractor/src/tsAsExpression.ts +++ b/packages/machine-extractor/src/tsAsExpression.ts @@ -1,14 +1,14 @@ -import { AnyParser } from "."; import { types as t } from "@babel/core"; +import { AnyParser } from "."; import { createParser } from "./createParser"; import { unionType } from "./unionType"; export const tsAsExpression = (parser: AnyParser) => { return createParser({ babelMatcher: t.isTSAsExpression, - parseNode: (node, context) => { - if (parser.matches(node.expression)) { - return parser.parse(node.expression, context); + parsePath: (path, context) => { + if (parser.matches(path.get("expression")?.node)) { + return parser.parse(path.get("expression"), context); } }, }); diff --git a/packages/machine-extractor/src/typeParameters.ts b/packages/machine-extractor/src/typeParameters.ts index d6e980f3..0cb20a87 100644 --- a/packages/machine-extractor/src/typeParameters.ts +++ b/packages/machine-extractor/src/typeParameters.ts @@ -7,19 +7,21 @@ export const TSTypeParameterInstantiation = ( ) => createParser({ babelMatcher: t.isTSTypeParameterInstantiation, - parseNode: (node, context) => { + parsePath: (path, context) => { return { - node, - params: node.params.map((param) => parser.parse(param, context)), + path, + node: path.node, + params: path.get("params").map((param) => parser.parse(param, context)), }; }, }); export const TSType = createParser({ babelMatcher: t.isTSType, - parseNode: (node) => { + parsePath: (path) => { return { - node, + node: path.node, + path, }; }, }); diff --git a/packages/machine-extractor/src/types.ts b/packages/machine-extractor/src/types.ts index fd7105d0..889d5358 100644 --- a/packages/machine-extractor/src/types.ts +++ b/packages/machine-extractor/src/types.ts @@ -1,5 +1,4 @@ -import { types as t } from "@babel/core"; -import { MachineConfig } from "xstate"; +import { NodePath, types as t } from "@babel/core"; import { MachineParseResult } from "./MachineParseResult"; export type Location = t.SourceLocation | null; @@ -7,6 +6,7 @@ export type Location = t.SourceLocation | null; export interface StringLiteralNode { value: string; node: t.Node; + path: NodePath; } export interface ParserContext { @@ -16,15 +16,18 @@ export interface ParserContext { export interface Parser { parse: ( - node: t.Node | undefined | null, - context: ParserContext, + node: NodePath | undefined | null, + context: ParserContext ) => Result | undefined; - matches: (node: T) => boolean; + matches: (node: t.Node) => boolean; } export interface AnyParser { - parse: (node: any, context: ParserContext) => Result | undefined; - matches: (node: any) => boolean; + parse: ( + node: NodePath | null | undefined, + context: ParserContext + ) => Result | undefined; + matches: (node: t.Node) => boolean; } export interface Comment { diff --git a/packages/machine-extractor/src/unionType.ts b/packages/machine-extractor/src/unionType.ts index ebdb1cfb..6e24714e 100644 --- a/packages/machine-extractor/src/unionType.ts +++ b/packages/machine-extractor/src/unionType.ts @@ -6,15 +6,17 @@ import { AnyParser, ParserContext } from "."; * return the same result */ export const unionType = ( - parsers: AnyParser[], + parsers: AnyParser[] ): AnyParser => { const matches = (node: any) => { return parsers.some((parser) => parser.matches(node)); }; - const parse = (node: any, context: ParserContext): Result | undefined => { - const possibleParsers = parsers.filter((parser) => parser.matches(node)); + const parse = (path: any, context: ParserContext): Result | undefined => { + const possibleParsers = parsers.filter((parser) => + parser.matches(path?.node) + ); for (const parser of possibleParsers) { - const result = parser.parse(node, context); + const result = parser.parse(path, context); if (result) return result; } }; diff --git a/packages/machine-extractor/src/utils.ts b/packages/machine-extractor/src/utils.ts index 07c750d6..adc1d201 100644 --- a/packages/machine-extractor/src/utils.ts +++ b/packages/machine-extractor/src/utils.ts @@ -1,4 +1,4 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { createParser } from "./createParser"; import { identifierReferencingVariableDeclaration, @@ -17,7 +17,7 @@ import { wrapParserResult } from "./wrapParserResult"; export const parserFromBabelMatcher = ( babelMatcher: (node: any) => node is T -) => createParser({ babelMatcher, parseNode: (node) => node }); +) => createParser({ babelMatcher, parsePath: (path) => path }); /** * Useful for when something might, or might not, @@ -28,10 +28,10 @@ export const maybeArrayOf = ( ): AnyParser => { const arrayParser = createParser({ babelMatcher: t.isArrayExpression, - parseNode: (node, context) => { + parsePath: (path, context) => { const toReturn: Result[] = []; - node.elements.map((elem) => { + path.get("elements").map((elem) => { const result = parser.parse(elem, context); if (result && Array.isArray(result)) { toReturn.push(...result); @@ -66,10 +66,10 @@ export const arrayOf = ( ): AnyParser => { return createParser({ babelMatcher: t.isArrayExpression, - parseNode: (node, context) => { + parsePath: (path, context) => { const toReturn: Result[] = []; - node.elements.map((elem) => { + path.get("elements").map((elem) => { const result = parser.parse(elem, context); if (result) { toReturn.push(result); @@ -83,13 +83,15 @@ export const arrayOf = ( export const objectMethod = createParser({ babelMatcher: t.isObjectMethod, - parseNode: (node, context) => { + parsePath: (path, context) => { return { - node, - key: wrapParserResult(Identifier, ({ node }) => ({ + node: path.node, + path, + key: wrapParserResult(Identifier, ({ node, path }) => ({ node: node, + path, value: node.name, - })).parse(node.key, context), + })).parse(path.get("key"), context), }; }, }); @@ -100,6 +102,7 @@ export const staticObjectProperty = ( createParser< t.ObjectProperty, { + path: NodePath; node: t.ObjectProperty; key?: KeyResult; } @@ -107,10 +110,11 @@ export const staticObjectProperty = ( babelMatcher: (node): node is t.ObjectProperty => { return t.isObjectProperty(node) && !node.computed; }, - parseNode: (node, context) => { + parsePath: (path, context) => { return { - node, - key: keyParser.parse(node.key, context), + node: path.node, + path, + key: keyParser.parse(path.get("key"), context), }; }, }); @@ -118,10 +122,11 @@ export const staticObjectProperty = ( export const spreadElement = (parser: AnyParser) => { return createParser({ babelMatcher: t.isSpreadElement, - parseNode: (node, context) => { + parsePath: (path, context) => { const result = { - node, - argumentResult: parser.parse(node.argument, context), + node: path.node, + path, + argumentResult: parser.parse(path.get("argument"), context), }; return result; @@ -141,6 +146,7 @@ export const dynamicObjectProperty = ( createParser< t.ObjectProperty, { + path: NodePath; node: t.ObjectProperty; key?: KeyResult; } @@ -148,22 +154,24 @@ export const dynamicObjectProperty = ( babelMatcher: (node): node is t.ObjectProperty => { return t.isObjectProperty(node) && node.computed; }, - parseNode: (node, context) => { + parsePath: (path, context) => { return { - node, - key: keyParser.parse(node.key, context), + node: path.node, + path, + key: keyParser.parse(path.get("key"), context), }; }, }); const staticPropertyWithKey = staticObjectProperty( - unionType<{ node: t.Node; value: string | number }>([ + unionType<{ node: t.Node; path: NodePath; value: string | number }>([ createParser({ babelMatcher: t.isIdentifier, - parseNode: (node) => { + parsePath: (path) => { return { - node, - value: node.name, + node: path.node, + path, + value: path.node.name, }; }, }), @@ -176,6 +184,7 @@ const dynamicPropertyWithKey = dynamicObjectProperty( maybeIdentifierTo( unionType<{ node: t.Node; + path: NodePath; value: string | number | undefined; }>([StringLiteral, NumericLiteral, TemplateLiteral]) ) @@ -183,7 +192,9 @@ const dynamicPropertyWithKey = dynamicObjectProperty( const propertyKey = unionType<{ node: t.ObjectMethod | t.ObjectProperty; + path: NodePath; key?: { + path: NodePath; node: t.Node; value: string | number | undefined; }; @@ -194,32 +205,33 @@ const propertyKey = unionType<{ * an object expression */ export const getPropertiesOfObjectExpression = ( - node: t.ObjectExpression | undefined, + path: NodePath | null | undefined, context: ParserContext ) => { const propertiesToReturn: { + path: NodePath; node: t.ObjectProperty | t.ObjectMethod; key: string; keyNode: t.Node; + keyPath: NodePath; property: t.ObjectMethod | t.ObjectProperty | t.SpreadElement; + propertyPath: NodePath; }[] = []; - node?.properties.forEach((property) => { - const propertiesToParse: ( - | t.ObjectMethod - | t.ObjectProperty - | t.SpreadElement - )[] = [property]; + path?.get("properties").forEach((property) => { + const propertiesToParse: NodePath< + t.ObjectMethod | t.ObjectProperty | t.SpreadElement + >[] = [property]; const spreadElementResult = spreadElementReferencingIdentifier( createParser({ babelMatcher: t.isObjectExpression, - parseNode: (node) => node, + parsePath: (path) => path, }) - ).parse(property, context); + ).parse(property as any, context); propertiesToParse.push( - ...(spreadElementResult?.argumentResult?.properties || []) + ...(spreadElementResult?.argumentResult?.get("properties") || []) ); propertiesToParse.forEach((property) => { @@ -228,8 +240,11 @@ export const getPropertiesOfObjectExpression = ( propertiesToReturn.push({ key: `${result.key?.value}`, node: result.node, - keyNode: result.key.node, - property, + path: result.path, + keyNode: result.key?.node, + keyPath: result.key?.path, + property: property.node, + propertyPath: property, }); } }); @@ -248,10 +263,12 @@ export type GetObjectKeysResult< }; } & { node: t.Node; + _path: NodePath; }; export interface ObjectPropertyInfo { node: t.Node; + _path: NodePath; _valueNode?: t.Node; } @@ -272,13 +289,15 @@ export const objectTypeWithKnownKeys = < maybeIdentifierTo( createParser>({ babelMatcher: t.isObjectExpression, - parseNode: (node, context) => { - const properties = getPropertiesOfObjectExpression(node, context); + parsePath: (path, context) => { + const properties = getPropertiesOfObjectExpression(path, context); + const parseObject = typeof parserObject === "function" ? parserObject() : parserObject; const toReturn: ObjectPropertyInfo = { - node, + node: path.node, + _path: path, }; properties?.forEach((property) => { @@ -290,11 +309,17 @@ export const objectTypeWithKnownKeys = < let result: any | undefined; if (t.isObjectMethod(property.node)) { - result = parser.parse(property.node, context); + result = parser.parse(property.path, context); } else if (t.isObjectProperty(property.node)) { - result = parser.parse(property.node.value, context); + result = parser.parse( + (property.path as NodePath).get("value"), + context + ); if (result) { result._valueNode = property.node.value; + result._valueNodePath = ( + property.path as NodePath + ).get("value"); } } @@ -309,11 +334,13 @@ export const objectTypeWithKnownKeys = < export interface ObjectOfReturn { node: t.Node; + path: NodePath; properties: { keyNode: t.Node; key: string; result: Result; property: t.ObjectMethod | t.ObjectProperty | t.SpreadElement; + propertyPath: NodePath; }[]; } @@ -328,11 +355,12 @@ export const objectOf = ( return maybeIdentifierTo( createParser({ babelMatcher: t.isObjectExpression, - parseNode: (node, context) => { - const properties = getPropertiesOfObjectExpression(node, context); + parsePath: (path, context) => { + const properties = getPropertiesOfObjectExpression(path, context); const toReturn = { - node, + node: path.node, + path, properties: [], } as ObjectOfReturn; @@ -340,9 +368,12 @@ export const objectOf = ( let result: Result | undefined; if (t.isObjectMethod(property.node)) { - result = parser.parse(property.node, context); + result = parser.parse(property.path, context); } else if (t.isObjectProperty(property.node)) { - result = parser.parse(property.node.value, context); + result = parser.parse( + (property.path as NodePath).get("value"), + context + ); } if (result) { @@ -351,6 +382,7 @@ export const objectOf = ( keyNode: property.keyNode, result, property: property.property, + propertyPath: property.propertyPath, }); } }); @@ -369,42 +401,57 @@ export const namedFunctionCall = ( name: string, argument1Parser: AnyParser, argument2Parser?: AnyParser -): AnyParser<{ - node: t.CallExpression; - argument1Result: Argument1Result | undefined; - argument2Result: Argument2Result | undefined; -}> => { +) => { const namedFunctionParser = maybeTsAsExpression( maybeIdentifierTo( createParser({ babelMatcher: t.isCallExpression, - parseNode: (node) => { - return node; + parsePath: (path) => { + return path; }, }) ) ); - return { - matches: (node: t.CallExpression) => { + return createParser< + t.CallExpression, + { + node: t.CallExpression; + path: NodePath; + argument1Result: Argument1Result | undefined; + argument2Result: Argument2Result | undefined; + } + >({ + babelMatcher: (node): node is t.CallExpression => { if (!namedFunctionParser.matches(node)) { return false; } + if (!("callee" in node)) { + return false; + } + if (!t.isIdentifier(node.callee)) { return false; } return node.callee.name === name; }, - parse: (node: t.CallExpression, context) => { + parsePath: (path, context) => { return { - node, - argument1Result: argument1Parser.parse(node.arguments[0], context), - argument2Result: argument2Parser?.parse(node.arguments[1], context), + node: path.node, + path: path, + argument1Result: argument1Parser.parse( + path.get("arguments")[0], + context + ), + argument2Result: argument2Parser?.parse( + path.get("arguments")[1], + context + ), }; }, - }; + }); }; export const isFunctionOrArrowFunctionExpression = ( diff --git a/packages/machine-extractor/src/wrapParserResult.ts b/packages/machine-extractor/src/wrapParserResult.ts index b8e955e1..7bb6e68f 100644 --- a/packages/machine-extractor/src/wrapParserResult.ts +++ b/packages/machine-extractor/src/wrapParserResult.ts @@ -1,4 +1,4 @@ -import { types as t } from "@babel/core"; +import { NodePath, types as t } from "@babel/core"; import { ParserContext } from "."; import { AnyParser } from "./types"; @@ -10,16 +10,16 @@ export const wrapParserResult = ( parser: AnyParser, changeResult: ( result: Result, - node: T, - context: ParserContext, - ) => NewResult | undefined, + path: NodePath, + context: ParserContext + ) => NewResult | undefined ): AnyParser => { return { matches: parser.matches, - parse: (node: any, context) => { - const result = parser.parse(node, context); + parse: (path: NodePath | null | undefined, context) => { + const result = parser.parse(path, context); if (!result) return undefined; - return changeResult(result, node, context); + return changeResult(result, path!, context); }, }; }; diff --git a/packages/recast-experiment/.gitignore b/packages/recast-experiment/.gitignore new file mode 100644 index 00000000..501f0e27 --- /dev/null +++ b/packages/recast-experiment/.gitignore @@ -0,0 +1,3 @@ +node_modules +lib +*.tsbuildinfo \ No newline at end of file diff --git a/packages/recast-experiment/.npmignore b/packages/recast-experiment/.npmignore new file mode 100644 index 00000000..24f83765 --- /dev/null +++ b/packages/recast-experiment/.npmignore @@ -0,0 +1,4 @@ +node_modules +lib/__tests__ +.github +examples \ No newline at end of file diff --git a/packages/recast-experiment/CHANGELOG.md b/packages/recast-experiment/CHANGELOG.md new file mode 100644 index 00000000..49c18d6a --- /dev/null +++ b/packages/recast-experiment/CHANGELOG.md @@ -0,0 +1,45 @@ +# @xstate/machine-extractor + +## 0.7.0 + +### Minor Changes + +- [#151](https://github.com/statelyai/xstate-tools/pull/151) [`795a057`](https://github.com/statelyai/xstate-tools/commit/795a057f73f0a38784548a1fcf055757f44d0647) Thanks [@mattpocock](https://github.com/mattpocock)! - Added parsing for the `@xstate/test` `createTestMachine` function + +## 0.6.4 + +### Patch Changes + +- [#140](https://github.com/statelyai/xstate-tools/pull/140) [`e073819`](https://github.com/statelyai/xstate-tools/commit/e0738191c61290c8f5a9ecdd507e6418ab551518) Thanks [@Andarist](https://github.com/Andarist)! - Fixed an issue with not being able to parse type-only import/export specifiers. + +* [#130](https://github.com/statelyai/xstate-tools/pull/130) [`99e4cb5`](https://github.com/statelyai/xstate-tools/commit/99e4cb57f3590448ddbcdc85a3104d29ef0fa79c) Thanks [@mattpocock](https://github.com/mattpocock)! - Fixed a bug where actions and conditions inside `choose` inside machine options would not be found in typegen. + +## 0.6.3 + +### Patch Changes + +- [#126](https://github.com/statelyai/xstate-tools/pull/126) [`c705a64`](https://github.com/statelyai/xstate-tools/commit/c705a64d95fa99046a7acd77f16b9b0dddd2e7ba) Thanks [@ericjonathan6](https://github.com/ericjonathan6)! - Fixed a bug where descriptions on transitions were not being visualised in VSCode. + +## 0.6.2 + +### Patch Changes + +- [#99](https://github.com/statelyai/xstate-tools/pull/99) [`5332727`](https://github.com/statelyai/xstate-tools/commit/5332727a7ad1d4ff00c81e006edc6ffb66f5da88) Thanks [@mattpocock](https://github.com/mattpocock)! - Fixed an issue where property keys passed an incorrect type would silently fail + +* [#100](https://github.com/statelyai/xstate-tools/pull/100) [`335f349`](https://github.com/statelyai/xstate-tools/commit/335f34934589dbb5c3e9685524c72b9a1badbc0e) Thanks [@mattpocock](https://github.com/mattpocock)! - Fixed a bug where inline guards were not being picked up in non-root events. + +## 0.6.1 + +### Patch Changes + +- [#84](https://github.com/statelyai/xstate-tools/pull/84) [`a73fce8`](https://github.com/statelyai/xstate-tools/commit/a73fce843ee04b0701d9d72046da422ff3a65eed) Thanks [@Andarist](https://github.com/Andarist)! - Fixed a bug where transition targets would not be parsed correctly if they were declared using a template literal. + +## 0.6.0 + +### Minor Changes + +- [#68](https://github.com/statelyai/xstate-tools/pull/68) [`a3b874b`](https://github.com/statelyai/xstate-tools/commit/a3b874b328cd6bf409861378ab2840dab70d3ff3) Thanks [@mattpocock](https://github.com/mattpocock)! - Added logic for parsing inline actions, services and guards. This allows for users of this package to parse and edit machines containing inline implementations predictably. + +### Patch Changes + +- [#69](https://github.com/statelyai/xstate-tools/pull/69) [`2210d4b`](https://github.com/statelyai/xstate-tools/commit/2210d4b5175384f87dc0b001ba68400701c35818) Thanks [@mattpocock](https://github.com/mattpocock)! - Fixed issue where tags appeared to be deleted when changes were made in VSCode. diff --git a/packages/recast-experiment/README.md b/packages/recast-experiment/README.md new file mode 100644 index 00000000..895b28a4 --- /dev/null +++ b/packages/recast-experiment/README.md @@ -0,0 +1,3 @@ +# xstate-parser + +Try it out at https://xstate-parser-example-site.vercel.app/ diff --git a/packages/recast-experiment/babel.config.js b/packages/recast-experiment/babel.config.js new file mode 100644 index 00000000..eb8f9005 --- /dev/null +++ b/packages/recast-experiment/babel.config.js @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "12" } }], + "@babel/preset-typescript", + "@babel/preset-react", + ], +}; diff --git a/packages/recast-experiment/package.json b/packages/recast-experiment/package.json new file mode 100644 index 00000000..5ba6c8dd --- /dev/null +++ b/packages/recast-experiment/package.json @@ -0,0 +1,25 @@ +{ + "name": "@xstate/recast-experiment", + "version": "0.7.0", + "main": "dist/xstate-recast-experiment.cjs.js", + "author": "Matt Pocock", + "license": "MIT", + "scripts": { + "test": "jest src", + "lint": "tsc" + }, + "dependencies": { + "@xstate/machine-extractor": "0.7.0", + "recast": "^0.21.1" + }, + "peerDependencies": { + "xstate": "^4" + }, + "devDependencies": { + "@types/jest": "^27.4.0", + "@types/node": "^16.0.1", + "jest": "^27.4.7", + "typescript": "^4.3.5", + "xstate": "^4.29.0" + } +} diff --git a/packages/recast-experiment/src/__tests__/index.test.ts b/packages/recast-experiment/src/__tests__/index.test.ts new file mode 100644 index 00000000..cf131e3b --- /dev/null +++ b/packages/recast-experiment/src/__tests__/index.test.ts @@ -0,0 +1,938 @@ +import { transformFile } from ".."; +import { NodePath, types } from "@babel/core"; +import { + MachineParseResult, + parseMachinesFromFile, + StateNodeReturn, +} from "@xstate/machine-extractor"; +import { MachineConfig } from "xstate"; +import { + InvokeConfigObjectType, + InvokeNode, +} from "@xstate/machine-extractor/src/invoke"; + +const unsafeRegex = /(^[0-9])|( )/g; + +const getObjectKeyAST = (key: string) => { + if (unsafeRegex.test(key) || key.length === 0) { + return types.stringLiteral(key); + } + return types.identifier(key); +}; + +const getParentPath = (path: string[]) => { + if (path.length === 0) return undefined; + return path.slice(0, -1); +}; + +const deleteState = (path: string[], machine: MachineParseResult) => { + // Remove the node from the machine + const stateNode = machine.getStateNodeByPath(path); + if (!stateNode?.ast._path.parentPath.removed) { + stateNode?.ast._path.parentPath?.remove(); + } + + // Remove the property from the parent states node + const parentPath = getParentPath(path); + if (parentPath) { + const parentNode = machine.getStateNodeByPath(parentPath); + + const keyNode = parentNode?.ast.states?.properties.find((property) => { + return property.key === path[path.length - 1]; + }); + + if (!keyNode?.propertyPath.removed) { + keyNode?.propertyPath.remove(); + } + } +}; + +const newChildToAst = ( + child: Omit +): types.ObjectExpression => { + if (child.children.length === 0) { + return types.objectExpression([]); + } + + return types.objectExpression([ + types.objectProperty( + types.identifier("states"), + types.objectExpression( + child.children.map((child) => { + return types.objectProperty( + getObjectKeyAST(child.key), + newChildToAst(child) + ); + }) + ) + ), + ]); +}; + +const addState = ( + parentPath: string[], + machine: MachineParseResult, + key: string, + children: AdditionChild[] +) => { + // Remove the node from the machine + const stateNode = machine.getStateNodeByPath(parentPath); + + const nodeAst = newChildToAst({ key, children }); + + if (stateNode?.ast.states?.path) { + stateNode?.ast.states?.path.pushContainer( + "properties", + types.objectProperty(getObjectKeyAST(key), nodeAst) + ); + } else { + stateNode?.ast._path.pushContainer( + "properties", + types.objectProperty( + types.identifier("states"), + types.objectExpression([ + types.objectProperty(getObjectKeyAST(key), nodeAst), + ]) + ) + ); + } +}; + +const renameState = ( + path: string[], + machine: MachineParseResult, + name: string +) => { + const parentPath = getParentPath(path); + if (parentPath) { + const parentNode = machine.getStateNodeByPath(parentPath); + + const keyNode = parentNode?.ast.states?.properties.find((property) => { + return property.key === path[path.length - 1]; + }); + + if (keyNode?.propertyPath.isObjectProperty()) { + keyNode.propertyPath.get("key").replaceWith(getObjectKeyAST(name)); + } + } +}; + +const changeStateNodeValue = < + TKey extends "id" | "history" | "initial" | "key" | "type" +>( + path: string[], + machine: MachineParseResult, + key: TKey, + value: MachineConfig[TKey] +) => { + const stateNode = machine.getStateNodeByPath(path); + + const valuePath = stateNode?.ast?.[key]?.path; + + if (!value) { + valuePath?.parentPath?.remove(); + return; + } + + let newValue; + + if (typeof value === "boolean") { + newValue = types.booleanLiteral(value); + } else if (typeof value === "number") { + newValue = types.numericLiteral(value); + } else { + newValue = types.stringLiteral(`${value}`); + } + + if (valuePath && "replaceWith" in valuePath) { + valuePath.replaceWith(newValue); + } else if (!valuePath) { + stateNode?.ast._path.pushContainer( + "properties", + types.objectProperty(types.identifier(key), newValue) + ); + } +}; + +describe("Recast experiment", () => { + it("REPL", () => { + const src = ` + createMachine({ + // Wow + initial: 'a', + type: 'parallel', + states: { + a: {}, + b: {}, + }, + });`; + + const newSrc = transformFile(src, ({ machines }) => { + const machine = machines[0]; + + changeStateNodeValue([], machine, "initial", "d"); + changeStateNodeValue([], machine, "type", undefined); + changeStateNodeValue([], machine, "id", "machine"); + + renameState(["b"], machine, "d"); + }); + + expect(newSrc).toMatchInlineSnapshot(` + " + createMachine({ + // Wow + initial: \\"d\\", + states: { + a: {}, + d: {}, + }, + id: \\"machine\\", + });" + `); + }); +}); + +interface PreservableItems { + states: { + [id: string]: { + /** + * The path of the state node in the current code + */ + pathInCode: string[]; + invokeIds: string[]; + }; + }; + // transitions: { + // [id: string]: { + // stateNodePath: string[]; + // }; + // }; +} + +interface InvokeUpdate { + id: string; + info: { + src: string; + id?: string; + }; +} + +interface NonRootStateUpdate { + key: string; + parentId: string; + invokes: InvokeUpdate[]; +} + +interface RootStateUpdate { + key: null; + parentId: null; + invokes: InvokeUpdate[]; +} + +interface ConfigUpdateEvent { + states: { + [id: string]: NonRootStateUpdate | RootStateUpdate; + }; + config: MachineConfig; +} + +const resolveRenamesAdditionsAndDeletionsOfStates = ( + db: PreservableItems, + update: ConfigUpdateEvent +) => { + const allStates = Object.entries(db.states); + + const statesToBeRenamed = allStates.filter(([id, item]) => { + if (!update.states[id]?.key) return false; + return ( + update.states[id]?.key !== item.pathInCode[item.pathInCode.length - 1] + ); + }); + + const statesToBeDeleted = allStates.filter(([id, item]) => { + return !update.states[id]; + }); + + const statesToBeAdded = Object.entries(update.states).filter( + (entry): entry is [string, NonRootStateUpdate] => { + const [id, item] = entry; + return !db.states[id] && item.parentId && db.states[item.parentId]; + } + ); + + return { + renames: statesToBeRenamed + .map(([id, item]) => { + return { + id, + newKey: update.states[id].key, + pathInCode: item.pathInCode, + }; + }) + .filter( + ( + item + ): item is { id: string; newKey: string; pathInCode: string[] } => { + return Boolean(item.newKey); + } + ), + deletions: statesToBeDeleted.map(([id, item]) => { + return { + id, + pathInCode: item.pathInCode, + }; + }), + additions: statesToBeAdded.map(([id, item]): Addition => { + return { + id, + parentPath: db.states[item.parentId].pathInCode, + key: item.key, + children: getAdditionChildren(id, update.states), + }; + }), + }; +}; + +const getAdditionChildren = ( + id: string, + states: ConfigUpdateEvent["states"] +): AdditionChild[] => { + const children = Object.entries(states).filter( + ([, item]) => item.parentId === id + ); + + return children.map(([id, item]) => { + return { + id, + key: item.key as string, + children: getAdditionChildren(id, states), + }; + }); +}; + +interface Addition { + id: string; + key: string; + parentPath: string[]; + children: AdditionChild[]; +} + +interface AdditionChild { + id: string; + children: AdditionChild[]; + key: string; +} + +describe("resolveRenamesAdditionsAndDeletionsOfStates", () => { + it("REPL", () => { + expect( + resolveRenamesAdditionsAndDeletionsOfStates( + { + states: { + root: { + pathInCode: [], + invokeIds: [], + }, + + a: { + pathInCode: ["a"], + invokeIds: [], + }, + + b: { pathInCode: ["b"], invokeIds: [] }, + }, + }, + + { + config: {}, + states: { + root: { + parentId: null, + key: null, + invokes: [], + }, + + a: { + key: "awdawdawd", + parentId: "root", + invokes: [], + }, + + c: { + key: "c", + parentId: "root", + invokes: [], + }, + + d: { + key: "d", + parentId: "c", + invokes: [], + }, + }, + } + ) + ).toMatchInlineSnapshot(` + Object { + "additions": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "id": "d", + "key": "d", + }, + ], + "id": "c", + "key": "c", + "parentPath": Array [], + }, + ], + "deletions": Array [ + Object { + "id": "b", + "pathInCode": Array [ + "b", + ], + }, + ], + "renames": Array [ + Object { + "id": "a", + "newKey": "awdawdawd", + "pathInCode": Array [ + "a", + ], + }, + ], + } + `); + }); +}); + +function uniqueId() { + return Math.random().toString(36).substr(2, 9); +} + +const createPreservableItems = ( + parseResult: MachineParseResult +): PreservableItems => { + const items: PreservableItems = { + states: {}, + }; + + parseResult.getAllStateNodes().map((stateNode) => { + const id = uniqueId(); + items.states[id] = { + pathInCode: stateNode.path, + invokeIds: stateNode.ast.invoke?.map(() => uniqueId()) || [], + }; + }); + + return items; +}; + +const resolveNewInvokes = ( + stateNode: StateNodeReturn, + newInvokes: { + id: string; + info: { src: string; id?: string }; + }[], + getNodePathByInvokeId: (id: string) => InvokeConfigObjectType | undefined +) => { + const invokesAsAST: types.ObjectExpression[] = newInvokes.map((invoke) => { + const existingInvokeNode = getNodePathByInvokeId(invoke.id); + + if (existingInvokeNode) { + // TODO + if (invoke.info.id) { + if (existingInvokeNode.id) { + existingInvokeNode.id?.path.replaceWith( + types.stringLiteral(invoke.info.id) + ); + } else { + existingInvokeNode._path.pushContainer( + "properties", + types.objectProperty( + types.identifier("id"), + types.stringLiteral(invoke.info.id) + ) + ); + } + } + + if (existingInvokeNode.src) { + if (existingInvokeNode.src.declarationType === "named") { + existingInvokeNode.src?.path.replaceWith( + types.stringLiteral(invoke.info.src) + ); + } + } else { + existingInvokeNode._path.pushContainer( + "properties", + types.objectProperty( + types.identifier("src"), + types.stringLiteral(invoke.info.src) + ) + ); + } + + return existingInvokeNode._path.node; + } + + return types.objectExpression([ + types.objectProperty( + types.identifier("src"), + types.stringLiteral(invoke.info.src) + ), + ...(invoke.info.id + ? [ + types.objectProperty( + types.identifier("id"), + types.stringLiteral(invoke.info.id) + ), + ] + : []), + ]); + }); + + if (invokesAsAST.length === 0) { + return; + } + + const invokesAsExpression = + invokesAsAST.length > 1 + ? types.arrayExpression(invokesAsAST) + : invokesAsAST[0]; + + // If there's no invoke node, add the new invokes + // as an array + + if (!stateNode.invoke) { + stateNode._path.pushContainer( + "properties", + types.objectProperty(types.identifier("invoke"), invokesAsExpression) + ); + return; + } + + stateNode.invoke._valueNodePath?.replaceWith(invokesAsExpression); + + // If the ids are not identical, replace the entire array + // and update their ids and src's in a single pass + + // Otherwise, only iterate through them and update their + // ids +}; + +describe("createPreservableItems", () => { + it("REPL", () => { + const src = ` + createMachine({ + initial: 'a', + states: { + a: {}, + b: { + initial: 'c', + states: { + c: {}, + }, + } + } + }) + `; + const items = createPreservableItems( + parseMachinesFromFile(src).machines[0] + ); + + expect(Object.values(items.states)).toMatchInlineSnapshot(` + Array [ + Object { + "invokeIds": Array [], + "pathInCode": Array [], + }, + Object { + "invokeIds": Array [], + "pathInCode": Array [ + "a", + ], + }, + Object { + "invokeIds": Array [], + "pathInCode": Array [ + "b", + ], + }, + Object { + "invokeIds": Array [], + "pathInCode": Array [ + "b", + "c", + ], + }, + ] + `); + }); +}); + +const transformations: (( + src: string, + preservedItems: PreservableItems, + event: ConfigUpdateEvent, + machineIndex: number +) => { + src: string; + preservedItems: PreservableItems; +})[] = [ + (src, preservedItems, event, machineIndex) => { + const newPreservedItems = Object.assign({}, preservedItems); + + const newSrc = transformFile(src, ({ machines }) => { + const machine = machines[machineIndex]; + + const stateChanges = resolveRenamesAdditionsAndDeletionsOfStates( + preservedItems, + event + ); + + stateChanges.renames.forEach((rename) => { + renameState(rename.pathInCode, machine, rename.newKey); + }); + stateChanges.deletions.forEach((deletion) => { + deleteState(deletion.pathInCode, machine); + }); + stateChanges.additions.forEach((addition) => { + addState(addition.parentPath, machine, addition.key, addition.children); + }); + + const addAdditionChildToPreservableItems = ( + child: AdditionChild, + parentPath: string[] + ) => { + const newPath = [...parentPath, child.key]; + newPreservedItems.states[child.id] = { + invokeIds: event.states[child.id].invokes.map((invoke) => invoke.id), + pathInCode: newPath, + }; + child.children.forEach((child) => { + addAdditionChildToPreservableItems(child, newPath); + }); + }; + + stateChanges.additions.forEach((addition) => { + const newPath = [...addition.parentPath, addition.key]; + newPreservedItems.states[addition.id] = { + invokeIds: event.states[addition.id].invokes.map( + (invoke) => invoke.id + ), + pathInCode: newPath, + }; + addition.children.forEach((child) => { + addAdditionChildToPreservableItems(child, newPath); + }); + }); + stateChanges.deletions.forEach((deletion) => { + delete newPreservedItems.states[deletion.id]; + }); + stateChanges.renames.forEach((rename) => { + const oldKey = rename.pathInCode[rename.pathInCode.length - 1]; + + /** + * -1 for root, 0 for first child, 1 for second child, etc. + */ + const depth = rename.pathInCode.length - 1; + + newPreservedItems.states[rename.id] = { + ...newPreservedItems.states[rename.id], + pathInCode: [...rename.pathInCode.slice(0, -1), rename.newKey], + }; + + if (depth === -1) { + return; + } + + Object.keys(newPreservedItems.states).forEach((id) => { + const path = newPreservedItems.states[id].pathInCode; + + /** + * If the path of this state at the depth of the rename is + * the same as the old key, update it to the new key. + */ + if (path[depth] === oldKey) { + path[depth] = rename.newKey; + } + }); + }); + }); + + return { + src: newSrc, + preservedItems: newPreservedItems, + }; + }, + (src, preservedItems, event, machineIndex) => { + const newPreservedItems = Object.assign({}, preservedItems); + + Object.entries(event.states).forEach(([id, state]) => { + newPreservedItems.states[id] = { + ...preservedItems.states[id], + invokeIds: state.invokes.map((invoke) => invoke.id), + }; + }); + + const newSrc = transformFile(src, ({ machines }) => { + const machine = machines[machineIndex]; + + const getInvokeNodeById = (invokeId: string) => { + const entry = Object.entries(preservedItems.states).find( + ([, state]) => { + return state.invokeIds.some( + (invokeIdToCheck) => invokeIdToCheck === invokeId + ); + } + ); + + if (!entry) { + return undefined; + } + + const [stateId, state] = entry; + + // Because of the search above, this will never be -1 + const invokeIndex = state.invokeIds.findIndex( + (invokeIdToCheck) => invokeIdToCheck === invokeId + ); + + const statePath = newPreservedItems.states[stateId]?.pathInCode; + + if (!statePath) return; + + return machine.getStateNodeByPath(statePath)?.ast.invoke?.[invokeIndex]; + }; + + Object.entries(newPreservedItems.states).map(([id, item]) => { + const stateNode = machine.getStateNodeByPath(item.pathInCode); + + if (!stateNode) return; + + resolveNewInvokes( + stateNode.ast, + event.states[id].invokes, + getInvokeNodeById + ); + }); + + Object.entries(newPreservedItems.states).map(([id, item]) => { + const stateNode = machine.getStateNodeByPath(item.pathInCode); + + if (!stateNode) return; + + const invokes = event.states[id].invokes; + + if (invokes.length === 0) { + stateNode.ast.invoke?._valueNodePath?.parentPath?.remove(); + } + }); + }); + + return { + src: newSrc, + preservedItems: newPreservedItems, + }; + }, +]; + +const transformFileFromEvent = ( + src: string, + preservedItems: PreservableItems, + event: ConfigUpdateEvent, + machineIndex: number +): { + src: string; + preservedItems: PreservableItems; +} => { + return transformations.reduce( + ({ src, preservedItems }, transform) => { + return transform(src, preservedItems, event, machineIndex); + }, + { + src, + preservedItems, + } + ); +}; + +describe("transformFileFromEvent", () => { + it("REPL", () => { + const src = ` + createMachine({ + initial: 'a', + states: { + a: { + on: { + AWESOME: 'b' + } + }, + b: { + data: {}, + invoke: [{ + src: 'mySrc', + data: { + amazing: true + }, + }, { + src: async () => {}, + }] + }, + } + }) + `; + + const preservedItems = createPreservableItems( + parseMachinesFromFile(src).machines[0] + ); + + const [rootStateId] = Object.entries(preservedItems.states).find( + ([, state]) => state.pathInCode.join() === "" + )!; + + const [aStateId] = Object.entries(preservedItems.states).find( + ([, state]) => state.pathInCode.join() === "a" + )!; + const [bStateId, bState] = Object.entries(preservedItems.states).find( + ([, state]) => state.pathInCode.join() === "b" + )!; + + const invokeId = bState.invokeIds[0]; + const invoke2Id = bState.invokeIds[1]; + + const event: ConfigUpdateEvent = { + config: {}, + states: { + [rootStateId]: { + parentId: null, + key: null, + invokes: [], + }, + [aStateId]: { + key: "a", + parentId: rootStateId, + invokes: [], + }, + [bStateId]: { + key: "adwaaw", + parentId: rootStateId, + invokes: [ + { + id: invokeId, + info: { + src: "mySrc", + id: "brilliant", + }, + }, + ], + }, + deeper: { + parentId: bStateId, + key: "newState", + invokes: [], + }, + superDeep: { + parentId: "deeper", + key: "wow", + invokes: [ + { + id: invoke2Id, + info: { + src: "awd", + }, + }, + ], + }, + }, + }; + + const result = transformFileFromEvent(src, preservedItems, event, 0); + + expect(result.src).toMatchInlineSnapshot(` + " + createMachine({ + initial: 'a', + states: { + a: { + on: { + AWESOME: 'b' + } + }, + adwaaw: { + data: {}, + + invoke: { + src: \\"mySrc\\", + + data: { + amazing: true + }, + + id: \\"brilliant\\" + }, + + states: { + newState: { + states: { + wow: { + invoke: { + src: \\"awd\\" + } + } + } + } + } + }, + } + }) + " + `); + + expect( + Object.values(result.preservedItems.states).map((state) => ({ + pathInCode: state.pathInCode, + invokes: state.invokeIds.length, + })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "invokes": 0, + "pathInCode": Array [], + }, + Object { + "invokes": 0, + "pathInCode": Array [ + "a", + ], + }, + Object { + "invokes": 1, + "pathInCode": Array [ + "adwaaw", + ], + }, + Object { + "invokes": 0, + "pathInCode": Array [ + "adwaaw", + "newState", + ], + }, + Object { + "invokes": 1, + "pathInCode": Array [ + "adwaaw", + "newState", + "wow", + ], + }, + ] + `); + }); +}); diff --git a/packages/recast-experiment/src/index.ts b/packages/recast-experiment/src/index.ts new file mode 100644 index 00000000..d639e51a --- /dev/null +++ b/packages/recast-experiment/src/index.ts @@ -0,0 +1,36 @@ +import { parseSync } from "@babel/core"; +import { ParseResult, traverseParseResult } from "@xstate/machine-extractor"; +import * as recast from "recast"; + +export const transformFile = ( + src: string, + mutate: (result: ParseResult) => void +): string => { + const ast = recast.parse(src, { + parser: { + parse: (source: string) => + parseSync(source, { + plugins: [ + `@babel/plugin-syntax-jsx`, + `@babel/plugin-proposal-class-properties`, + ], + overrides: [ + { + test: [`**/*.ts`, `**/*.tsx`], + plugins: [[`@babel/plugin-syntax-typescript`, { isTSX: true }]], + }, + ], + filename: "file.ts", + parserOpts: { + tokens: true, // recast uses this + }, + }), + }, + }); + + const parseResult = traverseParseResult(ast, src); + + mutate(parseResult); + + return recast.print(ast).code; +}; diff --git a/packages/recast-experiment/tsconfig.json b/packages/recast-experiment/tsconfig.json new file mode 100644 index 00000000..0cd07db9 --- /dev/null +++ b/packages/recast-experiment/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": [ + "ES2019" + ], + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "strict": true, + "outDir": "out", + "rootDir": "src", + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 18efdfc8..08953151 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2146,6 +2146,13 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +ast-types@0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" + integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg== + dependencies: + tslib "^2.0.1" + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -3336,7 +3343,7 @@ espree@^7.3.0, espree@^7.3.1: acorn-jsx "^5.3.1" eslint-visitor-keys "^1.3.0" -esprima@^4.0.0, esprima@^4.0.1: +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -5974,6 +5981,16 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recast@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.21.1.tgz#9b3f4f68c1fe9c1513a1c02ff572fdef02231de2" + integrity sha512-PF61BHLaOGF5oIKTpSrDM6Qfy2d7DIx5qblgqG+wjqHuFH97OgAqBYFIJwEuHTrM6pQGT17IJ8D0C/jVu/0tig== + dependencies: + ast-types "0.15.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -6841,6 +6858,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"