📜 ⬆️ ⬇️

Convert React to Angular using a universal abstract tree. Proof of concept

Introduction


Good day, my name is Vladimir Milenko, I am a Frontend-developer in the company Lightspeed, and today we will talk about the problem of the absence of components in a particular framework and attempts to automatically convert them.


Prehistory


Historically, both in eCommerce and in Retail products for admin panels we use React.JS as the main framework, but the platform for restaurants uses Angular, which prevents them from using our component library. Before my release, this problem became more acute, due to the need to bring UI / UX to one type. I decided to conduct a small study on the migration of components, make a Proof of Concept and share sensations. This will be the post.


Some theory


To understand the rest, you need to know the following notation:


AST is an abstract syntax tree, it is a representation of code in the form of a tree, there are no brackets, etc. An example of frequent use of AST is babel, it builds AST using parsers, and then leaves are transpiled with the types introduced in es6 - into es5-supported.


https://ru.m.wikipedia.org/wiki/Abstract_syntax_tree


Problem solving approach


The first thing that came to mind was of course to convert directly from React to Angular, then thinking well (and this does not always work on vacation), this idea was completely rejected due to the lack of opportunities for direct conversion without an intermediate tree.


A good idea is a universal tree with more abstraction, more top-level, if you can say so. The main idea was to translate complex structures into more abstract ones, about examples a little later.


The process will look something like this:


  1. Parsing js to AST
  2. Parse syntax parsing
  3. UST (universal abstract tree) generation based on the obtained top-level constructions
  4. TypeScript AST + Angular Template html generation from UST

The basic principles on which the world works (cross out) is a parser. I came to the conclusion that the best thing is to build the whole thing on matchers and predicates.


For the theoretical work of the parser, we need to describe the matcher function and the parser function, but we also add the desired type of input node to limit the list of matchers in which we will check the current node (not for the sake of optimization, but convenience).


Matcher returns true/false depending on the node passed to it. It does checks on the node, whether it is the parser that is needed or not.


If the matcher returns true , we will call the parsing function already. The parser function should return a UST node — an abstract description of what happened in the AST node being parsed.


The basic concept of parsing is that each parser should be able to trigger a parsing node without affecting the resultant tree. This will come in handy for us both in matching and in parsing, since we will generate children and sometimes make a start from children.


Parsing component input parameters


Perhaps this is one of the most interesting tasks. As we all know, a React component can take parameters from a variety of places: state, props, context, outer scope .


At the PoC stage, we consider only props , and even only in a certain design, but more on that later.


So, in AST there is no tracking variable, i.e. when looking at a particular node, you will see the Identifier , but this will not give you the slightest idea where this variable came from.


AST-traversal will come to the rescue, which will prompt the current scope of a particular node.


We will consider the following parameter as the input parameter of the component:


const {a} = this.props;


In other words, we will search for VariableDeclaration , where id is ObjectPattern , and init is MemberExpression , with property - necessarily 'props' .


From theory to practice


Used tools:


  1. Parsing AST - babylon
  2. Determination of types of nodes AST - babel-types
  3. Tree babel-traverse - babel-traverse
  4. Generate Angular Pseudo-Templates - parse5

Parsing Parsing Interface:


 export interface ParserPredicate { matchingType:string; isMatching: (token:any) => boolean; parse: (token:any) => any; } 

Well, immediately an example of implementation:


 export class JSXExpressionMap implements ParserPredicate { matchingType = 'JSXExpressionContainer'; parse(token: JSXExpressionContainer): any { const expression = token.expression as CallExpression; const callee = expression.callee as MemberExpression; const baseObject = (callee.object as Identifier).name; const arrowExpression = expression.arguments[0] as ArrowFunctionExpression; const renderOutput = resolverRegistry.resolve(arrowExpression.body); let baseItem = this.getBaseObjectName(callee); let newBaseItem = resolveVariable(token, baseItem); return { type: 'ForLoop', baseItem: { type: 'Identifier', name: newBaseItem }, arguments: arrowExpression.params, children: renderOutput, mutations: this.getMutations(callee) } } getBaseObjectName(callee: MemberExpression) { let temp = callee; while (!isIdentifier(temp.object)) { temp = temp.object.callee; } return (temp.object as Identifier).name; } getMutations(callee: MemberExpression) { if (!isCallExpression(callee.object)) return []; return [callee]; } isMatching(token: JSXExpressionContainer): boolean { if (!isCallExpression(token.expression)) return false; const expression = token.expression as CallExpression; if (!isMemberExpression(expression.callee)) return false; const callee = expression.callee as MemberExpression; if (!isIdentifier(callee.property)) return false; const fnToBeCalled = (callee.property as Identifier).name; if (fnToBeCalled === 'map') { return true; } return false; } } 

