The trials and tribulations of kicking off an AngularJS -> Vue.js migration 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. AngularJS With the tools we have today, AngularJS isn’t as strong a way to do things . The fact that it’s in maintenance mode says it all. For a new application or new features, ie. for a project that 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 to rewrite the whole AngularJS application at once, here’s how I went about doing it: any more not not Shipping some bundles 📦 Adding Vue 🖼️ Setting up Jest 🔧 Thoughts on running Vue inside AngularJS 🏃 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 are reduced to a few bundles of (depending on having eg. a vendor bundle) and possibly a few of CSS. script JavaScript 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: const angular = require('angular');function homeController($location,otherService) {const ctrl = this;// attach methods to ctrlreturn 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 and . require() 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 -ed using the right webpack loaders ( ). SCSS stylesheets and even Handlebar templates can also be compiled. require html-loader Why CommonJS instead of ES modules? ES modules are the module format defined in the ECMAScript spec. They look like the following: 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 seems pretty side-effectful, so CommonJS reflects this a bit more: . angular.module('some-name') 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 ( ). exposes functionality to wrap Vue components as an AngularJS directives. https://github.com/ngVue/ngVue ngVue The checklist goes like this: (vue-loader is to load/compile single file components) npm i --save ngVue vue vue-loader .vue add to the bundle: have somewhere ngVue require('ngVue') Register with AngularJS ngVue angular.module('myApp', ['ngVue']) Create a Vue component that is registered on the global Vue instance as a component const myComponent = {template: '<div>My Component</div>'};const MyVueComponent = Vue.component('my-component',MyComponent); Register the component as an AngularJS directive angular.module('myApp').directive('myComponent', ['createVueComponent' ,createVueComponent => createVueComponent(MyVueComponent)]); In an AngularJS template you can now use: ( allows you to pass data and functions from AngularJS to Vue as props) <my-component v-props-my-data="ctrl.myData"></my-component> vprops-* Full snippet that leverages webpack to load a single file component: 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, is required (see ), 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). vue-loader https://github.com/vuejs/vue-loader Setting up Jest 🔧 , , needs to be dummied in your Jest config: .html .scss .svg {"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 woes vue-jest 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 ( ) handles vs of files differently to Webpack. This is some sample code in a Vue component that imports another Vue single file component in CommonJS: vue-jest require import .vue 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 or . const MyComponent = require('./my-component.vue').default import MyComponent from './my-component.vue' For tests to pass, you need to do or use and transpile the modules using Babel const MyComponent = require('./my-component.vue') import AngularJS controllers love … transpiling ES modules through Babel breaks somehow this this Some solutions 🤔 Use ES6 import/export for the Vue components and tests, add a specific extension ( , ), disable on CommonJS files. : Coverage breaks due to the following issue (that is fixed now): .mjs .module.js babel-jest Drawback https://github.com/istanbuljs/babel-plugin-istanbul/pull/141 Monkey-patch using Jest inside your test: . : 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. jest.setMock('./my-component.vue', { default: MyComponent }); Drawback Rewrite the transformed code, using a custom pre-processor, so it behaves the same under Webpack and . vue-jest Fixing /Webpack CommonJS handling with a Jest preprocessor vue-jest The following preprocessor takes and converts it to (which is what Webpack seems to do under the hood). To use the following preprocessor, replace the -matching line in of Jest config with . Here is the full code for the preprocessor, walkthrough of the code/approach follows: require('./relative-path').default require('./relative-path') .vue "transform" ".*\\.(vue)$": "<rootDir>/vue-preprocessor" // vue-preprocessor.jsconst 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 and then rewrite to . This is done with the following: vue-jest require('./relative-path').default require('./relative-path') matches any where the string arg starts with ie it will match local but not of node modules (eg. ). A caveat is that this RegEx only works for single-quote requires… but that’s trivial to fix if the code uses double quotes. /(require)\('\..*'\).default/g require . require('./something-here') require required('vue') with a function argument is leveraged to run a custom replace on each match of the previous RegEx. That’s done with . String.replace match.replace('.default', '') 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 includes for each controller and each template ( ). script script type="ng-template" Each will call and each of those calls will register something on the global 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 . 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… script angular.module('app').{controller, directive, service} angular 'HomeController' 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 team. ngVue That means we get to keep the solid AngularJS working alongside new features written in Vue. Originally published at codewithhugo.com on May 30, 2018.