From AngularJS to Vue.js, CommonJS and Jest
Tags: vue, javascript, angular, webpack
Published: 2018-05-30 15:07:03

---
title: From AngularJS to Vue.js, CommonJS and Jest
published: true
tags: Vue, JavaScript, AngularJS, webpack
canonical_url: https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest/
cover_image: https://codewithhugo.com/img/justyn-warner-unsplash.jpg
---
> The trials and tribulations of kicking off an AngularJS -> Vue.js migration

AngularJS was pretty groundbreaking. It’s still impressive to this day, packed with a router, an HTTP client, a dependency injection system and a bunch of other things I haven’t necessarily had the pleasure of dealing with. It also wraps most browser APIs as injectable services, that’s pretty cool. The downside is that it’s complicated: services, filters, injectors, directives, controllers, apps, components. 

With the tools we have today, AngularJS isn’t as strong a way to do things *any more*. The fact that it’s in maintenance mode says it all. For a new application or new features, ie. for a project that *not* in maintenance mode, it’s doesn’t have the same niceties as other frameworks and libraries like Angular 2+, React or Vue. There should be a way *not* to rewrite the whole AngularJS application at once, here's how I went about doing it:

- [Shipping some bundles 📦](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#shipping-some-bundles)
  - [Why CommonJS instead of ES modules?](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#why-commonjs-instead-of-es-modules)
- [Adding Vue 🖼️](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#adding-vue)
- [Setting up Jest 🔧](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#setting-up-jest)
  - [Mock assets](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#mock-assets)
  - [CommonJS, Webpack and `vue-jest` woes](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#commonjs--webpack-and-vue-jest-woes)
  - [Some solutions 🤔](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#some-solutions)
  - [Fixing `vue-jest`/Webpack CommonJS handling with a Jest preprocessor](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#fixing-vue-jest-webpack-commonjs-handling-with-a-jest-preprocessor)
- [Thoughts on running Vue inside AngularJS 🏃‍](https://codewithhugo.com/from-angularjs-to-vue.js-commonjs-and-jest#thoughts-on-running-vue-inside-angularjs)

[Subscribe](https://buttondown.email/hugo) to get the latest posts right in your inbox (before anyone else).

# Shipping some bundles 📦

Bundling the AngularJS app allows you to send a few files with everything needed to run the single page application.
Includes using `script` are reduced to a few bundles of JavaScript (depending on having eg. a vendor bundle) and possibly a few of CSS.

Modernising the codebase using ES6 and beyond becomes possible with a transpilation step, and some bundlers even allow for loading of non-JS files into the JavaScript bundle which means templates and even assets can be sent down in the same payload.

Loading of JavaScript functionality not tied to browser APIs in a test environment using Node (+ JSDOM) becomes possible,
giving you the ability to leverage tools such as Jest, AVA or Mocha (instead of Jasmine + Karma or even Protractor).

This means the controllers look more like the following:
```js
const angular = require('angular');
function homeController(
  $location,
  otherService
) {
  const ctrl = this;
  // attach methods to ctrl
  return ctrl;
}
angular.module('myApp')
.controller('HomeController', [
  '$location',
  'otherService',
  homeController
]);
module.exports = {
  homeController
};
```

The above snippet leverages CommonJS which is Node’s default module system, its hallmarks are the use of  `require()` and `module.exports = `.

To bundle the application, Webpack allows us to take the AngularJS codebase that leverages CommonJS and output a few application bundles.
Templates can be `require`-ed using the right webpack loaders (`html-loader`). SCSS stylesheets and even Handlebar templates can also be compiled.

## Why CommonJS instead of ES modules?

ES modules are the module format defined in the ECMAScript spec. They look like the following:

```js
import angular from 'angular'
export function homeController() {}
```

The issue with ES modules are that they are static imports, ideally they shouldn’t have side-effects.
Including something that does `angular.module('some-name')` seems pretty side-effectful, so CommonJS reflects this a bit more: `require('./this-script-that-adds-something-to-angular')`.

# Adding Vue 🖼️ 

This part was surprisingly straightforward, to add Vue components to an AngularJS app ngVue is available ([https://github.com/ngVue/ngVue](https://github.com/ngVue/ngVue)).
`ngVue` exposes functionality to wrap Vue components as an AngularJS directives.

The checklist goes like this:

* `npm i --save ngVue vue vue-loader` (vue-loader is to load/compile `.vue` single file components)
* add `ngVue` to the bundle: have `require('ngVue')` somewhere
* Register `ngVue`  with AngularJS `angular.module('myApp', ['ngVue'])`
* Create a Vue component that is registered on the global Vue instance as a component
```js
const myComponent = {
  template: '<div>My Component</div>'
};
const MyVueComponent = Vue.component(
  'my-component',
  MyComponent
);
```
* Register the component as an AngularJS directive 
```js
angular
.module('myApp')
.directive('myComponent', [
  'createVueComponent' ,
  createVueComponent => createVueComponent(MyVueComponent)
]);
```
* In an AngularJS template you can now use: `<my-component v-props-my-data="ctrl.myData"></my-component>` (`vprops-*` allows you to pass data and functions from AngularJS to Vue as props)

Full snippet that leverages webpack to load a single file component:
```js
const angular = require('angular');
const { default: Vue } = require('vue');
const { default: MyComponent } = require('./my-component.vue');
const MyVueComponent = Vue.component('my-component', MyComponent)
angular
.module('myApp')
.directive('myComponent', [
  'createVueComponent' ,
  createVueComponent => createVueComponent(MyVueComponent)
]);
```

In order to load single file components like in the above example, `vue-loader` is required (see https://github.com/vuejs/vue-loader),
depending on how webpack is set up in a project, it can also affect how you process CSS (since single file components contain CSS as well as JavaScript and templates).

# Setting up Jest 🔧

## Mock assets

`.html`, `.scss`, `.svg` needs to be dummied in your Jest config:
```json
{
    "testRegex": ".*spec.js$",
    "moduleFileExtensions": [
      "js",
      "vue"
    ],
    "moduleNameMapper": {
      "\\.(html)$": "<rootDir>/src/mocks/template-mock.js"
    },
    "transform": {
      ".*\\.js$": "<rootDir>/node_modules/babel-jest",
      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    },
    "collectCoverageFrom": [
      "src/**/*.{js,vue}"
    ]
}
```

## CommonJS, Webpack and `vue-jest` woes 

Webpack doesn’t care about CommonJS vs ESM,  for all intents and purposes, Webpack treats them as the same thing. Here’s the catch: the recommended Jest plugin for Vue (`vue-jest`) handles `require` vs `import` of `.vue` files differently to Webpack.
This is some sample code in a Vue component that imports another Vue single file component in CommonJS:

```js
const MyOtherComponent = require('./my-other-component.vue').default;

export.default = {
  components: {
    MyOtherComponent
  }
};
```

The issue is the following:

- For the Webpack build to work, you need to use `const MyComponent = require('./my-component.vue').default` or `import MyComponent from './my-component.vue'`.
- For tests to pass, you need to do `const MyComponent = require('./my-component.vue')` or use `import` and transpile the modules using Babel
- AngularJS controllers love `this` … transpiling ES modules through Babel breaks `this` somehow

## Some solutions 🤔

1. Use ES6 import/export for the Vue components and tests, add a specific extension (`.mjs`, `.module.js`), disable `babel-jest` on CommonJS files.  
  **Drawback**: Coverage breaks due to the following issue (that is fixed now): https://github.com/istanbuljs/babel-plugin-istanbul/pull/141

2. Monkey-patch using Jest inside your test: `jest.setMock('./my-component.vue', { default: MyComponent });`.  
  **Drawback**: this is not a real fix, it makes the developer have to think about Vue vs bundled JavaScript vs JavaScript in test, which should appear the same in most situations.

3. Rewrite the transformed code, using a custom pre-processor, so it behaves the same under Webpack and `vue-jest`. 

## Fixing `vue-jest`/Webpack CommonJS handling with a Jest preprocessor

The following preprocessor takes `require('./relative-path').default` and converts it to `require('./relative-path')` (which is what Webpack seems to do under the hood).
To use the following preprocessor, replace the `.vue`-matching line in `"transform"` of Jest config with `".*\\.(vue)$": "<rootDir>/vue-preprocessor"`.
Here is the full code for the preprocessor, walkthrough of the code/approach follows:

```js
// vue-preprocessor.js
const vueJest = require('vue-jest');

const requireNonVendorDefaultRegex = /(require)\('\..*'\).default/g;

const rewriteNonVendorRequireDefault = code =>
  code.replace(requireNonVendorDefaultRegex, match =>
    match.replace('.default', '')
  );

module.exports = {
  process (src, filename, config, transformOptions) {
    const { code: rawCode, map } = vueJest.process(
      src,
      filename,
      config,
      transformOptions
    );
    const code = rewriteNonVendorRequireDefault(rawCode);
    return {
      code,
      map
    };
  }
};
```

At a high level, we process the code through `vue-jest` and then rewrite `require('./relative-path').default` to `require('./relative-path')`.
This is done with the following:

- `/(require)\('\..*'\).default/g` matches any `require` where the string arg starts with `.` ie it will match local `require('./something-here')` but not `require` of node modules (eg. `required('vue')`). A caveat is that this RegEx only works for single-quote requires… but that’s trivial to fix if the code uses double quotes.
-  `String.replace` with a function argument is leveraged to run a custom replace on each match of the previous RegEx. That’s done with `match.replace('.default', '')`.

# Thoughts on running Vue inside AngularJS 🏃‍

AngularJS is from a time before bundlers and a JavaScript module system.
The only contemporary bundling tool to AngularJS target JavaScript applications is the Google Closure Compiler.
For reference Browserify was released in 2011, webpack in 2012. AngularJS’ initial release was in 2010. 

That’s why we ended up with things like `script` includes for each controller and each template (`script type="ng-template"`).

Each `script` will call `angular.module('app').{controller, directive, service}`and each of those calls will register something on the global `angular`  instance and can then be consumed elsewhere. 
This is brittle since code that should be co-located gets spread across the codebase and gets referenced with strings like `'HomeController'`.
All it takes is 1 typo and we've got a bug that won’t be detected until we get the app in a certain state…

With Vue.js, Webpack and Jest, we can bundle, test and build with more confidence.
AngularJS was and still is great. What’s also great that we can migrate off it progressively, thanks to the `ngVue` team.

That means we get to keep the solid AngularJS working alongside new features written in Vue.

[Subscribe](https://buttondown.email/hugo) to get the latest posts right in your inbox (before anyone else).

Cover photo by [Justyn Warner](https://unsplash.com/photos/TzQZTfkanPA?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)