Based on the code above, it will become clear that this predicate is waiting at the input of JSXExpressionContainer, then various checks follow to determine whether this parser is really needed for the input node.
This predicate will work on the following JSX construction:


 { items.map(x=>(<li>{x}</li>) } 

Parsing


The parsing function parses the construct into pieces, it also allows you to find the mutations of the original parameter,


 { items.filter(x=>x>5).filter(x=>x>10).map//etc } 

Next comes the process of defining a variable, the function resolveVariable is responsible for this. It serves to determine the scope and search for the definition of this variable:


 export const resolveVariable = (token:any, identifier:string) => { let newIdentifier = identifier; traverse(resolverRegistry.ast, { enter: (path) => { if (path.node !== token) return; if (path.scope.bindings[identifier]) { const binding = path.scope.bindings[identifier]; const declaratorNode = binding.path.node as VariableDeclarator; if (isObjectPattern(declaratorNode.id) && isMemberExpression(declaratorNode.init)) { const init = declaratorNode.init as MemberExpression; if (isThisExpression(init.object) && isIdentifier(init.property)) { newIdentifier = resolverRegistry.registerVariable(identifier, init.property.name === 'props' ? 'Input' : 'Local'); } } } } }); return newIdentifier; }; 

In this code, we are fixedly looking for const {varName} = this.props . Since this is a PoC, this is quite enough. This function returns a uuid with a variable identifier to the process of building the template and the AST class of the component.


At the exit of the parser, we get a UST-node, with the type ForLoop .


Generating a template and class of a new component


In this case, we are starting to use parse5 and babel-types . Generators work on the principle of matchers, but without a predicate, in this case the generator is responsible for the complete generation of a particular type.


 export class ForLoopGenerator implements Generator { matchingType = 'ForLoop'; generate(node: any):any { const children = node.children; let key; const attrs:Array<any> = getAttributes(children.attributes.filter((x:any)=>x.name !== 'key')); const originalName = resolverRegistry.vars.get(node.baseItem.name); attrs.push({ name:'*ngFor', value: `let ${node.arguments[0].name} of ${originalName && originalName.name}` }); const htmlNode = { tagName:children.identifier.value, nodeName:children.identifier.value, attrs: attrs, childNodes: new Array<any>(), }; let keyAttribute = children.attributes.find((x:any) => x.name === 'key'); if (keyAttribute) { if (isMemberExpression(keyAttribute.value)) { const {value} = keyAttribute; key = `${value.object.name}.${value.property.name}`; } } for (let child of children.children) { htmlNode.childNodes.push(angularGenerator.generate(child)); } return htmlNode; } } 

Next, the resulting html nodes will be converted to html using parse5 .


Generate component class


At the moment, the class generator works very simply, it collects the input parameters from the variables and adds them to the class.


 export class AngularComponentGenerator { generateInputProps():Array<any> { const declarations:Array<any> = []; resolverRegistry.vars.forEach((value, key, map1) => { switch (value.type) { case 'Input': declarations.push( b.classProperty( b.identifier(value.name), undefined, undefined, [ b.decorator(b.identifier('Input')) ] ) ); } }); return declarations; } generate() { const src = b.file( b.program( [ b.exportNamedDeclaration( b.classDeclaration(b.identifier('MyComponent'), undefined, b.classBody( [ ...this.generateInputProps(), ] ), [ b.decorator(b.callExpression( b.identifier('Component'), [ b.objectExpression( [ b.objectProperty(b.identifier('selector'),b.stringLiteral('my-component'),false,false,[]), b.objectProperty(b.identifier('templateUrl'),b.stringLiteral('./my-component.component.html'),false,false,[]), ] ) ] )) ]), [], undefined, ) ] )); return generator(src).code; } } 

Results and conclusions


At the input component:


 import React from 'react'; class MyComponent extends React.Component { render() { const {a} = this.props; const {b} = this.props; return (<div className="asd"> <h1>Title</h1> { a } { b } <ul> { a.map(asd => (<li key={asd.key}>{asd}<text>asd</text></li>)) } </ul> { children } <h3>And here we go</h3> </div>) } } 

It turns out this pattern:


 <div class="asd"> <h1>Title</h1> {{a}} {{b}} <ul> <li *ngFor="let asd of a">{{asd}}<text>asd</text></li> </ul> <ng-content></ng-content> <h3>And here we go</h3> <div> 

And this class:


 @Component({ selector: "my-component", templateUrl: "./my-component.component.html" }) export class MyComponent { @Input() a; @Input() b; } 

General conclusions


  1. At the moment there is a feeling that the converter is possible
  2. We need a static predicate analyzer to prevent intersections
  3. A lot of work

Thanks for attention.


Link to the repository where the work is going. There are still a lot of crutches, but this is a PoC, so it can be.


')

Source: https://habr.com/ru/post/350772/


All Articles