Automating the Angular Conversion

tsaibot
Grubhub Bytes
Published in
7 min readAug 2, 2018

--

Photo by Pablo Merchán Montes on Unsplash

When AngularJS upgraded to version two, it changed almost everything. It even dropped the JS part of its name, moving away from JavaScript to TypeScript. That meant the approximately 500k lines of Javascript we had running both the Grubhub and Seamless consumer web applications needed a serious makeover to take advantage of the new features.

As this is the third post in our series on why and how we upgraded from AngularJS to Angular, we’re going to skip the bits we already discussed. If you want to understand why we would undertake such a massive overhaul of our most mission-critical product, start with the first post in this series: How I Learned to Stop AngularJS and Love the Angular.

To be fair, we couldn’t just write our script and convert the whole app; we had to manually prepare some of it. Before you take action on the process outlined below, you’re going to want to read Angular is coming: Preparing the upgrade. We really dug into the changes that would affect our underlying application logic, and learned a lot about TypeScript along the way.

And here we are, ready to create the script that will make everyone’s life easier.

For the purposes of this blog post I’ve created a small demo web application that leverages the Reddit API to query and render the Angular subreddit. It is built with TypeScript, Webpack & AngularJS and we will be referencing it throughout the article. Feel free to pull down the code from github and hack away at it yourself.

https://github.com/erictsai6/demo-reddit-app

Disclaimer

If you’re reading this article you’re likely of one of two camps. The first group of people just wants the source code of the script and will probably skip to the end of the article. I encourage those folks to read the entire article. While AngularJS is pretty opinionated about how your code should be structured, this script probably won’t just work right out of the box. We’ve provided the tools to adjust the script, so you can plug in your code and press play to speed up your conversion.

The other camp isn’t jumping to the bottom at all; they’re the type of engineer who thinks they already know how we did it. They think we did this all with regex replaces. “Regex replaces are flakey? Why didn’t you just write this in React?” they say, smugly chuckling at their three monitors.

🐐

For these naysayers, yes, sometimes we did have to rely on Regex to get what we needed out of the new code, but a lot of this was done by utilizing the abstract syntax tree (AST).

What is an abstract syntax tree?

What is the abstract syntax tree? In layman’s terms, it is a tree representation of your written code. Compilers use AST to understand the structure of source code before building that source into machine code. The tree contains an annotated hierarchy of every discrete element within source code. Refer to the picture below to get a better understanding of it.

In the image above, I’ve used astexplorer.net to analyze the search-input-component.ts file. The source code is on the left and its abstract syntax tree is on the right. When I hover over the searchQuery property of the SearchInputController class, it will automatically highlight its definition in the AST, which is extremely helpful when learning how to navigate it. I’ve provided the link below so you can explore it yourself.

How do I use the AST?

So that’s great, but how does that help me? Let’s use the SearchInputComponent as an example again.

If we wanted to convert this from AngularJS to Angular what would our strategy be? Well, one of the bigger Angular changes involves the different syntax for component bindings. Instead of passing in a plain object (as shown above) we’re now required to add special Input/Output decorators to properties of the component class.

If we knew for sure what the actual binding object was and we were able to extract out the key/value pairs, we could then programmatically recreate the Angular component class as needed. Using the SearchInputComponent class above with the AST, we can take the following steps:

  • Convert the source code into an AST tree
  • Extract out the class declaration
  • Followed by extracting out the constructor
  • Followed by iterating through the statements to find an expression that declared bindings

These bullet points can then be converted to the following code snippet.

This is a small but powerful example and this concept can easily be applied to:

  • Component lifecycle conversions, e.g. $onInit to ngOnInit
  • Combining the component and controller classes
  • Template URLs and template strings
  • Constructor dependency injection changes
  • and more…

Limitations of AST

AST is great for identifying and locating things in your source code but if you try rewriting everything with the abstract syntax tree you’re going to have a bad time. Let’s take a look at an example:

If we expand on that same bindings example, we also have to switch how we invoke the callback output function. Instead of just invoking it directly, we need to use an EventEmitter and invoke its emit method. See below to understand what that looks like.

The tree above doesn’t look that bad but this is an oversimplification and is shown this way for readability. It is missing critical metadata like start and end positioning, type, modifiers, etc. Let’s add the missing metadata for the ExpressionStatement.

Getting a bit complicated but still not too bad. Let’s go ahead to add the metadata for everything else now.

Whoa… as you can see AST breaks down code syntax pretty deeply, but it can get unwieldy and difficult to maintain. Recreating the abstract syntax tree yourself makes a simple problem more complicated. Instead what we recommend is to use regex to update the AngularJS code to match the new Angular patterns. It’s easier to reason about and I think you would agree the example below is much simpler.

Putting it all together

How do we utilize both the AST and regex replacements to create our conversion script? The first thing we do is load the AST of our AngularJS components so we can identify everything and normalize it as much as possible. The bullet list below is a string representation of our converted Angular component. It’s important to note that this conversion process also normalizes the new code so that it’s easier to run regex replacements on it afterwards.

  • Imports
  • — Remove unneeded imports
  • — Add any new imports
  • Variables
  • Interfaces
  • Enums
  • Decorator
  • — Selector
  • — Template (inline or external)
  • — Convert template
  • Class
  • — Properties
  • — Constructor
  • — Life cycle events
  • — Methods

This gets represented in code, and you’ll find that we’ve made our code very modular if you need to plug in something else that we didn’t account for. It also makes for easier unit testing as well.

After the code is normalized, we then take additional steps to:

  • Remove AngularJS specific services like $q, $timeout, etc.
  • Prettify the source code
  • Regex replacements on a large list
  • Update import references (if necessary)
  • Add emits to Output EventEmitters
  • Write converted source code to disk
  • Spec file creation

All of this is documented pretty well in the process-components.js file here as well in the comments.

An imperfect script

This is by no means a perfect script. We have provided the means to convert your components, services, and templates files, but you will have to manually update the following files:

  • Routing
  • Modules
  • App bootstrap
  • Partial unit tests

For most of our applications, these files are sparse, so extending the conversion script to account for approximately ten files wasn’t worth the development effort. You’ll notice that we’ve also added unit tests to the list even though our script will automatically generate a spec file.

Writing a conversion script for unit tests is not easy because they are not always written in a consistent manner. What our script does instead is read the describe and it blocks from each spec file and comment out the original testing code. This gives the developer enough context as to what the original unit test was trying to do and, as a result, makes manual conversion easier to do.

The conversion script

We went into this knowing that creating a perfect conversion solution was probably not a realistic goal. Although AngularJS/Angular are very opinionated, each person’s project is going to be different. What we have done instead is provide the tools so that you can hack away at it yourself. Please pull the code down and tweak the conversion script to fit your needs. You can even plug and play your own source files into the unit tests we have already set up in order to validate that the rewritten file is as you would expect. Refer to the README for more information.

Conclusion and preview for part 4

Converting your project from AngularJS to Angular will be a large effort regardless if you automate it or not. We chose this route to keep our developers engaged and happy (nobody wants to go around doing a find/replace), to reduce the possibility of human errors, and to convert many files in a short amount of time. We hope that this post and the code helps put you on the right path. Stay tuned for our last post of the series where we talk about hiccups we ran into while working in Angular and adjustments we had to make to ensure that the application was as performant as possible.

With assistance from Bill Beckelman.

Do you want to learn more about opportunities with our team? Visit the Grubhub careers page.

--

--

Lead software engineer @MongoDB, previously @CBinsights, @Grubhub